Skip to content

Steam, touch and local multiplayer

Steam achievements, stats and cloud files; touch input and on-screen controls; local multiplayer players and split screen.

Files: steam.odin, touch.odin, players.odin.

Steam: achievements, stats and cloud saves, through one API whatever runs them.

  • Achievements are listed in project.xml (<achievement name="FIRST_BLOOD" title="First Blood" description="Defeat an enemy"/>; the editor’s Project Settings > Steam): scripts unlock them by name (Steam_UnlockAchievement("FIRST_BLOOD"); the editor writes ACHIEVEMENT_X constants into scripts/assets.odin). Names are Steam’s API names, as set up on Steamworks.
  • Stats are numbers by name (Steam_SetStat, Steam_AddStat, Steam_GetStat): kills, distance walked. Steam_StoreStats sends them (and unlocked achievements), as Steam wants.
  • Cloud files (Steam_CloudWrite / Read / Delete / Exists): files that follow the player between computers. With the project’s steamCloud="true", Save_Game and Load_Game go through them.

The backend: the calls go to a SteamBackend (Game.steam.backend). Until the Steamworks SDK is part of the engine, every game runs the stub (STEAM_STUB_BACKEND): achievements and stats are kept in the game’s save storage (steam-stub; runs from the editor apart, like saves), cloud files are save files named steam-cloud-<file>, and each unlock is logged (“Steam (stub): achievement FIRST_BLOOD unlocked”). Games written against the API need no change when the real backend arrives. Steam_SetBackend replaces it (tests, or a backend of your own).

onAchievementUnlocked (Game.steam) is notified on each new unlock (data: the name as a string), so a game can show its own toast.

Example

if !e.Steam_IsAchievementUnlocked("FIRST_BLOOD") do e.Steam_UnlockAchievement("FIRST_BLOOD")
e.Steam_AddStat("ENEMIES_DEFEATED", 1)
e.Steam_StoreStats()

Touch input and on-screen controls: phones, tablets, Steam Deck and touch-screen laptops.

  • Touches (Input_GetTouches, Input_GetTouchCount): every finger on the screen this frame, in game coordinates (the app converts SDL’s finger positions like the mouse’s), with where it started, how far it moved and whether it just went down or up. A finger keeps its index while it’s down; lifted fingers are listed (released) for one frame.
  • The mouse from touches: SDL also turns the first finger into mouse presses, so menus, buttons and anything else that reads the mouse work with touches already.
  • VirtualJoystick nodes: a stick drawn on the screen, moved by a finger (or the mouse, to try it on a desktop). It pushes up to four input actions (touchActions="move_left,move_right, move_up,move_down"), so a game reading Input_GetVector moves the same with keys, a gamepad or the stick. VirtualJoystick_GetVector reads it directly. Floating joysticks appear where the thumb lands inside their zone.
  • TouchButton nodes: a round button pressing an action while touched (touchActions="jump"), with a label, and onPressed / onReleased events.

Each control takes one finger: the first that lands in its zone, until it lifts (it can move anywhere meanwhile). A finger taken by a control isn’t taken by another. Controls hidden in the tree, or in scenes not updating, take none.

Where to put them: on a layer the camera doesn’t move (a scene’s screen layers), so they stay put on the screen; their position is the middle of the stick or button. They draw unturned and unscaled (like Labels). By default they show only once the screen has been touched, or in development runs (Game.devKeys: Play from the editor), so desktop players don’t see them (TouchControlSettings.visibility).

Example

stick := e.CreateVirtualJoystick(scene, {"move_left", "move_right", "move_up", "move_down"})
stick.position = {120, 600}
stick.layer = .Layer8
jump := e.CreateTouchButton(scene, "jump", "A")
jump.position = {1160, 620}
jump.layer = .Layer8

