C hosts
A game needs no C: tide runs it in a window. This page is for working inside Tide's own repo, where CMake builds games, and for C programs that drive a game themselves: tests, tools, custom hosts. See AGENTS.md for building the repo.
tide_add_game
tide_add_game(<target> [SOURCES <file.tide|file.c>...] [HOST <file.c>...] [NAME <name>] [TITLE <title>] [STATS] [LAYOUT] [WARNINGS <text>...])- The game is every
.tidefile in the current source folder and its subfolders.SOURCESlists the files instead. - The game's C, which defines its
externfunctions (see Calling C), is every.cfile there but theHOSTones, or the.cfiles listed inSOURCES. Unliketide, CMake doesn't pick up prebuilt libraries: link them to the target yourself. - Without
HOST, the game is the whole program: a generatedmainruns it in a window, titledTITLE, or the game'stitlesetting, or<target>.STATSshows the frame rate, ping, bandwidth, tick, entity count and threads in a corner, astide run --statsdoes. On the web, it's<target>.html. - With
HOST, those C files are the program. They include<name>.h, the generated header, whereNAMEdefaults to<target>.LAYOUTalso describes the game's data layout, astide rundoes for hot reloading (tide_game_layout, intide/layout.h). - Warnings are errors in the repo's build: a warning from tidec fails it, unless part of its text is listed after
WARNINGS, and each of those has to be there. Tests use it for programs tidec warns about on purpose. <target>_scheduleis a build target that prints the game's schedule.
The standard host
tide/run.h is what a game without HOST uses:
#include "game.h"
#include "tide/run.h"
int main(int argc, char **argv)
{
tide_run(&(tide_run_desc){.title = "My game", .argc = argc, .argv = argv});
}It opens a window and runs the game in a session (tide/session.h): it samples this machine's input once per tick, draws the views and the GUI every frame, and takes --host [port], --join code and --connect address from the command line. --host opens the match, in a room (see Multiplayer), and a UDP port too, except on the web. title and tick_rate go over the game's settings (see Settings): leave them out for those, and game_name stands in for a title the game doesn't set. tide_run_desc also has width and height (960 by 540 by default) and stats.
Hosts include tide/platform.h, never raylib: the generated header names types after the game's components, and raylib defines many of the same names.
The generated header
The generated header is the API between the game and its host. Namespaced declarations have their namespace in their C name: Combat.Health is Combat_Health.
The world
tide_worldis the whole match. Its data is in pages it shares with its snapshots, so a world starts zeroed ({0}, static orcalloc), copying the struct isn't a snapshot (tide_world_copyis), andtide_world_free(w)lets it go.tide_world_init(w, dt)clears it (what it had goes), setsTime.dtand the singletons' defaults, and loadsMainif it's the match's.tide_world_start(w, dt, start)starts in another scene.tide_world_tick(w)runs every system once, then applies structural changes and events, and the tasks whose time has come go on.tide_world_tick_on(w, jobs)does the same on threads, with the same results:tide_platform_jobs()gives a pool with one per core (on the web, only in a cross-origin isolated page: NULL elsewhere), and sessions take it asjobsin their desc, as the standard host does. If that leaves no scene loaded, it loadsMainagain when it's the match's (tide_framedoes the same for a localMain).tide_world_ended(w)says whether the match is over: its last scene unloaded andMainis local. A server stops there and tells every player, who go offline withTIDE_DISCONNECT_ENDED.tide_get_<Component>(w, entity)gives an entity's component to change, orNULL.tide_read_<Component>(w, entity)gives it only to read, which leaves the pages the world shares with its snapshots shared.TIDE_AT(w, arch0_Body, Body, row)reads a row's component in an archetype's storage, andTIDE_ENTITY_AT(w, arch0_Body, row)its entity: for tests and tools that go through every entity.tide_world_player_joined(w, player)andtide_world_player_left(w, player)sendPlayerJoinedandPlayerLeft, handled at the end of the next tick.tide_world_copy(to, from)andtide_world_hash(w): snapshots and hashes. A snapshot shares the world's pages until one of them changes a page, so it costs the memory of what's different, and each page keeps its hash until it changes, so hashing reads what changed since the last time.tide_world_pack(w, out, capacity)andtide_world_unpack(w, data, size): the world as bytes, as hot reloading carries it over.tide_world_pack_delta(w, base, need, need_size, &size)andtide_world_unpack_delta(w, base, data, size): the world as a delta, as sessions send it: page by page, what differs frombase, a world the receiver has too, or from the receiver's own world, which lacks the pagesneedsays (tide_world_hash_pageslists the pages' hashes, andtide_world_need_pagessays which of them a world lacks), or the whole world, with neither. The bytes are tofree(). Unpacking checks the world's hash, and is false for bytes or a base that don't make it.tide_world_entity_count(w)andtide_world_print(w), for debugging.- Text fields are offsets into the world's heap: read one with
tide_text_read(&w->heap, field). A grid's cells are too:tide_grid_read(&w->heap, w->Field.cells, x, y, 0, &tide_shape_Grid2_int)gives a cell, or NULL outside the grid or where nothing in its chunk was ever set, which is zero, and the generated header names each grid type's shape.
Local state
tide_localis this machine's local state, outside every world.tide_local_init(local)clears it and sets its defaults, andtide_local_free(local)lets it go.tide_local_pack(local, out, capacity)andtide_local_unpack(local, data, size)are it as bytes, as hot reloading carries it over.TIDE_MAIN_IS_LOCALis defined whenMainis local.tide_frame(w, previous, alpha, local, draw, gui)runs every view once, blending the match betweenpreviousandwbyalpha, then applies the local changes they made, and local tasks whose time has come go on. PassNULLand 1 to drawwas it is, andNULLforwoutside a match.tide_local_frame_time(local, seconds)says how long this frame is, beforetide_frame: local tasks'Wait.Secondscounts it down. Without it, they wait forever.
Input
-
TIDE_HAS_INPUTis defined when the game has an input, andtide_inputnames its type. -
tide_input_sample(devices, local)runs the input'sSamplewith this machine's devices. Give it a copy of the devices that the GUI's own use is taken out of, so a click on a button isn't the game's too, then mark the real ones read, so a press counts once:ctide_devices sampled = devices; tide_gui_hide(&gui, &sampled); tide_input input = tide_input_sample(&sampled, &local); tide_devices_consume(&devices); -
tide_world_set_input(w, player, input)sets a player's input for the next tick, andtide_world_set_server_input(w, input)the server's. Both repair the input first: NaN, bounds, thenSanitize.
Sessions
tide_game_apiis the game as a session runs it (tide_gameintide/session.h), with its settings:tick_rateandtitle, 0 andNULLwhere it sets none. A session whose desc leavestick_rateat 0 starts its matches at the game's rate, or 60. The settings are in the generated.c, not the header, so they don't change the game's hash.- With host migration (
tide_game_api.host_migration), the host that runs a match tells its session the room's code and key every frame (tide_session_set_room, withtide_platform_room_codeandtide_platform_room_key). A session whose match lost its server says so (tide_session_migrating): the host goes to the room again (tide_platform_room_migrate), and oncetide_platform_room_migratedsays whether this machine hosts it now, callstide_session_take_overortide_session_join, or fails the session withTIDE_DISCONNECT_ENDEDif the match ended there. A match that ends tells its transports (tide_transport.end, before they close), which a room passes on to the relay.tide/host.hdoes all of it. tide_local_take_request(local, &request, &start)takes local code's session calls, likeSession.Start, in order: call it until it's false.Clipboard.Copycomes this way too, asTIDE_REQUEST_COPY: put its text on the clipboard withtide_platform_copy.tide_local_set_session,tide_local_connectedandtide_local_disconnectedtell local code where it stands;tide_local_set_sessiontakes whether the match is open too (tide_session_status'sopen), and the code of the room the match is in (tide_platform_room_code), or""while it's closed;tide_local_disconnectedtakes a kick's message (tide_session_event'smessage), or NULL.
A frame
A zeroed tide_draw_list and tide_gui are ready to use. A draw list grows as a frame needs, keeps its memory for the next one, and tide_draw_free lets it go.
tide_draw_reset(&draw);
tide_gui_begin(&gui, &devices, tide_platform_screen_size(), tide_platform_measure_text);
tide_local_frame_time(&local, seconds);
tide_frame(w, previous, alpha, &local, &draw, &gui);
tide_gui_end(&gui, &draw);
tide_platform_draw(&draw);Memory
Worlds grow as they need, with no limits but memory: entities, rows of each archetype, structural changes and events in a tick, and the text and lists in a world's heap. Running out of memory ends the program, saying so. So does a chain of events that never ends, once it's TIDE_MAX_CHAIN deep in one tick (100,000 by default): each event sent, or entity spawned, by a handler of the one before. The message names the event. An archetype keeps its rows in chunks, and the first starts small, so archetypes with a few entities take little memory.