-
Notifications
You must be signed in to change notification settings - Fork 2
Make Your First Game in 1 Hour
This guide walks you through building a small but complete desktop game using the Gondwana engine. By the end, you will have a running WinForms window, a sprite on a grid, and keyboard-driven movement — all the pieces you need to start building something real.
This version uses the Gondwana CLI and the WinForms GPU backbuffer path.
The completed example mirrors the structure of the Spot demo included in this repository (Demos/Spot/). Spot is a good next read once you finish this guide.
- What You'll Build
- Prerequisites
- Step 1 — Install the Gondwana CLI and templates
- Step 2 — Scaffold a GPU-backed WinForms project
- Step 3 — Add your sprite
- Step 4 — Confirm the generated GPU host
- Step 5 — Understand the host lifecycle
- Step 6 — Fill in
GameHost.cs - Step 7 — Run it
- What the Spot Demo adds
- Key mental model
- Further reading
A tiny game called Wanderer: a coloured bubble sits on an 8×8 grid. The player moves it one cell at a time with the arrow keys. The bubble animates smoothly between cells.
Concepts covered:
| Concept | Where it shows up |
|---|---|
| Project scaffolding | gondwana new winforms |
| GPU render path | --backbuffer gpu |
GameHostBase lifecycle |
Generated host override methods |
| Tilesheets & sprites |
LoadTilesheets and CreateSprites
|
| Scene & scene layer | CreateInitialScene |
| Keyboard input |
OnKeyboardAdapterInitialized and OnKeyDown
|
| Scripted movement |
MoveTo and ScriptedMovementStopped
|
| Clean shutdown | Generated GameWindow.cs
|
- .NET 8 SDK
- Visual Studio 2022 or the .NET CLI
- A square PNG image for your sprite
- 64×64 px is a good size
- You can borrow
bubble-blue.pngfromDemos/Spot/assets/
Install the CLI as a .NET global tool:
dotnet tool install --global Gondwana.CliInstall the Gondwana project templates:
gondwana templates installYou only need to do this once per machine. If the CLI is already installed, use
dotnet tool update --global Gondwana.Cliwhen you want the latest version.
Create a new WinForms Gondwana project using the GPU backbuffer:
gondwana new winforms Wanderer --backbuffer gpu
cd WandererYou can also use the short option:
gondwana new winforms Wanderer -b gpu
cd WandererThe CLI creates the startup shell for you:
| File | What it contains |
|---|---|
Wanderer.csproj |
Gondwana package references and asset content examples |
Program.cs |
Standard [STAThread] WinForms entry point |
GameWindow.cs |
WinForms window and render surface wiring |
GameHost.cs |
Your generated WandererGameHost class with override stubs |
assets/README.txt |
Notes for adding game assets |
Because this guide uses --backbuffer gpu, the generated host should use:
internal sealed class WandererGameHost : WinFormsGpuGameHost
{
internal WandererGameHost(WinFormGpuRenderSurfaceControl renderSurface)
: base(renderSurface) { }
}If your generated host instead uses WinFormsGameHost and WinFormBitmapRenderSurfaceControl, you generated the bitmap path. Re-run the scaffold command with:
gondwana new winforms Wanderer --backbuffer gpuCopy your PNG into the assets\ subfolder.
For this guide, the sprite is named:
assets\bubble-blue.png
Then make sure the file is included as content in Wanderer.csproj:
<ItemGroup>
<Content Include="assets\bubble-blue.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
</ItemGroup>The generated
.csprojmay already include a commented-out content example. Use that pattern.
Open GameHost.cs.
The updated WinForms template gives you a host with many override stubs already in place. For the GPU path, the important part is the base class and constructor:
internal sealed class WandererGameHost : WinFormsGpuGameHost
{
internal WandererGameHost(WinFormGpuRenderSurfaceControl renderSurface)
: base(renderSurface) { }That is the GPU path.
You should also see override stubs similar to these:
protected override void LoadTilesheets()
{
}
protected override Scene CreateInitialScene()
{
return Scene.Empty;
}
protected override void CreateSprites()
{
}
protected override void OnKeyboardAdapterInitialized()
{
}
protected override void UnhookEvents()
{
}Those are the main hooks this guide will fill in.
The generated GameWindow.cs handles the WinForms lifecycle for you.
At a high level:
OnLoad
→ create the game host
OnShown
→ initialize the game host
OnFormClosed
→ dispose the game host
When the host initializes, the useful sequence is:
Initialize()
→ OnInitializing
→ ConfigureLogging
→ ConfigurePlatform
→ OnConfigurePlatform
→ ConfigureInput
→ OnKeyboardAdapterInitialized
→ OnMouseAdapterInitialized
→ OnGamepadManagerInitialized
→ OnTouchAdapterInitialized
→ LoadContent
→ LoadAssets
→ LoadTilesheets
→ LoadAnimationCycles
→ CreateInitialScene
→ CreateInitialViews
→ OnSceneGraphCreated
→ BindScene
→ OnSceneBound
→ InitializeSceneObjects
→ CreateSprites
→ CreateDirectDrawings
→ InitializeEngine
→ OnEngineInitialized
→ StartEngine
→ OnEngineStarted
→ OnInitialized
For this first game, you only need to fill in a few game-specific methods:
LoadTilesheetsCreateInitialSceneCreateSpritesOnKeyboardAdapterInitializedUnhookEvents- the movement handlers
Everything else can stay empty. Empty override stubs are not moral failures. They are future parking spaces.
Open GameHost.cs.
The scaffolded file already includes several using statements. Add any of these that are missing:
using System.Drawing;
using System.Windows.Forms;
using Gondwana;
using Gondwana.Drawing;
using Gondwana.Drawing.Sprites;
using Gondwana.Drawing.Tilesheets;
using Gondwana.Input.Keyboard;
using Gondwana.Movement.Easing;
using Gondwana.Scenes;
using Gondwana.WinForms.Hosting;
using Gondwana.WinForms.Input.Keyboard;
using Gondwana.WinForms.Rendering;Then add these fields near the top of the class:
private const int Columns = 8;
private const int Rows = 8;
private const int CellPx = 64;
private Tilesheet _bubbleTilesheet = null!;
private Sprite _sprite = null!;
private int _gridX = 0;
private int _gridY = 0;
private bool _isMoving = false;A Tilesheet is an image atlas. TileSize tells Gondwana how large one frame is. A single-frame image is the simplest possible case.
Fill in LoadTilesheets:
protected override void LoadTilesheets()
{
_bubbleTilesheet = new Tilesheet("bubble", @"assets\bubble-blue.png");
_bubbleTilesheet.TileSize = new Size(64, 64);
}If you use a different sprite size, change TileSize to match your PNG.
Fill in CreateInitialScene:
protected override Scene CreateInitialScene()
{
var scene = new Scene();
scene.AddLayer(
columnCount: Columns,
rowCount: Rows,
width: CellPx,
height: CellPx,
zOrder: 10,
parallax: 1f,
coordinateSystem: CoordinateSystemTypes.Orthogonal);
scene[0].ShowGridLines = true;
return scene;
}This creates one 8×8 orthogonal layer. Each cell is 64×64 pixels.
The updated template gives you CreateSprites() for this. Use that instead of manually building sprites inside CreateSceneGraph.
Fill in CreateSprites:
protected override void CreateSprites()
{
var layer = Scene![0];
var frame = new Frame(_bubbleTilesheet, 0, 0);
_sprite = SpriteManager.Instance.CreateSprite(layer, frame);
_sprite.SetPosition(new(_gridX, _gridY));
_sprite.RenderSize = new Size(56, 56);
_sprite.VertAlign = VerticalAlignment.Middle;
_sprite.Visible = true;
}The sprite uses frame (0, 0) from the tilesheet. Since this guide uses a single-image tilesheet, that is the only frame.
Gondwana's keyboard adapter fires events on the engine thread. Override OnKeyboardAdapterInitialized to subscribe after the adapter is ready, then explicitly tell it which keys to watch.
protected override void OnKeyboardAdapterInitialized()
{
if (Engine.Input.KeyboardEventPoller is null)
return;
Engine.Input.KeyboardEventPoller.KeyDown += OnKeyDown;
Engine.Input.KeyboardEventPoller.StartMonitoringKey((int)Keys.Left);
Engine.Input.KeyboardEventPoller.StartMonitoringKey((int)Keys.Right);
Engine.Input.KeyboardEventPoller.StartMonitoringKey((int)Keys.Up);
Engine.Input.KeyboardEventPoller.StartMonitoringKey((int)Keys.Down);
}
protected override void UnhookEvents()
{
if (Engine.Input.KeyboardEventPoller is not null)
Engine.Input.KeyboardEventPoller.KeyDown -= OnKeyDown;
}
private void OnKeyDown(KeyDownEventArgs args)
{
if (args.KeyAction != KeyAction.Pressed)
return;
if (_isMoving)
return;
var key = WinFormsKeyboardAdapter.GetKeyFromString(args.KeyConfig.Key);
int newX = _gridX;
int newY = _gridY;
switch (key)
{
case Keys.Left:
newX--;
break;
case Keys.Right:
newX++;
break;
case Keys.Up:
newY--;
break;
case Keys.Down:
newY++;
break;
default:
return;
}
if (newX < 0 || newX >= Columns || newY < 0 || newY >= Rows)
return;
_gridX = newX;
_gridY = newY;
_isMoving = true;
_sprite.Movement.ScriptedMovementStopped += OnMovementStopped;
_sprite.Movement.MoveTo(
new(_gridX, _gridY),
0.15f,
EasingKind.SmootherStep,
0f);
}
private void OnMovementStopped(Gondwana.Movement.Scripted.ScriptedMovement _)
{
_sprite.Movement.ScriptedMovementStopped -= OnMovementStopped;
_isMoving = false;
}The _isMoving guard prevents stacking movement commands while the current animation is still running.
From the project folder:
gondwana runOr use plain dotnet:
dotnet runYou should see an 8×8 grid with a bubble in the top-left corner. Arrow keys glide it across the grid.
Tip: press
F5in Visual Studio to get a debugger-attached run. Gondwana logs atLogLevel.Warningby default; bump it toLogLevel.DebuginInitialize()if you want more engine output.
Spot (Demos/Spot/) is a direct extension of everything you just built:
| Spot feature | The extra pieces it uses |
|---|---|
| Two-to-four players |
Player[] array; SpotGame.NewGame() handles multi-player turn order |
| Clone / Jump moves |
MovementType enum + distance logic in SpotGameField.GetMovementType()
|
| Cell capture |
SpotGameField.CaptureAdjacentCells() run after each move lands |
| AI opponent |
SpotGameField.GetBestMovesForPlayer() + a Gondwana.Timers.Timer for the delay |
| Particle effects |
ParticleEmitter / ParticleSurface configured in CreateDirectDrawings()
|
| Score HUD |
TextBlock + DirectRectangle created in CreateDirectDrawings()
|
| Background music |
Engine.Managers.AudioResources.LoadFromFile() + audioResource.Play()
|
| Sprite animations |
sprite.StartJiggle(), sprite.PulseBy(), sprite.ResizeTo()
|
Read SpotGameHost.cs in full for a working example of every one of these.
GameHostBase
└─ Engine
├─ Scene
│ └─ SceneLayer
│ └─ SceneLayerTile → Sprite → Tilesheet frame
├─ ViewManager
│ └─ View
├─ DirectDrawingManager
└─ Input
For this guide, the WinForms host uses the GPU render path:
WinFormGpuRenderSurfaceControl
→ WinFormsGpuGameHost
→ GpuBackbuffer
→ GPU-backed Skia surface
The gameplay model is the same either way: scenes contain layers, layers contain tiles/sprites, and input changes game state. The difference is presentation: this guide renders through the GPU-backed surface rather than the bitmap render surface.
On bitmap backbuffers, Gondwana's dirty-region queue is the star of the show. On the GPU path, the important first-game lesson is simpler: build the scene, move the sprite, let the GPU-backed host present it.
- Engine Architecture Overview — GitHub Wiki
- API reference — https://isthimius.github.io/Gondwana/
- Wiki guides — https://github.com/Isthimius/Gondwana/wiki