Local multiplayer: players joining with a device (a gamepad, or the keyboard and mouse), each reading the project’s input actions from their own device only.

  • Joining (Players_StartJoining): while joining is on (a lobby), a press of the join input on a device no player has adds a player with that device: by default a gamepad’s South (Xbox A) or Start button, or the keyboard’s Enter or Space; with the project’s joinAction set, that action on the device. Players_Join adds one from code (a single-player game can make player 0 the keyboard at start). At most maxPlayers.
  • Per-player input: Player_IsActionPressed(1, "jump"), Player_GetVector(1, ...) read the same actions as Input_IsActionPressed, from that player’s device: a gamepad player’s gamepad bindings (any device number in them is ignored: the player’s own pad), the keyboard player’s key and mouse bindings (and touch controls). Input_IsActionPressed still reads every device.
  • Disconnects: a player whose gamepad is unplugged stays (connected false, onDisconnected); the next gamepad plugged in while one is missing goes back to the first player waiting (onReconnected). Players_Leave removes a player; the later ones move down a number.
  • Events (Game.input.players): onJoined (data: the player’s number, an int), onLeft, onDisconnected, onReconnected.
  • Split screen: a CameraView per player (camera_tools.odin) placed by SplitScreen_Rect.

Players are numbered from 0 in the order they joined.

Example

// A lobby: everyone presses A (or Enter) to join
e.Players_StartJoining()
// Later, in a player's script (its `player` field is exported: set per node)
// move := e.Player_GetVector(self.player, "left", "right", "up", "down")
SteamAchievementData :: struct {
// Steam's API name ("FIRST_BLOOD")
name: string,
// What players see, and how to get it (tools show them; Steam shows its own)
title: string,
description: string,
// Hidden until unlocked (on Steam's pages)
hidden: bool,
}

An achievement the project lists: <achievement name title description hidden/>

struct. Source: steam.odin:38

SteamSettings :: struct {
// The game's Steam app ID (0: not set)
appId: u32,
// Saves (`Save_Game`) go to Steam Cloud files
cloudSaves: bool,
}

The project’s Steam settings (project.xml’s steamAppId, steamCloud)

struct. Source: steam.odin:49

SteamBackend :: struct {
// Shown in logs ("stub")
name: string,
// Starts it (the project loaded); returns whether Steam is reachable
start: proc(steam: ^SteamState) -> bool,
// Writes what's pending and stops (the game ends)
stop: proc(steam: ^SteamState),
// Achievements by name
unlock: proc(steam: ^SteamState, name: string) -> bool,
clear: proc(steam: ^SteamState, name: string) -> bool,
isUnlocked: proc(steam: ^SteamState, name: string) -> bool,
// Stats by name
setStat: proc(steam: ^SteamState, name: string, value: f64) -> bool,
getStat: proc(steam: ^SteamState, name: string) -> (f64, bool),
// Sends stats and achievements
store: proc(steam: ^SteamState) -> bool,
// Cloud files by name
cloudWrite: proc(steam: ^SteamState, file: string, data: []byte) -> bool,
cloudRead: proc(steam: ^SteamState, file: string) -> ([]byte, bool),
cloudDelete: proc(steam: ^SteamState, file: string) -> bool,
cloudExists: proc(steam: ^SteamState, file: string) -> bool,
}

What runs the Steam calls. Every proc gets the game’s Steam state. The stub (STEAM_STUB_BACKEND) keeps everything locally; the Steamworks one will call Steam.

struct. Source: steam.odin:60

SteamState :: struct {
// The game (for its save names)
game: ^Game,
// The project's settings and achievements (in the game's arena)
settings: SteamSettings,
achievements: [dynamic]SteamAchievementData,
// What runs the calls (the stub by default)
backend: SteamBackend,
// Started (Game_LoadProject), and whether Steam answered
started, running: bool,
// A new unlock (data: the name, a string)
onAchievementUnlocked: EventSubject,
// The stub's own state: unlocked achievements and stats (heap)
stub: SteamStubData,
}

A game’s Steam state (Game.steam)

