Skip to main content

lockstep/
lockstep.rs

1//! Lockstep multiplayer: two players each move a crab with the arrow keys,
2//! and both windows show the same game, because both run the same
3//! simulation from the same inputs. Run it twice:
4//!
5//!   `cargo run --example lockstep -- host 7777`
6//!   `cargo run --example lockstep -- join 127.0.0.1:7777`
7//!
8//! (or on two machines on one network). Space shoves the other crab away.
9//!
10//! Over Steam (feature `steam`; Steam running; Valve's test app 480):
11//!
12//!   `cargo run --example lockstep --features steam -- steam`       host, press I to invite
13//!   `cargo run --example lockstep --features steam -- steam-join`  then accept the invite
14
15use ponko::*;
16use serde::{Deserialize, Serialize};
17
18// ANCHOR: input
19/// What one player does in one tick: all that crosses the network.
20#[derive(Serialize, Deserialize, Default)]
21struct Pad {
22    dir: Vec2,
23    shove: bool,
24}
25
26/// Reads this player's keys every frame; the session sends them.
27fn read_keys(ctx: &mut Ctx) {
28    let mut pad = Pad::default();
29    for (key, d) in [
30        (Key::Left, -Vec2::X),
31        (Key::Right, Vec2::X),
32        (Key::Up, -Vec2::Y),
33        (Key::Down, Vec2::Y),
34    ] {
35        if ctx.key_down(key) {
36            pad.dir += d;
37        }
38    }
39    pad.shove = ctx.key_down(Key::Space);
40    ctx.net.set_input(&pad);
41}
42// ANCHOR_END: input
43
44/// A crab, and whose it is.
45#[derive(Serialize, Deserialize, Clone, Copy)]
46struct Crab {
47    player: u32,
48    push: Vec2,
49}
50
51// ANCHOR: simulate
52/// The simulation: every peer runs it on every player's input, in the
53/// same order, so every peer ends up with the same world.
54fn move_crabs(ctx: &mut Ctx) {
55    let pads: Vec<Pad> = (0..ctx.net.players())
56        .map(|p| ctx.net.input::<Pad>(p).unwrap_or_default())
57        .collect();
58    let dt = ctx.dt();
59    let crabs: Vec<(Entity, Vec2, u32)> = ctx
60        .world
61        .query::<(Entity, &Transform2D, &Crab)>()
62        .iter()
63        .map(|(e, t, c)| (e, t.position, c.player))
64        .collect();
65    for &(e, at, player) in &crabs {
66        let pad = &pads[player as usize];
67        if pad.shove {
68            for &(other, other_at, _) in &crabs {
69                if other != e && at.distance(other_at) < 120.0 {
70                    ctx.world.get_mut::<Crab>(other).unwrap().push += (other_at - at).normalize_or_zero() * 900.0;
71                }
72            }
73        }
74        let crab = *ctx.world.get::<Crab>(e).unwrap();
75        let t = ctx.world.get_mut::<Transform2D>(e).unwrap();
76        t.position += (pad.dir.normalize_or_zero() * 250.0 + crab.push) * dt;
77        t.position = t.position.clamp(vec2(40.0, 40.0), vec2(760.0, 560.0));
78        ctx.world.get_mut::<Crab>(e).unwrap().push = crab.push * 0.9;
79    }
80}
81// ANCHOR_END: simulate
82
83fn draw(ctx: &mut Ctx) {
84    ctx.clear(Color::hex(0x20304a));
85    let crabs: Vec<(Vec2, u32)> = ctx
86        .world
87        .query::<(&Transform2D, &Crab)>()
88        .iter()
89        .map(|(t, c)| (t.position, c.player))
90        .collect();
91    for (at, player) in crabs {
92        let color = [Color::hex(0xf56565), Color::hex(0x63b3ed)][player as usize % 2];
93        ctx.draw_circle(at, 30.0, color);
94    }
95    let net = &ctx.net;
96    let status = match net.state() {
97        NetState::Connecting => "waiting for the other player...".to_string(),
98        NetState::Running => format!(
99            "you are player {} ({}), tick {}{}",
100            net.player() + 1,
101            ["red", "blue"][net.player() % 2],
102            ctx.sim_tick(),
103            net.desync().map_or(String::new(), |d| format!("  DESYNC: {d}"))
104        ),
105        NetState::Lost => format!("connection lost: {}", net.error().unwrap_or("?")),
106        NetState::Offline => "offline".into(),
107        _ => String::new(),
108    };
109    ctx.draw_text(&status, vec2(20.0, 20.0), 20.0, Color::WHITE);
110    #[cfg(feature = "steam")]
111    if ctx.net.state() == NetState::Connecting
112        && let Some(lobby) = ctx.net.steam_lobby()
113    {
114        let text = format!("Steam lobby {}: press I to invite a friend", lobby.raw());
115        ctx.draw_text(&text, vec2(20.0, 50.0), 20.0, Color::WHITE);
116        if ctx.key_pressed(Key::I) {
117            ctx.net.steam_invite();
118        }
119    }
120    if ctx.key_pressed(Key::Escape) {
121        ctx.quit();
122    }
123}
124
125fn main() {
126    let args: Vec<String> = std::env::args().skip(1).collect();
127    #[cfg(feature = "steam")]
128    if let Some(net) = steam_session(&args) {
129        return play(net);
130    }
131    // ANCHOR: session
132    let net = match args.first().map(String::as_str) {
133        Some("host") => Net::host(format!("0.0.0.0:{}", args.get(1).map_or("7777", |p| p)), 2),
134        Some("join") => Net::join(args.get(1).map_or("127.0.0.1:7777", |a| a)),
135        _ => {
136            eprintln!("usage: lockstep host [port] | join <host:port> | steam | steam-join");
137            return;
138        }
139    }
140    .expect("could not open a socket");
141    play(net);
142}
143
144fn play(net: Net) {
145    Game::new("Lockstep", 800, 600)
146        .lockstep(net)
147        .system(read_keys) // frame system: input out
148        .sim_system(move_crabs) // sim system: everyone's input in
149        .system(draw)
150        .run(|ctx| {
151            ctx.world.reflect::<Crab>(); // compared between peers
152            for player in 0..2 {
153                ctx.world.spawn((
154                    Transform2D::at(vec2(250.0 + 300.0 * player as f32, 300.0)),
155                    Crab {
156                        player,
157                        push: Vec2::ZERO,
158                    },
159                ));
160            }
161        });
162    // ANCHOR_END: session
163}
164
165// ANCHOR: steam
166/// Steam: the host creates a friends-only lobby and invites from the
167/// overlay; the friend accepts in Steam. No addresses, no port forwarding.
168#[cfg(feature = "steam")]
169fn steam_session(args: &[String]) -> Option<Net> {
170    let mode = args.first().map(String::as_str);
171    let invited = steam_lobby_from_args(); // launched by accepting an invite
172    if !matches!(mode, Some("steam" | "steam-join")) && invited.is_none() {
173        return None;
174    }
175    // 480 is Valve's test app; a real game uses its own app id.
176    let client = steamworks::Client::init_app(480).expect("Steam must be running and logged in");
177    if let Some(lobby) = invited {
178        return Some(Net::steam_join(&client, lobby));
179    }
180    if mode == Some("steam") {
181        return Some(Net::steam_host(&client, 2));
182    }
183    println!("waiting for you to accept a Steam invite...");
184    let lobby = steam_wait_for_invite(&client, std::time::Duration::from_secs(300))?;
185    Some(Net::steam_join(&client, lobby))
186}
187// ANCHOR_END: steam