Skip to content

Channels

Two ghosts sharing a computer

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:

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.

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; 

Implement ChannelHandler

Next, we have to implement the ChannelHandler trait:

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) 
    } 
} 

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

Subscribe to the channel

Subscribe to the "ping pong" channel to get a ChannelConnection which we will call conn:

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") { 
            // If we're in here, that means we connected the channel!
        } 
    }
}

Receive messages from the channel

Pull Pong messages using the ChannelConnection::recv method:

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!"); 
            } 
        }
    }
}

Send messages to the channel

Send a Ping message to the channel whenever the start button is pressed using the ChannelConnection::send method:

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() { 
                let _ = conn.send(&Ping); 
                log!("Sent ping to the server!"); 
            } 
        }
    }
}

Running your game

Review Your Code

Ensure that your src/lib.rs file is correct:

See full src/lib.rs source code
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:

[turbo-os]
api-url = "https://os.turbo.computer"

Next, create a free dev account at https://os.turbo.computer. The dashboard will have your user ID.

turbo run -w --user <YOUR_USER_ID>

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:

[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:

[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:

[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