struct. Source: steam.odin:84

SteamStubData :: struct {
unlocked: map[string]bool,
stats: map[string]f64,
dirty: bool,
}

What the stub keeps (written to save storage as JSON)

struct. Source: steam.odin:101

Touch :: struct {
// SDL's id for the finger (the same while it's down)
id: sdl.FingerID,
// Where it is, where it went down, and how far it moved this frame, in game coordinates
position, start, motion: [2]f32,
// How hard it presses (0 to 1; 1 on screens that can't tell)
pressure: f32,
// It went down this frame; it was lifted this frame (it's gone next frame)
pressed, released: bool,
// A VirtualJoystick or TouchButton took it
claimed: bool,
}

A finger on the screen

struct. Source: touch.odin:45

TouchVisibility :: enum {
// Once the screen has been touched, or in development runs (Play from the editor)
Touch_Screens,
// Always (also on desktops without touches)
Always,
// Never (it still works: an invisible touch zone)
Never,
}

When a touch control shows

enum. Source: touch.odin:59

TouchControlSettings :: struct {
// The stick's or button's radius, in game pixels
radius: f32,
// A stick moved less than this fraction of its radius counts as centered
deadzone: f32,
// The area that takes a finger, centered on the node (0 x 0: the circle itself)
zone: [2]f32,
// A joystick that starts where the finger lands in its zone (and goes back when it lifts)
floating: bool,
// When it's drawn
visibility: TouchVisibility,
// The mouse works it too (trying touch controls on a desktop)
mouse: bool,
// Its color (the knob and a pressed button are brighter) and opacity
color: sdl.Color,
opacity: f32,
}

A VirtualJoystick’s or TouchButton’s settings (scene files’ attributes; a plain value)

struct. Source: touch.odin:69

TouchControlKind :: enum {
Joystick,
Button,
}

What a touch control is

enum. Source: touch.odin:100

TouchControl :: struct {
// The node part: transform, hierarchy, hooks
using node: Node,
// Which control it is
kind: TouchControlKind,
// Its settings
settings: TouchControlSettings,
// The actions it presses: a joystick's left, right, up, down; a button's first ("" none). In
// the node's arena
actions: [4]string,
// Being held: by a finger (its id), or by the mouse
held, byMouse: bool,
finger: sdl.FingerID,
// Where the stick's base is now (a floating one moves), in game coordinates
base: [2]f32,
// The stick's position: -1..1 each way, deadzone applied (a button: {0, 0})
value: [2]f32,
// The input frame it last updated in (Input_Update uses controls that updated last frame)
updatedFrame: u64,
}

The part VirtualJoystick and TouchButton share: settings, the finger it holds, its actions

struct. Source: touch.odin:106

VirtualJoystick :: struct {
using control: TouchControl,
}

A stick on the screen moved by a finger, pushing input actions

struct. Source: touch.odin:128

TouchButton :: struct {
using control: TouchControl,
// Its label (in the node's arena; "" none)
text: string,
// Touched (data: none); let go
onPressed: EventSubject `event:"The button was touched"`,
onReleased: EventSubject `event:"The finger left the button"`,
}

A round button on the screen pressing an input action while touched

struct. Source: touch.odin:133

PlayerDeviceKind :: enum {
// The keyboard and mouse (and touch controls)
Keyboard,
// A gamepad
Gamepad,
}

What a player plays with

enum. Source: players.odin:41

Player :: struct {
// Its number (0 the first to join)
index: int,
// What it plays with
device: PlayerDeviceKind,
// Its gamepad (SDL's id; kept while unplugged, to tell it apart)
gamepad: sdl.JoystickID,
// Its gamepad is plugged in (a keyboard player always is)
connected: bool,
// Its actions this frame (one per input action, in order)
actions: [dynamic]PlayerActionState,
}

A local player

struct. Source: players.odin:49

PlayerActionState :: struct {
strength: f32,
down, pressed, released: bool,
}

