The OpenD Programming Language

arsd.game

An add-on for simpledisplay.d, joystick.d, and simpleaudio.d that includes helper functions for writing simple games (and perhaps other multimedia programs). Whereas simpledisplay works with an event-driven framework, arsd.game always uses a consistent timer for updates.

I AM NO LONGER HAPPY WITH THIS INTERFACE AND IT WILL CHANGE.

While arsd 11 included an overhaul (so you might want to fork an older version if you relied on it, but the transition is worth it and wasn't too hard for my game), there's still more stuff changing.

This is considered unstable as of arsd 11.0 and will not re-stabilize until some 11.x release to be determined in the future (and then it might break again in 12.0, but i'll commit to long term stabilization after that at the latest).

Public Imports

arsd.gamehelpers
public import arsd.gamehelpers;
Undocumented in source.
arsd.color
public import arsd.color;
Undocumented in source.
arsd.simpledisplay
public import arsd.simpledisplay;
Undocumented in source.
arsd.simpleaudio
public import arsd.simpleaudio;
Undocumented in source.
core.time
public import core.time;
Undocumented in source.
arsd.joystick
public import arsd.joystick;
Undocumented in source.

Members

Classes

GameHelperBase
class GameHelperBase

This is the base class for your game. Create a class based on this, then pass it to runGame.

GameScreen
class GameScreen(Game)

Tip: if your screen is a generic component reused across many games, you might pass GameHelperBase as the Game parameter.

GameScreenBase
class GameScreenBase

See GameScreen for the thing you are supposed to use. This is just for internal use by the arsd.game library.

OpenGlTexture
class OpenGlTexture

Simple class for putting a TrueColorImage in as an OpenGL texture.

ScreenPainterImpl
class ScreenPainterImpl

NOT fully compatible with simpledisplay's screenpainter, but emulates some of its api.

Functions

clearOpenGlScreen
void clearOpenGlScreen(SimpleWindow window)
create2dWindow
SimpleWindow create2dWindow(string title, int width, int height)

Creates a simple 2d (old-style) opengl simpledisplay window. It sets the matrix for pixel coordinates and enables alpha blending and textures.

runGame
deprecated void runGame(GameHelperBase game, int targetUpdateRate, int maxRedrawRate)

Deprecated, use the other overload instead.

runGame
void runGame(int targetUpdateRate, int maxRedrawRate)

Runs your game. It will construct the given class and destroy it at end of scope. Your class must have a default constructor and must implement GameHelperBase. Your class should also probably be final for a small, but easy performance boost.

Interfaces

BasicDrawing
interface BasicDrawing

Structs

VirtualController
struct VirtualController

The virtual controller is based on the SNES. If you need more detail, try using the joystick or keyboard and mouse members directly.

Detailed Description

The general idea is you provide a game class which implements a minimum of three functions: update, drawFrame, and getWindow. Your main function calls runGame!YourClass();.

getWindow is called first. It is responsible for creating the window and initializing your setup. Then the game loop is started, which will call update, to update your game state, and drawFrame, which draws the current state.

update is called on a consistent timer. It should always do exactly one delta-time step of your game work and the library will ensure it is called often enough to keep game time where it should be with real time. drawFrame will be called when an opportunity arises, possibly more or less often than update is called. drawFrame gets an argument telling you how close it is to the next update that you can use for interpolation.

How, exactly, you decide to draw and update is up to you, but I strongly recommend that you keep your game state inside the game class, or at least accessible from it. In other words, avoid using global and static variables.

It might be easier to understand by example. Behold:

import arsd.game;

final class MyGame : GameHelperBase {
	/// Called when it is time to redraw the frame. The interpolate member
	/// tells you the fraction of an update has passed since the last update
	/// call; you can use this to make smoother animations if you like.
	override void drawFrame(float interpolate) {
		glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_ACCUM_BUFFER_BIT);

		glLoadIdentity();

		glColor3f(1.0, 1.0, 1.0);
		glTranslatef(x, y, 0);
		glBegin(GL_QUADS);

		glVertex2i(0, 0);
		glVertex2i(16, 0);
		glVertex2i(16, 16);
		glVertex2i(0, 16);

		glEnd();
	}

	int x, y;
	override bool update() {
		x += 1;
		y += 1;
		return true;
	}

	override SimpleWindow getWindow() {
		// if you want to use OpenGL 3 or nanovega or whatever, you can set it up in here too.
		auto window = create2dWindow("My game");
		// load textures and such here
		return window;
	}
}

void main() {
	runGame!MyGame(20 /*targetUpdateRate - shoot for 20 updates per second of game state*/);
	// please note that it can draw faster than this; updates should be less than drawn frames per second.
}

Of course, this isn't much of a game, since there's no input. The GameHelperBase provides a few ways for your update function to check for user input: you can check the current state of and transition since last update of a SNES-style VirtualController through GameHelperBase.snes, or the computer keyboard and mouse through GameHelperBase.keyboardState and (FIXME: expose mouse). Touch events are not implemented at this time and I have no timetable for when they will be, but I do want to add them at some point.

The SNES controller is great if your game can work with it because it will automatically map to various gamepads as well as to the standard computer keyboard. This gives the user a lot of flexibility in how they control the game. If it doesn't though, you can try the other models. However, I don't recommend you try to mix them in the same game mode, since you wouldn't want a user to accidentally trigger the controller while trying to type their name, for example.

If you just do the basics here, you'll have a working basic game. You can also get additional features by implementing more functions, like override bool wantAudio() { return true; } will enable audio, for example. You can then trigger sounds and music to play in your update function.

Let's expand the example to show this:

// FIXME: paste in game2.d contents here

A game usually isn't just one thing, and it might help to separate these out. I call these GameScreens. The name might not be perfect, but the idea is that even a basic game might still have, for example, a title screen and a gameplay screen. These are likely to have different controls, different drawing, and some different state.

The MyGame handler is actually a template, so you don't have virtual function indirection and not all functions are required. The interfaces are just to help you get the signatures right, they don't force virtual dispatch at runtime.

Input

In the overview, I mentioned that there's input available through a few means. Among the functions are:

Checking capabilities: keyboardIsPresent, mouseIsPresent, gamepadIsPresent, joystickIsPresent, touchIsPresent - return true if there's a physical device for this (tho all can be emulated from just keyboard/mouse)

Gamepads, mouse buttons, and keyboards: wasPressed - returns true if the button was not pressed but became pressed over the update period. wasReleased - returns true if the button was pressed, but was released over the update period wasClicked - returns true if the button was released but became pressed and released again since you last asked without much other movement in between isHeld - returns true if the button is currently held down Gamepad specific (remember the keyboard emulates a basic gamepad): startRecordingButtons - starts recording buttons getRecordedButtons - gets the sequence of button presses with associated times stopRecordingButtons - stops recording buttons

You might use this to check for things like cheat codes and fighting game style special moves. Keyboard-specific: startRecordingCharacters - starts recording keyboard character input getRecordedCharacters - returns the characters typed since you started recording characters stopRecordingCharacters - stops recording characters and clears the recording

You might use this for taking input for chat or character name selection.

FIXME: add an on-screen keyboard thing you can use with gamepads too Mouse and joystick: startRecordingPath - starts recording paths, each point coming off the operating system is noted with a timestamp relative to when the recording started getRecordedPath - gets the current recorded path stopRecordingPath - stops recording the path and clears the recording.

You might use this for things like finding circles in Mario Party. Mouse-specific: // actually instead of capture/release i might make it a property of the screen. we'll see. captureCursor - captures the cursor inside the window releaseCursor - releases any existing capture currentPosition - returns the current position over the window, in pixels, with (0,0) being the upper left. changeInPosition - returns the change in position since last time you asked wheelMotion - change in wheel ticks since last time you asked Joystick-specific (be aware that the mouse will act as an emulated joystick): currentPosition - returns the current position of the stick, 0,0 being centered and -1, 1 being the upper left corner and 1,-1 being the lower right position. Note that there is a dead zone in the middle of joysticks that does not count so minute wiggles are filtered out. changeInPosition - returns the change in position since last time you asked

There may also be raw input data available, since this uses arsd.joystick. Touch-specific:

Window control

FIXME: no public functions for this yet.

You can check for resizes and if the user wants to close to give you a chance to save the game before closing. You can also call window.close();. The library normally takes care of this for you.

Minimized windows will put the game on hold automatically. Maximize and full screen is handled automatically. You can request full screen when creating the window, or use the simpledisplay functions in runInGuiThreadAsync (but don't if you don't need to).

Showing and hiding cursor can be done in sdpy too.

Text drawing prolly shouldn't bitmap scale when the window is blown up, e.g. hidpi. Other things can just auto scale tho. The library should take care of this automatically.

You can set window title and icon when creating it too.

Drawing

I try not to force any one drawing model upon you. I offer four options out of the box and any opengl library has a good chance of working with appropriate setup.

The out-of-the-box choices are:

  • Old-style OpenGL, 2d or 3d, with glBegin, glEnd, glRotate, etc. For text, you can use arsd.ttf.OpenGlLimitedFont
  • New-style OpenGL, 2d or 3d, with shaders and your own math libraries. For text, you can use arsd.ttf.OpenGlLimitedFont with new style flag enabled.
  • arsd.nanovega 2d vector graphics. Nanovega supports its own text drawing functions.
  • The BasicDrawing functions provided by arsd.game. To some extent, you'll be able to mix and match these with other drawing models. It is just bare minimum functionality you might find useful made in a more concise form than even old-style opengl or for porting something that uses a ScreenPainter. (not implemented)

Please note that the simpledisplay ScreenPainter will NOT work in a game drawFrame function.

You can switch between 2d and 3d modes when drawing either with opengl functions or with my helper functions like go2d (FIXME: not in the right module yet).

Images

use arsd.image and the OpenGlTexture object.

Text

use OpenGlLimitedFont and maybe OperatingSystemFont

3d models

FIXME add something

Audio

done through arsd.simpleaudio

Collision detection

Nanovega actually offers this but generally you're on your own. arsd's Rectangle functions offer some too.

Labeling variables

You can label and categorize variables in your game to help get and set them automatically. For example, marking them as @Saved and @ResetOnNewDungeon which you use to do batch updates. FIXME: implement this.

Random numbers

std.random works but might want another thing so the seed is saved with the game. An old school trick is to seed it based on some user input, even just time it took then to go past the title screen.

Screenshots

simpledisplay has a function for it. FIXME give a one-stop function here.

Stuff missing from raylib that might be useful

the screen space functions. the 3d model stuff.

Online play

FIXME: not implemented

If you make your games input strictly use the virtual controller functions, it supports multiple players. Locally, they can be multiple gamepads plugged in to the computer. Over the network, you can have multiple players connect to someone acting as a server and it sends input from each player's computers to everyone else which is exposed to the game as other virtual controllers.

The way this works is before your game actually starts running, if the game was run with the network flag (which can come from command line or through the runGame parameter), one player will act as the server and others will connect to them

There is also a chat function built in.

getUserChat(recipients, prompt) - tells the input system that you want to accept a user chat message. drawUserChat(Point, Color, Font) - returns null if not getting user chat, otherwise returns the current string (what about the carat?) cancelGetChat - cancels a getUserChat.

sendBotChat(recipients, sender, message) - sends a chat from your program to the other users (will be marked as a bot message)

getChatHistory getLatestChat - returns the latest chat not yet returned, or null if none have come in recently

Chat messages take an argument defining the recipients, which you might want to limit if there are teams.

In your Game object, there is a filterUserChat method you can optionally implement. This is given the message they typed. If you return the message, it will send it to other players. Or you can return null to cancel sending it on the network. You might then use the chat function to implement cheat codes like the old Warcraft and Starcraft games. If the player is not connected on the network, nothing happens even if you do return a message, since there is nobody to send it to.

You can also implement a chatHistoryLength which tells how many messages to keep in memory.

Finally, you can send custom network messages with sendNetworkUpdate and getNetworkUpdate, which work with your own arbitrary structs that represent data packets. Each one can be sent to recipients like chat messages but this is strictly for the program to read These take an argument to decide if it should be the tcp or udp connections.

Split screen

When playing locally, you might want to split your window for multiple players to see. The library might offer functions to help you in future versions. Your code should realize when it is split screen and adjust the ui accordingly regardless.

Library internals

To better understand why things work the way they do, here's an overview of the internal architecture of the library. Much of the information here may be changed in future versions of the library, so try to think more about the concepts than the specifics as you read.

The game clock

Thread layout

It runs four threads: a UI thread, a graphics thread, an audio thread, and a game thread.

The UI thread runs your getWindow function, but otherwise is managed by the library. It handles input messages, window resizes, and other things. Being built on arsd.simpledisplay, it is possible for you to do work in it with the runInGuiThread and runInGuiThreadAsync functions, which might be useful if, for example, you wanted to open other windows. But you should generally avoid it.

The graphics thread runs your load and drawFrame functions. It gets the OpenGL context bound to it after the window is created, and expects to always have it. Since OpenGL contexts cannot be simultaneously shared across two threads, this means your other functions shouldn't try to access any of these objects. (It is possible to release the context from one thread, then attach it in another - indeed, the library does this between getWindow and load - but doing this in your user code is not supported and you'd try it at your own risk.)

