Part 4: AI Opponent
Let’s add an AI opponent so single players can enjoy the game.
What You’ll Learn
- Simple AI that follows the ball
- Checking player count with
player_count() - Making AI beatable (not perfect)
Add Player Count FFI
#![allow(unused)]
fn main() {
#[link(wasm_import_module = "env")]
extern "C" {
// ... existing imports ...
fn player_count() -> u32;
}
}
Track Game Mode
Add a state variable to track whether we’re in two-player mode:
#![allow(unused)]
fn main() {
static mut IS_TWO_PLAYER: bool = false;
}
Simple AI Logic
Create an AI update function:
#![allow(unused)]
fn main() {
fn update_ai(paddle_y: &mut f32) {
unsafe {
// AI follows the ball
let paddle_center = *paddle_y + PADDLE_HEIGHT / 2.0;
let ball_center = BALL_Y + BALL_SIZE / 2.0;
let diff = ball_center - paddle_center;
// Only move if difference is significant (dead zone)
if diff.abs() > 5.0 {
// AI moves slower than max speed to be beatable
let ai_speed = PADDLE_SPEED * 0.7;
if diff > 0.0 {
*paddle_y += ai_speed;
} else {
*paddle_y -= ai_speed;
}
}
// Clamp to screen bounds
*paddle_y = clamp(*paddle_y, 0.0, SCREEN_HEIGHT - PADDLE_HEIGHT);
}
}
}
You’ll also need this helper:
#![allow(unused)]
fn main() {
fn abs(v: f32) -> f32 {
if v < 0.0 { -v } else { v }
}
}
Update the Game Loop
Modify update() to use AI when appropriate:
#![allow(unused)]
fn main() {
#[no_mangle]
pub extern "C" fn update() {
unsafe {
// Check if a second player is connected
IS_TWO_PLAYER = player_count() >= 2;
// Player 1 always uses input
update_paddle(&mut PADDLE1_Y, 0);
// Player 2: human or AI
if IS_TWO_PLAYER {
update_paddle(&mut PADDLE2_Y, 1);
} else {
update_ai(&mut PADDLE2_Y);
}
update_ball();
}
}
}
Show Game Mode
Let’s display the current mode. Add text FFI:
#![allow(unused)]
fn main() {
#[link(wasm_import_module = "env")]
extern "C" {
// ... existing imports ...
fn draw_text(ptr: *const u8, len: u32, x: f32, y: f32, size: f32, color: u32);
}
}
Add to render():
#![allow(unused)]
fn main() {
#[no_mangle]
pub extern "C" fn render() {
unsafe {
// ... existing drawing code ...
// Show mode indicator
if IS_TWO_PLAYER {
let text = b"2 PLAYERS";
draw_text(text.as_ptr(), text.len() as u32, 10.0, 10.0, 16.0, COLOR_GRAY);
} else {
let text = b"vs AI";
draw_text(text.as_ptr(), text.len() as u32, 10.0, 10.0, 16.0, COLOR_GRAY);
}
}
}
}
How the AI Works
The AI is intentionally imperfect:
- Follows the ball - Moves toward where the ball is
- Slower speed - Only 70% of max paddle speed
- Dead zone - Doesn’t jitter when ball is near center
- No prediction - Doesn’t anticipate where ball will go
This makes the AI beatable but still challenging.
Making AI Harder or Easier
#![allow(unused)]
fn main() {
// Easier AI (50% speed)
let ai_speed = PADDLE_SPEED * 0.5;
// Harder AI (90% speed)
let ai_speed = PADDLE_SPEED * 0.9;
// Perfect AI (instant tracking) - not fun!
*paddle_y = ball_center - PADDLE_HEIGHT / 2.0;
}
The Magic: Automatic Multiplayer
Here’s the key insight: when a second player connects, the game automatically becomes two-player. You don’t need to do anything special!
This works because:
- We check
player_count()every frame - Player 2 input is always read (even if unused)
- The switch from AI to human is seamless
When a friend joins your game online via Emberware’s netcode, player_count() increases and they take control of paddle 2.
Build and Test
cargo build --target wasm32-unknown-unknown --release
ember run target/wasm32-unknown-unknown/release/paddle.wasm
With one controller:
- You control player 1
- AI controls player 2
- “vs AI” appears in corner
Connect a second controller:
- Both players are human
- “2 PLAYERS” appears
Next: Part 5: Multiplayer - Understanding the rollback netcode magic.