A player’s action this frame

struct. Source: players.odin:63

PlayerSettings :: struct {
// The most that can join (1 to MAX_LOCAL_PLAYERS)
maxPlayers: int,
// The keyboard (and mouse) can be a player's device
keyboard: bool,
// The input action that joins ("": a gamepad's South or Start, the keyboard's Enter or Space).
// In the game's arena
joinAction: string,
}

The project’s players settings (project.xml’s <players max keyboard joinAction/>)

struct. Source: players.odin:69

PlayersState :: struct {
settings: PlayerSettings,
// Joining is on (`Players_StartJoining`)
joining: bool,
// The players, in order
list: [dynamic]Player,
// A player joined, left, lost its gamepad, got one back (data: the player's number)
onJoined, onLeft, onDisconnected, onReconnected: EventSubject,
// The events are set up (Players_Setup, or on first use)
ready: bool,
}

The game’s players (InputState.players)

struct. Source: players.odin:83

Steam_Setup :: proc(game: ^Game, settings: SteamSettings, achievements: []SteamAchievementData)

Sets the project’s Steam settings and achievements and starts the backend (Game_LoadProject does it with the project’s).

Inputs

  • game: The game
  • settings: App ID, cloud saves
  • achievements: The achievements (copied into the game’s arena)

procedure. Source: steam.odin:116

Steam_SetBackend :: proc(backend: SteamBackend, game: ^Game = nil)

Replaces what runs the Steam calls (before the project loads, or between runs).

Inputs

  • backend: The backend
  • game: The game (nil: the current one)

procedure. Source: steam.odin:140

Steam_IsRunning :: proc(game: ^Game = nil) -> bool

Whether Steam answers (the stub always does)

procedure. Source: steam.odin:149

Steam_BackendName :: proc(game: ^Game = nil) -> string

The backend’s name (“stub”)

procedure. Source: steam.odin:155

Steam_GetAchievements :: proc(game: ^Game = nil) -> []SteamAchievementData

The achievements the project lists

procedure. Source: steam.odin:161

Steam_UnlockAchievement :: proc(name: string, game: ^Game = nil) -> bool

Unlocks an achievement (once: unlocking it again does nothing). Steam keeps it once stats are stored (Steam_StoreStats); the stub writes it at once.

Inputs

  • name: Its API name (“FIRST_BLOOD”); one the project doesn’t list is warned about
  • game: The game (nil: the current one)

Returns

  • Whether it’s unlocked now

procedure. Source: steam.odin:177

Steam_IsAchievementUnlocked :: proc(name: string, game: ^Game = nil) -> bool

Whether an achievement is unlocked

procedure. Source: steam.odin:188

Steam_ClearAchievement :: proc(name: string, game: ^Game = nil) -> bool

Locks an achievement again (testing). Returns whether it worked

procedure. Source: steam.odin:194

Steam_SetStat :: proc(name: string, value: f64, game: ^Game = nil) -> bool

Sets a stat.

Inputs

  • name: Its API name (“ENEMIES_DEFEATED”)
  • value: The value
  • game: The game (nil: the current one)

Returns

  • Whether it was set

procedure. Source: steam.odin:210

Steam_AddStat :: proc(name: string, delta: f64, game: ^Game = nil) -> bool

Adds to a stat (0 when it was never set). Returns whether it was set

procedure. Source: steam.odin:216

Steam_GetStat :: proc(name: string, game: ^Game = nil) -> f64

A stat’s value (0 when it was never set)

procedure. Source: steam.odin:221

Steam_StoreStats :: proc(game: ^Game = nil) -> bool

Sends stats and unlocked achievements to Steam (the stub writes them). Returns whether it worked

procedure. Source: steam.odin:229

Steam_CloudWrite :: proc(file: string, data: []byte, game: ^Game = nil) -> bool

Writes a Steam Cloud file.

