# Turbo Docs > Make low-res, sprite-based 2D games as fast as possible. ## Example This is an example page. ## Cheatsheet \[↑ ↑ ↓ ↓ ← → ← → B A] ![Turbi happily carrying a giant power plug](/book.svg) ### Starting a Project To create a new project, you can run the command `turbo init `. For example, we could run the following command if we want to make a project called `hello-world`: ```bash [Terminal] turbo init hello-world ``` This will create a new project directory with the following structure: ```txt hello-world/ # Your project's root directory. ├── src/ # The directory of your code. │ └── lib.rs # The main file for the game. ├── Cargo.toml # Rust project manifest. └── turbo.toml # Turbo configuration. ``` Once the project has been initialized, you can run it with `turbo run -w `. For example, to run the `hello-world` project we created with the previous command: ```bash [Terminal] turbo run -w hello-world ``` :::note The `-w` flag enabled watch mode so your game window will automatically reflect changes you make to game code or assets. ::: **Learn More** * [Hello, World! Tutorial →](/learn/tutorials/hello-world) * [Using Turbo CLI →](/learn/guides/cli) ### Updating Your Config When you initialize a project, you'll see a file called `turbo.toml` in your project directory. If you open that file, you should see something like this: ```rust title="turbo.toml" name = "hello-world" version = "0.1.0" authors = ["Your Name"] description = "An awesome game made in Turbo!" [canvas] width = 256 height = 144 ``` **Learn More** * [Configuring Your Game →](/learn/guides/configuration) ### Managing Assets Assets go in special subdirectories within the project directory. All asset folders are optional, so if you don't have a certain kind of asset, there is no need to add an empty directory. ``` your-project-dir/ # Your project's root directory. ├── sprites/ # The directory of your sprite assets. ├── fonts/ # The directory of your font assets. ├── audio/ # The directory of your audio assets. ├── shaders/ # The directory of your shaders. ├── src/ # The directory of your code. │ └── lib.rs # The main file for the game. ├── Cargo.toml # Rust project manifest. └── turbo.toml # Turbo configuration. ``` :::tip Turbo will automatically update your game if assets are added/removed/modified while running. ::: **Learn More** * [Working with Sprites →](/learn/guides/sprites) * [Adding Custom Fonts →](/learn/guides/fonts) * [Playing Music and Sounds →](/learn/guides/audio) * [Making Custom Shaders →](/learn/guides/shaders) ### Drawing Shapes ```rs // Draw a 20x40px blue rectangle rect!(w = 20, h = 40, x = 70, y = 70, color = 0xff00ffff) // Draw an 16px diameter magenta circle circ!(d = 16, x = 120, y = 64, color = 0xff00ffff) ``` **Learn More** * [Rectangles API Docs →](/learn/api/rectangles) * [Circles API Docs →](/learn/api/circles) * [Ellipses API Docs →](/learn/api/ellipses) * [Lines API Docs →](/learn/api/lines) ### Displaying Text ```rs // Basic text text!("Hello, world!"); // Customized text with specified position, color, and font size text!( "Greetings, earthlings >:3", x = 30, y = 40, color = 0x00ff00ff, font = "small" ); ``` **Learn More** * [Text API Docs →](/learn/api/text) * [Hello, World Tutorial →](/learn/tutorials/hello-world) ### Handling Mouse & Touch Controls ```rs // The pointer control works for both mouse and touch inputs let p = pointer(); // Get the pointer x position let pointer_x = p.x; // Get the pointer y position let pointer_y = p.y; // Check if the pointer was just pressed this frame (mouse click or touch) if pointer.just_pressed() { // ... } ``` **Learn More** * [Pointer API Docs →](/learn/api/pointer) ### Handling Gamepad Controls ```rs // Get player 1's gamepad (Pass a 1 instead of a 0 for player 2's gamepad) let gp = gamepad(0); // Check if the `Up` button was just pressed this frame. if gp.up.just_pressed() { // ... } // Check if the `Down` button is currently being pressed. if gp.down.just_pressed() { // ... } // Check if the `A` button was just released this frame. if gp.a.just_released() { // ... } // Check if the `B` button is currently not being pressed. if gp.b.released() { // ... } ``` **Learn More** * [Gamepad API Docs →](/learn/api/gamepad) ### Generating Random Numbers ```rs // Get a random number let n = rand(); // Check if a random number is even let is_even = rand() % 2 == 0; // Check if a random number is odd let is_odd = rand() % 2 != 0; // Get a random number between 1-100 let n = 1 + (rand() % 100); // Probability Ranges match n { 1..=10 => { ... } // 10% chance 11..=50 => { ... } // 40% chance _ => { ... } // 50% chance } ``` **Learn More** * [Randomness API Docs →](/learn/api/randomness) ### Tracking Time In most cases, using the current game "tick" is sufficient for working with time in a game. It starts at `0` and increases by one each time a new frame is rendered. ```rs let t = tick(); ``` When you need an actual unix timestamp, we've also got you covered: ```rs let timestamp = time::now(); ``` **Learn More** * [Time API Docs →](/learn/api/time) ### Adjusting the Camera ```rust // Get the camera position let (x, y, z) = camera::xyz(); // Set the camera position // The position you set here is the center of the viewport in pixels. By default the camera starts at half of the width and height of your game's resolution. camera::set_xy(100, 100); // Move the camera position relative to the current camera position camera::move_x(5.0); camera::move_y(8.0); // Reset the camera to its original position camera::reset(); ``` **Learn More** * [Camera API Docs →](/learn/api/camera) ### Debugging Turbo's logging system prints directly to your terminal, making it easy to trace variable values, game state, and behavior during runtime. Whether you're narrowing down a bug or watching a value evolve, `log!` is your go-to for visibility without disruption. :::code-group ```rs [Your Game Code] // Log a static message log!("o hai!"); // Log a single value let some_var = "foo"; log!("one value: {:?}", some_var); // Log multiple values let another_var = 42; log!("multiple values: {:?} {:?}", some_var, another_var); // Log game state log!("My game state = {:?}", self); ``` ```txt [Output] [turbo] o hai! [turbo] one value: "foo" [turbo] multiple values: "foo" 42 [turbo] My game state = *your game state will show up here* ``` ::: :::tip[Avoid Noisy Logs] You probably don't want to log a value every frame of your game loop. It can clutter your console and prevent you from seeing the useful information you may be looking for. Instead, try including logs only when a specific action happens such as a keypress or mouse click. ::: **Learn More** * [Log API Docs →](/learn/api/log) * [Keyboard Shortcuts →](/learn/guides/keyboard-shortcuts) ### Turbo Game Structure Turbo provides a straightforward way to initialize the standard game state. -Create a new struct with GameState values. -Initialize the state in the new() function. -Run the game loop from the update() function. Turbo games run at 60 fps. Here is an example using the appropriate format. ```rs use turbo::*; // Turbo procedural macro #[turbo::game] #[derive(Debug, Clone, PartialEq, BorshSerialize, BorshDeserialize)] struct GameState { x_position: i32, y_position: i32, } impl GameState{ fn new()->Self{ // Return the GameState's initial value. [!code hl] // You can also run initialization code here before returning the state. Self { x_position: 30, y_position: 40, } } fn update(&mut self){ // Update State [!code hl] // The bulk of your game's logic goes here. Mutate state as-needed. } } ``` :::tip[Resetting Game State] In development, you can reset the state of the game to its initial state anytime by using a simple keyboard shortcut `Cmd+R` on MacOS/Linux and `Ctrl+R` on Windows. ::: **Learn More** * [Demos Github Repo →](http://github.com/super-turbo-society/turbo-demos) * [Rust SDK Docs →](https://docs.rs/turbo-genesis-sdk) import { Button } from 'vocs/components' ## Getting Started \[A sword wields no strength unless the hand that holds it has courage.] ![Turbi on a stack of books chilling with a buddy on a laptop](/beginner-friendly.svg) ### Overview Turbo is a *command-line based* game engine powered by WebAssembly and WebGPU. At this stage of development, Turbo is best suited for folks who have some basic coding skills. However, you can be an effective Turbo developer as long as you are willing to: :::steps ###### ✅ Make a 2D Game Turbo uses a quad-based render pipeline and solely focuses on low-res 2D games of any genre. It has no 3D features. ###### ✅ Use the Command Line Turbo runs on the command line via the `turbo` executable. You will need to use this command to initialize, run, and export your projects. ###### ✅ Write Some Rust We'll add more language support over time, but for now, our officially supported Turbo SDK is written in Rust. ::: Luckily, you don't have to be an expert at any of the above. ### Let's Go! Now that you're ready to get started, the time has come to... ## Installation \[It’s dangerous to go alone! Take this.] ![Turbi happily carrying a giant power plug](/turbi-plug.svg) :::info **Note for Windows Users** Before continuing, we recommend you install [Git for Windows](https://git-scm.com/download/win) and open **Git Bash as an administrator** to run the commands described in this documentation. ::: You can install `turbo` on the command line in 3 easy steps: ::::steps #### Install Rust :::code-group ```bash [MacOS / Linux] curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh ```
Download and run the [Rust Installer](https://static.rust-lang.org/rustup/dist/x86_64-pc-windows-gnu/rustup-init.exe).
::: :::note

If you already have rust installed, you can skip this step.

::: #### Add the WebAssembly Target ```bash [Terminal] rustup target add wasm32-unknown-unknown ``` :::note If you already have ready added the `wasm32-unknown-unknown` target with `rustup`, you can skip this step. ::: #### Install Turbo CLI ```bash [Terminal] curl -sSfL https://turbo.computer/install.sh | sh ``` :::: You can verify your installation by running `turbo -v`. This should output `turbo 0.9.0`. :::note If you wish to install `turbo` by manually downloading pre-built binaries, you can check out the [archive of all releases](https://github.com/super-turbo-society/turbo-cli/releases) on Github. You can also install a specific version `curl -sSfL https://turbo.computer/install.sh | TURBO_VERSION=0.0.0 sh`. Just replace `0.0.0` with a valid version. ::: ## Audio ### Overview The `audio` module provides methods for playing back project's audio files. :::tip[Check Your Setup] Check out [Playing Music & Sound →](/learn/guides/audio) to ensure your project is ready to play audio. ::: ### API :::info The following file formats are supported: `.wav`, `.mp3`, `.ogg`, and `.flac`. ::: #### `audio::play` Plays an audio file from your project's `audio` directory, ```rs audio::play(name: &str); ``` | Param | Type | Default | Description | | :----- | :----- | :------ | :----------------------------------- | | `name` | `&str` | | The name of the audio asset to play. | :::tip Use `audio::play` to play a sound effect from your audio folder. For example, if your sound effect is named `coin.wav` you can play it with `audio:play("coin")` ::: #### `audio::is_playing` Returns `true` if the given audio track is currently being played. ```rs audio::is_playing(name: &str) -> bool ``` | Param | Type | Default | Description | | :----- | :----- | :------ | :--------------------------------- | | `name` | `&str` | | The name of the audio asset check. | :::tip[Looping sound] You can loop a sound like this: ```rust if !audio::is_playing("background_music") { audio::play("background_music"); } ``` This will automatically restart the music once it ends. ::: #### `audio::pause` Pauses the given audio track. ```rs audio::pause(name: &str) ``` | Param | Type | Default | Description | | :----- | :----- | :------ | :------------------------------------ | | `name` | `&str` | | The name of the audio asset to pause. | #### `audio::stop` Stops the given audio track and resets its progress to the beginning. ```rs audio::stop(name: &str) ``` | Param | Type | Default | Description | | :----- | :----- | :------ | :----------------------------------- | | `name` | `&str` | | The name of the audio asset to stop. | #### `audio::get_volume` Gets the current volume of the sound identified by `name`, expressed as a percentage (`0.0` to `1.0`). ```rs audio::get_volume(name: &str) -> f32 ``` :::note Converts the internal decibel value to a linear scale using: `P = 10^(AdB / 10)`. ::: | Param | Type | Default | Description | | :----- | :----- | :------ | :------------------------------------------------ | | `name` | `&str` | | The name of the audio asset to get the volume of. | #### `audio::set_volume` Sets the volume of the sound identified by `name`, using a `0.0` to `1.0` linear scale. ```rs audio::set_volume(name: &str, volume: f32) ``` :::note Converts to decibels using: `AdB = 10 * log10(P)`. Values `<= 0.0` are clamped to `-80.0dB`. ::: | Param | Type | Default | Description | | :------- | :----- | :------ | :------------------------------------------------ | | `name` | `&str` | | The name of the audio asset to set the volume of. | | `volume` | `f32` | | Percentage of max volume (between 0.0-1.0) | #### `audio::mute` Mutes the sound identified by `name`. ```rs audio::mute(name: &str) ``` | Param | Type | Default | Description | | :----- | :----- | :------ | :----------------------------------- | | `name` | `&str` | | The name of the audio asset to mute. | :::note Sets the volume to `-80db`. ::: #### `audio::is_muted` Returns `true` if the sound identified by `name` is currently muted. ```rs audio::is_muted(name: &str) -> bool ``` | Param | Type | Default | Description | | :----- | :----- | :------ | :--------------------------------------------- | | `name` | `&str` | | The name of the audio asset to check if muted. | :::note This checks whether the effective linear volume is `-60db` or lower which is considered 0%. ::: #### `audio::unmute` Resets the sound's volume to the last volume before it was muted. ```rs audio::unmute(name: &str) ``` | Param | Type | Default | Description | | :----- | :----- | :------ | :------------------------------------- | | `name` | `&str` | | The name of the audio asset to unmute. | ## Bounds ![Bounds example showing a simple button row](/bounds_example.gif) ### Overview `Bounds` represents a rectangular region in 2D space. It is the core primitive for low-res immediate-mode graphics, providing essential methods for positioning, sizing, and geometric operations. ### API #### `bounds::screen` Returns the screen-space (fixed) bounds. This is typically useful for GUI elements that are unaffected by any camera position or zoom adjustments. ```rust bounds::screen() -> Bounds ``` #### `bounds::world` Returns the world-space (camera-relative) bounds. This is typically used to position bounds relative to game objects. ```rust bounds::world() -> Bounds ``` ### Usage #### Creating a bounds that fills the screen ```rs // Create a new bounds that fills the full space of your game screen let screen_bounds = bounds::screen(); ``` #### Centering a fixed-size bounds ```rs // Create a new bounds that fills the full space of your game screen let screen_bounds = bounds::screen(); // Create a new bounds that is 48 px wide and 14 px tall let bounds = bounds::size(48, 14) // Center it horizontally and vertically within the whole screen .anchor_center(&screen_bounds); ``` #### Adjusting your bounds In most cases, you can adjust your bounds with set pixel amounts, or by fractions of another bounds. ```rs // Create a bounds that is 200px wide and 100px tall let parent = bounds::size(200, 100); // Create a new bounds, half as wide as the parent let child = parent.adjust_width_by_fraction(0.5); ``` #### Using your bounds to draw a rectangle Once you have your bounds in the correct position, you can access its values. ```rs // Create a new bounds that fills the full space of your game screen let screen_bounds = bounds::screen(); // Create a new bounds that is 48 px wide and 14 px tall let bounds = bounds::size(48, 14) // Center it horizontally and vertically within the whole screen .anchor_center(&screen_bounds) // Move it up 16px .translate_y(-16); rect!( // Use the bounds to position and size the rectangle bounds = bounds, // Full with white color = 0xffffffff, ); ``` #### Making buttons with bounds Create a row of 3 interactive buttons using bounds. ```rs // Get pointer input let p = pointer::get(); // Create a vector of 3 bounds: // 1. Use the screen bounds to start // 2. Create an 8px inset on all sides // 3. Set height to 32px // 4. Anchor to the center of the screen // 5. Split into 3 columns with a 12px gap between them let screen_bounds = bounds::screen(); let buttons = screen_bounds .inset(8) .height(32) .anchor_center(&screen_bounds) .columns_with_gap(3, 12); // Loop over each Bounds and draw a button for (i, btn) in buttons.into_iter().enumerate() { // Set up button colors and labels by their index let (label, regular_color, hover_color, pressed_color) = match i { 0 => ("One", 0x8833AAff, 0xAA55CCff, 0x800080FF), 1 => ("Two", 0xCC3333ff, 0xFF5555ff, 0xFF0000FF), 2 => ("Three", 0x33CCFFff, 0x66DDFFFF, 0x00FFFFFF), _ => continue, }; // Determine the button color based on pointer state and position let color = if p.just_pressed_bounds(btn) { pressed_color } else if p.intersects_bounds(btn) { hover_color } else { regular_color }; // Use the bounds and pointer state to draw the button rect!( bounds = btn, color = color, border_radius = 2, ); // Use the button bounds with a 4px inset (inner padding) text_box!(label, bounds = btn.inset(4), font = "medium"); } ``` ![Bounds example showing a simple button row](/bounds_example.gif) ## Camera ### Overview The `camera` module provides methods to shift the window's viewport of your game's canvas. :::info[Coordinate System] By default, the camera position is in the center of the viewport in pixels. This means, the `x` position starts at half of the width and the `y` position starts at half the height of your game's resolution. The `z` position will always be `1.0` by default. ::: ### API #### `camera::focus` Use this to center the camera on a target position. ```rs camera::focus(xy: (i32, i32)) ``` | Param | Type | Default | Description | | :---- | :----------- | :------ | :------------------------------------------- | | `xy` | `(i32, i32)` | | The x and y position to focus the camera on. | :::tip[Example] Focusing the camera within the center of a bounds: ```rust let bounds = bounds::size(16, 16).position(32, 64); camera::focus(bounds.center()); ``` ::: #### `camera::pan_xyz` Eases the camera toward `target` over `duration` ticks using `easing`. ```rs camera::pan_xyz(target: (X, Y, f32), duration: usize, easing: Easing) ``` | Param | Type | Default | Description | | :--------- | :------------ | :------ | :-------------------------------------------------------------------------------------- | | `target` | `(X, Y, f32)` | | The target x and y position and zoom to ease the camera to. | | `duration` | `usize` | | The duration of the camera movement. | | `easing` | `Easing` | | The easing curve used to interpolate from the previous position to the target position. | #### `camera::pan_xy` Eases the camera toward `target` over `duration` ticks using `easing`, preserving the current zoom. ```rs camera::pan_xy(target: (X, Y), duration: usize, easing: Easing) ``` | Param | Type | Default | Description | | :--------- | :------- | :------ | :-------------------------------------------------------------------------------------- | | `target` | `(X, Y)` | | The target x and y position to ease the camera to. | | `duration` | `usize` | | The duration of the camera movement. | | `easing` | `Easing` | | The easing curve used to interpolate from the previous position to the target position. | :::tip[Example] Focusing the camera within the center of a bounds: ```rust let bounds = bounds::size(16, 16).position(32, 64); camera::pan_xy(bounds.center(), 30, Easing::EaseOutQuad); ``` ::: #### `camera::pan_x` Eases the camera's x coordinate toward `x`, preserving y and zoom. ```rs camera::pan_x(x: X, duration: usize, easing: Easing) ``` | Param | Type | Default | Description | | :--------- | :------- | :------ | :-------------------------------------------------------------------------------------- | | `x` | `X` | | The target x position to ease the camera to. | | `duration` | `usize` | | The duration of the camera movement. | | `easing` | `Easing` | | The easing curve used to interpolate from the previous position to the target position. | #### `camera::pan_y` Eases the camera's y coordinate toward `y`, preserving y and zoom. ```rs camera::pan_y(y: Y, duration: usize, easing: Easing) ``` | Param | Type | Default | Description | | :--------- | :------- | :------ | :-------------------------------------------------------------------------------------- | | `y` | `Y` | | The target x position to ease the camera to. | | `duration` | `usize` | | The duration of the camera movement. | | `easing` | `Easing` | | The easing curve used to interpolate from the previous position to the target position. | #### `camera::pan_z` Eases the camera’s zoom to the `z` zoom level, preserving x and y position. ```rs camera::pan_z(z: f32, duration: usize, easing: Easing) ``` | Param | Type | Default | Description | | :--------- | :------- | :------ | :-------------------------------------------------------------------------------------- | | `z` | `f32` | | The target zoom to ease the camera to. | | `duration` | `usize` | | The duration of the camera movement. | | `easing` | `Easing` | | The easing curve used to interpolate from the previous position to the target position. | #### `camera::shake` Applies a screen-space shake amount (in pixels) around the last known stable camera position. ```rust camera::shake(amount: usize) ``` | Param | Type | Description | | -------- | ------- | ----------------------------------------------- | | `amount` | `usize` | Maximum pixel offset in any direction per axis. | :::tip[Example] Applying a moderate shake effect: ```rust camera::shake(2); ``` ::: #### `camera::is_shaking` Returns `true` if a shake effect is currently active. ```rust camera::is_shaking() -> bool ``` #### `camera::shake_amount` Returns the current shake intensity (in pixels). ```rust camera::shake_amount() -> usize ``` #### `camera::remove_shake` Stops any ongoing camera shake and restores the stable position. ```rust camera::remove_shake() ``` #### `camera::xyz` Gets the `x`, `y`, and `z` coordinates of the camera. ```rs camera::xyz() -> (f32, f32, f32) ``` :::tip[Example] Basic Usage ```rs let (x, y, z) = camera::xyz(); ``` ::: #### `camera::xy` Gets the `x` and `y` coordinates of the camera, ignoring the z coordinate. ```rs camera::xy() -> (f32, f32) ``` :::tip[Example] Getting just the x and y position ```rs let (x, y) = camera::xy(); ``` ::: #### `camera::x` Returns the current camera's x coordinate. ```rs camera::x() -> f32 ``` :::tip[Example] Getting just the x position ```rs let x = camera::x(); ``` ::: #### `camera::y` Returns the current camera's y coordinate. ```rs camera::y() -> f32 ``` :::tip[Example] Getting just the y position ```rs let y = camera::y(); ``` ::: #### `camera::z` Returns the current camera's z coordinate, which represents the zoom level. ```rs camera::z() -> f32 ``` :::tip[Example] Getting the zoom level ```rs let zoom = camera::z(); ``` ::: #### `camera::set_xyz` Sets the camera's position to (x, y, z). The z value is clamped to a minimum of 0.0. ```rs camera::set_xyz(x: X, y: Y, z: f32) ``` | Param | Type | Default | Description | | :---- | :---- | :------ | :------------------------------- | | `x` | `X` | | X position of the camera. | | `y` | `Y` | | Y position of the camera. | | `z` | `f32` | | Z position (zoom) of the camera. | :::tip[Example] Setting the camera position and zoom ```rust camera::set_xyz(100, 100, 2.0); ``` ::: #### `camera::set_xy` Sets the `x` and `y` coordinates of the camera while retaining the current z (zoom) value. ```rs camera::set_xy(x: X, y: Y) ``` | Param | Type | Default | Description | | :---- | :--- | :------ | :------------------------ | | `x` | `X` | | X position of the camera. | | `y` | `Y` | | Y position of the camera. | :::tip[Example] Setting both the camera `x` and `y` position to `100`: ```rust camera::set_xy(100, 100); ``` ::: #### `camera::set_x` Sets the camera's x coordinate, leaving y and z unchanged. ```rs camera::set_x(x: X) ``` | Param | Type | Default | Description | | :---- | :--- | :------ | :------------------------ | | `x` | `X` | | X position of the camera. | :::tip[Example] Setting just the x position ```rust camera::set_x(150); ``` ::: #### `camera::set_y` Sets the camera's y coordinate, leaving x and z unchanged. ```rs camera::set_y(y: Y) ``` | Param | Type | Default | Description | | :---- | :--- | :------ | :------------------------ | | `y` | `Y` | | Y position of the camera. | :::tip[Example] Setting just the y position ```rust camera::set_y(200); ``` ::: #### `camera::set_z` Sets the camera's z coordinate (zoom), leaving x and y unchanged. ```rs camera::set_z(z: f32) ``` | Param | Type | Default | Description | | :---- | :---- | :------ | :--------------------- | | `z` | `f32` | | The camera zoom level. | :::tip[Example] Set zoom level ```rust camera::set_z(2.0); ``` ::: #### `camera::move_xyz` Moves the camera by the specified deltas in x, y, and z. ```rs camera::move_xyz(delta_x: X, delta_y: Y, delta_z: f32) ``` | Param | Type | Default | Description | | :-------- | :---- | :------ | :------------------------------------------------ | | `delta_x` | `X` | | Number of pixels to move the camera horizontally. | | `delta_y` | `Y` | | Number of pixels to move the camera vertically. | | `delta_z` | `f32` | | Amount to change the zoom level. | :::tip[Example] Moving the camera 5px to the right, 10px up, and zooming in by 0.5: ```rust camera::move_xyz(5.0, -10.0, 0.5); ``` ::: #### `camera::move_xy` Moves the camera in the x and y directions by the specified deltas. ```rs camera::move_xy(delta_x: X, delta_y: Y) ``` | Param | Type | Default | Description | | :-------- | :--- | :------ | :------------------------------------------------ | | `delta_x` | `X` | | Number of pixels to move the camera horizontally. | | `delta_y` | `Y` | | Number of pixels to move the camera vertically. | :::tip[Example] Moving the camera 5px to the right and 10px up: ```rust camera::move_xy(5.0, -10); ``` ::: #### `camera::move_x` Moves the camera in the x direction by the specified delta. ```rs camera::move_x(delta_x: X) ``` | Param | Type | Default | Description | | :-------- | :--- | :------ | :------------------------------------------------ | | `delta_x` | `X` | | Number of pixels to move the camera horizontally. | :::tip[Example] Moving the camera 10px to the right: ```rust camera::move_x(10); ``` ::: #### `camera::move_y` Moves the camera in the y direction by the specified delta. ```rs camera::move_y(delta_y: Y) ``` | Param | Type | Default | Description | | :-------- | :--- | :------ | :---------------------------------------------- | | `delta_y` | `Y` | | Number of pixels to move the camera vertically. | :::tip[Example] Moving the camera 5px down: ```rust camera::move_y(5); ``` ::: #### `camera::move_z` Moves the camera's zoom by the specified delta. ```rs camera::move_z(delta_z: f32) ``` | Param | Type | Default | Description | | :-------- | :---- | :------ | :------------------------------- | | `delta_z` | `f32` | | Amount to change the zoom level. | :::tip[Example] Zooming in by 0.5: ```rust camera::move_z(0.5); ``` ::: #### `camera::reset` Resets the camera position to the original center of the canvas with zoom level 1.0. ```rs camera::reset() ``` :::tip[Example] Resetting the camera to default position: ```rust camera::reset(); ``` ::: #### `camera::reset_xy` Resets both the camera's x and y coordinates to the center of the screen. ```rs camera::reset_xy() ``` :::tip[Example] Resetting position but keeping zoom: ```rust camera::reset_xy(); ``` ::: #### `camera::reset_x` Resets the camera's x coordinate to the horizontal center of the screen. ```rs camera::reset_x() ``` :::tip[Example] Resetting just the x position: ```rust camera::reset_x(); ``` ::: #### `camera::reset_y` Resets the camera's y coordinate to the vertical center of the screen. ```rs camera::reset_y() ``` :::tip[Example] Resetting just the y position: ```rust camera::reset_y(); ``` ::: #### `camera::reset_z` Resets the camera's z coordinate (zoom) to 1.0 while keeping x and y centered. ```rs camera::reset_z() ``` :::tip[Example] Resetting zoom to default: ```rust camera::reset_z(); ``` ::: ### Usage #### Basic Camera Positioning ```rust // Instantly move camera to a position let player_x = 100; let player_y = 150; camera::set_xy(player_x, player_y); ``` #### Focusing on a Position ```rust // Using Bounds to snap to the center of a 16x16 player sprite let player_bounds = bounds::size(16, 16).position(player_x, player_y); let player_center = player_bounds.center(); camera::focus(player_center); ``` #### Panning to a Position with Easing ```rust // Smoothly move camera towards the player sprite; let duration = 120; // frames let easing = Easing::EaseOutQuad; let is_done = camera::pan_xy(player_center, duration, easing); // is_done is `true` when the camera has reaced the target destination ``` #### Camera Zoom and Effects ```rust // Zoom in for dramatic effect camera::set_zoom(2.0); // Smooth zoom transition let current_zoom = camera::zoom(); let target_zoom = 1.5; let zoom_speed = 0.02; camera::set_zoom(current_zoom + (target_zoom - current_zoom) * zoom_speed); // Reset to default view camera::reset(); ``` #### Screen Shake Effects ```rust // Explosion shake camera::shake(8); // Smaller shake for footsteps camera::shake(1); // Reset if the camera is shaking and the start button is pressed if camera::is_shaking() && gamepad::get(0).start.just_pressed() { camera::reset_shake(); // Same as camera::shake(0) } ``` #### Camera Controls ```rust // Keyboard camera movement let kb = keyboard::get(); let move_speed = 2.0; if kb.arrow_left().pressed() { camera::move_x(-move_speed); } if kb.arrow_right().pressed() { camera::move_x(move_speed); } if kb.arrow_up().pressed() { camera::move_y(-move_speed); } if kb.arrow_down().pressed() { camera::move_y(move_speed); } // Mouse wheel zoom let m = mouse::get(); let zoom_amount = 0.001; if m.delta_y > 0.0 { camera::move_zoom(zoom_amount); } if m.delta_y < 0.0 { camera::move_zoom(-zoom_amount); } // Reset camera with key if kb.key_r().just_pressed() { camera::reset(); } ``` #### Dynamic Camera Behaviors ```rust // Look-ahead camera (shows more space in movement direction) let player_vel_x = player.velocity_x; let player_vel_y = player.velocity_y; let look_ahead_distance = 50.0; camera::set_xy( player.x + player_vel_x * look_ahead_distance, player.y + player_vel_y * look_ahead_distance ); // Camera that follows with deadzone let (cam_x, cam_y) = camera::xy(); let deadzone_size = 32.0; let follow_speed = 0.03; let dx = player.x - cam_x; let dy = player.y - cam_y; if dx.abs() > deadzone_size { camera::move_x(dx.signum() * (dx.abs() - deadzone_size) * follow_speed); } if dy.abs() > deadzone_size { camera::move_y(dy.signum() * (dy.abs() - deadzone_size) * follow_speed); } ``` #### Multi-Target Camera ```rust // Camera that shows all players fn focus_on_players(players: &[Player]) { if players.is_empty() { return; } // Find bounding box of all players let min_x = players.iter().map(|p| p.x).fold(f32::INFINITY, f32::min); let max_x = players.iter().map(|p| p.x).fold(f32::NEG_INFINITY, f32::max); let min_y = players.iter().map(|p| p.y).fold(f32::INFINITY, f32::min); let max_y = players.iter().map(|p| p.y).fold(f32::NEG_INFINITY, f32::max); // Focus on the center of all players let center_x = (min_x + max_x) / 2.0; let center_y = (min_y + max_y) / 2.0; camera::set_xy(center_x, center_y); // Adjust zoom based on spread let spread = ((max_x - min_x).max(max_y - min_y) / 200.0).max(0.5).min(2.0); camera::set_zoom(1.0 / spread); } ``` ## Circles ![Circle Screenshot](/circle_screenshot.png) ### Overview You can draw circles to your game's canvas using the `circ!` macro. ### API #### `circ!` Draws circles. ```rust circ!( d = u32, x = i32, y = i32, color = u32, border_size = u32, border_color = u32, ) ``` | Param | Type | Default | Description | | :------------- | :----- | :----------- | :---------------------------------------------------------------------- | | `d` | `u32` | `0` | Diameter of the circle. | | `x` | `i32` | `0` | X position of the circle. | | `y` | `i32` | `0` | Y position of the circle. | | `color` | `u32` | `0xffffffff` | Hex color to fill the circle with. | | `border_size` | `u32` | `0` | Border width in pixels. | | `border_color` | `u32` | `0x000000` | Border color as RGBA hex. | | `fixed` | `bool` | `false` | If `true`, the circle's size and position are unaffected by the camera. | ::::tip[Example] Here's a 16px diameter magenta circle in the center of a 256x144 canvas: ```rust circ!(d = 16, x = 120, y = 64, color = 0xff00ffff); ``` :::details[Preview] ![Circle Screenshot](/circle_screenshot.png) ::: :::: ## Background Color ### Overview You can clear your game's canvas and set the background color using the `clear` method. ### API #### `clear` Erases everything on the canvas and sets the background color to the given value. ```rs clear(color: u32) ``` | Param | Type | Default | Description | | :------ | :---- | :------ | :---------------------------- | | `color` | `u32` | | A hexdecimal code for a color | :::note By default, the screen will clear to black (`0x000000ff`) each frame if `clear` is not called during the game loop. ::: :::tip[Example] Set the background color to white (`0xffffffff`): ```rust clear(0xffffffff); ``` ::: ## Ellipses ![Ellipse Screenshot](/ellipse_screenshot.png) ### Overview You can draw ellipses to your game's canvas using the `ellipse!` macro. ### API #### `ellipse!` Draws ellipses. ```rs ellipse!( x = i32, y = i32, w = u32, h = u32, color = u32, rotation = u32, border_size = i32, border_color = u32, fixed = bool, bounds = Bounds, ) ``` | Param | Type | Default | Description | | :------------- | :------- | :----------- | :----------------------------------------------------------------------- | | `x` | `i32` | `0` | X position of the ellipse. | | `y` | `i32` | `0` | Y position of the ellipse. | | `w` | `u32` | `0` | Width of the ellipse in pixels. | | `h` | `u32` | `0` | Height of the ellipse in pixels. | | `color` | `u32` | `0xffffffff` | Hex color to fill ellipse with. | | `rotation` | `u32` | `0` | Degrees of rotation. Positive clockwise. Negative counter-clockwise. | | `border_size` | `i32` | `0` | Border width in pixels. | | `border_color` | `u32` | `0x000000` | Border color as RGBA hex. | | `fixed` | `bool` | `false` | If `true`, the ellipse's size and position are unaffected by the camera. | | `bounds` | `Bounds` | | Sets the `x`, `y`, `w`, `h` of the ellipse. | ::::tip[Example - Basic] Using some basic options. ```rs ellipse!(x = 50, y = 50, w = 30, h = 20, color = 0xffff00ff); ``` :::details[Preview] ![Ellipse Screenshot](/ellipse_screenshot.png) ::: :::: ::::tip[Example - Advanced] Using rotation and borders. ```rs ellipse!( x = 100, y = 56, w = 64, h = 48, rotation = 45, color = 0xffff00ff, border_size = 2, border_color = 0x00ffffff, ); ``` :::details[Preview] ![Ellipse Screenshot](/ellipsev_screenshot.png) ::: :::: ## Events ### Overview Use `Events` to interface with the parent window for your game after you export to the web. This is most commonly used to start and stop ads, but can also be used for any interaction between your game and the website it is hosted on. ### API #### `events::emit` This sends an event that you can read from your `main.js` after you export your game. ```rust events::emit(name: &str, data: &str) ``` ### Usage #### Reading Events in JavaScript After you export, you can read your event calls by using an `EventListener`. Add this code to your `main.js` file. ```js window.addEventListener("turboGameEvent", (e) => { console.log(e.detail); // Check for the name of the event, and perform the required action if (e.detail && e.detail.name === "your_event_name") { //This will fire when your turbo event is called console.log("Detected Turbo Event"); } }); ``` #### Pausing and Resuming You can also fully pause your game from JavaScript (e.g. while an ad is playing). You can do that using this code in your `main.js`: ```js turbo.pause(); ``` ```js turbo.resume(); ``` ## Gamepad ### Overview Turbo gamepad layouts are similar to a SNES controller. Gamepad controls for players 1 and 2 are automatically mapped to the keyboard: | Gamepad | Keyboard (P1) | Keyboard (P2) | | ------- | ------------------- | ------------- | | Up | `W` or `UpArrow` | `I` | | Down | `S` or `DownArrow` | `J` | | Left | `A` or `LeftArrow` | `K` | | Right | `D` or `RightArrow` | `L` | | A | `Z` | `M` | | B | `X` | `,` | | X | `C` | `.` | | Y | `V` | `/` | | Start | `Space` | `[` | | Select | `Enter` | `]` | :::note With actual gamepads, left analog stick input is mapped to direction buttons. Beyond that, buttons not listed in the table above are ignored. ::: :::info On mobile web, touch swipes/drags are treated as `gamepad` direction presses. ::: ### API #### `gamepad::get` Gets the gamepad data for a player. ```rs gamepad::get(gamepad_index: u32) -> Gamepad ``` | Param | Type | Default | Description | | :-------------- | :---- | :------ | :--------------------- | | `gamepad_index` | `u32` | | The gamepad input for. | ### Usage #### Basics To retrieve the gamepad state of a player, use the `gamepad` function. ```rs // Get the gamepad state for player 1 let p1_gamepad = gamepad::get(0); // Get the gamepad state for player 2 let p2_gamepad = gamepad::get(1); ``` :::note Gamepads are `0`-indexed, so `0` is player 1, `1` is player 2, etc. ::: #### Button Methods To check the button states for players, utilize the following methods after obtaining the gamepad state using the `gamepad` function: ```rs let gp = gamepad::get(0); if gp.a.just_pressed() { // Fired at the instant the button/touch goes down } if gp.a.pressed() { // True every frame the button is held down } if gp.a.just_released() { // Fired at the moment the button is released } if gp.a.released() { // True every frame the button is NOT held down } ``` :::note This example just demonstrates checking the a button, but you can check all other buttons listed in the table above (`gp.start`, `gp.up`, etc) ::: ## Keyboard ### Overview The `Keyboard` API provides access to the full set of tracked keys, including standard input, modifiers, and extended function keys. Each key provides per-frame state transitions for ergonomic game or UI input handling. ### API #### `keyboard::get` Returns the current keyboard state. ```rust keyboard::get() -> Keyboard ``` ### Usage #### Reading Key State ```rust let keyboard = keyboard::get(); if keyboard.key_a().just_pressed() { // 'A' key was just pressed } if keyboard.space().pressed() { // Space is being held } if keyboard.digit_8().just_released() { // `8` is not being pressed } if keyboard.enter().just_released() { // Enter was just released } ``` :::tip Each key on the keyboard has a dedicated method named using the pattern `key*()`, `digit*()`, or `\_()` (e.g. `arrow_left()`, `numpad_enter()`), matching its semantic role. These methods return the current state for that key, enabling direct and readable access without manual key code lookups. ::: #### Input Text & Characters Use `.chars()` to get typed characters (including Shift transformations). ```rust let keyboard = keyboard::get(); let typed_chars: Vec = keyboard.chars(); let typed_text: String = keyboard.text(); ``` #### Example: Typing to a Text Field Because Turbo uses immediate mode, you must store any text input across frames inside your game state. For example: ```rs #[turbo::game] struct GameState { buffer: String } ``` You could place the following in your `update` method to collect keyboard input from multiple frames into a `String`: ```rust let keyboard = keyboard::get(); // Append keyboard input to the buffer for c in keyboard.chars() { match c { // Clear the buffer when Enter is pressed '\n' => self.buffer.clear(), // Append all other chars to the buffer ch => self.buffer.push(ch), } } // Remove the last character when backspace is pressed if keyboard.backspace().just_pressed() { self.buffer.pop(); } ``` If you want to include line breaks, you can simply extend the buffer without looping over each `char` and pushing them individually: ```rs self.buffer.extend(keyboard.chars()); ``` You would just need to come up with some other way to clear the buffer. :::note[Note] * `Keyboard` and `KeyCode` are local ergonomic wrappers around low-level data from the runtime. * Button state is frame-based: use `just_pressed()`, `pressed()`, `just_released()`, or `released()` for typical interactions. * Some keys do not have a valid representation as a `char` or `String` * The key state map includes all keys tracked by the platform, including browser/media keys and international layout variants. Use `keyboard::get()` once per frame and cache the result to avoid redundant FFI calls. ::: ## Lines ![Path Screenshot](/path_screenshot.png) ### Overview You can draw lines to your game's canvas using the `path!` macro. :::note Rust's std library already includes a `line!` macro, so we opted for `path!` to avoid namespace conflicts. ::: ### API #### `path!` Draws lines. ```rs path!( start = (i32, i32), end = (i32, i32), size = u32, color = u32, rounded = bool, fixed = bool, ) ``` | Param | Type | Default | Description | | :-------- | :----------- | :----------- | :---------------------------------------------------------------------- | | `start` | `(i32, i32)` | `(0, 0)` | The starting point of the line | | `end` | `(i32, i32)` | `(0, 0)` | The end point of the line | | `size` | `u32` | `1` | The line width in pixels | | `color` | `u32` | `0xffffffff` | Color that fills the line as RGBA hex | | `rounded` | `bool` | `false` | Stylize the line to be square or round | | `fixed` | `bool` | `false` | If `true`, the sprite's size and position are unaffected by the camera. | ::::tip[Example] Basic Usage ```rs path!( start = (0, 0), end = (256, 144), size = 4, color = 0xff00ffff, ); ``` :::details[Preview] ![Path Screenshot](/path_screenshot.png) ::: :::: ## Log ### Overview You can log messages to your console while the game is running via the `log!` macro. ### API #### `log!` Logs a message to the `stdout` on native and to the devtools console on web. ```rust log!(&str, ...args) ``` ### Usage :::tip[Avoid Noisy Logs] You probably don't want to log a value every frame of your game loop. It can clutter your console and prevent you from seeing the useful information you may be looking for. Instead, try including logs only when a specific action happens such as a keypress or mouse click. ::: #### Log a static message ```rust log!("hello!"); ``` #### Interpolate variables Log your game state: ```rust let state = GameState::new(); log!("My game state = {:?}", state); ``` You can interpolate multiple vars: ```rust let (canvas_width, canvas_height) = canvas::resolution(); log!("Canvas - width = {:?}, height = {:?}", canvas_width, canvas_height); ``` ## Mouse ### Overview The `Mouse` API provides a unified interface for querying the current mouse position, button states, and scroll behavior. It supports both screen-space (for UI) and world-space (for in-game interactions) access. ### API #### `mouse::screen` Returns the current mouse state in fixed screen-space pixel coordinates. ```rust mouse::screen() -> ScreenMouse ``` #### `mouse::world` Returns the current mouse state transformed into world-space coordinates, relative to the current camera view. ```rust mouse::world() -> WorldMouse ``` ### Usage #### Reading Mouse Position & State ```rust // World-space position (accounts for camera zoom & pan) let world_mouse = mouse::world(); let (world_x, world_y) = world_mouse.xy(); // Screen-space position (e.g. for UI) let screen_mouse = mouse::screen(); let (screen_x, screen_y) = screen_mouse.xy(); // Mouse button states if screen_mouse.left.just_pressed() { // Fired on the frame the left button goes down } if screen_mouse.left.pressed() { // True every frame the left button is held } if screen_mouse.left.just_released() { // Fired on the frame the left button goes up } if screen_mouse.left.released() { // True every frame the left button is not held } // Scroll wheel delta (horizontal and vertical) let (scroll_dx, scroll_dy) = screen_mouse.scroll_xy(); ``` #### Hit-Testing Bounds ```rs let target = Bounds::new(100, 100, 64, 64); // Screen-space let screen_mouse = mouse::screen(); if screen_mouse.intersects_bounds(target) { // Mouse relative to the screen is inside `target` } if screen_mouse.left_clicked_bounds(target) { // Mouse relative to the screen just pressed on `target` } if screen_mouse.right_clicked_bounds(target) { // Mouse relative to the screen just right-pressed on `target` } // World-space let world_mouse = mouse::world(); if world_mouse.intersects_bounds(target) { // Mouse relative to the camera is inside `target` } if world_mouse.left_clicked_bounds(target) { // Mouse relative to the camera just left-pressed on `target` } if world_mouse.right_clicked_bounds(target) { // Mouse relative to the camera just right-pressed on `target` } ``` #### Hit-Testing Arbitrary Rectangles Suppose you have a sprite at (50, 30) sized 100×20: ```rs // Screen-space let screen_mouse = mouse::screen(); if screen_mouse.intersects(50, 30, 100, 20) { // Mouse is over the rectangle in screen coordinates } if screen_mouse.intersects(50, 30, 100, 20) && screen_mouse.just_pressed() { // Mouse just clicked/tapped the rectangle in screen coordinates } // World-space let world_mouse = mouse::world(); if world_mouse.intersects(50, 30, 100, 20) { // Mouse is over the rectangle in world coordinates } if world_mouse.intersects(50, 30, 100, 20) && world_mouse.just_pressed() { // Mouse just clicked/tapped the rectangle in world coordinates } ``` ## Nine-Slice ![Nine\_Slice Screenshot](/nine_slice.png) ### Overview > 9-slice scaling (also known as Scale 9 grid, 9-slicing or 9-patch) is a 2D image resizing technique to proportionally scale an image by splitting it in a grid of nine parts. > > – [Wikipedia](https://en.wikipedia.org/wiki/9-slice_scaling) You can use nine-slice sprites to create UI elements, like buttons and text boxes, that can be resized without distorting the borders. ### `nine_slice!` Draws a nine-slice sprite to the game's canvas. ```rs nine_slice!( name: &str, margins = (u32, u32, u32, u32) x = i32, y = i32, w = u32, h = u32, fixed = true, bounds = Bounds, ) ``` | Param | Type | Default | Description | | :-------- | :--------------------- | :------ | :-------------------------------------------------------------------------- | | `name` | `&str` | | Name of the sprite to use as a nine-slice. | | `margins` | `(u32, u32, u32, u32)` | `0` | The margins of the nine-slice sprite. | | `x` | `i32` | `0` | X position of the nine-slice. | | `y` | `i32` | `0` | Y position of the nine-slice. | | `w` | `u32` | `0` | The width of the nine-slice. | | `h` | `u32` | `0` | The height of the nine-slice. | | `fixed` | `bool` | `false` | If `true`, the nine-slice's size and position are unaffected by the camera. | | `bounds` | `Bounds` | | Sets the `x`, `y`, `w`, `h` of the nine-slice. | :::note To render a nine-slice turbo needs to know the sprite file name and the margins. The margins are the distances in pixels of each edge, starting from the left and moving clockwise. ::: ::::tip[Example] To draw a nine-slice, call the macro like this: ```rust nine_slice!( "nslice_sprite_name", margins = (8, 8, 8, 8), x = 28, y = 20, w = 200, h = 50 ); ``` :::details[Preview] ![Nine\_Slice Screenshot](/nine_slice.png) ::: :::: ## Pointer ### Overview The `Pointer` API provides a single interface for handling both mouse and touch inputs to maximize cross-platform compatibility. ### API #### `pointer::screen` A pointer input (e.g. mouse or touch) with position in fixed screen-space pixel coordinates. ```rust pointer::screen() -> ScreenPointer ``` #### `pointer::world` A pointer input transformed into world-space coordinates, relative to the current camera view. ```rust pointer::world() -> WorldPointer ``` ### Usage #### Reading Pointer Position & State ```rust // World‐space position (accounts for camera zoom & pan) // Useful for interaction with camera-relative elements let world_pointer = pointer::world(); let (world_x, world_y) = world.xy(); // Screen-space position (e.g. for UI) // Useful for interaction with fixed-position elements let screen_pointer = pointer::screen(); let (screen_x, screen_y) = screen.xy(); // Button & touch states if screen_pointer.just_pressed() { // Fired at the instant the button/touch goes down } if screen_pointer.pressed() { // True every frame the button/touch is held } if screen_pointer.just_released() { // Fired at the instant the button/touch goes up } if screen_pointer.released() { // True every frame the button/touch is up } ``` \::: #### Hit-Testing Bounds ```rs let target = Bounds::new(100, 100, 64, 64); // Screen-space let screen_pointer = pointer::screen(); if screen_pointer.intersects_bounds(target) { // Pointer relative to the screen is inside `target` } if screen_pointer.just_pressed_bounds(target) { // Pointer relative to the screen just pressed on `target` } // World-space let world_pointer = pointer::world(); if world_pointer.intersects_bounds(target) { // Pointer relative to the camera is inside `target` } if world_pointer.just_pressed_bounds(target) { // Pointer relative to the camera just pressed on `target` } ``` #### Hit-Testing Arbitrary Rectangles Suppose you have a sprite at (50, 30) sized 100×20: ```rs // Screen-space let screen_pointer = pointer::screen(); if screen_pointer.intersects(50, 30, 100, 20) { // Pointer is over the rectangle in screen coordinates } if screen_pointer.intersects(50, 30, 100, 20) && screen_pointer.just_pressed() { // Pointer just clicked/tapped the rectangle in screen coordinates } // World-space let world_pointer = pointer::world(); if world_pointer.intersects(50, 30, 100, 20) { // Pointer is over the rectangle in world coordinates } if world_pointer.intersects(50, 30, 100, 20) && world_pointer.just_pressed() { // Pointer just clicked/tapped the rectangle in world coordinates } ``` ## Randomness ### Overview Turbo's `random` module provides high-quality random number generation using the system's entropy source. It offers uniform distribution across various numeric types and includes utilities for common randomization tasks like shuffling and selection. ### API #### `random::u8` Returns a random `u8`. ```rust random::u8() -> u8 ``` #### `random::u16` Returns a random `u16`. ```rust random::u16() -> u16 ``` #### `random::u32` Returns a random `u32`. ```rust random::u32() -> u32 ``` #### `random::u64` Returns a random `u64`. ```rust random::u64() -> u64 ``` #### `random::i8` Returns a random `i8`. ```rs random::i8() -> i8 ``` #### `random::i16` Returns a random `i16`. ```rs random::i16() -> i16 ``` #### `random::i32` Returns a random `i32`. ```rs random::i32() -> i32 ``` #### `random::i64` Returns a random `i64`. ```rs random::i64() -> i64 ``` #### `random::f32` Returns a random `f32`. ```rs random::f32() -> f32 ``` #### `random::f64` Returns a random `f64`. ```rs random::f64() -> f64 ``` #### `random::between` Returns a random number between two values (both bounds inclusive). ```rust random::between(lower: T, upper: T) -> T ``` :::note `T` has the following constraints ``` where T: NumCast + Copy + PartialOrd + Default + Debug, T: Add + Sub + Rem + One + Zero, T: ToPrimitive + FromPrimitive, ``` ...basically, integers and floats. ::: #### `random::within_range` Returns a random `usize` within flexible range bounds: ```rust random::within_range(bounds: impl RangeBounds) -> usize ``` #### `random::bool` Returns `true` or `false` with 50/50 probability: ```rust random::bool() -> bool ``` #### `random::shuffle` Randomly shuffles a slice in-place using Fisher-Yates algorithm: ```rust random::shuffle(slice: &mut [T]) ``` #### `random::pick` Returns a random reference from a slice, or `None` if empty: ```rust ranodm::pick<'a, T>(slice: &'a [T]) -> Option<&'a T> ``` ### Usage #### Percentages ```rust // Generates an f32 between 0.0 and 1.0. (random::f64 is also available) let percentage = random::f32(); // Basically a coin toss if random::bool() { // 50% chance true } else { // 50% change false } ``` #### Dice and Game Mechanics ```rust // Roll a 6-sided die let roll: u8 = random::between(1, 6); // Critical hit chance (5% chance) let critical = random::between(1, 100) <= 5; // Random spawn position let x: f32 = random::between(-50.0, 50.0); let y: f32 = random::between(-30.0, 30.0); ``` #### Index Within a Range ```rust random::within_range(0..100) // Half-open range (0 - 99) random::within_range(10..=20) // Closed range (10 - 20) random::within_range(..50) // Unbounded start (0 - 49) random::within_range(10..) // Unbounded end (10 - (usize::MAX - 1)) ``` #### Probability Distributions ```rust // Weighted random events match random::between(1, 100) { 1..=10 => { /* 10% chance - rare event */ } 11..=30 => { /* 20% chance - uncommon event */ } 31..=80 => { /* 50% chance - common event */ } _ => { /* 20% chance - normal event */ } } ``` #### Array Operations ```rust // Pick random enemy type let enemy_types = ["goblin", "orc", "skeleton", "dragon"]; if let Some(enemy) = random::pick(&enemy_types) { spawn_enemy(enemy); } // Shuffle deck of cards let mut deck = (1..=52).collect::>(); random::shuffle(&mut deck); ``` #### Floating Point Percentages ```rust // Random percentage for effects let opacity: f32 = random::f32(); // 0.0 to 1.0 let scale: f32 = random::between(0.8, 1.2); // Random angle in radians let angle: f64 = random::between(0.0, 2.0 * std::f64::consts::PI); ``` #### Particle System ```rust // Create a particle with random properties struct Particle { x: f32, y: f32, vel_x: f32, vel_y: f32, life: f32, color: u32, size: f32, } // Spawn explosion particles fn spawn_explosion_particles(origin_x: f32, origin_y: f32) { for _ in 0..50 { let particle = Particle { x: origin_x + random::between(-5.0, 5.0), y: origin_y + random::between(-5.0, 5.0), vel_x: random::between(-200.0, 200.0), vel_y: random::between(-250.0, -50.0), // Upward bias life: random::between(1.0, 3.0), color: { let r = random::between(200, 255); let g = random::between(100, 200); let b = random::between(0, 50); (r << 16) | (g << 8) | b }, size: random::between(2.0, 8.0), }; particles.push(particle); } } // Fire particle system fn update_fire_particles() { if random::between(1, 100) <= 80 { // 80% chance each frame let particle = Particle { x: fire_x + random::between(-10.0, 10.0), y: fire_y, vel_x: random::between(-20.0, 20.0), vel_y: random::between(-80.0, -40.0), life: random::between(0.5, 1.5), color: { let r = 255; let g = random::between(150, 255); let b = random::between(0, 100); (r << 16) | (g << 8) | b }, size: random::between(1.0, 4.0), }; fire_particles.push(particle); } } // Snow particles fn spawn_snow_particle() -> Particle { Particle { x: random::between(-50.0, screen_width + 50.0), y: -10.0, vel_x: random::between(-5.0, 5.0), vel_y: random::between(30.0, 60.0), life: random::between(5.0, 10.0), color: 0xFFFFFF, // White size: random::between(1.0, 3.0), } } ``` :::info[Technical Notes] * **Quality**: Uses system RNG (`turbo_genesis_ffi::sys::rand()`) for high-quality entropy * **Performance**: Optimized for game development use cases * **Precision**: `f32()` and `f64()` can return exactly `1.0`, useful for percentage calculations * **Bias**: Minor statistical bias may occur with very large `usize` ranges or non-power-of-2 integer spans * **Full Range Support**: `between()` handles full-domain ranges (e.g., `0..u64::MAX`) efficiently ::: ## Rectangles ![Rect Screenshot](/rect_screenshot.png) ### Overview You can draw rectangles to your game's canvas using the `rect!` macro. ### API #### `rect!` Draws rectangles. ```rust rect!( w = u32, h = u32, x = i32, y = i32, color = u32, rotation = i32, border_size = u32, border_color = u32, border_radius = u32, fixed = bool, bounds = Bounds, ) ``` | Param | Type | Default | Description | | :-------------- | :------- | :----------- | :------------------------------------------------------------------------- | | `w` | `u32` | `0` | Width of the rectangle in pixels. | | `h` | `u32` | `0` | Height of the rectangle in pixels. | | `x` | `i32` | `0` | X position of the left side of the rectangle in pixels. | | `y` | `i32` | `0` | Y position of the top side of the rectangle in pixels. | | `color` | `u32` | `0xffffffff` | Color that fills the rectangle as RGBA hex. | | `rotation` | `i32` | `0` | Degrees of rotation. Positive clockwise. Negative counter-clockwise. | | `border_size` | `i32` | `0` | Border width in pixels. | | `border_color` | `u32` | `0x000000` | Border color as RGBA hex. | | `border_radius` | `u32` | `0` | Border radius in pixels. | | `fixed` | `bool` | `false` | If `true`, the rectangle's size and position are unaffected by the camera. | | `bounds` | `Bounds` | | Sets the `x`, `y`, `w`, `h` of the rectangle. | ::::tip[Example - Basic] Draw a rectangle with some basic options: ```rust rect!(w = 20, h = 40, x = 70, y = 70, color = 0x0000ffff); ``` :::details[Preview] ![Rect Screenshot](/rect_screenshot.png) ::: :::: ::::tip[Example - Advanced Usage] Draw a rectangle with rotation and border options: ```rust rect!( x = 112, y = 56, w = 32, h = 32, color = 0x0000ffff, rotation = 45, border_size = 2, border_color = 0xffffffff, border_radius = 4, ); ``` :::details[Preview] ![Rectv Screenshot](/rectv_screenshot.png) ::: :::: ## Save and Load ### Overview Use `Save` and `Load` to store data from Turbo into local storage, and retrieve it the next time the same user plays the game. #### `local::save()` Saves your game to the browser's local storage by converting data into a `borsh::vec`. Any data that is used in your Turbo game can be converted to `borsh`, you don't need to worry about matching a certain type. ```rust local::save(data: &[u8]) -> Result ``` #### `local::load()` Loads your game by reading the `borsh::vec` created by `local::save`. ```rust local::load() -> Result, i32> ``` ### Usage #### Saving Add this function into your `Gamestate's` `impl` to save the entire `Gamestate`: ```rust pub fn save_local(&self) { //serialize the GameState let data = borsh::to_vec(self); if let Ok(d) = data { let _ = local::save(&d); } else { log!("error saving"); } } ``` Call this function whenever you require a save: ```rust self.save_local(); ``` #### Loading ```rust pub fn load_local() -> GameState { let data = local::load().unwrap_or_else(|_| vec![]); match borsh::from_slice(&data) { Ok(state) => return state, Err(_) => { log!("error loading game state"); return GameState::new(); } } } ``` Call this function whenever you require a load to get the stored `GameState`: ```rust *self = GameState::load_local(); ``` #### Autoloading On Boot When booting up your game if data exists it will be loaded, if not it will create a new save. ```rust use turbo::*; #[turbo::game] struct GameState { // Add fields here } impl GameState { pub fn new() -> Self { // initialize your game state GameState::load_local() } pub fn create() -> Self { Self { } } pub fn update(&mut self) { // This is where your main game loop code goes // The stuff in this block will run ~60x per sec text!("Hello, world!!!"); } } ``` #### Delete Saved Data Wipe the `GameState`, returning it back to its defaulted values. ```rust *self = GameState::create(); ``` Save after doing this to wipe your previous data. ## Sprites ![Doge Sprite Screenshot](/doge_animation_screencap.gif) ### Overview You can draw static sprites and animations to the game's canvas using the `sprite!` macro. :::tip[Check Your Setup] Check out [Working with Sprites →](/learn/guides/sprites) to ensure your project is ready to work with sprite assets. ::: ### API #### `sprite!` Sprites from your project's `sprites` folder can be drawn using the `sprite` macro. ```rust sprite!( name: &str, x = i32, y = i32, w = u32, h = u32, color = u32, background_color = u32, opacity = f32, rotation = i32, scale_x = f32, scale_y = f32, flip_x = bool, flip_y = bool, animation_key = &str, animation_speed = f32, default_sprite = &str, frame = usize, tx = i32, ty = i32, repeat = bool, cover = bool, fixed = bool, bounds = Bounds, ) ``` | Param | Type | Default | Description | | :----------------- | :------- | :---------------- | :---------------------------------------------------------------------------------------------------------------------------- | | `name` | `&str` | | The sprite's name (filename minus the extension). `name` and `animation_key` are mutually exclusive - *use one or the other.* | | `x` | `i32` | `0` | The sprite's x position. | | `y` | `i32` | `0` | The sprite's y position. | | `w` | `u32` | `[sprite width]` | The width to draw the sprite. By default, this is the sprite image width. | | `h` | `u32` | `[sprite height]` | The height to draw the sprite. By default, this is the sprite image height. | | `color` | `u32` | `0xffffffff` | Color that will overlay the sprite (multiplied) . | | `background_color` | `u32` | `0x00000000` | Color that will fill transparent pixels within the given `w` and `h`. | | `opacity` | `f32` | `1.0` | Sprite opacity - `0.0` = fully transparent. `1.0` = fully opaque. | | `rotation` | `i32` | `0` | Degrees of rotation. | | `scale_x` | `f32` | `1.0` | Horizontal scaling - `1.0` is 100% (aka, no scaling). | | `scale_y` | `f32` | `1.0` | Vertical scaling - `1.0` is 100% (aka, no scaling). | | `flip_x` | `bool` | `false` | If `true`, the sprite will be flipped horizontally. | | `flip_y` | `bool` | `false` | If `true`, the sprite will be flipped vertically. | | `animation_key` | `&str` | | The animation to associate with the sprite. `name` and `animation_key` are mutually exclusive - *use one or the other.* | | `animation_speed` | `f32` | `1.0` | The playback speed of the animation as a percentage (`1.0` is normal speed). | | `default_sprite` | `&str` | | The default sprite to use when an animation completes. Use in tandem with `animation_key`. | | `frame` | `usize` | | Use a specific frame from an animated file, instead of animating it. | | `tx` | `i32` | `0` | The `x` position of the texture within the given `w` and `h`. | | `ty` | `i32` | `0` | The `y` position of the texture within the given `w` and `h`. | | `repeat` | `bool` | `false` | Whether to repeat/tile the sprite within the given `w` and `h`. | | `cover` | `bool` | `true` | If `true`, the sprite will scale to the given `w` and `h`. | | `fixed` | `bool` | `false` | If `true`, the sprite's size and position are unaffected by the camera. | | `bounds` | `Bounds` | | Sets the `x`, `y`, `w`, `h` of the sprite. | #### `animation::get` Gets the `SpriteAnimation` data associated with a sprite animation key. ```rs animation::get(animation_key: &str) -> &mut SpriteAnimation ``` | Param | Type | Default | Description | | :-------------- | :----- | :------ | :----------------------- | | `animation_key` | `&str` | | The sprite animation key | ### Usage #### Static Sprites To draw a sprite, simply call the macro and include any parameters you need. The only one that is required is the file name (minus the extension): ```rust sprite!("goblin", x = 120, y = 50, rotate = 180); ``` And with that, we have ourselves an upside-down goblin: ![Goblin Sprite Screenshot](/goblin_sprite_screenshot.png) Let's break it down: * The first parameter (`"goblin"`) is the filename of your sprite excluding the file extension. * Adjust the `x` and `y` parameters to position the sprite where you want it. * Adjust the `rotate` parameter to control the sprite's degrees of rotation. #### Basic Looping Animations The sprite macro will follow any animation rules set in the file, so if you use an animated file type (like `.gif` or `.webp`), it will animate automatically. ![Animated Doge](/doge.webp) So if we write some code like this: ```rust // Display a sprite named "doge". sprite!("doge", x = 112, y = 50); ``` ...it will be rendered like this: ![Doge Sprite Screenshot](/doge_animation_screencap.gif) The speed and playback settings of the animation are determined by the settings on your file. #### Advanced Animations ##### Introduction A `SpriteAnimation` is the data structure that holds your animations values. Create a new `SpriteAnimation` by passing an animation key to `animation::get`: ```rust let anim = animation::get("foo"); ``` If there is no entry for the value you put in, then it will create a new animation. If the value exists already, then it will return that to you. Then, you can change the animation values, including setting the sprite that you want this animation to play: ```rust let anim = animation::get("foo"); // [!code hl] anim.use_sprite("turbi_idle"); // set the sprite file you want to use [!code hl] anim.set_speed(1.5); // increase the speed of the animation [!code hl] ``` Once you have your settings correct, you can call the `sprite!` macro to draw the animation: ```rust let anim = animation::get("foo"); anim.use_sprite("turbi_idle"); // set the sprite file you want to use anim.set_speed(1.5); // increase the speed of the animation sprite!(animation_key = "foo"); // [!code hl] ``` :::tip[Using Animation Keys] The string `"foo"` in the prior examples does NOT set the animation sprite. It is a unique value used to identify an animation instance. So if you have 10 different sprite animations, they each need a unique animation key. ::: ##### Transitioning animations There are several ways you can transition between animations, but one of the simplest is to use the `default_sprite` field in the `sprite!` macro. If the animation's sprite isn't set, or if it's animation is finished, then it will use the `default_sprite` instead. This is great for any time you want to play an animation all the way through one time, and then immediately go back to a default. For example, if your character dashes, you could play the dash animation, but keep an idle animation as the default. Then, when the dash is done, you'll go back to idle. ```rust let anim = animation::get("player_character"); if gamepad(0).a.just_pressed() { anim.use_sprite("player_dash"); anim.set_repeat(1); // only play the animation 1 time } sprite!( animation_key = "player_character", default_sprite = "player_idle", // turbo will use this sprite once the dash animation has completed one loop ); ``` The `default_sprite` is used if the `animation_key` doesn't have a sprite assigned to it, or if the assigned animation is finished. :::tip[Looping Animations] If a sprite is set to loop infinitely, it will not return true for `.done`. Make sure to use `.set_repeat(1)` or adjust your export settings on that file if you always want it to only play once. ::: ##### Animation Values `SpriteAnimation` has a number of functions you can call to change it's values. The general pattern is that you can access a value, or change it with `set_`. | Param | Type | Default | Description | | --------------- | -------------------------- | ------------------------------- | ------------------------------------------------------------------------------------------------------------------------- | | `repeat` | `usize` | based on sprite export settings | How many times the sprite will play through it's animation before it stops. | | `direction` | `SpriteAnimationDirection` | `Forward` | Which direction to play the animation (`Forward`, `Backward`, `PingPong`, `PingPongReverse`). | | `fill_forwards` | `bool` | `false` | When this is `true`, the animation stays on the last frame when it ends. If it is `false`, it returns to the first frame. | | `speed` | `f32` | `1.0` | How fast the animation plays multiplied by the speed set in the export settings. | | `delay` | `f32` | `0.0` | The delay (in milliseconds) before the animation starts. | | `paused` | `bool` | `false` | Pause the animation. | There are also a few helpful functions you can call on your `SpriteAnimation`. | Method name | Description | | ------------------ | ---------------------------------------------------- | | `use_sprite(&str)` | The name of the sprite file to use on this animation | | `restart()` | Restarts the animation from the beginning | | `pause()` | Pauses the animation | | `resume()` | Resumes the animation | ##### Aseprite Tags Turbo supports working directly with aseprite files. If you use tags to organize multiple animations in your aseprite animation timeline, you can target those with a special synax: ```rs sprite!("aseprite_file_name#tag-name"); ``` ## Text ![Magic Missile Screenshot](/magic_missile_screenshot.png) ### Overview You can draw text to your game's canvas by using the `text!` macro. :::tip[Typography Tip] Three fonts are available by default: `"small"`, `"medium"`, and `"large"`. If you want to use a font of your choice, be sure to check out the [Adding Custom Fonts →](/learn/guides/fonts). ::: ### API #### `text!` Draws ASCII text. ```rs text!( &str, x = i32, y = i32, color = u32, font = &str, fixed = bool, ) ``` | Param | Type | Default | Description | | :------ | :----- | :----------- | :-------------------------------------------------------------------- | | - | `&str` | - | Text to be displayed. | | `x` | `i32` | `0` | Starting x position of the text. | | `y` | `i32` | `0` | Starting y position of the text. | | `color` | `u32` | `0xffffffff` | Hex color to display text in. | | `font` | `&str` | `"medium"` | Size to display text in: `"small"`, `"medium"`, or `"large"`. | | `fixed` | `bool` | `false` | If `true`, the text's size and position are unaffected by the camera. | ### Usage #### Basic Usage ```rs text!("Hello, world!"); ``` ![Hello, World Screenshot](/hello_world_screenshot.png) #### Advanced Usage Customized text with specified position, color, and font size: ```rs text!( "Greetings, earthlings >:3", x = 30, y = 40, color = 0x00ff00ff, font = "small" ); ``` ![Greetings, Earthlings Screenshot](/greetings_earthlings_screenshot.png) #### Custom Fonts You can add custom fonts by creating a folder called `fonts` and adding a font file in there. Then call the font by using it's name (minus the file extension). ```rs text!("Magic Missile!!", font = "OldWizard"); ``` ![Magic Missile Screenshot](/magic_missile_screenshot.png) :::tip Head over to [Adding Custom Fonts →](/learn/guides/fonts) to learn more about custom fonts! ::: ## Text Box ![Make Games Fast Screenshot](/make_games_fast_screenshot.png) ### Overview Draw a rectangle into your games canvas that contains text. Wrap, align, and position the textbox by using the `text_box!` macro. :::tip[Typography Tip] Three alignments are available by default: `"left"`, `"center"`, and `"right"`. Be sure to check out [text](https://docs.turbo.computer/learn/api/text) for more clarity on the basic text macro!. ::: ### API #### `text_box!` Draws a text box with the ability to align, wrap, and position text ```rs text_box!( &str, font = &str, scale = f32, start = usize, end = usize, w = u32, h = u32, x = i32, y = i32, color = u32, align = &str, preserve_whitespace = bool, fixed = bool, bounds = Bounds, ) ``` | Param | Type | Default | Description | | :-------------------- | :------- | :---------------- | :-------------------------------------------------------------------------------------- | | - | `&str` | - | Text to be displayed. | | `font` | `&str` | `"medium"` | Size to display text in: `"small"`, `"medium"`, or `"large".` | | `scale` | `f32` | `1.0` | Scale of text box. | | `start` | `usize` | `0` | Starting character to be displayed. | | `end` | `usize` | `text.len()` | Ending character to be displayed, defaults to entire text. | | `w` | `u32` | `[screen width]` | Width of the text box in pixels. | | `h` | `u32` | `[screen height]` | Height of the text box in pixels. | | `x` | `i32` | `0` | Starting x position of the text. | | `y` | `i32` | `0` | Starting y position of the text. | | `color` | `u32` | `0xffffffff` | Hex color to display text in. | | `align` | `&str` | `"left"` | Text alignment. Available options: `"left"`, `"center"`, or `"right"`. | | `preserve_whitespace` | `bool` | `true` | If `true`, newline (`'\n'`), tab (`'\t'`), and space (`' '`) chars will not be ignored. | | `fixed` | `bool` | `false` | If `true`, the text box's size and position are unaffected by the camera. | | `bounds` | `Bounds` | | Sets the `x`, `y`, `w`, `h` of the text box. | ### Usage #### Basic Usage ```rs text_box!( "Welcome to Turbo!", font = "medium", width = 50, height = 20, ); ``` :::details[Preview] ![Welcome To Turbo](/welcome_to_turbo_screenshot.png) ::: #### Advanced Usage Wrap and adjust the `text_box` by declaring different `width`, `height`, and `align` values. Position it with different `x` and `y` values: ```rs text_box!( "Turbo makes UI elements easy and fast!", x = 80, y = 40, w = 100, h = 80, font = "medium", align = "center", color = 0x000000ff ); ``` ![Turbo Makes UI Easy Screenshot](/turbo_makes_ui_easy_screenshot.png) :::tip Changing the `scale` increases the distance between individual characters and line spacing! ::: ## Time ### Overview Turbo provides two main APIs for handling time: `tick()` for deterministic, monotonic time and `time::now()` for real-world Unix timestamps. Monotonic time is ideal for game logic. It increases consistently with each game loop frame and is not affected by user system clock changes, time zones, or daylight savings. Use `tick` to measure elapsed time or schedule time-based events during gameplay. Use `time::now` when displaying the real-world time or comparing to external timestamps. ### API #### `time::tick()` Returns the number of frames that have elapsed since the game started. ```rust time::tick() -> u32 ``` #### `time::now()` Returns the current system time as a Unix timestamp (seconds since the Unix epoch). ```rust time::now() -> u32 ``` ### Usage #### Doing Something on an Interval ```rust if time::tick() % 60 == 0 { // This will be `true` once every 60 frames (~1s) } if time::tick() % 60 < 30 { // This will be `true` for the first 30 frames (~0.5s) of every 60 frames (~1s) } ``` #### Screen Shake for 10 Frames ```rust #[turbo::game] struct GameState { shake_start: Option, } ``` Then in your `update` method: ```rust if player_took_damage() { self.shake_start = Some(time::tick()); } let shake_active = self.shake_start .map(|start| time::tick() - start < 10) .unwrap_or(false); if shake_active { camera::shake(5); } ``` #### Calculating Durations ```rust #[turbo::game] struct GameState { last_tick: usize, event_start: u64, } ``` Then in your `update` method: ```rust // Number of frames elapsed let delta_frames = time::tick() - self.last_tick; // Number of milliseconds elapsed let millis = time::now() - self.event_start; ``` ## Tween ![Tween example showing smooth animation](/tween-example.gif) ### Overview `Tween` is a generic struct that handles smooth transitions between values over time. ### Usage #### Create a new tween ```rs // Create a new tween starting at 0.0 let mut tween = Tween::new(0.0); ``` #### Set a target ```rs // Tween will transition from its current value to 100.0 tween.set(100.0); ``` #### Set duration ```rs // Tween will complete in 60 ticks tween.set_duration(60); ``` #### Set easing ```rs // Use quadratic easing tween.set_ease(Easing::EaseInQuad); ``` #### Get current value of tween ```rs // Get the current value of the tween let current = tween.get(); ``` #### Check if complete ```rs if tween.done() { // Tween has finished } ``` #### Add to target ```rs // Add 10 to the end value tween.add(10.0); ``` #### Complete Example Create a tween that moves across the screen horizontally. ```rs use turbo::*; #[turbo::game] struct GameState { position: Tween, } impl GameState{ fn new() -> Self { Self { position: Tween::new(0.0) // Start value .duration(120) // Duration in frames .ease(Easing::EaseInOutQuad) // Easing type .set(220.0) // End value } } fn update(&mut self) { // Get the current value of the tween let val = self.position.get(); // Draw a circle using that value circ!(x = val, y = 72, d=8, color = 0x32CD32ff); } } ``` ![Tween example showing smooth animation](/tween-example.gif) :::tip The full list of `Tween` easing variants can be found in the [SDK documentation on crates.io](https://docs.rs/turbo-genesis-sdk/latest/turbo_genesis_sdk/tween/enum.Easing.html#variants) ::: ## Playing Music & Sounds ### Walkthrough Follow these simple steps to integrate sound into your Turbo game: ::::steps #### Create an `audio` folder Inside your project directory, create a folder named `audio`. This folder will contain all your sound files. ``` your-project-dir/ # Your project's root directory. ├── audio/ # The directory of your audio assets. ├── src/ # The directory of your code. │ └── lib.rs # The main file for the game. ├── Cargo.toml # Rust project manifest. └── turbo.toml # Turbo configuration. ``` #### Put audio files in the folder The following file formats are supported: `.wav`, `.mp3`, `.ogg`, and `.flac`. :::warning[Pay Attention to File Sizes] Large and/or unused audio files can bloat your game size and cause long load times or even errors on the web. Make sure audio files are as small as possible and remove audio files that aren't being used in your game. ::: #### Make some noise ##### One-Shot Sound Effects Use `audio::play` to play a sound effect from your audio folder. For example, if your sound effect is named `coin.wav`: ```rs audio::play("coin") ``` ##### Looping Audio Tracks You can loop audio by checking if the audio track has stopped playing and restarting it if so: ```rust if !audio::is_playing("background_music") { audio::play("background_music"); } ``` ##### Pausing and Stopping Pausing a track, will stop it from playing. When resumed with `audio::play`, it will continue from the point at which it was paused. ```rust audio::pause("background_music"); ``` Pausing a track, will stop it from playing. When resumed with `audio::play`, it will restart from the beginning. ```rust audio::stop("background_music"); ``` :::: ## Using Turbo Command-Line Interface (CLI) \[From here on out... you're Big Boss.] ### Overview Turbo's CLI is your command center — fast, minimal, and built for speedrunners who'd rather ship games than click through menus. Whether you're initializing your first project or exporting a fully playable web build, every command is designed to get out of your way and let you focus on the game. ### Commands :::code-group ```sh [help] Create, develop, and export Turbo games Usage: turbo Commands: init Initializes a new Turbo project in Rust run Runs a Rust Turbo project export Exports a Rust Turbo project in web format help Print this message or the help of the given subcommand(s) Options: -v, --version Print version -h, --help Print help -V, --version Print version ``` ```sh [init] Initializes a new Turbo project in Rust Usage: turbo init Arguments: The name of the project Options: -h, --help Print help -V, --version Print version ``` ```sh [run] Runs a Rust Turbo project Usage: turbo run [OPTIONS] [PROJECT_DIR] Arguments: [PROJECT_DIR] Path to the root of your Turbo project [default: .] Options: -w, --watch Rebuild automatically on file changes -A, --always-on-top Keep game window on top of other windows --clippy Experimental "Clippy" mode (makes the window transparent and removes the title bar if possible) -p, --program-id Program ID for uploading to Turbo OS [default: ] -u, --user Turbo OS user ID (will prompt for one-time code) [default: ] --guest Authenticate anonymously --clear-cache Remove target/ to force clean build -c, --config Override default path to turbo.toml config [default: ] -h, --help Print help -V, --version Print version ``` ```sh [export] Exports a Rust Turbo project in web format Usage: turbo export [OPTIONS] [PROJECT_DIR] Arguments: [PROJECT_DIR] [default: .] Options: -c, --config [default: ] -o, --export-dir -b, --include-virtual-gamepad -h, --help Print help -V, --version Print version ``` ::: ## Configuring Your Game Your project will initialize with a file called `turbo.toml`. You can modify this file to change various configuration options for your game. ### Metadata Basic metadata for your game. ```toml [turbo.toml] name = "My Project Name" version = "0.1.0" authors = ["Anonymous"] description = "An awesome game made in Turbo!" ``` | Option | Type | Default | Description | | ------------- | ---------- | ------------ | ----------------------------------------- | | `name` | `string` | `"Untitled"` | Game name. Also becomes the window title. | | `version` | `string` | | Game version. | | `authors` | `string[]` | | List of authors. | | `description` | `string` | | Game description. | :::tip The `name` option determines the game window title. The rest are pure metadata and have no side-effects. ::: ### Canvas Adjust your game resolution and rendering behavior. :::code-group ```toml [Basic] [canvas] width = 128 height = 128 ``` ```toml [Auto-size] [canvas] # Dynamically resize to fill the whole window instead of maintaing a fixed aspect ratio. # The number given is the ratio of the window's size to your game size. auto-size = 1.0 ``` ::: | Option | Type | Default | Description | | ----------- | -------- | ------- | -------------------------------------------------------------------------------------------------------------- | | `width` | `number` | `256` | Game canvas width in pixels. | | `height` | `number` | `144` | Game canvas height in pixels. | | `auto-size` | `number` | | If set, width and height are ignored and the game canvas will scale with the window by the given scale factor. | | `fps` | `number` | `60` | Game frame rate. | :::warning The `fps` option is currently experimental and may have bugs. ::: ### Font Configure default text rendering behavior and load custom fonts. :::code-group ```toml [Basic] [font] default-size = 12 ``` ```toml [Changing Defaults] [font] default-font = "large" ``` ```toml [Individual Font Settings] [font] default-size = 12 default-font = "Bar" # You can remap the name and change the glyph size of individual fonts [font.settings.Bar] file = "Foo.ttf" # The source font file size = 16 # The pt size the render this font's glyphs at ``` ::: | Option | Type | Default | Description | | ----------------------- | ------ | ---------- | ---------------------------------------------------------------- | | `include-builtin-fonts` | `bool` | `true` | Whether to include system fonts: `small`, `medium`, and `large`. | | `default-font` | `bool` | `"medium"` | Name of the font to use when none is specified. | | `default-size` | `bool` | `12` | Default font size to render font glyphs at. | ### Shader Configure shaders. ```toml [turbo.toml] [shader] default-surface-shader = "my-custom-shader" ``` | Option | Type | Default | Description | | ------------------------ | -------- | ------- | ---------------------------------------- | | `default-surface-shader` | `string` | | The shader your game will use by default | ### Turbo OS Configure the Turbo OS network host. ```toml [turbo.toml] [turbo-os] api-url = "https://os.turbo.computer" ``` | Option | Type | Default | Description | | --------- | -------- | ------- | ----------------------------------------------------------------------- | | `api-url` | `string` | | The host URL to authenticate Turbo OS users with and upload programs to | ## Debugging Your Code \[Ore no na wa Erā da…] ### Overview Whether you're tracing logic bugs, inspecting runtime values, or verifying rendering behavior, Turbo gives you the tools to observe your game's inner state without leaving the dev loop. ### Logging Data Turbo's logging system prints directly to your terminal, making it easy to trace variable values, game state, and behavior during runtime. Whether you're narrowing down a bug or watching a value evolve, `log!` is your go-to for visibility without disruption. :::code-group ```rust [Your Game Code] let some_var = "foo"; log!("one value: {:?}", some_var); let another_var = 42; log!("multiple values: {:?} {:?}", some_var, another_var); ``` ```bash [Console Output] [turbo] one value: "foo" [turbo] multiple values: "foo" 42 ``` ::: ## Adding Custom Fonts ### Walkthrough ::::steps #### Create a `fonts` folder Inside your project directory, create a folder named `fonts`. This folder will contain all your game fonts. ``` your-project-dir/ # Your project's root directory. ├── fonts/ # The directory of your font assets. ├── src/ # The directory of your code. │ └── lib.rs # The main file for the game. ├── Cargo.toml # Rust project manifest. └── turbo.toml # Turbo configuration. ``` #### Put fonts in the folder The following file formats are supported: `.bdf`, `.ttf`, and `.otf`. :::tip[Tip: Use Bitmap Fonts] Bitmap fonts work best with Turbo. You can find a lot of good ones free online. One site we recommend is [BitfontMaker2](https://www.pentacom.jp/pentacom/bitfontmaker2/gallery/). ::: When you run your game, Turbo will automatically include any font assets in your project's `fonts` directory. In your code, you can refer to these fonts by their file stem (the filename minus the extension). For example, let's say you added a font called `awesome.ttf` to the `fonts` directory. In your code, you would use it like this: ```rs text!("lorem ipsum...", font = "awesome") ``` :::: **Learn More** * [Text API docs →](/learn/api/text) ## Keyboard Shortcuts \[Like a Wario Stadium triple hop... but for your fingies] ### Overview Some developers reach for a mouse. Others master the keys. Turbo’s built-in keyboard shortcuts let you reset game state, capture screenshots, and inspect your project’s internals — all without breaking flow. Whether you’re testing quickly or debugging visuals, these shortcuts are designed for speed. ### Resetting Game State Reset your game without closing the window. | Shortcut | MacOS/Linux | Windows | Description | | ---------- | ---------------------- | ----------------------- | -------------------------------------------------------------- | | Reset Game |
`Cmd` + `R`
|
`Ctrl` + `R`
| In development mode, you can reset a game to its initial state | ### Taking Screenshots Capture an image of your game while it's running. | Shortcut | MacOS/Linux | Windows | Description | | ------------------ | ---------------------- | ----------------------- | --------------------------------------------------------------------------------- | | Capture Screenshot |
`Cmd` + `1`
|
`Ctrl` + `1`
| Saves a file called `{PROJECT_NAME}-screenshot.png` to the project root directory | ### Inspecting Spritesheets Get detailed information about your game's internal spritesheet. | Shortcut | MacOS/Linux | Windows | Description | | --------------------- | ---------------------- | ----------------------- | ----------------------------------------------------------------------------------- | | Capture Spritesheet |
`Cmd` + `2`
|
`Ctrl` + `2`
| Saves a file called `{PROJECT_NAME}-spritesheet.png` to the project root directory | | Dump Spritesheet Data |
`Cmd` + `3`
|
`Ctrl` + `3`
| Saves a file called `{PROJECT_NAME}-spritesheet.json` to the project root directory | ## Understanding Network Code ![Packet storm illustration](/turbi-reading-nobg.png) ### Overview Turbo OS is designed for networked multiplayer games and services where reliability, latency, and state synchronization are critical. This guide outlines the key **best practices** for building stable and performant netcode using Turbo's APIs. ### Core Principles ::::steps #### Authenticate During Development To upload and update programs, you must run your project as an **authenticated user**: ```sh turbo run -w --user ``` :::note Using the `--guest` flag will not allow you to upload program updates ::: Your user ID and authentication code can be found in your [Turbo OS Dashboard](https://os.turbo.computer/dashboard). *** #### Get a Fresh Channel Connection Each Frame Channel subscriptions are **poll-based**, not event-driven. Call `Channel::subscribe` every update frame to maintain the connection. ```rs if let Some(conn) = MyChannel::subscribe("default") { while let Ok(msg) = conn.recv() { log!("Received: {:?}", msg); } } ``` :::warning You won't stay connected unless you call `subscribe()` **each frame**. ::: *** #### Watch Files Each Frame File updates are **push-based**. You must `watch()` files in every update loop to stay in sync with the latest file data: ```rs let counter = Counter::watch("counter") .parse() .unwrap_or(Counter { value: 0 }); ``` :::warning Loading a file takes more than one frame (\~16ms). Be sure to call `watch()` **each frame**. ::: *** #### Gate Messages Behind User Intent **Do not execute commands or send channel messages each frame.** Always check for a deliberate player action first: ```rs if gamepad::get(0).start.just_pressed() { let cmd = Ping; let _ = conn.send(&cmd); } ``` *** #### Commands vs Channels * **Use Commands when:** * You are writing to a file or mutating program state. * You want the server to store a result and keep a track record of player actions. * Player actions are discrete and not latency-sensitive (e.g. "cast spell", "equip item"). * **Use Channels when:** * You need real-time communication (e.g. player position, chat, combat). * You want broadcast behavior (multiple clients receive). * Latency is critical and persistent state isn't required. **Summary** At the cost of latency, Commands support a larger total number of players, persistent game data, robust player analytics, and a verifiable track record of in-game transactions. If you are more concerned with latency and your game state is ephemeral, Channels are typically a better choice. Channels are also a recommended default if you are not sure where to start. :::tip You can combine both. For example: use a channel for combat actions, and commands to commit kills or rewards. ::: #### Optimize Communication Efficient message design is critical for fast-paced gameplay and scalability. Small, infrequent messages reduce latency, lower bandwidth usage, and increase the number of players your game can support. ##### 🧠 General Principles * **Minimize data size**\ Keep your `Send` and `Recv` structs and enums as small as possible—remove unused fields and prefer compact types like `u8` and `bool` where possible. Be cautious of fields that can grow to any size such as `String`, `Vec`, `HashMap`, `BTreeMap`, etc. * **Avoid chatty behavior**\ Do not send messages every frame. Only send when there is meaningful state change or clear player intent. * **Use intervals, not floods**\ Broadcast shared state or sync events every 100-200ms ideally, not on every tick. * **Prefer delta over full state**\ Only send what's changed. Full dumps are rarely necessary. * **Simulate locally**\ Use client-side prediction and smoothing (e.g. tweens, lerps) to handle visual continuity. ##### 🚦 Client Messaging Tips * **Throttle**\ Enforce cooldowns or time windows between repeated messages, e.g. one message per 200ms. * **Debounce**\ Wait for a burst of inputs to settle before sending a single representative message. * **Deduplicate**\ Avoid sending repeated identical messages (e.g. holding a key should not fire the same action every frame). * **Gate behind intent**\ All messages—whether commands or channel sends—should be driven by clear user input (e.g. a button press), not polling or idle animations. Well-designed message protocols are the difference between a game that feels snappy and one that crumbles under real-world latency. *** #### Design for Errors Turbo OS gives you fast, reliable tools—but real networks are messy. You can’t control every player’s connection. Plan for failure. **Common issues:** * Players can disconnect at unexpected times * Slow or unstable connections are common * Server-side issues can cause temporary degradation * Messages do not arrive at the same time for all players * Even reliable timers do not guarantee synchronized delivery **Best practices:** * Clean up state when players disconnect * Handle idle or inactive players to prevent blocking others * Never assume success—always handle both `Ok` and `Err` * Avoid `unwrap()` and use clear error handling * Expect messages to arrive late or out-of-order * Read your logs—they are your best debugging tool :::tip[Monitor Your Programs] **Channels**: Every channel connection logs an inspector URL to the command line during development. Use it to monitor state, events, and logs in real time. **Commands**: Commands can be monitored in real-time from their program's analytics page which can be found on your [Turbo OS Dashboard](https://os.turbo.computer/dashboard). ::: #### Test with Multiple Accounts Prior to deploying your game on the web, you can open multiple game windows locally and test with guest accounts. Using the `--guest` flag will generate a new random user which can be handy for testing multiplayer locally during development. ``` turbo run -w --guest ``` :::warning Guest accounts do not have permission to upload program modifications. You must authenticate with the `--user` flag to make changes to channels and commands. ::: ### Best Practices Checklist | ✅ Do This | 🚫 Don't Do This | | ----------------------------------------- | -------------------------------------------------- | | Pass `--user` to update programs | Try to make program updates with `--guest` | | `subscribe()` to channels **every frame** | Call `subscribe()` once and assume it's persistent | | `watch()` files **every frame** | Assume files take one frame to load | | Send channel messages on user input | Send channel messages every frame | | Execute commands on user input | Execute commands every frame | | Expect and handle errors | Assume the network is always reliable | | Use inspector + analytics to debug | Ignore server logs and error outputs | ### Next Steps * Read the [Channels](/learn/api/networking/channels) documentation for fast-paced multiplayer patterns. * Learn how to sync state with [Commands](/learn/api/networking/commands). :::: ## Creating Online Games Coming soon... ## Rust Language Basics \[All your bugs are belong to Rust] ![Two fuzzballs standing on top of each other reaching for a star](/helping.png) ### Why Rust? Rust helps you build more reliable games by catching common bugs before your game even runs. Its special system for tracking who "owns" each piece of data helps prevent crashes and weird glitches that would be hard to find otherwise. Plus, Rust code runs fast and efficiently, making it great for games that need smooth performance even with lots of action on screen. With that said, the syntax can look strange at first! Let's look at some common Rust coding patterns to get you started. :::tip[Development Tip] If you are using VS Code for your project, we recommend installing the [Rust Analyzer](https://rust-analyzer.github.io/) extension. It can solve lots of syntax-related errors and gives helpful tips to get around some of Rust's trickier eccentricities. ::: ### Rust Terminology #### Variables and Mutability Use `let` to define a variable. If you want that variable to be changed later on, use `mut` when you define it: ```rust let score = 0; // This can't be changed later // Add 'mut' to make a variable changeable let mut health = 100; health = health - 10; // This works because health is mutable ``` #### Number Types In Rust, every number has a specific type. If you want to do operations between numbers, they need to be the same type. ```rust let player_count: u32 = 4; // Unsigned integer (no negatives) let score_diff: i32 = -10; // Signed integer (can be negative) let x_pos: f32 = 13.5; // Floating point number // Converting between types let integer_position = x_pos as i32; // Converts 13.5 to 13 ``` #### Collections `Vec` creates a dynamic array of any data type. ```rust let mut players = Vec::new(); // Empty vector players.push("Player 1"); // Add elements players.push("Player 2"); // You can also initialize with values let scores = vec![100, 85, 90]; ``` #### Strings There are two different text types in Rust: `String` and `&str`. Generally you'll use `String` for data that needs to exist for multiple frames, but you'll use `&str` when you want to display text on screen. ```rust let message: String = "Game Over"; // Owned string let title: &str = "Space Adventure"; // String slice ``` ### Struct and Impl **Structs** in Rust function similarly to **Classes** in other languages. They group different types of data together in one place. Here is an example for a `Player` struct in a platformer game. ```rust // Define game entities with structs #[derive(BorshSerialize, BorshDeserialize, PartialEq, Debug, Clone)] //Turbo structs need to derive these 5 traits to serialize properly struct Player { position: (f32, f32), velocity: (f32, f32), health: u32, is_jumping: bool, } ``` :::warning Any `struct` included in the **Turbo** `GameState` needs to derive the 5 traits mentioned above. You don't have to worry too much about how they work, just be sure to include them or else your game will not compile. ::: Add an `impl` below your struct to create **functions**. ```rust // Add functions to your structs with impl impl Player { // Constructor fn new(start_x: f32, start_y: f32) -> Self { Self { position: (start_x, start_y), velocity: (0.0, 0.0), health: 100, is_jumping: false, } } // Method with mutable access fn jump(&mut self) { if !self.is_jumping { self.velocity.y = -10.0; self.is_jumping = true; } } // Method with immutable access fn is_alive(&self) -> bool { self.health > 0 } } ``` :::note Use `->` to denote a return type for your functions. In Rust, if the final line of your function ends without a semicolon, it will be read as the `return` value. You can also use the `return` keyword for the same purpose. ::: ### Ownership and Borrowing Every piece of data in your game has exactly one owner. When you pass data around, you're either transferring ownership or temporarily borrowing it. Borrowing can be read-only `&` or read-write `&mut`. While this system can seem rigid, it prevents lots of bugs that would be difficult to track down later on. ```rust // Ownership - each value has exactly one owner let player_position = (10.0, 20.0); let current_position = player_position; // ownership moved here // player_position is no longer valid! // Borrowing - temporarily access without taking ownership fn update_player(player: &mut Player) { // mutable borrow player.position.0 += player.velocity.x; } fn render_player(player: &Player) { // immutable borrow // Draw at player.position (can read but not modify) sprite!("player_sprite", x = player.position.0, y = player.position.1); } // You can have either: // - Multiple immutable borrows (&T) // - OR exactly one mutable borrow (&mut T) // But never both at the same time! ``` :::tip It is fairly common to run into issues trying to borrow the same data multiple times. You can usually use `.clone()` to create a fresh instance that you can borrow again. Since Turbo games are generally very performance light, you don't have to worry about doing this! ::: ### Vec and Iteration In Rust, the functionality for a collection of objects is done through **Vectors** (shortened to `Vec`). We can initialize a `Vec` to hold any type of data. Rust supplies us with lots of powerful tools to iterate and manipulate the data inside of a `Vec`, making them incredibly useful for game development - perfect for storing collections of enemies, items or particles! ```rust let mut enemies = Vec::new(); enemies.push(Enemy::new(100.0, 200.0)); enemies.push(Enemy::new(150.0, 250.0)); // Access elements if !enemies.is_empty() { let first_enemy = &enemies[0]; // borrowing the first enemy } // Iteration - process all game objects for enemy in &mut enemies { enemy.update(); // update each enemy } // Common patterns for game development enemies.retain(|enemy| enemy.is_alive()); // removes dead enemies from the enemies vec // Modify items while filtering with retain_mut enemies.retain_mut(|enemy| { if enemy.health < 20 { enemy.set_retreat_mode(); // make low-health enemies retreat true // keep this enemy } else { enemy.health > 0 // only keep enemies that are still alive } }); ``` **Learn More** Now that you have the basics down, it's time to start working on a game. After all, the best way to learn any coding langauge is by coding! * [Demos Github Repo →](http://github.com/super-turbo-society/turbo-demos) * [Pancake Cat Walkthrough →](/learn/tutorials/pancake-cat) * [Video Tutorials →](https://www.youtube.com/@makegamesfast) ## Writing Custom Shaders ### Walkthrough Use [WebGPU Shader Language (WGSL)](https://google.github.io/tour-of-wgsl/) to add visual effects to your game. :::warning Custom shaders are in an early stage of development. Breaking changes are likely in future updates. ::: ::::steps #### Create a `shaders` folder Inside your project directory, create a folder named `shaders`. This folder will contain all your game shaders. ``` your-project-dir/ # Your project's root directory. ├── shaders/ # The directory of your shaders. ├── src/ # The directory of your code. │ └── lib.rs # The main file for the game. ├── Cargo.toml # Rust project manifest. └── turbo.toml # Turbo configuration. ``` #### Add a shader Let's add a shader called `my-awesome-shader.wgsl` to the `shaders` directory. Here's some boilerplate you can use as a starting point: ```wgsl [shaders/my-awesome-shader.wgsl] // Global uniform with viewport and tick fields struct Global { camera: vec3, tick: u32, viewport: vec2, } @group(0) @binding(0) var global: Global; // Vertex input to the shader struct VertexInput { @location(0) pos: vec2, @location(1) uv: vec2, }; // Output color fragment from the shader struct VertexOutput { @builtin(position) position: vec4, @location(1) uv: vec2, }; // Main vertex shader function @vertex fn vs_main(in: VertexInput) -> VertexOutput { var out: VertexOutput; out.position = vec4(in.pos, 0., 1.); out.uv = in.uv; return out; } // Bindings for the texture @group(1) @binding(0) var t_canvas: texture_2d; // Sampler for the texture @group(1) @binding(1) var s_canvas: sampler; // Main fragment shader function @fragment fn fs_main(in: VertexOutput) -> @location(0) vec4 { var color: vec4 = textureSample(t_canvas, s_canvas, in.uv); return color; } ``` #### Modify the shader So far, our shader isn't doing anything custom. So let's make it cycle the intensity of red, green, and blue color channels over time. At the bottom of the shader file add the `applyColorCycle` function and use it in the `fs_main` fragment shader entrypoint to modify the existing `color` variable: ```wgsl [shaders/my-awesome-shader.wgsl] @fragment fn fs_main(in: VertexOutput) -> @location(0) vec4 { var color: vec4 = textureSample(t_canvas, s_canvas, in.uv); color = applyColorCycle(color, in.uv, global.tick); // [!code ++] [!code hl] return color; } fn applyColorCycle(color: vec4, uv: vec2, tick: u32) -> vec4 { // [!code ++] [!code hl] let time: f32 = f32(tick) * 0.1; // [!code ++] [!code hl] let r: f32 = 0.5 + 0.5 * sin(time + uv.x); // [!code ++] [!code hl] let g: f32 = 0.5 + 0.5 * sin(time + uv.y + 2.0); // [!code ++] [!code hl] let b: f32 = 0.5 + 0.5 * sin(time + uv.x + 4.0); // [!code ++] [!code hl] return mix(vec4(r, g, b, color.a), color, 0.8); // [!code ++] [!code hl] } // [!code ++] [!code hl] ``` :::tip[Instant Updates] This is just a starting point. You can continue to modify your shader while your game is running. Your game will visually update immediately as you make changes. ::: #### Use the shader There are two ways to use the shader in your game. Depending on your use-case, you may even wish to use both approaches. They are not mutually-exclusive. *** ##### **Option A** - Set it in the config If you plan to use a single custom shader 100% of the time, this should have you covered. Add or modify the `shader` section of your Turbo config by setting a `default-surface-shader`: ```toml [turbo.toml] [shader] default-surface-shader = "my-awesome-shader" ``` *** ##### **Option B** - Activate it at runtime You can dynamically swap shaders while your game is running. ##### `shaders::set` Activate a custom shader: ```rs shaders::set("my-awesome-shader"); ``` ##### `shaders::reset` Deactivate any active custom shaders: ```rs shaders::reset(); ``` ##### `shaders::get` Get the name of the currently active custom shader: ```rs let shader_name = shaders::get(); ``` :::note You can only have one active shader at a time. When no custom shader is enabled, `""` will be returned. ::: :::: ## Sprites ### Walkthrough ::::steps #### Create a `sprites` folder Inside your project directory, create a folder named `sprites`. This folder will contain all your game sprites. ``` your-project-dir/ # Your project's root directory. ├── sprites/ # The directory of your sprite assets. ├── src/ # The directory of your code. │ └── lib.rs # The main file for the game. ├── Cargo.toml # Rust project manifest. └── turbo.toml # Turbo configuration. ``` #### Put images in the folder The following image formats are supported: * `.png` * `.bmp` * `.jpg`/`.jpeg`. * `.gif` * `.webp` * `.ase`/`asprite` #### Draw some sprites Now you're ready to draw some sprites! Use the sprite filename without the extension: ```rs sprite!("my-sprite-name", x = 64, y = 64) ``` If you are using Aseprite files, tags are supported. You can use animations from a specific tag with this syntax: ```rs sprite!("my-sprite-name#my-tag", x = 64, y = 64) ``` :::tip[Development Tip] You can use subfolders inside the `sprites` folder! Use `my-folder-name/my-file-name` to access those files. ::: :::: **Learn More** * [Sprite API docs →](/learn/api/sprites) ## Publishing to the Web ### Walkthrough ::::steps #### Export the Game Run [`turbo export`](/learn/guides/cli). This will create a `www` directory with the files required to run your game on the web. ``` your-project-dir/ # Your project's root directory. ├── www/ # The files needed to run your game on the web. ├── src/ # The directory of your code. │ └── lib.rs # The main file for the game. ├── Cargo.toml # Rust project manifest. └── turbo.toml # Turbo configuration. ``` #### Upload the Files Upload the `www` dir to a hosting service. :::tip[Tip: Free Web Hosting Services] There are many options to host websites online for free. Here are some recommendations: * **Netlify**: One option that lets you simply drag-and-drop the `www` directory is [https://app.netlify.com/drop](https://app.netlify.com/drop). * **Itch**: If you have an [itch.io](https://itch.io) account, you can also upload a new project at [https://itch.io/game/new](https://itch.io/game/new). Be sure to change the "Kind of project" to `HTML` and zip the `www` directory before uploading it. ::: :::: ## Tutorial Name \[Write something enticing] ![alt text for the final result preview image or video](/screenshot-placeholder.png) ### Overview :::info[Summary] > In this tutorial, you will learn blah blah blah. **Difficulty** > ★☆☆☆☆ **Time Estimate** > \< 5 minutes **What You'll Learn** * [x] How to foo * [x] How to bar * [x] How to baz ::: ### Walkthrough :::steps #### First Thing foo ```rs // insert code here ``` #### Second Thing bar ```rs // insert code here ``` #### Third Thing baz ```rs // insert code here ``` ::: ### Conclusion ... ### Next Steps ... ## Character Sheet \[Organize your player stats, level up your dev strats] ![Turbo game window gif of the character screen](/character-screen-gif.gif) ### Overview :::info[Summary] > In this tutorial you'll build an RPG character sheet which will display your stats, items, and loadout! **Difficulty** > ★★★☆☆ **Time Estimate** > \~20 minutes **What You'll Learn** * [x] How to use multiple Rust files * [x] How State Machines make it easy to organize your code * [x] How to use Enums in Rust * [x] How to import and use sprites * [x] An introduction to `Tweens`, `Bounds`, `Gamepad`, and `Camera` * [x] How to utilize various Turbo Macros ::: ### Walkthrough :::tip[Development Tip] The full source code of this game is [available on Github](https://github.com/Stophii/character-sheet) and a video guide on state machines is [available on Youtube](https://www.youtube.com/watch?v=0LuQa6T_pKQ\&t=2s) ::: ::::steps #### Initialize the Project Begin by creating a new project called `character-sheet` ```bash [Terminal] turbo init character-sheet ``` This initializes a Rust project with the following structure: ``` character-sheet/ # Your project's root directory. ├── src/ # The directory of your code. │ └── lib.rs # The main file for the game. ├── Cargo.toml # Rust project manifest. └── turbo.toml # Turbo configuration. ``` #### Create `state.rs` and `player.rs` files Inside your project directory, create a file named `state.rs`. This file will contain your `state machine`. You'll learn more about `state machines` as you progress through this tutorial. ``` your-project-dir/ # Your project's root directory. ├── src/ # The directory of your code. │ └── lib.rs # The main file for the game. | └── state.rs # The main file for your state machine. ├── Cargo.toml # Rust project manifest. └── turbo.toml # Turbo configuration. ``` Create another file and call it `player.rs`. This file will contain your player struct. ``` your-project-dir/ # Your project's root directory. ├── src/ # The directory of your code. │ └── lib.rs # The main file for the game. | └── state.rs # The main file for your state machine. | └── player.rs # The main file for your player. ├── Cargo.toml # Rust project manifest. └── turbo.toml # Turbo configuration. ``` #### Enabling Other Files Add the following lines of code to the top of each file. * [x] lib.rs ```rs [src/lib.rs] use turbo::*; use state::*; // [!code ++] [!code focus] [!code hl] mod state; // [!code ++] [!code focus] [!code hl] use player::*; // [!code ++] [!code focus] [!code hl] mod player; // [!code ++] [!code focus] [!code hl] ``` * [x] state.rs ```rs [src/state.rs] use crate::*; // [!code ++] [!code focus] [!code hl] ``` * [x] player.rs ```rs [src/player.rs] use crate::*; // [!code ++] [!code focus] [!code hl] ``` This will allow the code we write in `state.rs` and `player.rs` to be recognized inside of `lib.rs` and vice versa. Don't forget to save each file with `Cmd+S` or `Ctrl+S`. #### Run the Game At this point, we can run our game and leave it running as we make changes. Don't worry, it is just a blank screen for now! ```bash [Terminal] turbo run -w character-sheet ``` :::tip[Development Tip] If you want it to always appear above your other open windows you can run it with ```bash [Terminal] turbo run -w -A character-sheet ``` The `-A` will make it "always on top". ::: Now lets go through each individual file and add some code to start making our character sheet work. #### `state.rs` Add this code to your `state.rs` file. This is an enum we'll use for switching screens: ```rs [src/state.rs] use crate::*; #[turbo::serialize] // add this above structs and enums // [!code ++] [!code focus] [!code hl] pub enum Screen { // [!code ++] [!code focus] [!code hl] Character, // [!code ++] [!code focus] [!code hl] Inventory, // [!code ++] [!code focus] [!code hl] Loadout, // [!code ++] [!code focus] [!code hl] } // [!code ++] [!code focus] [!code hl] ``` :::tip[Development Tip] An enum lets you define a collection of related named values. In this case we use it for the different screens in our game. Using `enums` makes your code much more readable and less error prone. ::: #### `player.rs` Add this code to your `player.rs` file. These `structs` and `enums` will build out the basis of our character, inventory, and loadout screens. ```rs [src/player.rs] use crate::*; #[turbo::serialize] // add this above structs and enums // [!code ++] [!code focus] [!code hl] pub struct Player { // [!code ++] [!code focus] [!code hl] pub name: String, // [!code ++] [!code focus] [!code hl] pub max_hp: f32, // [!code ++] [!code focus] [!code hl] pub current_hp: f32, // [!code ++] [!code focus] [!code hl] pub atk: f32, // [!code ++] [!code focus] [!code hl] pub crit: f32, // [!code ++] [!code focus] [!code hl] pub inventory: Vec, // [!code ++] [!code focus] [!code hl] pub inventory_index: usize, // [!code ++] [!code focus] [!code hl] pub inventory_slots: usize, // [!code ++] [!code focus] [!code hl] pub loadout: Loadout, // [!code ++] [!code focus] [!code hl] pub loadout_index: usize, // [!code ++] [!code focus] [!code hl] pub loadout_slots: usize, // [!code ++] [!code focus] [!code hl] } // [!code ++] [!code focus] [!code hl] impl Player { // [!code ++] [!code focus] [!code hl] pub fn new() -> Self { // [!code ++] [!code focus] [!code hl] Self { // [!code ++] [!code focus] [!code hl] name: "Shell".to_string(), // [!code ++] [!code focus] [!code hl] max_hp: 100.0, // [!code ++] [!code focus] [!code hl] current_hp: 100.0, // [!code ++] [!code focus] [!code hl] atk: 1.0, // [!code ++] [!code focus] [!code hl] crit: 2.0, // [!code ++] [!code focus] [!code hl] inventory: vec![ // [!code ++] [!code focus] [!code hl] Item { // [!code ++] [!code focus] [!code hl] name: "HP Potion".to_string(), // [!code ++] [!code focus] [!code hl] description: "HP +50".to_string(), // [!code ++] [!code focus] [!code hl] quantity: 1, // [!code ++] [!code focus] [!code hl] } // [!code ++] [!code focus] [!code hl] ], // [!code ++] [!code focus] [!code hl] inventory_index: 0, // [!code ++] [!code focus] [!code hl] inventory_slots: 4, // [!code ++] [!code focus] [!code hl] loadout: Loadout { // [!code ++] [!code focus] [!code hl] weapon: Weapon::Sword, // [!code ++] [!code focus] [!code hl] passive: "ATK Up".to_string(), // [!code ++] [!code focus] [!code hl] }, // [!code ++] [!code focus] [!code hl] loadout_index: 0, // [!code ++] [!code focus] [!code hl] loadout_slots: 2, // [!code ++] [!code focus] [!code hl] } // [!code ++] [!code focus] [!code hl] } // [!code ++] [!code focus] [!code hl] } // [!code ++] [!code focus] [!code hl] #[turbo::serialize] // [!code ++] [!code focus] [!code hl] pub enum Weapon { // [!code ++] [!code focus] [!code hl] Sword, // [!code ++] [!code focus] [!code hl] Gun, // [!code ++] [!code focus] [!code hl] } // [!code ++] [!code focus] [!code hl] #[turbo::serialize] // [!code ++] [!code focus] [!code hl] pub struct Item { // [!code ++] [!code focus] [!code hl] pub name: String, // [!code ++] [!code focus] [!code hl] pub description: String, // [!code ++] [!code focus] [!code hl] pub quantity: u32, // [!code ++] [!code focus] [!code hl] } // [!code ++] [!code focus] [!code hl] #[turbo::serialize] // [!code ++] [!code focus] [!code hl] pub struct Loadout { // [!code ++] [!code focus] [!code hl] pub weapon: Weapon, // [!code ++] [!code focus] [!code hl] pub passive: String, // [!code ++] [!code focus] [!code hl] } // [!code ++] [!code focus] [!code hl] ``` By adding an `impl` for the `Player` we can initialize it in `GameState` with a `new()` function. We also have an `enum` for our `Weapon` and `structs` for the `Item` and `Loadout`. We'll be altering these values and adding more functions to our `impl` later! We also added in values like the index and slots for our `inventory` and `loadout` #### `lib.rs` & `GameState` Initialization Now we can head into `lib.rs` and add the following fields to `GameState` ```rs [src/lib.rs] use turbo::*; use state::*; mod state; use player::*; mod player; #[turbo::game] struct GameState { screen: Screen, // [!code ++] [!code focus] [!code hl] player: Player, // [!code ++] [!code focus] [!code hl] frames: u32, // [!code ++] [!code focus] [!code hl] spawned: bool, // [!code ++] [!code focus] [!code hl] equip: bool, // [!code ++] [!code focus] [!code hl] } impl GameState { pub fn new() -> Self { // initialize your game state Self { screen: Screen::Character, // [!code ++] [!code focus] [!code hl] player: Player::new(), // [!code ++] [!code focus] [!code hl] frames: 0, // [!code ++] [!code focus] [!code hl] spawned: false, // [!code ++] [!code focus] [!code hl] equip: false, // [!code ++] [!code focus] [!code hl] } } pub fn update(&mut self) { // This is where your main game loop code goes // The stuff in this block will run ~60x per sec } } ``` #### Adding in the State Machine Now we'll add an empty state machine to the `state.rs` file. We'll use it to change between screens. ```rs [src/state.rs] use crate::*; #[turbo::serialize] // add this above structs and enums pub enum Screen { Character, Inventory, Loadout, } pub fn state_machine(state: &mut GameState) { // [!code ++] [!code focus] [!code hl] match state.screen { // [!code ++] [!code focus] [!code hl] Screen::Character => { // [!code ++] [!code focus] [!code hl] } // [!code ++] [!code focus] [!code hl] Screen::Inventory => { // [!code ++] [!code focus] [!code hl] } // [!code ++] [!code focus] [!code hl] Screen::Loadout => { // [!code ++] [!code focus] [!code hl] } // [!code ++] [!code focus] [!code hl] } // [!code ++] [!code focus] [!code hl] } // [!code ++] [!code focus] [!code hl] ``` And then we'll call the `state_machine` function from our update loop, so that it is always running. ```rs [src/lib.rs] pub fn update(&mut self) { // This is where your main game loop code goes // The stuff in this block will run ~60x per sec state_machine(self); // [!code ++] [!code focus] [!code hl] } ``` Make sure to save your project after adding it in. :::tip[Development Tip] Saving your project is done with `Cmd+S` (MacOS) or `Ctrl+S` (Windows). If you ever need to reset your game, you can do that with `Cmd+R` (MacOS) or `Ctrl+R` (Windows). It is smart to save after every step! ::: #### Adding in our player sprite Now that we have the foundation, we can work on the visuals! Lets start by displaying our character in the middle of the screen and start creating their character screen Inside your project directory, create a folder named `sprites`. ``` your-project-dir/ # Your project's root directory. ├── sprites/ # The directory of your sprite assets. ├── src/ # The directory of your code. │ └── lib.rs # The main file for the game. | └── state.rs # The main file for your state machine. | └── player.rs # The main file for your player. ├── Cargo.toml # Rust project manifest. └── turbo.toml # Turbo configuration. ``` Add the following files to the `sprites` directory. * [x] [player.aseprite](https://raw.githubusercontent.com/Stophii/character-sheet/master/sprites/player.aseprite) * [x] [potion.aseprite](https://raw.githubusercontent.com/Stophii/character-sheet/master/sprites/potion.aseprite) :::tip[Development Tip] You can see the player sprite is a `.aseprite` file. [Aseprite](https://www.aseprite.org/) is a great tool for pixel art, and Turbo has a deep integration that lets you read animation tags straight from the files. You can read more about it [here.](https://docs.turbo.computer/learn/api/sprites) ::: #### Character Screen Lets display our player character and start building a stat screen around him in the `Screen::Character` portion of our `state_machine` We just need one line of code to draw the sprite. Make sure to save after this, and now you should see the character on screen. ```rs [src/state.rs] pub fn state_machine(state: &mut GameState) { match state.screen { Screen::Character => { sprite!("player#Idle"); // [!code ++] [!code focus] [!code hl] } Screen::Inventory => { } Screen::Loadout => { } } } ``` ![Character displayed in one line of code](/character-sheet.png) We want to add a little bit more though. Go ahead add the following and lets break it down: ```rs [src/state.rs] pub fn state_machine(state: &mut GameState) { match state.screen { Screen::Character => { state.frames += 1; // [!code ++] [!code focus] [!code hl] let character = animation::get("character"); // [!code ++] [!code focus] [!code hl] sprite!( // [!code ++] [!code focus] [!code hl] animation_key = "character", // [!code ++] [!code focus] [!code hl] default_sprite = "player#Idle", // [!code ++] [!code focus] [!code hl] ); // [!code ++] [!code focus] [!code hl] if !state.spawned { // [!code ++] [!code focus] [!code hl] character.use_sprite("player#Spawn"); // [!code ++] [!code focus] [!code hl] character.set_repeat(1); // [!code ++] [!code focus] [!code hl] state.spawned = true; // [!code ++] [!code focus] [!code hl] } // [!code ++] [!code focus] [!code hl] rect!(x = 140, y = 5, w = 100, h = 130, color = 0x000000ff, border_color = 0xffffffff, border_size = 1, border_radius = 2); // [!code ++] [!code focus] [!code hl] let stats = format!("{}'s stats\n\nHp: {}/{}\nAtk: {}\nCrit: {}%", state.player.name, state.player.max_hp, state.player.current_hp, state.player.atk, state.player.crit); // [!code ++] [!code focus] [!code hl] text_box!(&stats,x = 142, y = 6, w = 98, h = 129); // [!code ++] [!code focus] [!code hl] if state.frames >= 360 { // [!code ++] [!code focus] [!code hl] let choices = ["player#Jump", "player#Crouch", "player#Dash"]; // [!code ++] [!code focus] [!code hl] let idx = (random::u32() as usize) % choices.len(); // [!code ++] [!code focus] [!code hl] let anim = choices[idx]; // [!code ++] [!code focus] [!code hl] character.use_sprite(anim); // [!code ++] [!code focus] [!code hl] character.set_repeat(1); // [!code ++] [!code focus] [!code hl] state.frames = 0; // [!code ++] [!code focus] [!code hl] } // [!code ++] [!code focus] [!code hl] } Screen::Inventory => { } Screen::Loadout => { } } } ``` ![Character sheet with stats and animation loop every 6 seconds](/stepcharacter.gif) Nice, now we have our character screen set up! The first part of our code sets up our sprite animation. Then we check if we've `spawned` yet, and if not, we use the `player#Spawn` animation, and set it to repeat 1 time. After that we use the `format!()` macro to build out the text that will go in our text box. Using `format!()` lets us grab the values from our variables and represent them as text. Lastly, we made a system to change the character animation every 360 frames. Turbo's update loop runs at 60 frames per second, so 360 frames is 6 seconds. We put all the available animations into the `choices` array, and then choose one randomly and set that for our animation with `use_sprite`. Then we reset frames to 0 so we can start the count over again. #### Adding State Machine controls Lets build out the inventory screen next. Our first step needs to be adding functionality to switch screens. ```rs [src/lib.rs] pub fn update(&mut self) { // This is where your main game loop code goes // The stuff in this block will run ~60x per sec state_machine(self); if gamepad::get(0).a.just_pressed() { // [!code ++] [!code focus] [!code hl] self.screen = Screen::Inventory; // [!code ++] [!code focus] [!code hl] } // [!code ++] [!code focus] [!code hl] if gamepad::get(0).b.just_pressed() { // [!code ++] [!code focus] [!code hl] self.screen = Screen::Character; // [!code ++] [!code focus] [!code hl] } // [!code ++] [!code focus] [!code hl] if gamepad::get(0).x.just_pressed() { // [!code ++] [!code focus] [!code hl] self.screen = Screen::Loadout; // [!code ++] [!code focus] [!code hl] } // [!code ++] [!code focus] [!code hl] } ``` Now if we press `a` we can switch to the `Screen::Inventory` and if we press `b` we can switch back to `Screen::Character`. We added in pressing `x` to switch to `Screen::Loadout`. :::tip[Development Tip] Here is how the `gamepad` buttons map to the keyboard: | Gamepad | Keyboard (P1) | | ------- | ------------- | | a | `z` | | b | `x` | | x | `c` | | y | `v` | | start | `space` | | select | `enter` | Check out a full list in our [API](https://docs.turbo.computer/learn/api/gamepad) ::: #### Adding `Tweens` and `Const` If this is your first time seeing or hearing about `tweens`, awesome! `Tweens` allow for smooth transitions of `f32` (decimal) values. They're easy to implement, and great for making your game look polished! We need one `Tween` instance in the `GameState` for each value we want to be able to manipulate. In total we are going to add 8 tweens and initialize them, as well as a `Const` to the top. `Consts` are just values that don't change. ```rs [src/lib.rs] const POS: [(f32, f32); 4] = [(118.0,49.0), (154.0,34.0), (127.0,15.0), (101.0,34.0)]; // [!code ++] [!code focus] [!code hl] #[turbo::game] struct GameState { screen: Screen, player: Player, frames: u32, spawned: bool, // x value tweens // [!code ++] [!code focus] [!code hl] slot_one_x: Tween, // [!code ++] [!code focus] [!code hl] slot_two_x: Tween, // [!code ++] [!code focus] [!code hl] slot_three_x: Tween, // [!code ++] [!code focus] [!code hl] slot_four_x: Tween, // [!code ++] [!code focus] [!code hl] // y value tweens // [!code ++] [!code focus] [!code hl] slot_one_y: Tween, // [!code ++] [!code focus] [!code hl] slot_two_y: Tween, // [!code ++] [!code focus] [!code hl] slot_three_y: Tween, // [!code ++] [!code focus] [!code hl] slot_four_y: Tween, // [!code ++] [!code focus] [!code hl] } impl GameState { pub fn new() -> Self { // initialize your game state Self { screen: Screen::Character, player: Player::new(), frames: 0, spawned: false, // x value tweens // [!code ++] [!code focus] [!code hl] slot_one_x: Tween::new(118.0) // [!code ++] [!code focus] [!code hl] .ease(Easing::EaseOutCubic) // [!code ++] [!code focus] [!code hl] .duration(15), // [!code ++] [!code focus] [!code hl] slot_two_x: Tween::new(155.0) // [!code ++] [!code focus] [!code hl] .ease(Easing::EaseOutCubic) // [!code ++] [!code focus] [!code hl] .duration(15), // [!code ++] [!code focus] [!code hl] slot_three_x: Tween::new(128.0) // [!code ++] [!code focus] [!code hl] .ease(Easing::EaseOutCubic) // [!code ++] [!code focus] [!code hl] .duration(15), // [!code ++] [!code focus] [!code hl] slot_four_x: Tween::new(102.0) // [!code ++] [!code focus] [!code hl] .ease(Easing::EaseOutCubic) // [!code ++] [!code focus] [!code hl] .duration(15), // [!code ++] [!code focus] [!code hl] // y value tweens // [!code ++] [!code focus] [!code hl] slot_one_y: Tween::new(49.0) // [!code ++] [!code focus] [!code hl] .ease(Easing::EaseOutCubic) // [!code ++] [!code focus] [!code hl] .duration(15), // [!code ++] [!code focus] [!code hl] slot_two_y: Tween::new(35.0) // [!code ++] [!code focus] [!code hl] .ease(Easing::EaseOutCubic) // [!code ++] [!code focus] [!code hl] .duration(15), // [!code ++] [!code focus] [!code hl] slot_three_y: Tween::new(15.0) // [!code ++] [!code focus] [!code hl] .ease(Easing::EaseOutCubic) // [!code ++] [!code focus] [!code hl] .duration(15), // [!code ++] [!code focus] [!code hl] slot_four_y: Tween::new(35.0) // [!code ++] [!code focus] [!code hl] .ease(Easing::EaseOutCubic) // [!code ++] [!code focus] [!code hl] .duration(15), // [!code ++] [!code focus] [!code hl] } } pub fn update(&mut self) { // This is where your main game loop code goes // The stuff in this block will run ~60x per sec state_machine(self); if gamepad::get(0).a.just_pressed() { self.screen = Screen::Inventory; } if gamepad::get(0).b.just_pressed() { self.screen = Screen::Character; } if gamepad::get(0).x.just_pressed() { self.screen = Screen::Loadout; } } } ``` The `Tweens` will give our inventory screen a lot of personality. The `Const` represent where our inventory slots will be located on the canvas. Now we can build out that `Screen::Inventory` :::tip[Development Tip] [`Tweens`](https://docs.turbo.computer/learn/api/tween) need an initial starting value as `f32`, an `easing`, and a duration as `usize` to initialize. Feel free to tweak the `duration` and `easing` to suit your preference! ::: #### Inventory Screen Inside our state machine in `state.rs` add the following: ```rs [src/state.rs] Screen::Character => { camera::reset(); // [!code ++] [!code focus] [!code hl] state.frames += 1; let character = animation::get("character"); sprite!( animation_key = "character", default_sprite = "player#Idle", ); if !state.spawned { character.use_sprite("player#Spawn"); character.set_repeat(1); state.spawned = true; } rect!(x = 140, y = 5, w = 100, h = 130, color = 0x000000ff, border_color = 0xffffffff, border_size = 1, border_radius = 2); let stats = format!("{}'s stats\n\nHp: {}/{}\nAtk: {}\nCrit: {}%", state.player.name, state.player.current_hp, state.player.max_hp, state.player.atk, state.player.crit); text_box!(&stats,x = 142, y = 6, w = 98, h = 129); match state.player.loadout.weapon { Weapon::Sword => { state.player.atk = 6.0 } Weapon::Gun => { state.player.crit = 20.0} } if state.frames >= 360 { let choices = ["player#Jump", "player#Crouch", "player#Dash"]; let idx = (random::u32() as usize) % choices.len(); let anim = choices[idx]; character.use_sprite(anim); character.set_repeat(1); state.frames = 0; } } Screen::Inventory => { camera::set_xyz(110, 50, 2.0); // [!code ++] [!code focus] [!code hl] // define the slots and currently selected slot // [!code ++] [!code focus] [!code hl] let len = POS.len(); // [!code ++] [!code focus] [!code hl] let idx = state.player.inventory_index % len; // [!code ++] [!code focus] [!code hl] let p1 = POS[(0 + idx) % len]; // [!code ++] [!code focus] [!code hl] let p2 = POS[(1 + idx) % len]; // [!code ++] [!code focus] [!code hl] let p3 = POS[(2 + idx) % len]; // [!code ++] [!code focus] [!code hl] let p4 = POS[(3 + idx) % len]; // [!code ++] [!code focus] [!code hl] // simplify the .get() of tween // [!code ++] [!code focus] [!code hl] let one_x = state.slot_one_x.get(); // [!code ++] [!code focus] [!code hl] let one_y = state.slot_one_y.get(); // [!code ++] [!code focus] [!code hl] let two_x = state.slot_two_x.get(); // [!code ++] [!code focus] [!code hl] let two_y = state.slot_two_y.get(); // [!code ++] [!code focus] [!code hl] let three_x = state.slot_three_x.get(); // [!code ++] [!code focus] [!code hl] let three_y = state.slot_three_y.get(); // [!code ++] [!code focus] [!code hl] let four_x = state.slot_four_x.get(); // [!code ++] [!code focus] [!code hl] let four_y = state.slot_four_y.get(); // [!code ++] [!code focus] [!code hl] // pair the x and y into one slot // [!code ++] [!code focus] [!code hl] let slot_one = (one_x, one_y); // [!code ++] [!code focus] [!code hl] let slot_two = (two_x, two_y); // [!code ++] [!code focus] [!code hl] let slot_three = (three_x, three_y); // [!code ++] [!code focus] [!code hl] let slot_four = (four_x, four_y); // [!code ++] [!code focus] [!code hl] // alter the size of the square according to if it's in the "front" slot // [!code ++] [!code focus] [!code hl] let size_one = if state.player.inventory_index == 0 {(36, 36)} else {(17,17)}; // [!code ++] [!code focus] [!code hl] let size_two = if state.player.inventory_index == 3 {(36, 36)} else {(17,17)}; // [!code ++] [!code focus] [!code hl] let size_three = if state.player.inventory_index == 2 {(36, 36)} else {(17,17)}; // [!code ++] [!code focus] [!code hl] let size_four = if state.player.inventory_index == 1 {(36, 36)} else {(17,17)}; // [!code ++] [!code focus] [!code hl] // set the tween based on state.player.inventory_index // [!code ++] [!code focus] [!code hl] state.slot_one_x.set(p1.0); state.slot_one_y.set(p1.1); // [!code ++] [!code focus] [!code hl] state.slot_two_x.set(p2.0); state.slot_two_y.set(p2.1); // [!code ++] [!code focus] [!code hl] state.slot_three_x.set(p3.0); state.slot_three_y.set(p3.1); // [!code ++] [!code focus] [!code hl] state.slot_four_x.set(p4.0); state.slot_four_y.set(p4.1); // [!code ++] [!code focus] [!code hl] // draw it! // [!code ++] [!code focus] [!code hl] rect!(xy = slot_one, wh = size_one, color = 0xffffff77, border_radius = 4); // [!code ++] [!code focus] [!code hl] rect!(xy = slot_two, wh = size_two, color = 0xffffff44, border_radius = 4); // [!code ++] [!code focus] [!code hl] rect!(xy = slot_three, wh = size_three, color = 0xffffff44, border_radius = 4); // [!code ++] [!code focus] [!code hl] rect!(xy = slot_four, wh = size_four, color = 0xffffff44, border_radius = 4); // [!code ++] [!code focus] [!code hl] sprite!("potion#HP Charge", xy = if state.player.inventory_index == 0 {(slot_one.0 + 1.0, slot_one.1+ 1.0) } else { slot_one }, scale = if state.player.inventory_index == 0 {1.0} else {0.5}); // [!code ++] [!code focus] [!code hl] sprite!( // [!code ++] [!code focus] [!code hl] animation_key = "character", // [!code ++] [!code focus] [!code hl] default_sprite = "player#Idle", // [!code ++] [!code focus] [!code hl] ); // [!code ++] [!code focus] [!code hl] let debug = match state.player.inventory.get(state.player.inventory_index) { // [!code ++] [!code focus] [!code hl] Some(item) => format!("{} x{}\n{}", item.name, item.quantity, item.description), // [!code ++] [!code focus] [!code hl] None => "".to_string(), // [!code ++] [!code focus] [!code hl] }; // [!code ++] [!code focus] [!code hl] match state.player.inventory_index { // [!code ++] [!code focus] [!code hl] 0 => {text!(&debug, x = 47, y = 15)}, // [!code ++] [!code focus] [!code hl] _ => {text!("Empty slot...", x = 50, y = 15)}, // [!code ++] [!code focus] [!code hl] } // [!code ++] [!code focus] [!code hl] } ``` And add the controls to `lib.rs` ```rs [src/lib.rs] pub fn update(&mut self) { // This is where your main game loop code goes // The stuff in this block will run ~60x per sec state_machine(self); if gamepad::get(0).a.just_pressed() { self.screen = Screen::Inventory; } if gamepad::get(0).b.just_pressed() { self.screen = Screen::Character; } if gamepad::get(0).x.just_pressed() { self.screen = Screen::Loadout; } if gamepad::get(0).left.just_pressed() { // [!code ++] [!code focus] [!code hl] self.player.inventory_index = (self.player.inventory_index + self.player.inventory_slots - 1) % self.player.inventory_slots; // [!code ++] [!code focus] [!code hl] } // [!code ++] [!code focus] [!code hl] if gamepad::get(0).right.just_pressed() { // [!code ++] [!code focus] [!code hl] self.player.inventory_index = (self.player.inventory_index + 1) % self.player.inventory_slots; // [!code ++] [!code focus] [!code hl] } // [!code ++] [!code focus] [!code hl] } ``` ![Inventory screen completed with character and tweening](/stepinventory.gif) We make use of [`camera::set_xyz`](https://docs.turbo.computer/learn/api/camera) [`tween`](https://docs.turbo.computer/learn/api/tween) and of course [`sprite!`](https://docs.turbo.computer/learn/api/sprites) and [`rect!`](https://docs.turbo.computer/learn/api/rectangles) again. The `Tweens` allow for smooth movement of the `x` and `y` values of our squares which are our inventory slots. We also change the size of each square if it is in the front slot. We also change the size of the item. The controls change the value of `state.inventory_index` and we change the values of our `tweens` based on what that inventory index is. We have wrapping as well so we never hit an "end". Zooming in with the `camera` allows you to see our character's face and item sprites as the player looks through their inventory. It also makes the item descriptions and names larger and easier to view. Our main character's idle animation has them blink, which is a nice touch while browsing the inventory system. Next up is the `Screen::Loadout` #### Loadout Screen Add the following code inside the `Screen::Loadout` of our `state_machine` ```rs [src/state.rs] Screen::Loadout => { let character = animation::get("character"); // [!code ++] [!code focus] [!code hl] let equipcolorsword = if matches!(state.player.loadout.weapon, Weapon::Sword) { 0x00FFFFFF } else { 0xFFFFFFFF }; // [!code ++] [!code focus] [!code hl] let equipcolorgun = if matches!(state.player.loadout.weapon, Weapon::Gun) { 0x00FFFFFF } else { 0xFFFFFFFF }; // [!code ++] [!code focus] [!code hl] sprite!( // [!code ++] [!code focus] [!code hl] animation_key = "character", // [!code ++] [!code focus] [!code hl] default_sprite = "player#Idle", // [!code ++] [!code focus] [!code hl] ); // [!code ++] [!code focus] [!code hl] rect!(x = 140, y = 5, w = 100, h = 60, color = 0x000000ff, border_color = 0xffffffff, border_size = 1, border_radius = 2); // [!code ++] [!code focus] [!code hl] let stats = format!("{}'s loadout\n\nWpn: {:?}\n\nPassive: {}", state.player.name, state.player.loadout.weapon, state.player.loadout.passive); // [!code ++] [!code focus] [!code hl] text_box!(&stats,x = 142, y = 6, w = 98, h = 129); // [!code ++] [!code focus] [!code hl] let sword = Bounds::new(140, 80, 40, 30); // [!code ++] [!code focus] [!code hl] let gun = Bounds::new(200, 80, 40, 30); // [!code ++] [!code focus] [!code hl] rect!(bounds = sword, color = 0x000000ff, border_color = equipcolorsword, border_size = 1, border_radius = 2); // [!code ++] [!code focus] [!code hl] rect!(bounds = gun, color = 0x000000ff, border_color = equipcolorgun, border_size = 1, border_radius = 2); // [!code ++] [!code focus] [!code hl] text_box!("Sword", x = sword.x(), y = sword.y() + 10, w = sword.w(), align = "center"); // [!code ++] [!code focus] [!code hl] text_box!("Gun", x = gun.x(), y = gun.y() + 10, w = gun.w(), align = "center"); // [!code ++] [!code focus] [!code hl] if state.player.loadout_index == 1 { // [!code ++] [!code focus] [!code hl] rect!(x = 215, y = 67, w = 10, h = 10, rotation = 45); // [!code ++] [!code focus] [!code hl] } else { // [!code ++] [!code focus] [!code hl] rect!(x = 155, y = 67, w = 10, h = 10, rotation = 45); // [!code ++] [!code focus] [!code hl] } // [!code ++] [!code focus] [!code hl] if state.equip { // [!code ++] [!code focus] [!code hl] match state.player.loadout_index { // [!code ++] [!code focus] [!code hl] 0 => { // [!code ++] [!code focus] [!code hl] character.use_sprite("player#Sword"); // [!code ++] [!code focus] [!code hl] character.set_repeat(1); // [!code ++] [!code focus] [!code hl] state.equip = false; // [!code ++] [!code focus] [!code hl] state.player.loadout.weapon = Weapon::Sword; // [!code ++] [!code focus] [!code hl] state.player.loadout.passive = "ATK up".to_string(); // [!code ++] [!code focus] [!code hl] state.player.atk = 6.0; // [!code ++] [!code focus] [!code hl] state.player.crit = 2.0; // [!code ++] [!code focus] [!code hl] }, // [!code ++] [!code focus] [!code hl] 1 => { // [!code ++] [!code focus] [!code hl] character.use_sprite("player#Gun"); // [!code ++] [!code focus] [!code hl] character.set_repeat(1); // [!code ++] [!code focus] [!code hl] state.equip = false; // [!code ++] [!code focus] [!code hl] state.player.loadout.weapon = Weapon::Gun; // [!code ++] [!code focus] [!code hl] state.player.loadout.passive = "CRIT up".to_string(); // [!code ++] [!code focus] [!code hl] state.player.atk = 4.0; // [!code ++] [!code focus] [!code hl] state.player.crit = 20.0; // [!code ++] [!code focus] [!code hl] } // [!code ++] [!code focus] [!code hl] _ => { // [!code ++] [!code focus] [!code hl] // [!code ++] [!code focus] [!code hl] } // [!code ++] [!code focus] [!code hl] } // [!code ++] [!code focus] [!code hl] } // [!code ++] [!code focus] [!code hl] rect!(x = 140, y = 65, w = 100, h = 6, color = 0x000000ff); // [!code ++] [!code focus] [!code hl] } ``` Now we need to spruce up our controls in the `lib.rs`: ```rs [src/lib.rs] pub fn update(&mut self) { // This is where your main game loop code goes // The stuff in this block will run ~60x per sec state_machine(self); match self.screen { // [!code ++] [!code focus] [!code hl] Screen::Character => { // [!code ++] [!code focus] [!code hl] if gamepad::get(0).a.just_pressed() { // [!code ++] [!code focus] [!code hl] self.screen = Screen::Inventory; // [!code ++] [!code focus] [!code hl] } // [!code ++] [!code focus] [!code hl] if gamepad::get(0).x.just_pressed() { // [!code ++] [!code focus] [!code hl] self.screen = Screen::Loadout; // [!code ++] [!code focus] [!code hl] } // [!code ++] [!code focus] [!code hl] }, // [!code ++] [!code focus] [!code hl] Screen::Inventory => { // [!code ++] [!code focus] [!code hl] if gamepad::get(0).left.just_pressed() { // [!code ++] [!code focus] [!code hl] self.player.inventory_index = (self.player.inventory_index + self.player.inventory_slots - 1) % self.player.inventory_slots; // [!code ++] [!code focus] [!code hl] } // [!code ++] [!code focus] [!code hl] // [!code ++] [!code focus] [!code hl] if gamepad::get(0).right.just_pressed() { // [!code ++] [!code focus] [!code hl] self.player.inventory_index = (self.player.inventory_index + 1) % self.player.inventory_slots; // [!code ++] [!code focus] [!code hl] } // [!code ++] [!code focus] [!code hl] // [!code ++] [!code focus] [!code hl] if gamepad::get(0).b.just_pressed() { // [!code ++] [!code focus] [!code hl] self.screen = Screen::Character; // [!code ++] [!code focus] [!code hl] } // [!code ++] [!code focus] [!code hl] }, // [!code ++] [!code focus] [!code hl] Screen::Loadout => { // [!code ++] [!code focus] [!code hl] if gamepad::get(0).right.just_pressed() { // [!code ++] [!code focus] [!code hl] self.player.loadout_index = (self.player.loadout_index + self.player.loadout_slots - 1) % self.player.loadout_slots; // [!code ++] [!code focus] [!code hl] } // [!code ++] [!code focus] [!code hl] // [!code ++] [!code focus] [!code hl] if gamepad::get(0).left.just_pressed() { // [!code ++] [!code focus] [!code hl] self.player.loadout_index = (self.player.loadout_index + 1) % self.player.loadout_slots; // [!code ++] [!code focus] [!code hl] } // [!code ++] [!code focus] [!code hl] // [!code ++] [!code focus] [!code hl] if gamepad::get(0).a.just_pressed() { // [!code ++] [!code focus] [!code hl] self.equip = true; // [!code ++] [!code focus] [!code hl] } // [!code ++] [!code focus] [!code hl] // [!code ++] [!code focus] [!code hl] if gamepad::get(0).b.just_pressed() { // [!code ++] [!code focus] [!code hl] self.screen = Screen::Character; // [!code ++] [!code focus] [!code hl] } // [!code ++] [!code focus] [!code hl] } // [!code ++] [!code focus] [!code hl] } // [!code ++] [!code focus] [!code hl] } ``` ![Loadout screen completed with character playing animation specific to weapon selected](/steploadout.gif) Awesome! Now we have controls that are specific to each state. We can enter our `Screen::Inventory` with `a` and we can leave with `b`. We can enter our `Screen::Loadout` with `x` and leave that screen with `b`. On the loadout screen itself we can use the arrow keys to switch between sword and gun and equip it with `a`. After equipping our weapon of choice we get an animation to reflect what we have equipped as well as a blue highlight. We made use of `bounds` for the sword and gun UI choices. `Bounds` is a useful tool when making your game work on multiple resolutions as well as adding touch/tap controls! ### Conclusion You have made a working character sheet! you used a lot of tools that will be valuable to master like the `state machine`, `tweening`, and `bounds`. You used the basics like `rect!` `sprite!` and `text_box!`. You can use this as the start of an RPG game! If you want to do more, I suggest the following: :::warning[Quest] * [ ] Add your own sprites to change the main character * [ ] Fill out the rest of the inventory with items instead of empty slots * [ ] Add another weapon option * [ ] Tween the size of the sprites in the inventory menu * [ ] Add indicators for controls * [ ] Add a title screen! ::: ### Next Steps * Complete the `Quest` above, post your results in the [Turbo discord](https://discord.com/invite/makegamesfast)! * Try your hand at the [pancake cat tutorial](/learn/tutorials/pancake-cat) :::: ## Hello, World! \[Your very own Turbo legend is about to unfold] ![Turbo game window with the text "Hello, world!!!"](/hello-world.png) ### Overview :::info[Summary] > In this tutorial, you will learn the basics of Turbo game development by building a simple game that does only one thing – displays a message saying, "Hello, world!!!". **Difficulty** > ★☆☆☆☆ **Time Estimate** > \< 5 minutes **What You'll Learn** * [x] How to initialize a Turbo project * [x] How to run and auto-refresh your game * [x] How to modify in-game text * [x] Basics of positioning and styling text with Turbo macros ::: ### Walkthrough ::::steps #### Initialize the Project Begin by creating a new project called `hello-world`: ```bash [Terminal] turbo init hello-world ``` This initializes a Rust project with the following structure: ``` hello-world/ # Your project's root directory. ├── src/ # The directory of your code. │ └── lib.rs # The main file for the game. ├── Cargo.toml # Rust project manifest. └── turbo.toml # Turbo configuration. ``` #### Run the Game Next, run your game with the following command: ```bash [Terminal] turbo run -w hello-world ``` :::note The `-w` flag auto-refreshes your game window as you code. Just be sure to watch the console for compiler errors. ::: Give it a moment to compile. Once it completes, a game window will appear. ![Turbo game window with the text "Hello, world!!!"](/hello-world.png) By default, a new Turbo game will have some boilerplate code that displays, "Hello, world!!!". So our work here is done. Just kidding. We're going to jazz it up a bit. Continue on to the next step. #### Update the Text Leave the `turbo run` command running. Open the `hello-world` project in your preferred editor. View `hello-world/src/lib.rs`. Inside, you should see something like this: ```rust title="hello-world/src/lib.rs" showLineNumbers use turbo::*; #[turbo::game] struct GameState { // Add fields here } impl GameState { pub fn new() -> Self { // initialize your game state Self { } } pub fn update(&mut self) { // This is where your main game loop code goes // The stuff in this block will run ~60x per sec text!("Hello, world!!!"); } } ``` Modify the text, save the file, and check out your game window. ```rust title="hello-world/src/lib.rs" showLineNumbers use turbo::*; #[turbo::game] struct GameState { // Add fields here } impl GameState { pub fn new() -> Self { // initialize your game state Self { } } pub fn update(&mut self) { // [!code focus] // This is where your main game loop code goes [!code focus] // The stuff in this block will run ~60x per sec [!code focus] text!("Hello, world!!!") // [!code --] [!code focus] text!("Yuuurrr!"); // [!code ++] [!code focus] [!code hl] } // [!code focus] } ``` It should now display your updated text. ![Turbo game window with the text "yuuurrr!!!"](/yuuurrr.png) #### Add Some Style The `text!` macro has several optional parameters you can experiment with: ```rust title="hello-world/src/lib.rs" showLineNumbers use turbo::*; #[turbo::game] struct GameState { // Add fields here } impl GameState { pub fn new() -> Self { // initialize your game state Self { } } pub fn update(&mut self) { // [!code focus] // This is where your main game loop code goes [!code focus] // The stuff in this block will run ~60x per sec [!code focus] text!("Yuuurrr!"); // [!code --] [!code focus] [!code hl] text!( // [!code ++] [!code focus] [!code hl] "Let's gooo!", // Text to display [!code ++] [!code focus] [!code hl] font = "large", // Font ("small", "medium", and "large" are available by default) [!code ++] [!code focus] [!code hl] color = 0xff00ffff, // Text color (in hexadecimal) // [!code ++] [!code focus] [!code hl] x = 32, // Starting x position of the text // [!code ++] [!code focus] [!code hl] y = 48, // Starting y position of the text // [!code ++] [!code focus] [!code hl] ); // [!code ++] [!code focus] [!code hl] } // [!code focus] } ``` Check out the [Text docs](/learn/api/text) to see more examples of how to work with text in Turbo. :::: ### Conclusion You've taken your first step into the world of Turbo — initialized a project, launched a live-updating game window, and styled your very first in-game message. It might not seem like much, but this is how every great game begins: with one line of code and the will to make something fun. ### Next Steps From here, you're ready to: * Build your first interactive game * Explore sprites, input, and sound * Learn how game state works in Turbo Your very own Turbo legend *has* begun — let's keep going. ## Pancake Cat \[Eat pancakes, become a legend.] ![Turbo game window with an orange cat head and falling pancakes](/pancake-cat-screenshot.png) ### Overview :::info[Summary] > In this tutorial, you will build Pancake Cat — a fast-paced game where you control a hungry orange cat that catches falling pancakes. The more pancakes you eat, the higher your score. **Difficulty** > ★★☆☆☆ **Time Estimate** > \~20 minutes **What You'll Learn** * [x] How to initialize and run a Turbo project * [x] How to define and update game state * [x] How to render sprites and handle input * [x] How to implement collision detection ::: ### Walkthrough :::tip[Development Tip] The full source code of this game is [available on Github](https://github.com/super-turbo-society/turbo-demos/tree/main/pancake-cat) ::: ::::steps #### Initialize the Project Begin by creating a new project called `pancake-cat`: ```bash [Terminal] turbo init pancake-cat ``` This initializes a Rust project with the following structure: ``` pancake-cat/ # Your project's root directory. ├── src/ # The directory of your code. │ └── lib.rs # The main file for the game. ├── Cargo.toml # Rust project manifest. └── turbo.toml # Turbo configuration. ``` #### Create a `sprites` Folder Inside your project directory, create a folder named `sprites`. This folder will contain all your game sprites. ``` your-project-dir/ # Your project's root directory. ├── sprites/ # The directory of your sprite assets. ├── src/ # The directory of your code. │ └── lib.rs # The main file for the game. ├── Cargo.toml # Rust project manifest. └── turbo.toml # Turbo configuration. ``` #### Add Sprite Assets Add the following files to the `sprites` directory. * [x] [munch\_cat.webp](https://raw.githubusercontent.com/super-turbo-society/turbo-demos/refs/heads/main/pancake-cat/sprites/munch_cat.webp) * [x] [heart.png](https://raw.githubusercontent.com/super-turbo-society/turbo-demos/refs/heads/main/pancake-cat/sprites/heart.png) #### Run the Game At this point, we can run our game and leave it running as we make changes. Don't worry, it is just a blank screen for now! ```bash [Terminal] turbo run -w pancake-cat ``` #### Game State Initialization Add this code to the top of your `lib.rs` file. We'll explain what everything does later on: ```rs [src/lib.rs] use turbo::*; #[turbo::serialize] struct Pancake { x: f32, y: f32, vel: f32, radius: f32, } #[turbo::game] struct GameState { frame: u32, last_munch_at: u32, cat_x: f32, cat_y: f32, cat_r: f32, pancakes: Vec, score: u32, } impl GameState { fn new() -> Self { Self { frame: 0, last_munch_at: 0, cat_x: 128.0, cat_y: 112.0, cat_r: 8.0, pancakes: vec![], score: 0, } } fn update(&mut self) { // we will come back to this later. } } ``` #### Update the Game Loop Use the Turbo game loop to run your game logic. :::tip[Development Tip] `update()` runs 60 times per second, and is repsonsible for updating all changes to your game state. ::: #### Drawing the Cat Render the cat sprite with animation frame logic handled by Turbo: ```rs [src/lib.rs] sprite!("munch_cat", x = self.cat_x - self.cat_r, y = self.cat_y - 16.0); ``` #### Cat Movement The first thing to do is handle player input to move the cat. Add this code in the update() function: ```rs [src/lib.rs] if gamepad::get(0).left.pressed() { self.cat_x -= 2.; } if gamepad::get(0).right.pressed() { self.cat_x += 2.; } ``` Now click over to your game window and press left and right on your keyboard. You should see the cat moving around! #### Pancake Generation Randomly spawn pancakes: ```rs [src/lib.rs] if random::rand() % 64 == 0 { let pancake = Pancake { x: (rand() % 256) as f32, y: 0.0, vel: (rand() % 3 + 1) as f32, radius: (rand() % 10 + 5) as f32, }; self.pancakes.push(pancake); } ``` :::tip[Development Tip] We defined the `Pancake` struct in our `new` function earlier. Now we are adding individual instances of `Pancake` to the game. Each `Pancake` has a position (`x` and `y`), a velocity (`vel`), and a size (`radius`). We use randomness so the Pancakes are all a little different. We also created a `Vec` in our `new` function. Once we create the new `Pancake` we add it to that `Vec` so that we can track it in our `GameState`. ::: #### Pancake Movement & Collision Now we can add some code that moves the Pancakes downwards, and checks if they are overlapping the cat. When they overlap, we add 1 to `self.score`. ```rs [src/lib.rs] let cat_center = (self.cat_x + self.cat_r, self.cat_y + self.cat_r); self.pancakes.retain_mut(|p| { p.y += p.vel; let dx = cat_center.0 - (p.x + p.radius); let dy = cat_center.1 - (p.y + p.radius); let distance = (dx * dx + dy * dy).sqrt(); let radii_sum = self.cat_r + p.radius; let radii_diff = (self.cat_r - p.radius).abs(); if radii_diff <= distance && distance <= radii_sum { self.score += 1; self.last_munch_at = self.frame; false } else if p.y < 144. + (p.radius * 2.) { true } else { false } }); ``` :::tip[Development Tip] `retain_mut` is a method in Rust that lets you look through and mutate every element in a `Vec` and only **retain** the ones that pass a certain condition. Any Pancakes that return `false` are removed from the `Vec`. In this case, we are removing any Pancakes that overlap the Cat, and also removing the ones that are below the bottom of the screen. The rest of the Pancakes will remain on screen, to give the player a chance to catch them. ::: #### Background Tiles Draw a simple animated background: ```rs [src/lib.rs] clear(0x00ffffff); let frame = (self.frame as i32) / 2; for col in 0..9 { for row in 0..6 { let x = ((col * 32 + frame) % (272 + 16)) - 32; let y = ((row * 32 + frame) % (144 + 16)) - 24; sprite!("heart", x = x, y = y); } } self.frame += 1; ``` #### Drawing the Cat Render the cat sprite with animation frame logic handled by Turbo: ```rs [src/lib.rs] sprite!("munch_cat", x = self.cat_x - self.cat_r, y = self.cat_y - 16.0); ``` #### Drawing the Pancakes Draw each Pancake by making 3 concentric circles: ```rs [src/lib.rs] for p in &self.pancakes { circ!(x = p.x, y = p.y + 1.0, d = p.radius + 2., color = 0x000000aa); circ!(x = p.x, y = p.y, d = p.radius + 1., color = 0xf4d29cff); circ!(x = p.x, y = p.y, d = p.radius, color = 0xdba463ff); } ``` #### Speech Bubble Display a speech bubble with “MUNCH!” when the cat catches a pancake: ```rs [src/lib.rs] if self.frame >= 64 && self.frame.saturating_sub(self.last_munch_at) <= 60 { rect!(w = 30, h = 10, x = self.cat_x + 32.0, y = self.cat_y); circ!(d = 10, x = self.cat_x + 28.0, y = self.cat_y); rect!(w = 10, h = 5, x = self.cat_x + 28.0, y = self.cat_y + 5.0); circ!(d = 10, x = self.cat_x + 56.0, y = self.cat_y); text!("MUNCH!", x = self.cat_x + 33.0, y = self.cat_y + 3.0, font = "small", color = 0x000000ff); } ``` #### Score Display Draw the player's score in the top-left corner: ```rs [src/lib.rs] text!("Score: {}", self.score; x = 10, y = 10, font = "large", color = 0xffffffff); ``` :::tip[Development Tip] Save your code, and then click over to your Turbo game window to see your changes! If you want to reset your score, press `ctrl+R` on Windows or `cmd+R` on Mac. That will restart your game from the initial state. ::: :::: ### Conclusion You just built a fully playable game from scratch using Turbo: input handling, game state, rendering, and collision — all in a tight feedback loop with real-time updates. You've met the cat. You’ve caught the pancakes. And you’ve laid the foundation for every game you’ll make next. ### Next Steps * Swap out hearts or pancakes for your own sprite sets * Add [`sounds`](/learn/api/audio) when the cat munches or misses * Track lives or missed pancakes for added challenge * Create a title screen and end state * Export your game to the web using [`turbo export`](/learn/guides/cli) import Snippet from './_template.mdx' ## Space Shooter \[Manuever in zero G, shoot the baddies.] ![Turbo game window with the finished Space Shooter game in action.](/space-shooter-gameplay.gif) ### Overview :::info[Summary] > In this tutorial, you will build a retro Space Shooter game! **Difficulty** > ★★★☆☆ **Time Estimate** > \~40 minutes **What You'll Learn** * [x] Organize large projects with multiple files * [x] Define game structs and manage them through `GameState` * [x] Iterate through lists to draw sprites * [x] Check for collisions ::: ### Walkthrough :::tip[Development Tip] The full source code of this game is [available on Github](https://github.com/super-turbo-society/turbo-demos/tree/main/space-shooter) ::: ::::steps #### Initialize the Project Begin by creating a new project called `space-shooter`: ```bash [Terminal] turbo init space-shooter ``` This initializes a Rust project with the following structure: ``` space-shooter/ # Your project's root directory. ├── src/ # The directory of your code. │ └── lib.rs # The main file for the game. ├── Cargo.toml # Rust project manifest. └── turbo.toml # Turbo configuration. ``` #### Setup the `turbo.toml` With the project initialized, open the `turbo.toml` and change the `height` of the `[canvas]` property. This will adjust the aspect ratio and resolution of the game. ```rs [turbo.toml] [canvas] width = 256 height = 144 // [!code focus] [!code hl] [!code --] height = 512 // [!code focus] [!code hl] [!code ++] ``` You can also change the `description` and `authors` properties while here. #### Add Sprite and Audio Assets Inside your project directory, create a folder named `sprites` and a folder named `audio`. These folders will contain all of the game's sprites and audio files. ``` space-shooter/ # Your project's root directory. ├── audio/ # The directory of your audio assets. ├── sprites/ # The directory of your sprite assets. ├── src/ # The directory of your code. │ └── lib.rs # The main file for the game. ├── Cargo.toml # Rust project manifest. └── turbo.toml # Turbo configuration. ``` Add the following files to the `audio` folder: * [x] [projectile\_player.wav](https://github.com/super-turbo-society/turbo-demos/raw/main/space-shooter/audio/projectile_player.wav) * [x] [projectile\_enemy.wav](https://github.com/super-turbo-society/turbo-demos/raw/main/space-shooter/audio/projectile_enemy.wav) * [x] [projectile\_hit.wav](https://github.com/super-turbo-society/turbo-demos/raw/main/space-shooter/audio/projectile_hit.wav) Add the following files to the `sprites` folder: > Player Sprites > > * [x] [player.gif](https://github.com/super-turbo-society/turbo-demos/raw/main/space-shooter/sprites/player.gif) > * [x] [player\_shooting.gif](https://github.com/super-turbo-society/turbo-demos/raw/main/space-shooter/sprites/player_shooting.gif) > * [x] [projectile\_player.gif](https://github.com/super-turbo-society/turbo-demos/raw/main/space-shooter/sprites/projectile_player.gif) > * [x] [projectile\_player\_hit.gif](https://github.com/super-turbo-society/turbo-demos/raw/main/space-shooter/sprites/projectile_player_hit.gif) > > Enemy sprites > > * [x] [turret.gif](https://github.com/super-turbo-society/turbo-demos/raw/main/space-shooter/sprites/turret.gif) > * [x] [projectile\_enemy.gif](https://github.com/super-turbo-society/turbo-demos/raw/main/space-shooter/sprites/projectile_enemy.gif) > * [x] [projectile\_enemy\_hit.gif](https://github.com/super-turbo-society/turbo-demos/raw/main/space-shooter/sprites/projectile_enemy_hit.gif) :::tip[Development Tip] You may add sub-folders to keep your `audio` or `sprites` directories more organized. If you do, you must specify the file path starting from the root folder when referencing them e.g `"player/player_shooting"`. ::: :::tip[Development Tip] At this point, we can run our game and leave it running as we make changes. Don't worry, it is just a blank screen for now! ```bash [Terminal] turbo run -w space-shooter ``` ::: #### Game State Initialization Add this code to the top of your `lib.rs` file. ```rs [src/lib.rs] use turbo::*; #[turbo::game] struct GameState { } impl GameState { fn new() -> Self { Self { } } fn update(&mut self) { self.draw(); } fn draw(&self) { } } ``` :::tip[Development Tip] `#[turbo::game]` and the `GameState` struct are what run the game and keep track of everything. We store data that needs to be persistent between frames in the `GameState` struct, such as the player, enemies and projectiles. Then, we update and draw our stored data in the `update()` function. ::: We haven't added any structs or data yet, but we will soon. All we did so far is set up a `draw()` function to keep our drawing and updating logic separate. This helps keep our code organized as we start adding more complex logic. #### Set Up Project Structure To manage the size of this project, we will utilize sub files to organize our code. Sub files must be accessible throughout the project, so we will set up a model for files to reference each other. Create a new folder named `model` inside the `src/` folder in your project directory and create two files inside it, `player.rs` and `mod.rs`. ``` space-shooter/ # Your project's root directory. ├── audio/ # The directory of your audio assets. ├── sprites/ # The directory of your sprite assets. ├── src/ # The directory of your code. │ ├── model/ # The directory for sub files. │ │ └── mod.rs # A reference to all sub files in this folder. │ │ └── player.rs # A sub file to contain the Player struct. │ └── lib.rs # The main file for the game. ``` At the top of the `lib.rs` file, add the following lines: ```rs [src/lib.rs] use turbo::*; mod model; // [!code focus] [!code hl] [!code ++] pub use model::*; // [!code focus] [!code hl] [!code ++] ``` This code allows `lib.rs` to reference any files `mod.rs` references. Add the following code to the `mod.rs` file: ```rs [src/model/mod.rs] use super::*; mod player; pub use player::*; ``` This code allows `mod.rs` to reference both `lib.rs` and `player.rs`, in turn allowing `lib.rs` to access `player.rs` as well. Whenever we create a new subfile we have to add `mod` and `use` statements to `mod.rs`. Finally, add the following line to the top of `player.rs` ```rs [src/model/player.rs] use super::*; ``` This lets the subfile reference everything defined in `mod.rs`, including definitions in `lib.rs` like `GameState`, because `mod.rs` includes a `use super::*` as well. Now that the project is set up, we can start defining our game structs! #### Player We'll start by creating a `Player` Struct and adding some variables to update and draw it. Copy the following code into the `player.rs` file: ```rs [src/model/player.rs] use super::*; #[turbo::serialize] pub struct Player { pub hitbox: Bounds, x: f32, y: f32, dx: f32, // dx and dy used for velocity dy: f32, pub hp: u32, pub hit_timer: u32, // used for invincibility frames and drawing shoot_timer: u32, // used for rate of fire shooting: bool, // used for shooting animation // variables used by the HUD to display information pub score: u32, } impl Player { pub fn new() -> Self { let x = ((screen().w() / 2) - 8) as f32; let y = (screen().h() - 64) as f32; Player { // Initialize all fields with default values hitbox: Bounds::new(x, y, 16, 16), x, y, dx: 0.0, dy: 0.0, hp: 3, hit_timer: 0, shoot_timer: 0, shooting: false, score: 0, } } pub fn update(&mut self) { } pub fn draw(&self) { } } ``` These are all the variables we need to track the player as they move and shoot onscreen. :::tip[Development Tip] `Bounds` is a Turbo struct that represents a rectangle with variables `x`, `y`, `w`, and `h`. It has many useful functions, one of which we'll be using to check collisions between hitboxes. You can learn more about them in the [`Bounds`](/learn/api/bounds) documentation. ::: We also outline `new()`, `update()` and `draw()` functions. These will be common functions across our game structs. We call `new()` when we want to create a new instance of this struct, in this case during initialization of the `GameState`, and call `update()` and `draw()` once per frame in the `GameState` `update()` scope. #### Updating and Drawing the Player Next, we will add some code to the `update()` and `draw()` functions of the `Player` struct. This code will handle player movement and rendering the player sprite. Replace the empty functions in the `player.rs` file with the following functions: ```rs [src/model/player.rs] pub fn update(&mut self) { if self.hp > 0 { // Player movement let deceleration = 0.9; // Adjust this value to control deceleration speed self.dx *= deceleration; // Reduce xy delta by deceleration factor self.dy *= deceleration; // Record keyboard input let mut x_input = 0.0; let mut y_input = 0.0; if gamepad::get(0).up.pressed() { y_input = -1.0; } if gamepad::get(0).down.pressed() { y_input = 1.0; } if gamepad::get(0).left.pressed() { x_input = -1.0; } if gamepad::get(0).right.pressed() { x_input = 1.0; } // Apply input to dx and dy, normalizing diagonal movement let magnitude = ((x_input * x_input + y_input * y_input) as f32).sqrt(); if x_input != 0.0 { self.dx = x_input / magnitude; } if y_input != 0.0 { self.dy = y_input / magnitude; } let speed = 2.0; self.x = (self.x + self.dx * speed) // Translate position by input delta multiplied by speed .clamp(0.0, (screen().w() - self.hitbox.w() - 2) as f32); // Clamp to screen bounds self.y = (self.y + self.dy * speed) .clamp(0.0, (screen().h() - self.hitbox.h() - 2) as f32); // Set hitbox position based on float xy values self.hitbox = self.hitbox.position(self.x, self.y); } } ``` :::info[Code Breakdown] * Only update this scope when `hp` is above 0 * Capture keyboard input with `x_input` and `y_input` * Set `dx` and `dy` to input values while normalizing diagonal values * Translate stored `x` and `y` positions by `dx` and `dy` multiplied by a `speed` value * Clamp `x` `y` positions to screen bounds * Set `hitbox` position for collision checking * Before these steps, deaccelerate `dx` and `dy` every frame ::: ```rs [src/model/player.rs] pub fn draw(&self) { if self.hp > 0 { // Get reference to SpriteAnimation for player let anim = animation::get("p_key"); // Begin to construct the string for which sprite to use let mut sprite = "player".to_string(); // Assign the sprite string to the SpriteAnimation anim.use_sprite(&sprite); sprite!( animation_key = "p_key", x = self.x, y = self.y, ); } } ``` :::info[Code Breakdown] * Only update this scope when `hp` is above 0 * Create a local `SpriteAnimation` variable, `anim`, accessed using the key string `"p_key"` * Construct a string for `anim` to use, setting which file in the `sprites` folder it will draw * Draw a `sprite!()`, passing the same key used to set up the `SpriteAnimation`, `"p_key"`. ::: :::tip[Development Tip] Using `SpriteAnimation` is a powerful way to manage animated sprites in Turbo. We will be using them to seamlessly swap sprites, track animation states, and control looping. You can learn more about them in the [`Sprites`](/learn/api/sprites) documentation. ::: Great! The `Player` struct is now ready to be used in the game. Our final step before seeing it in action is to return to our `lib.rs` file to add a `player` variable to the `GameState` struct and call these functions we've just defined! ```rs [src/lib.rs] use turbo::*; mod model; pub use model::*; #[turbo::game] struct GameState { player: Player, // [!code focus] [!code hl] [!code ++] } impl GameState { fn new() -> Self { Self { player: Player::new(), // [!code focus] [!code hl] [!code ++] } } fn update(&mut self) { self.draw(); self.player.update(); // [!code focus] [!code hl] [!code ++] } fn draw(&self) { self.player.draw(); // [!code focus] [!code hl] [!code ++] } } ``` After saving all your changes, return to the Turbo game window to fly your player ship around! Use the arrow keys to move. Try changing the `speed` and `deceleration` values to find what feels best. ![Player flying around empty game scene](/space-shooter-movement.gif) #### Projectiles Now that we have a start on our player controller, let's add projectiles for it to shoot! We'll start by creating a new file in our `model` folder called `projectile.rs` and reference it in `mod.rs`. ``` space-shooter/ # Your project's root directory. ├── audio/ # The directory of your audio assets. ├── sprites/ # The directory of your sprite assets. ├── src/ # The directory of your code. │ ├── model/ # The directory for sub files. │ │ └── mod.rs # A reference to all sub files in this folder. │ │ └── player.rs # A sub file to contain the Player struct. │ │ └── projectile.rs # A sub file to contain the Projectile struct. │ └── lib.rs # The main file for the game. ``` ```rs [src/model/mod.rs] use super::*; mod player; pub use player::*; mod projectile; // [!code focus] [!code hl] [!code ++] pub use projectile::*; // [!code focus] [!code hl] [!code ++] ``` Copy the following code into the new `projectile.rs` file: ```rs [src/model/projectile.rs] use super::*; // Enum to determine who fired the projectile #[turbo::serialize] #[derive(PartialEq)] pub enum ProjectileOwner { Enemy, Player, } #[turbo::serialize] pub struct Projectile { pub hitbox: Bounds, x: f32, y: f32, // use f32s to track xy positions for more precise movement pub velocity: f32, angle: f32, anim_key: String, // unique, randomly generated key to be used for SpriteAnimations pub collided: bool, // Used to control the sprite and update state pub destroyed: bool, // Used to remove projectile from game pub damage: u32, pub projectile_owner: ProjectileOwner, } impl Projectile { pub fn new(x: f32, y: f32, velocity: f32, angle: f32, projectile_owner: ProjectileOwner) -> Self { let audio = match projectile_owner { ProjectileOwner::Enemy => "projectile_enemy", ProjectileOwner::Player => "projectile_player", }; audio::play(audio); Projectile { // Initialize all fields with default values hitbox: Bounds::new(x, y, 6, 6), x, y, velocity, angle, anim_key: random::u32().to_string(), destroyed: false, collided: false, damage: 1, projectile_owner, } } pub fn update(&mut self, player: &mut Player) { } pub fn draw(&self) { } } ``` Once again, we outline all the variables we will need to update and draw our projectiles. :::info[Code Breakdown] * Just like the player, `x`, `y`, and `hitbox` variables are used for movement and collisions, plus `velocity` and `angle` * `anim_key` is a unique string randomly generated on initialization, so every instance of `Projectile` can reference unique `SpriteAnimation`s * bools `destroyed` and `collided` will manage the projectile's state; how it updates, its animation state, and removal from the game * `damage` and `projectile_owner` will determine how projectiles interact with other entities in the game ::: :::tip[Development Tip] An `enum` is a way to define a type that can be one of several different variants. In this case, we use the `ProjectileOwner` enum to determine if a projectile was fired by the player or an enemy. We will be using enums for many things in this game, such as powerups and enemy types. ::: Unlike the `Player`, when we create a `new()` `Projectile` we call `audio::play()` to play a sound effect, determined by the `projectile_owner`. :::tip[Development Tip] `audio` is a straightforward module that will play audio files we store in our `audio` folder. Learn about its functions in the [`Audio`](/learn/api/audio) documentation. ::: #### Updating and Drawing Projectiles Now we can fill out our `update()` and `draw()` functions for the `Projectile` struct like we did for `Player`: ```rs [src/model/projectile.rs] pub fn update(&mut self, player: &mut Player) { // If the projectile hasn't collided, update it as normal if !self.collided { // update projectile position let radian_angle = self.angle.to_radians(); self.x += self.velocity * radian_angle.cos(); self.y += self.velocity * radian_angle.sin(); // flag the projectile to be destroyed if it goes off screen if self.y < -(self.hitbox.h() as f32) && self.x < -(self.hitbox.w() as f32) && self.x > screen().w() as f32 && self.y > screen().h() as f32 { self.destroyed = true; } // if the projectile has collided, } else { // get reference to the SpriteAnimation of the projectile let anim = animation::get(&self.anim_key); // flag projectile as destroyed when the hit animation is done if anim.done() { self.destroyed = true; } } // Set hitbox position based on float xy values self.hitbox = self.hitbox.position(self.x, self.y); } ``` :::info[Code Breakdown] * Check if it has collided and update contextually * If it hasn't collided, update its `x` and `y` position based on its `velocity` and `angle`, destroying it if it goes off screen * If it has collided, check if its hit animation is done, then flag it to be destroyed * Set `hitbox` position for collision checking ::: ```rs [src/model/projectile.rs] pub fn draw(&self) { // Get reference to SpriteAnimation for projectile let anim = animation::get(&self.anim_key); // Begin to construct the string for which sprite to use let owner = match self.projectile_owner { ProjectileOwner::Enemy => "enemy", ProjectileOwner::Player => "player", }; if !self.collided { anim.use_sprite(&format!("projectile_{}", owner)); } else { anim.use_sprite(&format!("projectile_{}_hit", owner)); anim.set_repeat(0); anim.set_fill_forwards(true); } sprite!( animation_key = &self.anim_key, x = self.x, y = self.y ); } ``` :::info[Code Breakdown] * Like the `Player`, create a local `SpriteAnimation` variable, `anim`, accessed using the key string `anim_key` * Construct a string for `anim` to use, setting which file in the `sprites` folder it will draw * This string is determined by `projectile_owner` and `collided` * If `collided`, set the animation to not repeat and to hold on the last frame when done in order to only play the hit animation once * Draw a `sprite!()`, passing the same key used to set up the `SpriteAnimation`, `anim_key` ::: #### Integrate Projectiles into the Game Now that the `Projectile` struct is ready, we can integrate it into our game! First, return to `lib.rs` and create a `projectiles` variable in the `GameState` struct. Then, add a loop for the `GameState` to update and draw all projectiles in the `update()` function. ```rs [src/lib.rs] use turbo::*; mod model; pub use model::*; #[turbo::game] struct GameState { player: Player, projectiles: Vec, // [!code focus] [!code hl] [!code ++] } impl GameState { fn new() -> Self { Self { player: Player::new(), projectiles: vec![], // [!code focus] [!code hl] [!code ++] } } fn update(&mut self) { self.draw(); self.player.update(); self.projectiles.retain_mut(|projectile| { // [!code focus] [!code hl] [!code ++] projectile.update(&mut self.player); // [!code focus] [!code hl] [!code ++] !projectile.destroyed // [!code focus] [!code hl] [!code ++] }); // [!code focus] [!code hl] [!code ++] } fn draw(&self) { self.player.draw(); for projectile in self.projectiles.iter() { // [!code focus] [!code hl] [!code ++] projectile.draw(); // [!code focus] [!code hl] [!code ++] } // [!code focus] [!code hl] [!code ++] } } ``` :::tip[Development Tip] `retain_mut()` is a method in Rust that lets you look through and mutate every element in a `Vec` and only **retain** the ones that pass a certain condition. Any `Projectile`s that return `false` in this scope are removed from the `Vec`. In this case, we are removing any `Projectile`s that have been flagged as `destroyed`. ::: The `GameState` now has a `vec`, or list, of projectiles. Every projectile we create in the game will be added to this list so that the `GameState` can then update and draw them. But, we still need to add projectiles to this list! Time to return to our `Player` struct and add shooting functionality. Add a parameter to the `update()` function and paste the following code at the end: ```rs [src/model/player.rs] pub fn update(&mut self, projectiles: &mut Vec) { // [!code focus] [!code hl] [!code ++] ... // Set hitbox position based on float xy values self.hitbox = self.hitbox.position(self.x, self.y); // Shooting projectiles // [!code focus] [!code hl] [!code ++] // check if shoot button is pressed // [!code focus] [!code hl] [!code ++] if gamepad::get(0).start.pressed() || gamepad::get(0).a.pressed() { // [!code focus] [!code hl] [!code ++] self.shooting = true; // flag shooting state for animation // [!code focus] [!code hl] [!code ++] // if shoot timer is 0, shoot a projectile // [!code focus] [!code hl] [!code ++] if self.shoot_timer == 0 { // [!code focus] [!code hl] [!code ++] let fire_rate = 15; // [!code focus] [!code hl] [!code ++] self.shoot_timer += fire_rate; // add cooldown to shoot timer // [!code focus] [!code hl] [!code ++] let projectile_speed = 5.0; // [!code focus] [!code hl] [!code ++] for i in 0..=1 { // [!code focus] [!code hl] [!code ++] projectiles.push( // [!code focus] [!code hl] [!code ++] Projectile::new( // [!code focus] [!code hl] [!code ++] self.x + i as f32 * 13.0, // [!code focus] [!code hl] [!code ++] self.y - 8.0, // [!code focus] [!code hl] [!code ++] projectile_speed, // [!code focus] [!code hl] [!code ++] -90.0, // [!code focus] [!code hl] [!code ++] ProjectileOwner::Player, // [!code focus] [!code hl] [!code ++] ) // [!code focus] [!code hl] [!code ++] ); // [!code focus] [!code hl] [!code ++] } // [!code focus] [!code hl] [!code ++] } // [!code focus] [!code hl] [!code ++] // if not shooting // [!code focus] [!code hl] [!code ++] } else { // [!code focus] [!code hl] [!code ++] self.shooting = false; // flag shooting state for animation // [!code focus] [!code hl] [!code ++] } // [!code focus] [!code hl] [!code ++] // decrement shoot timer // [!code focus] [!code hl] [!code ++] self.shoot_timer = self.shoot_timer.saturating_sub(1); // [!code focus] [!code hl] [!code ++] } } ``` :::info[Code Breakdown] * Add a `&mut Vec` parameter to the function so the player can mutate the projectile list * Check if the shoot button is pressed on keyboard, and set `shooting` bool for animation state * Using `shoot_timer`, check if enough time has elapsed to shoot, then add a rate of fire cooldown * Use a `for` loop to `push()`, or add, two `new()` projectiles to the `projectiles` vec, constructed with the player's position * Decrease `shoot_timer` every frame ::: While we're editing the `Player` struct, let's modify its draw function to swap sprites when shooting: ```rs [src/model/player.rs] pub fn draw(&self) { // Get reference to SpriteAnimation for player let anim = animation::get("p_key"); // Begin to construct the string for which sprite to use let mut sprite = "player".to_string(); if self.shooting { // [!code focus] [!code hl] [!code ++] sprite.push_str("_shooting"); // [!code focus] [!code hl] [!code ++] } // [!code focus] [!code hl] [!code ++] // Assign the sprite string to the SpriteAnimation anim.use_sprite(&sprite); // Draw sprite sprite!( animation_key = "p_key", x = self.x, y = self.y, ); } ``` Now, if the `shooting` bool is true, we add `"_shooting"` to the end of our `sprite` string, making it `"player_shooting"`. Finally, we need to update the call to the `Player`'s `update()` function in `lib.rs` in order to pass our projectile list: ```rs [src/lib.rs] self.player.update() // [!code focus] [!code hl] [!code --] self.player.update(&mut self.projectiles); // [!code focus] [!code hl] [!code ++] ``` Save all your work, and you should now be able to shoot projectiles in the game window by pressing Space or Z! ![Player shooting projectiles](/space-shooter-projectiles.gif) Play around with the values for `fire_speed` and `projectile_speed` to give the game your own spin. #### Enemies Now that we have a framework for combat, it's time to add enemies! Start once again by creating a new file in our `model` folder called `enemy.rs` and reference it in `mod.rs`. ``` space-shooter/ # Your project's root directory. ├── audio/ # The directory of your audio assets. ├── sprites/ # The directory of your sprite assets. ├── src/ # The directory of your code. │ ├── model/ # The directory for sub files. │ │ └── enemy.rs # A sub file to contain the Enemy struct. │ │ └── mod.rs # A reference to all sub files in this folder. │ │ └── player.rs # A sub file to contain the Player struct. │ │ └── projectile.rs # A sub file to contain the Projectile struct. │ └── lib.rs # The main file for the game. ``` ```rs [src/model/mod.rs] use super::*; mod enemy; // [!code focus] [!code hl] [!code ++] pub use enemy::*; // [!code focus] [!code hl] [!code ++] mod player; pub use player::*; mod projectile; pub use projectile::*; ``` Copy the following code into the new `enemy.rs` file: ```rs [src/model/enemy.rs] use super::*; // Different types of enemies #[turbo::serialize] #[derive(PartialEq)] pub enum EnemyType { Meteor, Shooter, Tank, Turret, Zipper, } #[turbo::serialize] // Struct for Enemies pub struct Enemy { enemy_type: EnemyType, pub hitbox: Bounds, x: f32, y: f32, pub angle: f32, pub hp: u32, hit_timer: u32, // used for hit animation pub destroyed: bool, } impl Enemy { // Initialize different enemy types with different properties pub fn new(enemy_type: EnemyType) -> Self { let (x,y) = ((random::u32() % screen().w()).saturating_sub(32) as f32, -32.0); // Set initial properties based on enemy type Self { enemy_type: enemy_type, hitbox: Bounds::new(x, y, 16, 16), x, y, angle: 0.0, hp: 8, hit_timer: 0, destroyed: false, } } pub fn update(&mut self, projectiles: &mut Vec) { } pub fn draw(&self) { } } ``` Again, we outline the struct for updating and drawing. This time, we add some logic to the `new()` function to randomly position enemies when we spawn them. And we include a `destroyed` bool, like we do with projectiles. The `EnemyType` `enum` will be used to add variety to our enemies in the future. For now, we will only implement one simple enemy, the `Turret` variant. #### Updating and Drawing Enemies Same as before, fill out the blank `update()` and `draw()` functions: ```rs [src/model/enemy.rs] pub fn update(&mut self, projectiles: &mut Vec) { // Move down let speed = 0.5; self.y += speed; // Random chance to fire projectile if random::u32() % 250 == 0 { // Create and shoot projectiles from enemy towards the player projectiles.push(Projectile::new( self.x + (self.hitbox.w() as f32 * 0.5) - (self.hitbox.w() as f32 * 0.5), self.y + (self.hitbox.h() as f32), 2.5, 90.0, ProjectileOwner::Enemy, )); } // Flag to destroy if moved offscreen if self.y > (screen().h() + self.hitbox.h()) as f32 { self.destroyed = true; } // Set hitbox position based on float xy values self.hitbox = self.hitbox.position(self.x, self.y); } ``` :::info[Code Breakdown] * Simply translate `y` position to move the enemy down * Random chance to fire projectile, adding a `new()` projectile to the `projectiles` list like the `Player` does * Set `projectile_owner` to `Enemy` and change the angle so it travels downwards * Flag to destroy if moved outside of screen bounds * Set `hitbox` position for collision checking ::: ```rs [src/model/enemy.rs] pub fn draw(&self) { // Construct the string for which sprite to use let sprite = match self.enemy_type { EnemyType::Tank => "tank", EnemyType::Shooter => "shooter", EnemyType::Turret => "turret", EnemyType::Zipper => "zipper", EnemyType::Meteor => "meteor", }; // Draw sprite sprite!( &sprite, x = self.hitbox.x(), y = self.hitbox.y(), ); } ``` :::info[Code Breakdown] * Unlike our other structs, we don't use `SpriteAnimation` because our enemy sprites aren't animated * Construct a string and use it to draw a `sprite!()` * We set up logic for using sprites based on `EnemyType` even though we're only implementing `Turret` ::: #### Integrate Enemies into the Game Now we have an `Enemy` struct that updates and draws, so let's add some logic to the `GameState` to spawn them on an interval and call those functions. First, add new properties to the `GameState`, then define a functions to spawn enemies. Then, call the spawning function, as well as updating and drawing all spawned enemies. ```rs [src/lib.rs] use turbo::*; mod model; pub use model::*; #[turbo::game] struct GameState { player: Player, enemies: Vec, // [!code focus] [!code hl] [!code ++] projectiles: Vec, } impl GameState { fn new() -> Self { Self { player: Player::new(), enemies: vec![], // [!code focus] [!code hl] [!code ++] projectiles: vec![], } } fn update(&mut self) { self.draw(); self.player.update(&mut self.projectiles); self.spawn_enemies(); // [!code focus] [!code hl] [!code ++] self.enemies.retain_mut(|enemy| { // [!code focus] [!code hl] [!code ++] enemy.update(&mut self.projectiles); // [!code focus] [!code hl] [!code ++] !enemy.destroyed // [!code focus] [!code hl] [!code ++] }); // [!code focus] [!code hl] [!code ++] self.projectiles.retain_mut(|projectile| { projectile.update(); !projectile.destroyed }); } fn draw(&self) { self.player.draw(); for enemy in self.enemies.iter() { // [!code focus] [!code hl] [!code ++] enemy.draw(); // [!code focus] [!code hl] [!code ++] } // [!code focus] [!code hl] [!code ++] for projectile in self.projectiles.iter() { projectile.draw(); } } fn spawn_enemies(&mut self) { // [!code focus] [!code hl] [!code ++] let spawn_rate = 100; // [!code focus] [!code hl] [!code ++] // Spawn a new enemy if the tick is a multiple of the spawn rate and there are less than 24 enemies already spawned // [!code focus] [!code hl] [!code ++] if time::tick() % spawn_rate == 0 && self.enemies.len() < 24 { // [!code focus] [!code hl] [!code ++] // Spawn a new Turret enemy // [!code focus] [!code hl] [!code ++] self.enemies.push( // [!code focus] [!code hl] [!code ++] Enemy::new(EnemyType::Turret) // [!code focus] [!code hl] [!code ++] ); // [!code focus] [!code hl] [!code ++] } // [!code focus] [!code hl] [!code ++] } // [!code focus] [!code hl] [!code ++] } ``` Save all your work, and you should see a swarm of enemies flooding your game window! :::tip[Development Tip] A little noisy, huh? We'll be adding a state manager later on so the game doesn't just start right away. But if you want peace and quiet until then you can comment out the `audio::play()` function in the `new()` function in `projectile.rs`! ::: The last step to finishing our combat system is to check for collisions between ships and projectiles, and manage their hit points! #### Collisions and HP Before we register collisions between ships and projectiles, we'll define damage functions for the `Player` and `Enemy` structs. Add these functions inside of the `impl` scope for each struct: ```rs [src/model/player.rs] pub fn take_damage(&mut self, damage: u32) { self.hp = self.hp.saturating_sub(damage); // reduce HP by damage amount camera::shake(5.0); // camera shake self.hit_timer = 30; // invincibility frame timer and drawing flag } ``` ```rs [src/model/enemy.rs] pub fn take_damage(&mut self, player: &mut Player, damage: u32) { // reduce HP by damage amount and set hit timer for hit effect self.hp = self.hp.saturating_sub(damage); self.hit_timer = 15; // frames to show hit effect // Destroy enemy and increase player score if hp is 0 if self.hp == 0 { self.destroyed = true; player.score += 20; } } ``` Also add these lines to the end of the `Player` `update()` and `Enemy` `update()` functions: ```rs [src/model/player.rs] self.hit_timer = self.hit_timer.saturating_sub(1); // Remove the camera shake if self.hit_timer == 0 { camera::remove_shake(); } ``` ```rs [src/model/enemy.rs] self.hit_timer = self.hit_timer.saturating_sub(1); ``` These functions will decrease hp values for both `Player` and `Enemy` when called. `Player` will shake the camera, while `Enemy` will flag `destroyed` and increase the `Player`'s score. Both structs have a `hit_timer` variable we will use for animating a damaged state, which needs to be decreased every frame. :::tip[Development Tip] The `camera` module is an easy way to control the viewport of your game. Our game's camera is static so we aren't using it much, but the built-in camera shake adds juice! Learn more about it in the [`Camera`](/learn/api/camera) documentation. ::: Now we can expand our `Projectile` struct. Add some logic to check for collisions, and call these damage functions: ```rs [src/model/projectile.rs] pub fn update(&mut self, player: &mut Player, enemies: &mut Vec) { // [!code focus] [!code hl] [!code ++] // If the projectile hasn't collided, update it as normal if !self.collided { // update projectile position let radian_angle = self.angle.to_radians(); self.x += self.velocity * radian_angle.cos(); self.y += self.velocity * radian_angle.sin(); // flag the projectile to be destroyed if it goes off screen if self.y < -(self.hitbox.h() as f32) && self.x < -(self.hitbox.w() as f32) && self.x > screen().w() as f32 && self.y > screen().h() as f32 { self.destroyed = true; } // Checking for collisions with player or enemies based on projectile owner // [!code focus] [!code hl] [!code ++] match self.projectile_owner { // [!code focus] [!code hl] [!code ++] // Check collision with player // [!code focus] [!code hl] [!code ++] ProjectileOwner::Enemy => { // [!code focus] [!code hl] [!code ++] if self.hitbox.intersects(&player.hitbox) // [!code focus] [!code hl] [!code ++] && player.hp > 0 // [!code focus] [!code hl] [!code ++] && player.hit_timer == 0 { // player doesn't have i-frames [!code focus] [!code hl] [!code ++] player.take_damage(self.damage); // [!code focus] [!code hl] [!code ++] audio::play("projectile_hit"); // [!code focus] [!code hl] [!code ++] self.collided = true; // [!code focus] [!code hl] [!code ++] } // [!code focus] [!code hl] [!code ++] } // [!code focus] [!code hl] [!code ++] // Check collision with enemies // [!code focus] [!code hl] [!code ++] ProjectileOwner::Player => { // [!code focus] [!code hl] [!code ++] for enemy in enemies.iter_mut() { // [!code focus] [!code hl] [!code ++] if self.hitbox.intersects(&enemy.hitbox) && !enemy.destroyed { // [!code focus] [!code hl] [!code ++] enemy.take_damage(player, self.damage); // [!code focus] [!code hl] [!code ++] // [!code focus] [!code hl] [!code ++] audio::play("projectile_hit"); // [!code focus] [!code hl] [!code ++] self.collided = true; // [!code focus] [!code hl] [!code ++] break; // Exit loop after first collision // [!code focus] [!code hl] [!code ++] } // [!code focus] [!code hl] [!code ++] } // [!code focus] [!code hl] [!code ++] } // [!code focus] [!code hl] [!code ++] } // [!code focus] [!code hl] [!code ++] // if the projectile has collided, } else { // get reference to the SpriteAnimation of the projectile let anim = animation::get(&self.anim_key); // flag projectile as destroyed when the hit animation is done if anim.done() { self.destroyed = true; } } // Set hitbox position based on float xy values self.hitbox = self.hitbox.position(self.x, self.y); } ``` :::info[Code Breakdown] * First, determine if this projectile should check collisions with `Player` or `Enemy` based on its `projectile_owner` * If it should collide with the player, check if it has invincibility frames or is already at 0 HP * If it should collide with an enemy, check if it is already `destroyed` * In either case, check if the projectile's hitbox overlaps with the ship's hitbox using `bounds::intersects()` * If a collision is registered: * Set `collided` to true for animation state transition * Use `audio::play()` to play a hit sound effect * Call `take_damage()` on either `Player` or `Enemy` :::
And for our last step, return to `lib.rs` to update the call to our projectiles' `update()` function: ```rs [src/lib.rs] fn update(&mut self) { self.draw(); self.player.update(&mut self.projectiles); self.spawn_enemies(); self.enemies.retain_mut(|enemy| { enemy.update(&mut self.projectiles); !enemy.destroyed }); self.projectiles.retain_mut(|projectile| { projectile.update(&mut self.player, &mut self.enemies); // [!code focus] [!code hl] [!code ++] !projectile.destroyed }); } ``` Save everything, and click back into your game window. Move around, shoot some enemies, and take some damage. :::tip[Development Tip] Now that the Player can die and we haven't programmed an in-game reset, you can hot reload to play again! Hit `ctrl+R` or `cmd+R` at anytime while playing to reset the `GameState`! ::: ![Completed Game Loop!](/space-shooter-part-1.gif) #### Game State Machine Now that our core gameplay is running, let's add some logic to set up a start and end, and a way to replay the game. Start by creating a new `enum` in `lib.rs` and properties in `GameState`: ```rs [src/lib.rs] use turbo::*; mod model; pub use model::*; #[turbo::serialize] // [!code focus] [!code hl] [!code ++] #[derive(PartialEq)] // [!code focus] [!code hl] [!code ++] enum Screen { // [!code focus] [!code hl] [!code ++] Menu, // [!code focus] [!code hl] [!code ++] Game, // [!code focus] [!code hl] [!code ++] } // [!code focus] [!code hl] [!code ++] #[turbo::game] struct GameState { screen: Screen, // [!code focus] [!code hl] [!code ++] start_tick: usize, // [!code focus] [!code hl] [!code ++] player: Player, enemies: Vec, projectiles: Vec, } impl GameState { fn new() -> Self { Self { screen: Screen::Menu, // [!code focus] [!code hl] [!code ++] start_tick: 0, // [!code focus] [!code hl] [!code ++] player: Player::new(), enemies: vec![], projectiles: vec![], } } ... ``` We'll use this enum to manage what we call in our `update()` scope, and the `tick` keeps track of when the game started for future timing logic: ```rs [src/lib.rs] fn update(&mut self) { self.draw(); // Menu [!code focus] [!code hl] [!code ++] if self.screen == Screen::Menu { // [!code focus] [!code hl] [!code ++] if gamepad::get(0).start.just_pressed() || gamepad::get(0).a.just_pressed() { // [!code focus] [!code hl] [!code ++] self.screen = Screen::Game; // transition scene [!code focus] [!code hl] [!code ++] self.start_tick = time::tick(); // [!code focus] [!code hl] [!code ++] } // [!code focus] [!code hl] [!code ++] // Game [!code focus] [!code hl] [!code ++] } else { // [!code focus] [!code hl] [!code ++] self.player.update(&mut self.projectiles); self.spawn_enemies(); self.enemies.retain_mut(|enemy| { enemy.update(&mut self.projectiles); !enemy.destroyed }); self.projectiles.retain_mut(|projectile| { projectile.update(&mut self.player, &mut self.enemies); !projectile.destroyed }); // Game Over [!code focus] [!code hl] [!code ++] if self.player.hp == 0 { // [!code focus] [!code hl] [!code ++] if gamepad::get(0).start.just_pressed() || gamepad::get(0).a.just_pressed() { // [!code focus] [!code hl] [!code ++] *self = GameState::new(); // [!code focus] [!code hl] [!code ++] } // [!code focus] [!code hl] [!code ++] } // [!code focus] [!code hl] [!code ++] } // [!code focus] [!code hl] [!code ++] } ``` :::info[Code Breakdown] * Check what `Screen` the `GameState` is currently in * If `Menu`, wait for user input then change `screen` to `Game` * If `Game`, do our update logic as we had it before * In the `Game` scope, check if the player is below 0 hp * If so, wait for user input to reset the `GameState` ::: Now our game has a simple, built-in loop. Let's just add some text to show the player what's happening. Add this function to the `impl` scope of `GameState`: ```rs [src/lib.rs] fn draw_screen(&self) { // If in menu or game over state if self.screen == Screen::Menu || self.player.hp == 0 { // Determine title string let title = if self.screen == Screen::Menu { "SPACE SHOOTER" } else { "GAME OVER" }; // Draw title text!( &title, x = (screen().w() as i32 / 2) - (title.chars().count() as i32 * 4), y = (screen().h() as i32 / 2) - 16, font = "large" ); // Draw prompt to start game, blinking every half second if time::tick() / 4 % 8 < 4 { text!( "Press A or Start", x = (screen().w() as i32 / 2) - 38, y = (screen().h() as i32 / 2) + 8, ); } } } ``` :::info[Code Breakdown] * Only update this scope when `screen` is `Menu` or the player's hp is 0 * Determine the `title` string by which of the above is true * Draw `text!()` for the title and input prompt * The input prompt blinks based on the game tick ::: And lastly, call the new function at the end of `GameState`'s `draw()` function: ```rs [src/lib.rs] fn draw(&self) { self.player.draw(); for projectile in self.projectiles.iter() { projectile.draw(); } for enemy in self.enemies.iter() { enemy.draw(); } self.draw_screen(); // [!code focus] [!code hl] [!code ++] } ``` #### Displaying Game Info Next, let's give the player some info about their status by adding a simple heads up display! We'll create a UI to display the player's HP, score, and notifications. Before we get to drawing, add some properties in the `Player` struct to store notifications: ```rs [src/model/player] #[turbo::serialize] pub struct Player { pub hitbox: Bounds, x: f32, y: f32, dx: f32, // dx and dy used for velocity dy: f32, pub hp: u32, pub hit_timer: u32, // used for invincibility frames and drawing shoot_timer: u32, // used for rate of fire shooting: bool, // used for shooting animation // variables used by the HUD to display information pub score: u32, notifications: Vec, // [!code focus] [!code hl] [!code ++] notification_timer: usize, // [!code focus] [!code hl] [!code ++] } impl Player { pub fn new() -> Self { let x = ((screen().w() / 2) - 8) as f32; let y = (screen().h() - 64) as f32; Player { // Initialize all fields with default values hitbox: Bounds::new(x, y, 16, 16), x, y, dx: 0.0, dy: 0.0, hp: 3, hit_timer: 0, shoot_timer: 0, shooting: false, score: 0, notifications: vec![ // [!code focus] [!code hl] [!code ++] "Use arrow keys to move.".to_string(), // [!code focus] [!code hl] [!code ++] "Press SPACE or A to shoot.".to_string(), // [!code focus] [!code hl] [!code ++] "Defeat enemies and collect powerups.".to_string(), // [!code focus] [!code hl] [!code ++] "Try to not die. Good luck!".to_string(), // [!code focus] [!code hl] [!code ++] ], // [!code focus] [!code hl] [!code ++] notification_timer: 0, // [!code focus] [!code hl] [!code ++] } } ``` This `vec` and `usize` timer will keep track of the notifications we'll display to the player. The list will act as a queue, where the first element is displayed, and the timer will increment whenever the queue is not empty. Add the following lines to the end of the `Player` `update()` function: ```rs [src/model/player] pub fn update(&mut self, projectiles: &mut Vec) { ... self.hit_timer = self.hit_timer.saturating_sub(1); // Remove the camera shake if self.hit_timer == 0 { camera::remove_shake(); } // Notifications timer // [!code focus] [!code hl] [!code ++] if self.notifications.len() > 0 { // [!code focus] [!code hl] [!code ++] self.notification_timer += 1; // [!code focus] [!code hl] [!code ++] // Remove current notification if timer expires // [!code focus] [!code hl] [!code ++] if self.notification_timer >= 120 - 1 { // [!code focus] [!code hl] [!code ++] self.notification_timer = 0; // [!code focus] [!code hl] [!code ++] let _ = self.notifications.remove(0); // [!code focus] [!code hl] [!code ++] } // [!code focus] [!code hl] [!code ++] } // [!code focus] [!code hl] [!code ++] } ``` This scope runs when there are notifications to display, increasing the notifications timer, resetting it and removing the first displayed notification from the list. We aren't going to send any notifications in this tutorial, but you could use them when significant events happen in the game, like if the player finds a new power up or gets an achievement. Now we can create a function to draw all of our heads up display information! Add the following function to the `impl` scope of the `Player` struct: ```rs [src/model/player] pub fn draw_hud(&self) { let hud_height = 16; // Height of the HUD panel let hud_padding = 4; // Padding inside the HUD // Background rectangle rect!( x = -1, y = -1, w = screen().w() + 2, h = hud_height + 2, border_size = 1, color = 0x000000ff, border_color = 0xffffffff, ); // Display Health let health_text = format!("HP: {}", self.hp); text!( &health_text, x = hud_padding, y = hud_padding, font = "large", color = 0xffffffff ); // Display Score let score_text = format!("SCORE: {:0>5}", self.score); let score_text_x = // anchor to the right side of the screen screen().w() as i32 - (score_text.chars().count() as i32 * 8) - hud_padding; text!( &score_text, x = score_text_x, y = hud_padding, font = "large", color = 0xffffffff ); // Draw notifications for notif in self.notifications.iter() { // center the text based on width of characters let len = notif.chars().count(); let w = len * 5; let x = (screen().w() as usize / 2) - (w / 2); rect!( x = x as i32 - 4, y = 24 - 2, w = w as u32 + 4, h = 12, color = 0x5fcde4ff ); text!( ¬if, x = x as i32, y = 24, font = "medium", color = 0xffffffff ); break; } } ``` This function simply uses `rect!()`s and `text!()`s with some math for centering to display information already stored in the `Player` struct. So now that we have all our info in one place, we can call our new function in the `GameState`'s `draw()` function: ```rs [src/lib.rs] fn draw(&self) { self.player.draw(); for projectile in self.projectiles.iter() { projectile.draw(); } for enemy in self.enemies.iter() { enemy.draw(); } self.player.draw_hud(); // [!code focus] [!code hl] [!code ++] self.draw_screen(); } ``` Save your files and check your game window for a more informed playthrough! :::: ### Conclusion Wow. After all that code, you have a playable game loop with a solid foundation. Great work! We created `Player`, `Enemy`, and `Projectile` structs, are updating and drawing them using the `GameState`, checking for collisions between them all, and organized 400+ lines of code into easily readable sub files! ### Next Steps * Add more enemy types. Give them new sprites and movement patterns. * Create power-ups for the player to collect * Change the `speed`, `fire_rate` or `enemy.hp` to personalize the game * Use `turbo -export` to export your game and [host it on the web](/learn/guides/web-publishing) ## Space Shooter (Part 2) \[Manuever in zero G, shoot the baddies.] ![Turbo game window with the finished Space Shooter game in action.](/space-shooter-gameplay.gif) ### Overview :::info[Summary] > In Part 2 of our Space Shooter tutorial, we'll expand on Part 1 by adding enemy types, powerups, and a UI. **Difficulty** > ★★★★☆ **Time Estimate** > \~1 hour **What You'll Learn** * Manage game state with state machine * Advanced `enum`s with type properties ::: ### Walkthrough :::tip[Development Tip] The full source code of this game is [available on Github](https://github.com/super-turbo-society/turbo-demos/tree/main/space-shooter). If you haven't completed [Part 1](/learn/tutorials/space-shooter-part-1), we recommend you start there! ::: ::::steps #### Game State Machine As promised in the last part, let's start by adding some logic to delay the start of the game until the player inputs, as well as adding an in-game reset. Start by creating a new enum in `lib.rs` and properties in `GameState`: ```rs [src/lib.rs] use turbo::*; mod model; pub use model::*; #[turbo::serialize] // [!code focus] [!code hl] [!code ++] #[derive(PartialEq)] // [!code focus] [!code hl] [!code ++] enum Screen { // [!code focus] [!code hl] [!code ++] Menu, // [!code focus] [!code hl] [!code ++] Game, // [!code focus] [!code hl] [!code ++] } // [!code focus] [!code hl] [!code ++] #[turbo::game] struct GameState { screen: Screen, // [!code focus] [!code hl] [!code ++] start_tick: usize, // [!code focus] [!code hl] [!code ++] player: Player, enemies: Vec, projectiles: Vec, } impl GameState { fn new() -> Self { Self { screen: Screen::Menu, // [!code focus] [!code hl] [!code ++] start_tick: 0, // [!code focus] [!code hl] [!code ++] player: Player::new(), enemies: vec![], projectiles: vec![], } } ... ``` We'll use this enum to manage what we call in our `update()` scope, and record the `tick` the game started for future timing logic: ```rs [src/lib.rs] fn update(&mut self) { self.draw(); // Menu [!code focus] [!code hl] [!code ++] if self.screen == Screen::Menu { // [!code focus] [!code hl] [!code ++] if gamepad::get(0).start.just_pressed() || gamepad::get(0).a.just_pressed() { // [!code focus] [!code hl] [!code ++] self.screen = Screen::Game; // transition scene [!code focus] [!code hl] [!code ++] self.start_tick = time::tick(); // [!code focus] [!code hl] [!code ++] } // [!code focus] [!code hl] [!code ++] // Game [!code focus] [!code hl] [!code ++] } else { // [!code focus] [!code hl] [!code ++] self.player.update(&mut self.projectiles); self.spawn_enemies(); self.enemies.retain_mut(|enemy| { enemy.update(&mut self.projectiles); !enemy.destroyed }); self.projectiles.retain_mut(|projectile| { projectile.update(&mut self.player, &mut self.enemies); !projectile.destroyed }); // Game Over [!code focus] [!code hl] [!code ++] if self.player.hp == 0 { // [!code focus] [!code hl] [!code ++] if gamepad::get(0).start.just_pressed() || gamepad::get(0).a.just_pressed() { // [!code focus] [!code hl] [!code ++] *self = GameState::new(); // [!code focus] [!code hl] [!code ++] } // [!code focus] [!code hl] [!code ++] } // [!code focus] [!code hl] [!code ++] } // [!code focus] [!code hl] [!code ++] } ``` :::info[Code Breakdown] * Check what `Screen` the `GameState` is currently in * If `Menu`, wait for user input then change `screen` to `Game` * If `Game`, do our update logic as we had it before * In the `Game` scope, check if the player is below 0 hp * If so, wait for user input to reset the `GameState` ::: Now our game has a simple, built-in loop. Let's just add some text to show the player what's happening. Add this function to the `impl` scope of `GameState`: ```rs [src/lib.rs] fn draw_screen(&self) { // If in menu or game over state if self.screen == Screen::Menu || self.player.hp == 0 { // Determine title string let title = if self.screen == Screen::Menu { "SPACE SHOOTER" } else { "GAME OVER" }; // Draw title text!( &title, x = (screen().w() as i32 / 2) - (title.chars().count() as i32 * 4), y = (screen().h() as i32 / 2) - 16, font = "large" ); // Draw prompt to start game, blinking every half second if time::tick() / 4 % 8 < 4 { text!( "Press A or Start", x = (screen().w() as i32 / 2) - 38, y = (screen().h() as i32 / 2) + 8, ); } } } ``` :::info[Code Breakdown] * Only update this scope when `screen` is `Menu` or the player's hp is 0 * Determine the `title` string by which of the above is true * Draw `text!()` for the title and input prompt * The input prompt blinks based on the game tick ::: And lastly, call the new function at the end of `GameState`'s `draw()` function: ```rs [src/lib.rs] fn draw(&self) { self.player.draw(); for projectile in self.projectiles.iter() { projectile.draw(); } for enemy in self.enemies.iter() { enemy.draw(); } self.draw_screen(); // [!code focus] [!code hl] [!code ++] } ``` #### Displaying Game Info With so many moving parts in our game already, let's give the player some info about their status with a heads up display! We'll create a UI to display the player's HP, score, and notifications. Before we get to drawing, add some properties in the `Player` struct to store notifications: ```rs [src/model/player] #[turbo::serialize] pub struct Player { pub hitbox: Bounds, x: f32, y: f32, dx: f32, // dx and dy used for velocity dy: f32, pub hp: u32, pub hit_timer: u32, // used for invincibility frames and drawing shoot_timer: u32, // used for rate of fire shooting: bool, // used for shooting animation // variables used by the HUD to display information pub score: u32, notifications: Vec, // [!code focus] [!code hl] [!code ++] notification_timer: usize, // [!code focus] [!code hl] [!code ++] } impl Player { pub fn new() -> Self { let x = ((screen().w() / 2) - 8) as f32; let y = (screen().h() - 64) as f32; Player { // Initialize all fields with default values hitbox: Bounds::new(x, y, 16, 16), x, y, dx: 0.0, dy: 0.0, hp: 3, hit_timer: 0, shoot_timer: 0, shooting: false, score: 0, notifications: vec![ // [!code focus] [!code hl] [!code ++] "Use arrow keys to move.".to_string(), // [!code focus] [!code hl] [!code ++] "Press SPACE or A to shoot.".to_string(), // [!code focus] [!code hl] [!code ++] "Defeat enemies and collect powerups.".to_string(), // [!code focus] [!code hl] [!code ++] "Try to not die. Good luck!".to_string(), // [!code focus] [!code hl] [!code ++] ], // [!code focus] [!code hl] [!code ++] notification_timer: 0, // [!code focus] [!code hl] [!code ++] } } ``` This `vec` and `usize` timer will keep track of the notifications we'll display to the player. The list will act as a queue, where the first element is displayed, and the timer will increment whenever the queue is not empty. Add the following lines to the end of the `Player` `update()` function: ```rs [src/model/player] pub fn update(&mut self, projectiles: &mut Vec) { ... self.hit_timer = self.hit_timer.saturating_sub(1); // Remove the camera shake if self.hit_timer == 0 { camera::remove_shake(); } // Notifications timer // [!code focus] [!code hl] [!code ++] if self.notifications.len() > 0 { // [!code focus] [!code hl] [!code ++] self.notification_timer += 1; // [!code focus] [!code hl] [!code ++] // Remove current notification if timer expires // [!code focus] [!code hl] [!code ++] if self.notification_timer >= 120 - 1 { // [!code focus] [!code hl] [!code ++] self.notification_timer = 0; // [!code focus] [!code hl] [!code ++] let _ = self.notifications.remove(0); // [!code focus] [!code hl] [!code ++] } // [!code focus] [!code hl] [!code ++] } // [!code focus] [!code hl] [!code ++] } ``` This scope runs when there are notifications to display, increasing the notifcations timer, reseting it and removing the first displayed notifications from the list. Now we can create a function to draw all of our heads up display information! Add the following function to the `impl` scope of the `Player` struct: ```rs [src/model/player] pub fn draw_hud(&self) { let hud_height = 16; // Height of the HUD panel let hud_padding = 4; // Padding inside the HUD // Background rectangle rect!( x = -1, y = -1, w = screen().w() + 2, h = hud_height + 2, border_size = 1, color = 0x000000ff, border_color = 0xffffffff, ); // Display Health let health_text = format!("HP: {}", self.hp); text!( &health_text, x = hud_padding, y = hud_padding, font = "large", color = 0xffffffff ); // Display Score let score_text = format!("SCORE: {:0>5}", self.score); let score_text_x = // anchor to the right side of the screen screen().w() as i32 - (score_text.chars().count() as i32 * 8) - hud_padding; text!( &score_text, x = score_text_x, y = hud_padding, font = "large", color = 0xffffffff ); // Draw notifications for notif in self.notifications.iter() { // center the text based on width of characters let len = notif.chars().count(); let w = len * 5; let x = (screen().w() as usize / 2) - (w / 2); rect!( x = x as i32 - 4, y = 24 - 2, w = w as u32 + 4, h = 12, color = 0x5fcde4ff ); text!( ¬if, x = x as i32, y = 24, font = "medium", color = 0xffffffff ); break; } } ``` This function simply uses `rect!()`s and `text!()`s with some math for centering to display information already stored in the `Player` struct. So now that we have all our info in one place, we can call our new function in the `GameState`'s `draw()` function: ```rs [src/lib.rs] fn draw(&self) { self.player.draw(); for projectile in self.projectiles.iter() { projectile.draw(); } for enemy in self.enemies.iter() { enemy.draw(); } self.player.draw_hud(); // [!code focus] [!code hl] [!code ++] self.draw_screen(); } ``` Save your files and check your game window for a more informed playthrough! #### Ship Stats The next two big features we'll be adding are enemy types and powerups. To prepare for those additions we'll create a data struct that will help modify our `Player` and `Enemy` structs. This data struct will simply store values for our ships' stats, like `max_hp` and `speed`. They will also store a new `enum` for the `EnemyStrategy`, which we will use later to determine enemy behavior. So, begin by creating a new file in the `model` folder named `ship_stats.rs`, and reference it in the `mod.rs` file: ``` space-shooter/ # Your project's root directory. ├── audio/ # The directory of your audio assets. ├── sprites/ # The directory of your sprite assets. ├── src/ # The directory of your code. │ ├── model/ # The directory for sub files. │ │ └── enemy.rs # A sub file to contain the Enemy struct. │ │ └── mod.rs # A reference to all sub files in this folder. │ │ └── player.rs # A sub file to contain the Player struct. │ │ └── projectile.rs # A sub file to contain the Projectile struct. │ │ └── ship_stats.rs # A sub file to contain the ShipStats struct. │ └── lib.rs # The main file for the game. ``` ```rs [src/model/mod.rs] use super::*; mod enemy; pub use enemy::*; mod player; pub use player::*; mod projectile; pub use projectile::*; mod ship_stats; // [!code focus] [!code hl] [!code ++] pub use ship_stats::*; // [!code focus] [!code hl] [!code ++] ``` Now, copy the following code into the new `ship_stats.rs` file: ```rs [src/model/ship_stats.rs] use super::*; #[turbo::serialize] pub struct ShipStats { pub max_hp: u32, pub speed: f32, pub fire_rate: u32, pub projectile_speed: f32, // Enemy stats the player doesn't use pub points: u32, pub strategy: EnemyStrategy, } #[turbo::serialize] pub enum EnemyStrategy { TargetPlayer, ShootDown, MoveDown, ZigZag, } ``` The `Player` and every `Enemy` will store an instance of this `ShipStats` struct, which we will use in their update logic. The `Player` won't use the last few variables, but here we've outlined the four `EnemyStratgey`s we will implement. In this file, we will also create `const` variables for our `Player` and `Enemy` structs to copy on initialization. Starting with the player, add the following lines to the end of the file: ```rs [src/model/ship_stats.rs] pub const PLAYER_STATS: ShipStats = ShipStats { max_hp: 3, speed: 2.0, fire_rate: 15, projectile_speed: 5.0, // must assign even if the player won't use these points: 0, strategy: EnemyStrategy::MoveDown }; ``` Now that we've defined `ShipStats` and a `const` for the `Player` to use, let's refactor the `Player` struct to utilize it! First, add a `ShipStats` variable to the player and initialize it to the `const` we made: ```rs [src/model/player.rs] #[turbo::serialize] pub struct Player { ... pub hp: u32, stats: ShipStats, // [!code focus] [!code hl] [!code ++] pub hit_timer: u32, // used for invincibility frames and drawing ... } impl Player { pub fn new() -> Self { ... Player { ... hp: PLAYER_STATS.max_hp, // [!code focus] [!code hl] [!code ++] stats: PLAYER_STATS, // [!code focus] [!code hl] [!code ++] hit_timer: 0, ... } } ``` Then, make the following revisions in the `player.rs` file: ```rs [src/model/player.rs] pub fn update(&mut self, projectiles: &mut Vec) { ... // Apply input to dx and dy, normalizing diagonal movement let magnitude = ((x_input * x_input + y_input * y_input) as f32).sqrt(); if x_input != 0.0 { self.dx = x_input / magnitude; } if y_input != 0.0 { self.dy = y_input / magnitude; } let speed = 2.0; // [!code focus] [!code hl] [!code --] self.x = (self.x + self.dx * self.stats.speed) // Translate position by input delta multiplied by speed // [!code focus] [!code hl] [!code ++] .clamp(0.0, (screen().w() - self.hitbox.w() - 2) as f32); // Clamp to screen bounds self.y = (self.y + self.dy * self.stats.speed) // [!code focus] [!code hl] [!code ++] .clamp(0.0, (screen().h() - self.hitbox.h() - 2) as f32); // Set hitbox position based on float xy values self.hitbox = self.hitbox.position(self.x, self.y); // Shooting projectiles // check if shoot button is pressed if gamepad::get(0).start.pressed() || gamepad::get(0).a.pressed() { self.shooting = true; // flag shooting state for animation // if shoot timer is 0, shoot a projectile if self.shoot_timer == 0 { let fire_rate = 15; // [!code focus] [!code hl] [!code --] self.shoot_timer += self.stats.fire_rate; // add cooldown to shoot timer // [!code focus] [!code hl] [!code ++] let projectile_speed = 5.0; // [!code focus] [!code hl] [!code --] for i in 0..=1 { projectiles.push( Projectile::new( self.x + i as f32 * 13.0, self.y - 8.0, self.stats.projectile_speed, // [!code focus] [!code hl] [!code ++] -90.0, ProjectileOwner::Player, ) ); } } // if not shooting } else { self.shooting = false; // flag shooting state for animation } ... } ``` Now that the `Player` stores an instance of a `ShipStats` struct, we can modify its values when we implement powerups! Implementing this struct doesn't affect our game yet, but you can play around with the values in the `const PLAYER_STATS` to see its affect. And, now we can use it to implement new enemy types! #### Enemy Types As of now, we only have one enemy type that spawns. Let's add some variety with new sprites and behaviors! We'll start by adding more sprites to our `sprites` folder: > * [x] [meteor.gif](https://raw.githubusercontent.com/super-turbo-society/turbo-demos/refs/heads/main/space-shooter/sprites/meteor.gif) > * [x] [shooter.gif](https://raw.githubusercontent.com/super-turbo-society/turbo-demos/refs/heads/main/space-shooter/sprites/shooter.gif) > * [x] [tank.gif](https://raw.githubusercontent.com/super-turbo-society/turbo-demos/refs/heads/main/space-shooter/sprites/tank.gif) > * [x] [zipper.gif](https://raw.githubusercontent.com/super-turbo-society/turbo-demos/refs/heads/space-shooter/sprites/zipper.gif) Next, let's define stats for each of the `EnemyType`s we defined back in the `enemy.rs` file using the `ShipStats` struct. Add the following `const`s to the `ship_stats.rs` file: ```rs [src/model/ship_stats.rs] pub const METEOR_STATS: ShipStats = ShipStats { max_hp: 2, speed: 2.0, fire_rate: 0, // meteors don't shoot projectile_speed: 0.0, points: 20, strategy: EnemyStrategy::MoveDown }; pub const SHOOTER_STATS: ShipStats = ShipStats { max_hp: 5, speed: 1.0, fire_rate: 25, projectile_speed: 3.5, points: 30, strategy: EnemyStrategy::TargetPlayer }; pub const TANK_STATS: ShipStats = ShipStats { max_hp: 10, speed: 0.25, fire_rate: 10, projectile_speed: 2.5, points: 50, strategy: EnemyStrategy::ShootDown }; pub const TURRET_STATS: ShipStats = ShipStats { max_hp: 6, speed: 0.5, fire_rate: 20, projectile_speed: 4.0, points: 40, strategy: EnemyStrategy::ShootDown }; pub const ZIPPER_STATS: ShipStats = ShipStats { max_hp: 4, speed: 1.0, fire_rate: 0, // zippers don't shoot projectile_speed: 0.0, points: 20, strategy: EnemyStrategy::ZigZag }; ``` Now we have `const ShipStats` for each `EnemyType`, which we'll store an instance of on initialization in the `Enemy` struct, just like the `Player`. In the `Enemy` struct, add a variable to store this instance, and some logic to the `new()` function in order to assign stats based on the passed `EnemyType`: ```rs [src/model/enemy.rs] #[turbo::serialize] // Struct for Enemies pub struct Enemy { enemy_type: EnemyType, pub hitbox: Bounds, x: f32, y: f32, pub angle: f32, pub hp: u32, stats: ShipStats, // [!code focus] [!code hl] [!code ++] hit_timer: u32, // used for hit animation pub destroyed: bool, } impl Enemy { // Initialize different enemy types with different properties pub fn new(enemy_type: EnemyType) -> Self { let (x,y) = ((random::u32() % screen().w()).saturating_sub(32) as f32, -32.0); let stats = match enemy_type { // [!code focus] [!code hl] [!code ++] EnemyType::Meteor => METEOR_STATS, // [!code focus] [!code hl] [!code ++] EnemyType::Shooter => SHOOTER_STATS, // [!code focus] [!code hl] [!code ++] EnemyType::Tank => TANK_STATS, // [!code focus] [!code hl] [!code ++] EnemyType::Turret => TURRET_STATS, // [!code focus] [!code hl] [!code ++] EnemyType::Zipper => ZIPPER_STATS, // [!code focus] [!code hl] [!code ++] }; // [!code focus] [!code hl] [!code ++] let size = // [!code focus] [!code hl] [!code ++] if enemy_type == EnemyType::Meteor { 8 } // [!code focus] [!code hl] [!code ++] else if enemy_type == EnemyType:: Tank { 32 } // [!code focus] [!code hl] [!code ++] else { 16 }; // [!code focus] [!code hl] [!code ++] // Set initial properties based on enemy type Self { enemy_type: enemy_type, hitbox: Bounds::new(x, y, size, size), // [!code focus] [!code hl] [!code ++] x, y, angle: 0.0, hp: stats.max_hp, // [!code focus] [!code hl] [!code ++] stats, // [!code focus] [!code hl] [!code ++] hit_timer: 0, destroyed: false, } } ... } ``` Now in the `new()` function, we use a match statement of the passed `EnemyType` in order to store an instance of the matching `const` stat block we defined in `ship_stats.rs`. We also create a size variable for our hitbox to match our new sprite sizes, also determined by the passed `EnemyType`. #### Integrating Enemy Types Great! We can now create different enemies for each of our `EnemyType`s. Let's update the rest of the `Enemy` struct to implement these changes. #### Powerups #### Updating and Drawing Powerups #### Integrating Powerups #### Difficulty Ramping #### Final Details :::: ### Conclusion ... ### Next Steps ... import Snippet from './_template.mdx' ## Channels ![Two ghosts sharing a computer](/ghosts_at_computer.png) ### Overview The `channel` API lets your game subscribe to a Turbo OS channel to send and receive incoming messages asynchronously. Channels are good for fast-paced multiplayer gameplay and other features that require low latency + group broadcasts such as chat. ### Creating a Channel (Server) To demonstrate, let's make a "ping pong" channel that accepts "ping" messages and sends back "pong" messages in response: #### Create Send and Recv types for the channel Let's define our `Ping` and `Pong` structs: ```rs [src/lib.rs] use turbo::*; #[turbo::serialize] pub struct Ping; #[turbo::serialize] pub struct Pong; ``` We will send `Ping` to the channel and receive `Pong` from the channel. #### Use the Channel macro Here, we use the `turbo::os::channel` macro to tag our ChannelHandler with program and channel names. ```rs [src/lib.rs] use turbo::*; #[turbo::serialize] pub struct Ping; #[turbo::serialize] pub struct Pong; #[turbo::os::channel(program = "pingpong", name = "main")] // [!code focus] [!code hl] pub struct PingPongChannel; // [!code focus] [!code hl] ``` :::note The macro will ensure our program is uploaded and will add an additional `subscribe` method to `PingPongChannel` which will be useful when interacting with the channel on the client side. ::: #### Implement `ChannelHandler` Next, we have to implement the `ChannelHandler` trait: ```rs [src/lib.rs] use turbo::*; #[turbo::serialize] pub struct Ping; #[turbo::serialize] pub struct Pong; #[turbo::os::channel(program = "pingpong", name = "main")] pub struct PingPongChannel; impl ChannelHandler for PingPongChannel { // [!code focus] [!code hl] type Recv = Ping; // incoming from client [!code focus] [!code hl] type Send = Pong; // outgoing to client [!code focus] [!code hl] fn new() -> Self { // [!code focus] [!code hl] Self // [!code focus] [!code hl] } // [!code focus] [!code hl] fn on_data(&mut self, user_id: &str, data: Self::Recv) -> Result<(), std::io::Error> { // [!code focus] [!code hl] log!("Got {:?} from {:?}", data, user_id); // [!code focus] [!code hl] Self::send(user_id, Pong) // [!code focus] [!code hl] } // [!code focus] [!code hl] } // [!code focus] [!code hl] ``` The `new` method is called when our channel first opens, and the `on_data` method is called whenever we receive a message from a player. \:::: ### Connecting to a Channel (Client) Last, we can hook up the client :::steps #### Subscribe to the channel Subscribe to the "ping pong" channel to get a `ChannelConnection` which we will call `conn`: ```rs [src/lib.rs] use turbo::*; #[turbo::game] struct GameState {} impl GameState { fn new() -> Self { Self {} } fn update(&mut self) { if let Some(conn) = PingPongChannel::subscribe("default") { // [!code focus] [!code hl] // If we're in here, that means we connected the channel! [!code focus] [!code hl] } // [!code focus] [!code hl] } } ``` #### Receive messages from the channel Pull Pong messages using the `ChannelConnection::recv` method: ```rs [src/lib.rs] use turbo::*; #[turbo::game] struct GameState {} impl GameState { fn new() -> Self { Self {} } fn update(&mut self) { if let Some(conn) = PingPongChannel::subscribe("default") { while let Ok(msg) = conn.recv() { // [!code focus] [!code hl] log!("Received pong from server!"); // [!code focus] [!code hl] } // [!code focus] [!code hl] } } } ``` #### Send messages to the channel Send a Ping message to the channel whenever the start button is pressed using the `ChannelConnection::send` method: ```rs [src/lib.rs] use turbo::*; #[turbo::game] struct GameState {} impl GameState { fn new() -> Self { Self {} } fn update(&mut self) { if let Some(conn) = PingPongChannel::subscribe("default") { while let Ok(msg) = conn.recv() { log!("Received pong from server!"); } if gamepad::get(0).start.just_pressed() { // [!code focus] [!code hl] let _ = conn.send(&Ping); // [!code focus] [!code hl] log!("Sent ping to the server!"); // [!code focus] [!code hl] } // [!code focus] [!code hl] } } } ``` ::: ### Running your game ::::steps #### Review Your Code Ensure that your `src/lib.rs` file is correct: :::details[See full src/lib.rs source code] ```rs [src/lib.rs] use turbo::*; #[turbo::serialize] pub struct Ping; #[turbo::serialize] pub struct Pong; #[turbo::os::channel(program = "pingpong", name = "main")] pub struct PingPongChannel; impl ChannelHandler for PingPongChannel { type Recv = Ping; // incoming from client type Send = Pong; // outgoing to client fn new() -> Self { Self } fn on_data(&mut self, user_id: &str, data: Self::Recv) -> Result<(), std::io::Error> { log!("Got {:?} from {:?}", data, user_id); Self::send(user_id, Pong) } } #[turbo::game] struct GameState; impl GameState { fn update(&mut self) { // Subscribe to the "ping pong" channel to get a `ChannelConnection` if let Some(conn) = PingPongChannel::subscribe("default") { // Pull Pong messages using the `ChannelConnection::recv` method while let Ok(msg) = conn.recv() { log!("Received pong from server!"); } if gamepad::get(0).start.just_pressed() { // Send a Ping message using the `ChannelConnection::send` method let _ = conn.send(&Ping); log!("Sent ping to the server!"); } } } } ``` ::: #### Update your config Update your `turbo.toml` file to include the following section: ```toml [turbo-os] api-url = "https://os.turbo.computer" ``` Next, create a free dev account at [https://os.turbo.computer](https://os.turbo.computer). The dashboard will have your user ID. ```sh turbo run -w --user ``` It will prompt for a one-time password you can also find on your Turbo OS dashboard. Once your game window opens, it will upload your program: ```sh [turbo] Uploading "pingpong" (YOUR_PROGRAM_ID)... [turbo] Uploaded "pingpong" (YOUR_PROGRAM_ID) ✨ ``` When connects to the channel, you will see some output like the following: ```sh [turbo] Channel connection open: - program : YOUR_PROGRAM_ID - channel name : main - channel id : default - inspector url: https://os.turbo.computer/channels/YOUR_PROGRAM_ID/main/default/inspector ``` If you press the start button on a gamepad or space on a keyboard, you will see the following output on the command line: ```sh [turbo] Sent ping to the server! [turbo] Received pong from server! ``` #### Check the Channel Inspector You can visit that "inspector url" to view the channel inspector page. There, you can observe messages sent to/from the server, user connection/disconnection events, and logs: ![Channel inspector screenshot](/pingpong-inspector.png) :::tip[TIP] **This page is essential for debugging.** Use it to track server logs and channel activity. ::: :::: ## Commands ![Turbi talking into a megaphone](/turbo-megaphone.png) ### Overview This example demonstrates how to use Turbo Genesis `command` and `document` APIs to build a simple counter program with two commands: `add` and `reset`. ### Creating Commands (Server) :::steps #### The Counter document ```rs [src/lib.rs] #[turbo::os::document(program = "counter")] pub struct Counter { /// Current counter value pub value: i32, } ``` #### The add command The `add` command reads the `counter` program file (creating it if it doesn't exist), updates the stored `Counter`, increments its value, and writes it back. ```rs [src/lib.rs] use turbo::os::server::*; #[turbo::os::command(program = "counter", name = "add")] pub struct Add { /// Amount to add to the counter amount: i32, } impl CommandHandler for Add { fn run(&mut self, user_id: &str) -> Result<(), std::io::Error> { // Read existing counter or default to 0 let mut counter = fs::read("counter") .unwrap_or(Counter { value: 0 }); // Apply increment counter.value += self.amount; log!("Incremented = {:?}", counter); // Persist updated counter fs::write("counter", &counter)?; Ok(()) } } ``` #### The reset command The `reset` command overwrites the `counter` file with a `Counter` initialized to `0`. ```rs [src/lib.rs] #[turbo::os::command(program = "counter", name = "reset")] pub struct Reset; impl CommandHandler for Reset { fn run(&mut self, user_id: &str) -> Result<(), std::io::Error> { // Overwrite the counter file with a Counter set to 0 fs::write("counter", &Counter { value: 0 })?; Ok(()) } } ``` ::: ### Using Commands (Client) Now let's use the commands to update the `counter` document and display its value in the game. :::steps #### Watch the Counter document The counter data is stored in a program file named `counter`. ```rs [src/lib.rs] #[turbo::game] struct GameState; impl GameState { fn update(&mut self) { // Watch the counter file and parse its contents when it loads [!code focus] [!code hl] let counter = Counter::watch("counter") // [!code focus] [!code hl] .parse() // [!code focus] [!code hl] .unwrap_or(Counter { value: 0 }); // [!code focus] [!code hl] // Draw the counter data [!code focus] [!code hl] text!("{:#?}", counter); // [!code focus] [!code hl] } } ``` #### Send some commands Let's allow the player to add a random value or reset the counter file. ```rs [src/lib.rs] #[turbo::game] struct GameState; impl GameState { fn update(&mut self) { // Watch the counter file and parse its contents when it loads let counter = Counter::watch("counter") .parse() .unwrap_or(Counter { value: 0 }); // Draw the counter data text!("{:#?}", counter); // When the "a" button (Z on keyboard) is pressed, send an Add command [!code focus] [!code hl] if gamepad::get(0).a.just_pressed() { // [!code focus] [!code hl] let cmd = Add { amount: random::between(-100, 100) }; // [!code focus] [!code hl] cmd.exec(); // [!code focus] [!code hl] } // [!code focus] [!code hl] // When the "b" button (X on keyboard) is pressed, send a Reset command [!code focus] [!code hl] if gamepad::get(0).b.just_pressed() { // [!code focus] [!code hl] let cmd = Reset; // [!code focus] [!code hl] cmd.exec(); // [!code focus] [!code hl] } // [!code focus] [!code hl] } } ``` ::: ### Running your game ::::steps #### Review Your Code Ensure that your `src/lib.rs` file is correct: :::details[See full src/lib.rs source code] ```rs [src/lib.rs] use turbo::*; use turbo::os::server::*; #[turbo::os::document(program = "counter")] pub struct Counter { /// Current counter value pub value: i32, } #[turbo::os::command(program = "counter", name = "add")] pub struct Add { /// Amount to add to the counter amount: i32, } impl CommandHandler for Add { fn run(&mut self, user_id: &str) -> Result<(), std::io::Error> { // Read existing counter or default to 0 let mut counter = fs::read("counter") .unwrap_or(Counter { value: 0 }); // Apply increment counter.value += self.amount; log!("Incremented = {:?}", counter); // Persist updated counter fs::write("counter", &counter)?; Ok(()) } } #[turbo::os::command(program = "counter", name = "reset")] pub struct Reset; impl CommandHandler for Reset { fn run(&mut self, user_id: &str) -> Result<(), std::io::Error> { // Overwrite the counter file with a Counter set to 0 fs::write("counter", &Counter { value: 0 })?; Ok(()) } } #[turbo::game] struct GameState; impl GameState { fn update(&mut self) { // Watch the counter file and parse its contents when it loads let counter = Counter::watch("counter") .parse() .unwrap_or(Counter { value: 0 }); // Draw the counter data text!("{:#?}", counter); // When the "a" button (Z on keyboard) is pressed, send an Add command if gamepad::get(0).a.just_pressed() { let cmd = Add { amount: random::between(-100, 100) }; cmd.exec(); } // When the "b" button (X on keyboard) is pressed, send a Reset command if gamepad::get(0).b.just_pressed() { let cmd = Reset; cmd.exec(); } } } ``` ::: #### Update your config Update your `turbo.toml` file to include the following section: ```toml [turbo.toml] [turbo-os] api-url = "https://os.turbo.computer" ``` #### Run with a user ID Next, create a free dev account at [https://os.turbo.computer](https://os.turbo.computer). Your dashboard will display your user ID. Run your game with: ```bash [command line] turbo run -w --user ``` It will prompt you for a one-time password, which you'll find on your Turbo OS dashboard. When your game launches, it will upload the program: ```bash [command line] [turbo] Uploading "counter" (YOUR_PROGRAM_ID)... [turbo] Uploaded "counter" (YOUR_PROGRAM_ID) ✨ ``` #### Check your dashboard On your [Turbo OS Dashboard](https://os.turbo.computer/dashboard), scroll to the bottom to see uploaded programs. The `counter` program will appear there. Click the blue arrow to view the program analytics page: ![Turbo OS Program Analytics Page](/program-analytics.png) :::tip You can also directly access the analytics page via:\ `https://os.turbo.computer/programs/PROGRAM_ID/analytics` ::: As you send `add` and `reset` commands, you can inspect the live activity feed to see logs and how the commands were processed: ![Turbo OS Live Activity Feed](/live-activity-feed.png) :::tip[TIP] **This page is essential for debugging.** Use it to track your commands' behavior and server responses. ::: ::::