Skip to content

Overall program structure

novabyte edited this page Feb 20, 2025 · 10 revisions

This page is a work in progress. You can help by contributing to the page.

The goal of this page is to provide an abstract overview of the components that make up (Open)RCT2, and how they fit together and/or communicate.

Entry Point

The main entry point for non-Windows systems (Windows instead uses its own DLL proxy) is located in the Cli.cpp file. The main function here calls CommandLine::HandleCommandDefault which handles a few simple commands (about, help) and sets some defaults for global variables like if the game should run in headless mode and paths where game data can be found. Once it checks that all of that happened properly it sets the game to run headless with no graphics (because no graphics have been loaded yet) and creates the game context by calling CreateContext.

Game Context (services)

The game context is comprised of 3 main components: PlatformEnvironment, AudioContext, and UiContext. The PlatformEnvironment informs OpenRCT2 in how to go about loading paths depending on the OS environment it's running in. AudioContext handles playing music and sound effects. UiContext handles the window or screen that OpenRCT2 is presented on. The default game context that is created has dummy audio and ui contexts, and is returned.

Game Initialization & Launch

Initialization

After the game context is created we can call RunOpenRCT2 which will try to Initialise and then Launch the game.

Initialise handles a lot of stuff but the most prominent things it handles are creating the exception handling, creating the AssetPackManager, creating the window, makes sure things are loaded that need to be, initializes Audio, copies files, initializes all viewports, initializes the context, sets the active scene, and initializes repositories and the script engine.

click for full list
  • creating the exception handling
  • whether to show a changelog
  • handles configuring the language
  • gets or prompts for the installation path of RCT2
  • creating the AssetPackManager for managing assets
  • creating the DiscordService if enabled for interacting with discord
  • issuing various warnings
  • creating the window
  • ensuring the user content directories exist
  • initializes Audio
  • populates audio devices
  • initializes ride sounds and info
  • sets whether game sounds are on or off
  • initializes the chat
  • copies original user files
  • loads the base graphics
  • initializes lighting fx
  • initializes all viewports
  • initializes the game context
  • sets the active scene
  • initializes the repositories
  • initializes the script engine

Launch

If Initialise completes with no error and returns true the we can Launch the game. First we check to see if there is a new version of the game so we can inform the player if there is. Then, we switch to the startup scene which by default is set in OpenRCT2.cpp to StartupAction::Title. If the game is running in headless mode, then StartupAction::None or StartupAction::Open are the only actions. Otherwise, depending on the startup action we will decide what scene to load and set it as the active scene and initialize the game networking. By default the title scene is chosen.

Game Loop

After the startup scene is set we can finally jump into the game loop by calling RunGameLoop. The game loop is mainly concerned with keeping track of if the game is finished (if the player quits/closes the game) and if it isn't then it checks to see if a variable frame should be run to capture state, and then calls RunFrame to run the next frame of gameplay.

Run Frame

At this point we know we are dealing with logic for a single-frame of gameplay. Any code put here will most likely run once per frame, so be mindful of performance, but don't optimize until necessary. The first point of order is to get the time that has elapsed since the last frame referred to as deltaTime (delta often standing for "the change in", so the "change in time since the last frame") which is an important variable in most, if not all games to ensure smooth interpolation between frames since the time between frames can differ each time.

We then check again to see if a variable frame should be run to capture any change in state. If it changes from running a variable frame to a fixed frame we need to reset the entity positions back to their end of tick positions of the previous tick. Then, we update the time accumulators (which keep track of game ticks and real time) and then we either RunVariableFrame or RunFixedFrame.

Variable Frame

When running a variable frame we first check if we should draw anything and grab a reference to the EntityTweener in case we have to. Then we let the UI process its messages, which really are just the window inputs captured by SDL (the third party library used for window management) for that frame. There are a lot of events that are handled in this function so there will be a section dedicated to input handling and this will be gone over in depth there.

Next we check if the accumulated ticks is greater or equal to the set kGameUpdateTimeMS (which is the game update interval in milliseconds, (1000 / 40fps) = 25ms) and if so, we loop running tweener.PreTick if we shouldDraw, then we Tick the game, subtract kGameUpdateTimeMS from the ticks accumulated, and then we run tweener.PostTick if we shouldDraw. This repeats until ticks accumulated is less than kGameUpdateTimeMS. There should probably be a more in-depth section about how the game handles tracking ticks and time and the math behind it.

Once all of the ticks have run we call ContextHandleInput to allow the context to do its input handling which is the game specific input. Once all of the game input is handled we can update all of the windows by calling WindowUpdateAll to reflect the changes in each window. Then, if we shouldDraw we tween and then Draw.

Fixed Frame

When running a fixed frame we go straight to letting the UI do its input handling. Then, we wait for the ticks accumulated to become greater or equal to kGameUpdateTimeMS. Once that condition is met we loop and only Tick until it is no longer true.

Similarly to a variable frame, once all of the ticks have run that need to we let the context do its input handling by calling ContextHandleInput and then let all of the windows update by calling WindowUpdateAll. Then, if we ShouldDraw, we call Draw.

Ticks

Pre Tick

Pre ticks currently only apply to the tweener and is only run in variable frames. The tweener.PreTick function first Restores all entities which loops through all entities and moves their position by calling ent->MoveTo(const &CoordsXYZ) which will check to see if the position is valid before moving the entity to that position. Then it Resets by clearing all Entities, PrePos and PostPos. Lastly it repopulates the Entites which also populates PrePos.

Tick

Tick is called in both variable and fixed frames, and at a high level is a wrapper function that calls the various Tick functions of services that need to Tick. Right now there is not a unified interface for ticking. Multiple objects just have Tick functions defined within them. I can see an opportunity for a unified interface here for better modularity and extensibility.

As well as ticking the various things that need ticked, this wrapper function also takes care of updating gCurrentDeltaTime and some other time related housekeeping. It also takes care of updating the chat and processing the evaluation queue of the scripting engine.

Post Tick

Post ticks currently only apply to the tweener and is only run in variable frames. The tweener.PostTick function loops through all Entities and populates PostPos for each entity.

Drawing

Draw at a high level takes care of running the drawing and painting functions using an object that implements IDrawingEngine. Specific details for drawing will be elsewhere, but this interface allows us to implement different renderers in a standardized way. Currently there are only two drawing engines, the software based X8DrawingEngine and the hardware based OpenGLDrawingEngine.

Object Loading

outdated

Every save file has a list of all objects that are required for the scenario. Objects are split into 11 different types. There is a maximum number of objects for each type that can be loaded in each save. When the game first starts it creates an installed objects list of all available objects that are in the object folder. When a save is loaded it checks the installed objects list for a location of the object and loads it. Every object has a checksum to prevent corruption issues. The checksum is checked every time an object is loaded. For more information about the object structure see Objects.

Clone this wiki locally