Inputs

  • file: Its name (“save1”; letters, digits, -, _ and .)
  • data: Its contents
  • game: The game (nil: the current one)

Returns

  • Whether it was written

procedure. Source: steam.odin:245

Steam_CloudRead :: proc(file: string, game: ^Game = nil) -> ([]byte, bool)

Reads a Steam Cloud file (temp allocated). Returns false when there’s none

procedure. Source: steam.odin:251

Steam_CloudDelete :: proc(file: string, game: ^Game = nil) -> bool

Deletes a Steam Cloud file. Returns whether there was one

procedure. Source: steam.odin:258

Steam_CloudExists :: proc(file: string, game: ^Game = nil) -> bool

Whether a Steam Cloud file exists

procedure. Source: steam.odin:264

TouchControl_Defaults :: proc(kind: SceneNodeKind) -> TouchControlSettings

A touch control kind’s default settings (a button is smaller); other kinds get a joystick’s

procedure. Source: touch.odin:93

Input_GetTouches :: proc(game: ^Game = nil) -> []Touch

Every finger on the screen this frame (and those lifted this frame, released).

Inputs

  • game: The game (nil: the current one)

Returns

  • The touches (valid until the next update)

Example

for touch in e.Input_GetTouches() {
if touch.pressed && !touch.claimed do e.Debug_Circle(scene, touch.position, 20, {255, 0, 0, 255}, seconds = 0.5)
}

procedure. Source: touch.odin:156

Input_GetTouchCount :: proc(game: ^Game = nil) -> int

How many fingers are down (lifted ones not counted)

procedure. Source: touch.odin:163

Input_IsTouchScreen :: proc(game: ^Game = nil) -> bool

Whether the screen has been touched since the game started (touch controls show then)

procedure. Source: touch.odin:172

CreateVirtualJoystick :: proc(scene: ^Scene, actions: [4]string = {}, settings := DEFAULT_TOUCH_CONTROL_SETTINGS) -> ^VirtualJoystick

Creates a VirtualJoystick and adds it to the scene (at the start of the next update).

Inputs

  • scene: The scene
  • actions: The input actions it pushes: left, right, up, down (“” for none)
  • settings: Its radius, deadzone, zone, look

Returns

  • The joystick

Example

stick := e.CreateVirtualJoystick(scene, {"move_left", "move_right", "move_up", "move_down"})
stick.settings.floating = true
stick.settings.zone = {400, 400}

procedure. Source: touch.odin:193

CreateTouchButton :: proc(scene: ^Scene, action := "", text := "", settings := TouchControlSettings{}) -> ^TouchButton

Creates a TouchButton and adds it to the scene (at the start of the next update).

Inputs

  • scene: The scene
  • action: The input action it presses (“” none: listen to onPressed)
  • text: Its label
  • settings: Its radius, zone, look (the radius defaults to TOUCH_BUTTON_RADIUS)

Returns

  • The button

Example

jump := e.CreateTouchButton(scene, "jump", "A")
jump.position = {1160, 620}

procedure. Source: touch.odin:217

VirtualJoystick_GetVector :: proc(joystick: ^VirtualJoystick) -> [2]f32

A joystick’s position: -1..1 each way (x right, y down), the deadzone applied

procedure. Source: touch.odin:234

TouchButton_IsPressed :: proc(button: ^TouchButton) -> bool

Whether a touch button is held

procedure. Source: touch.odin:240

TouchControl_SetActions :: proc(control: ^TouchControl, actions: [4]string)

Sets the actions a touch control presses (a joystick’s left, right, up, down; a button’s first).

Inputs

  • control: A VirtualJoystick or TouchButton
  • actions: The action names (“” none)

procedure. Source: touch.odin:252

TouchControl_ParseActions :: proc(text: string) -> (actions: [4]string)

A control’s actions as scene files store them: comma-separated, a joystick’s left, right, up, down ("move_left,move_right,move_up,move_down"; empty ones for none).

