Multiplayer

Multiplayer Overview (Simplified)

awe takes care of running your server logic behind the scenes, spinning up secure virtual machines so you can experiment freely without worrying about infrastructure. You write your multiplayer code in scripts; we’ll handle the rest.


1. Server & Client Basics

Server-Only Logic

Use the @ServerOnly() decorator for code that should only run on the server.

import { ScriptBehavior, Env, ServerOnly } from '@oo/scripting'
 
export default class MyLogic extends ScriptBehavior {
  @ServerOnly()
  onReady() {
    console.log("Server-Only: won't run on client.")
  }
}

Client & Server Messages

  • Client-Side:
    GameClient.onMessage("my-message", (data) => {
      console.log("Client received:", data)
    })
     
    GameClient.send("my-message", { foo: "bar" })
  • Server-Side:
    GameServer.onMessage("my-message", (data, clientId) => {
      console.log("Server received from client:", clientId, data)
    })
     
    GameServer.send("my-message", { serverData: 123 }, clientId) 
    // Or broadcast to all using '*' for clientId

2. Remote Procedure Calls (RPC)

RPCs let you call methods on the server or client directly.

  • @Rpc.Server(): Client → Server
  • @Rpc.Client(): Server → Client
import { ScriptBehavior, Rpc } from '@oo/scripting'
 
export default class MyRPCs extends ScriptBehavior {
 
  // CLIENT calls this on SERVER
  @Rpc.Server()
  sendMessageToServer(text: string) {
    console.log("Server got:", text)
  }
 
  // SERVER calls this on CLIENT
  @Rpc.Client()
  showMessageOnClient(msg: string) {
    console.log("Client sees message:", msg)
  }
}

Options:

  • reply: true waits for a return value.
  • timeout sets a max wait time.
  • broadcast can be used to send to all clients in one call (though GameServer.send(...) can do that too).

3. Network State (netState)

A quick way to sync small bits of data (like positions, animations) between server and clients is to store them in a special object.

import { ScriptBehavior, $Param as P, Env } from '@oo/scripting'
 
export default class SimpleSync extends ScriptBehavior {
  netState = P.Object({
    pos: P.Vector3(),
    rot: P.Number(),
    anim: P.String(),
  })
 
  @ServerOnly()
  onReady() {
    // Server sets initial data
    this.netState.pos.set(0, 1, 0)
    this.netState.rot = 0
    this.netState.anim = "idle"
  }
 
  onFrame(dt) {
    // If it's a client, smoothly interpolate or animate
    if (!Env.isServer) {
      // e.g. this.host.position.lerp(this.netState.pos, 0.1)
    }
  }
}

4. Putting It All Together

  1. Server-Only Methods: Ensure secure logic with @ServerOnly() or by checking Env.isServer.
  2. Messaging: Send/receive custom messages with GameClient.onMessage() / GameClient.send() and GameServer.onMessage() / GameServer.send().
  3. RPCs: Decorate methods with @Rpc.Server() or @Rpc.Client() for direct calls across the network.
  4. netState Sync: Keep a small state object for positions, animations, or other immediate data.

All of this happens seamlessly because awe orchestrates server-side VMs for you. You can develop advanced multiplayer features without spinning up or configuring servers manually. Experiment, iterate, and deploy with ease!