The audio thread is created if wantAudio is true and is communicated to via the audio object in your game class. The library manages it for you and the methods in the audio object tell it what to do. You are permitted to call these from your update function, or to load sound assets from your load function.

Finally, the game thread is responsible for running your update function at a regular interval. The library coordinates sharing your game state between it and the graphics thread with a mutex. You can get more fine-grained control over this by overriding updateWithManualLock. The default is for drawFrame and update to never run simultaneously to keep data sharing to a minimum, but if you know what you're doing, you can make the lock time very very small by limiting the amount of writable data is actually shared. The default is what it is to keep things simple for you and should work most the time, though.

Most computer programs are written either as batch processors or as event-driven applications. Batch processors do their work when requested, then exit. Event-driven applications, including many video games, wait for something to happen, like the user pressing a key or clicking the mouse, respond to it, then go back to waiting. These might do some animations, but this is the exception to its run time, not the rule. You are assumed to be waiting for events, but can requestAnimationFrame for the special occasions.

But this is the rule for the third category of programs: time-driven programs, and many video games fall into this category. This is what arsd.game tries to make easy. It assumes you want a timed update and a steady stream of animation frames, and if you want to make an exception, you can pause updates until an event comes in. FIXME: pauseUntilNextInput. designFps = 0, requestAnimationFrame, requestAnimation(duration)

Webassembly implementation

See Also

Meta

History

The GameHelperBase, indeed most the module, was completely redesigned in November 2022. If you have code that depended on the old way, you're probably better off keeping a copy of the old module and not updating it again.

However, if you want to update it, you can approximate the old behavior by making a single GameScreen and moving most your code into it, especially the drawFrame and update methods, and returning that as the firstScreen.