Inputs

  • text: The comma-separated names

Returns

  • The four names (slices of text)

procedure. Source: touch.odin:267

TouchControl_FormatActions :: proc(given: [4]string) -> string

The actions as scene files store them (temp allocated; trailing empty ones left out)

procedure. Source: touch.odin:279

Players_Setup :: proc(game: ^Game, settings: PlayerSettings)

Sets the players settings (Game_LoadProject does it with the project’s).

Inputs

  • game: The game
  • settings: Max players, the keyboard, the join action (copied)

procedure. Source: players.odin:102

Players_StartJoining :: proc(game: ^Game = nil)

Turns joining on: a press of the join input on a free device adds a player

procedure. Source: players.odin:114

Players_StopJoining :: proc(game: ^Game = nil)

Turns joining off (the lobby is done)

procedure. Source: players.odin:119

Players_IsJoining :: proc(game: ^Game = nil) -> bool

Whether joining is on

procedure. Source: players.odin:124

Players_Join :: proc(device: PlayerDeviceKind, gamepad := 0, game: ^Game = nil) -> int

Adds a player with a device, from code.

Inputs

  • device: The keyboard, or a gamepad
  • gamepad: Which gamepad (its number among the connected ones, 0 the first) for a gamepad player
  • game: The game (nil: the current one)

Returns

  • The new player’s number; -1 when the game is full, the device is taken, or there’s no such gamepad

Example

// A one-player game: the keyboard is player 0
e.Players_Join(.Keyboard)

procedure. Source: players.odin:145

Players_Leave :: proc(index: int, game: ^Game = nil)

Removes a player. The players after it move down a number (player 2 becomes 1).

Inputs

  • index: The player’s number
  • game: The game (nil: the current one)

procedure. Source: players.odin:165

Players_Clear :: proc(game: ^Game = nil)

Removes every player

procedure. Source: players.odin:178

Players_Count :: proc(game: ^Game = nil) -> int

How many players have joined

procedure. Source: players.odin:186

Players_Get :: proc(index: int, game: ^Game = nil) -> ^Player

A player (nil: no such player); valid until a player joins or leaves

procedure. Source: players.odin:192

Player_IsActionPressed :: proc(player: int, name: string, game: ^Game = nil) -> bool

Whether a player’s action is held

procedure. Source: players.odin:199

Player_IsActionJustPressed :: proc(player: int, name: string, game: ^Game = nil) -> bool

Whether a player’s action went down this frame

procedure. Source: players.odin:205

Player_IsActionJustReleased :: proc(player: int, name: string, game: ^Game = nil) -> bool

Whether a player’s action was let go this frame

procedure. Source: players.odin:211

Player_GetActionStrength :: proc(player: int, name: string, game: ^Game = nil) -> f32

How strongly a player’s action is pressed (0 to 1)

procedure. Source: players.odin:217

Player_GetAxis :: proc(player: int, negative, positive: string, game: ^Game = nil) -> f32

A player’s axis from two actions: positive minus negative (-1 to 1)

procedure. Source: players.odin:223

Player_GetVector :: proc(player: int, negativeX, positiveX, negativeY, positiveY: string, game: ^Game = nil) -> [2]f32

A player’s direction from four actions, at most 1 long (diagonals aren’t faster).

Inputs

  • player: The player’s number
  • negativeX, positiveX, negativeY, positiveY: Left, right, up, down
  • game: The game (nil: the current one)

Returns

  • The direction (y down)

procedure. Source: players.odin:238

Player_Rumble :: proc(player: int, low, high: f32, seconds: f32, game: ^Game = nil) -> bool

Rumbles a gamepad player’s gamepad (low and high frequency motors 0 to 1). Returns whether it took

procedure. Source: players.odin:246

Player_DeviceName :: proc(player: int, game: ^Game = nil) -> string

What a player plays with, for a lobby (“Keyboard”, the gamepad’s name, “(unplugged)”)

procedure. Source: players.odin:256

SplitScreen_Rect :: proc(player, count: int, area: [2]f32, gap: f32 = 0) -> sdl.FRect

Where a player’s view goes on a split screen: the area split for count players (1: all of it; 2: side by side; 3: one on top, two below; 4: four corners; more: a grid of columns).

Inputs

  • player: The player’s number (0 first)
  • count: How many players share the screen
  • area: The screen (the game’s size)
  • gap: Pixels between views

Returns

  • The player’s rectangle

Example

// Given: views: []^e.CameraView, one per player
for view, i in views {
r := e.SplitScreen_Rect(i, len(views), game.size)
view.position = {r.x, r.y}
view.size = {r.w, r.h}
}

procedure. Source: players.odin:288

STEAM_STUB_BACKEND :: SteamBackend{
name = "stub",
start = stubStart,
stop = stubStop,
unlock = proc(steam: ^SteamState, name: string) -> bool {
if name == "" do return false
steam.stub.unlocked[strings.clone(name)] = true
steam.stub.dirty = true
log.infof("Steam (stub): achievement %s unlocked", name)
stubWrite(steam)
return true
},
clear = proc(steam: ^SteamState, name: string) -> bool {
if name in steam.stub.unlocked {
key, _ := delete_key(&steam.stub.unlocked, name)
delete(key)
steam.stub.dirty = true
stubWrite(steam)
}
return true
},
isUnlocked = proc(steam: ^SteamState, name: string) -> bool {
return steam.stub.unlocked[name]
},
setStat = proc(steam: ^SteamState, name: string, value: f64) -> bool {
if name == "" do return false
if _, ok := steam.stub.stats[name]; ok {
steam.stub.stats[name] = value
} else {
steam.stub.stats[strings.clone(name)] = value
}
steam.stub.dirty = true
return true
},
getStat = proc(steam: ^SteamState, name: string) -> (f64, bool) {
value, ok := steam.stub.stats[name]
return value, ok
},
store = proc(steam: ^SteamState) -> bool {
return stubWrite(steam)
},
cloudWrite = proc(steam: ^SteamState, file: string, data: []byte) -> bool {
name, ok := stubCloudName(steam, file)
return ok && Platform_SaveWrite(name, data)
},
cloudRead = proc(steam: ^SteamState, file: string) -> ([]byte, bool) {
name, ok := stubCloudName(steam, file)
if !ok do return nil, false
return Platform_SaveRead(name)
},
cloudDelete = proc(steam: ^SteamState, file: string) -> bool {
name, ok := stubCloudName(steam, file)
if !ok || !Platform_SaveExists(name) do return false
Platform_SaveDelete(name)
return true
},
cloudExists = proc(steam: ^SteamState, file: string) -> bool {
name, ok := stubCloudName(steam, file)
return ok && Platform_SaveExists(name)
},
// ... 1 more lines (see the source)

The stub backend: achievements and stats kept in the game’s save storage (steam-stub), cloud files as save files (steam-cloud-<file>), each unlock logged. Every game runs it until the Steamworks backend exists.

constant. Source: steam.odin:274

DEFAULT_TOUCH_CONTROL_SETTINGS :: TouchControlSettings{radius = 64, deadzone = 0.2, mouse = true, color = {255, 255, 255, 255}, opacity = 0.5}

A joystick’s defaults (a TouchButton’s radius is TOUCH_BUTTON_RADIUS)

constant. Source: touch.odin:88

TOUCH_BUTTON_RADIUS :: 40

A TouchButton’s radius by default

constant. Source: touch.odin:90

MAX_LOCAL_PLAYERS :: 8

The most local players a game can have

constant. Source: players.odin:38

DEFAULT_PLAYER_SETTINGS :: PlayerSettings{maxPlayers = 4, keyboard = true}

Four players, the keyboard among them, joining with the default buttons

constant. Source: players.odin:80