Build Your First Kwyll Game: Gem Patrol

In this tutorial, we will build a small top-down game called Gem Patrol. The player will explore several locations, collect gems, avoid moving enemies, and fire projectiles. The finished project will be a starting point that you can experiment with rather than a complete game.

Along the way, we will use the main building blocks of a Kwyll project: Screens, Instruments, Tiles, Room Types, Locations, Object Types, Object Instances, sounds, and Logic.

You do not need any previous experience with node-based logic. We will build each piece in small steps and test the game regularly.

What we are going to build

Gem Patrol will contain four Locations:

Entrance Yard     Upper Yard
      |                |
Stone Corridor -- Store Room

The two Yard Locations will both use the same Yard Room Type. This will introduce an important Kwyll idea: a Room Type defines a reusable kind of room, while a Location is one particular use of it on the Map.

The controls will be:

  • Left, Right, Up, and Down to move

  • Fire to shoot in the direction the player is facing

By the end of the tutorial, the game will:

  • move the player between connected Locations;

  • stop the player at solid walls;

  • collect gems painted into the room tilemap;

  • remember collected gems after the player leaves and returns;

  • display the number of collected gems in an Instrument;

  • move enemies around the rooms; and

  • let projectiles destroy enemies.

Save the project regularly. It is also useful to keep a copy after each major section so that you have an easy point to return to.

Milestone 1: Build the game world

Our first milestone is the game world without a player. We will make the game Screen, draw a small tile set, create three Room Types, and arrange four Locations on the Map.

When this milestone is complete, Preview will show the first Location inside our game Screen. Nothing will move yet.

Create and save the project

  1. Start Kwyll and create a new project.

  2. Save it as gem_patrol.tres.

Choose a folder where you can keep the project and any copies you make while following the tutorial.

New Project

Before adding anything, take a moment to notice the main editors in Kwyll. We will use the Screens, Rooms, and Map editors in this milestone. Later, we will also spend time in the Object and Logic editors.

Set up the game Screen

A Screen controls what the player sees around the game itself. It can contain the game window, a tilemap used as a border or background, and Instruments that display values such as a score or number of lives.

Before moving the game window, resize it:
  1. Click the gear icon in the top-right of the main toolbar to open Project Settings.

  2. Open the game-window settings.

  3. Set the window Width to 32 cells and its Height to 22 cells.

  4. Accept the changes.

This makes the game window 256 pixels wide and 176 pixels high, leaving two 8-pixel rows for the status area on a 256-by-192-pixel Screen.

Resize the game window in Project Settings before moving it in the Screen Editor. Moving the default-sized game window without first making it smaller can cause Kwyll to crash.
Game Window Size
Now configure the Screen:
  1. Open the Screens editor.

  2. Select the existing Screen. If the project does not contain one, add a new Screen.

  3. Open its properties.

  4. Change its name to Game.

  5. Enable Game Screen? so that the game window appears on it.

  6. Enable Start Screen? so that this is the first Screen shown when the game starts.

  7. Accept the changes.

Game Screen Properties

In the Screen Layout, the blue rectangle represents the game window. Drag it down by two character rows to leave a narrow status area above it. Its final position should be X 0, Y 2 in screen cells.

Leave enough space above the game window for a number showing how many gems have been collected.

Game Screen Layout

Add the gem counter

An Instrument is a value displayed on a Screen. We will add the counter now and connect it to the game Logic in a later milestone.

  1. In the Instruments list, add an Instrument.

  2. Open the new Instrument’s properties.

  3. Name it GemCount.

  4. Set its Type to Integer.

  5. Set its initial Value to 0.

  6. Give it enough width to display at least two digits.

  7. Position it in the status area above the game window.

  8. Accept the changes.

The yellow rectangle in the Screen Layout represents the Instrument. You can drag it to move it or use its bottom-right handle to change its size. Both position and size snap to the 8-by-8 grid.

Gem Count Properties
Game Screen Complete
For now, the Instrument will always show 0. Later, Screen Logic will update it whenever the player collects a gem.

Draw the game tiles

Rooms use a tilemap for their background. A tile is a small reusable picture; on the ZX Spectrum target each tile is 8 by 8 pixels. Tiles can also carry collision and Type information that affects the game.

For this tutorial, keep the artwork simple. Clear shapes make it easier to concentrate on how the project is assembled.

Create these four tiles:

Tile Appearance Purpose

Floor

A plain or lightly patterned square

Walkable background

Wall

A solid brick or block pattern

Stops the player and projectiles

Gem

A small diamond shape

Collected when the player touches it

Decorative floor

A variation of the floor

Makes the rooms easier to distinguish

Add some colour

Kwyll can give a tile default Ink, Paper, and Bright values. These colours are used unless they are replaced by colour information painted into a tilemap or by Logic while the game is running.

Open the tile properties and use these defaults:

Tile Ink Paper Bright

Floor

Leave unset

Leave unset

Off

Wall

Cyan

Black

Off

Gem

Yellow

Black

On

Decorative floor

Green

Black

Off

Leaving the Floor colours unset allows it to use the default colours of its Location. The other defaults make the walls, gems, and decorations easy to recognise without painting colour attributes over every room.

ZX Spectrum colour applies to an 8-by-8 attribute cell. Because each of our tiles is also 8 by 8 pixels, these defaults fit naturally with the room tilemap.
Basic Tiles

Configure the wall

Open the Wall tile’s properties and set its default collision on all four sides. This will eventually prevent the Player from moving through it.

The Floor, Decorative floor, and Gem should not have collision.

Wall Collision

Give the gem a Type

Open the Gem tile’s properties and set its Type to 1.

Type is a number that Logic can use to recognise a tile. In a later milestone, the Player’s Touch Tile trigger will run when the Player touches a tile with a non-zero Type. The value 1 will tell our Logic that the touched tile is a gem.

The other tiles should keep Type 0.

Gem Type

At this point, check that:

  • the Wall has collision on all sides;

  • the Gem has Type 1; and

  • the two Floor tiles have neither collision nor a non-zero Type.

Create the Yard Room Type

A Room Type defines a reusable space. It contains a tilemap and can also contain objects, markers, variables, and Logic. A Room Type does not appear in the game until it is placed on the Map as a Location.

  1. Open the Rooms editor.

  2. Add a Room Type.

  3. Open its properties and name it Yard.

  4. Fill the room with the Floor tile.

  5. Draw Wall tiles around the outside edge.

  6. Leave one opening in the middle of the bottom edge.

  7. Add a few Decorative floor tiles.

  8. Paint two or three Gem tiles in positions that can be reached from the opening.

Keep the opening at least as wide as the Player sprite that you intend to draw. A two-tile-wide opening is a safe starting point for a 16-pixel-wide Player.

Yard Room

The same Yard will later be used for both Entrance Yard and Upper Yard. Giving it a single bottom opening means both uses of the Room Type have the same connection pattern, and neither produces an opening that leads nowhere.

Create the Corridor Room Type

  1. Add another Room Type and name it Corridor.

  2. Fill it with Floor.

  3. Surround it with Wall tiles.

  4. Leave openings in the right and top edges.

  5. Use Decorative floor to make a clear path through the room.

  6. Add one or two Gem tiles.

The top opening will connect to Entrance Yard and the right opening to the Store Room.

Leave a reasonably open space in the middle. We will place a horizontally moving enemy there in a later milestone.

Corridor Room

Create the Store Room Type

  1. Add a third Room Type and name it Store Room.

  2. Fill it with Floor and surround it with Walls.

  3. Leave openings in the left and top edges.

  4. Use short internal walls or blocks to give the room a different shape.

  5. Add two or three Gem tiles.

Do not make the passages too narrow. The Store Room will later contain a vertically moving enemy, and the Player needs enough space to avoid it.

Store Room

Place the Locations on the Map

Open the Map editor. The asset list contains the three Room Types that we have created.

Drag a Room Type from the asset list onto the Map to create a Location. We need four Locations:

Location name Room Type

Entrance Yard

Yard

Stone Corridor

Corridor

Upper Yard

Yard

Store Room

Store Room

  1. Drag Yard onto the Map.

  2. Open the new Location’s properties and name it Entrance Yard.

  3. Drag Corridor below Entrance Yard and name the Location Stone Corridor.

  4. Drag Yard onto the Map a second time, place it to the right of Entrance Yard, and name the Location Upper Yard.

  5. Drag Store Room below Upper Yard and to the right of Stone Corridor. Name the Location Store Room.

  6. Select Entrance Yard and use the Start Location button to make it the Location shown when the game begins.

Arrange the Locations so that their matching room openings meet:

Entrance Yard     Upper Yard
      |                |
Stone Corridor -- Store Room
Place First Location
Entrance Yard Properties
Four Location Map

Take a moment to compare Entrance Yard and Upper Yard. They are two Locations, but both refer to the same Yard Room Type. If you return to the Rooms editor and change the Yard tilemap, both Locations will use the updated design.

Later, each Yard Location will nevertheless remember its own collected gems. That works because variable values associated with Room Logic are stored on the individual Location.

Preview the first milestone

Save the project and open Preview. Start the game.

You should see:

  • the Game Screen;

  • the game window in the position you chose;

  • the GemCount Instrument showing 0; and

  • one of the Locations drawn inside the game window.

Nothing moves yet because the project does not contain a Player Object. That is exactly where we expect to be.

Milestone 1 Preview

Milestone 1 checklist

Before continuing, confirm that:

  • the project is saved as gem_patrol.tres;

  • the game window is 32 cells wide and 22 cells high;

  • the game window is positioned at X 0, Y 2 on the Screen;

  • Game is both a Game Screen and the Start Screen;

  • GemCount is an Integer Instrument with an initial value of 0;

  • the Wall tile has collision on every side;

  • the Gem tile has Type 1;

  • the Wall, Gem, and Decorative floor have the suggested default colours;

  • the project has Yard, Corridor, and Store Room Room Types;

  • the Map has four named Locations;

  • Entrance Yard is marked as the Start Location;

  • Entrance Yard and Upper Yard both use the Yard Room Type; and

  • the openings between neighbouring Locations line up.

If your project matches this list, save another copy as gem_patrol_milestone_1.tres.

Milestone 2: Add the Player

Our second milestone will turn the collection of rooms into a world we can explore. We will draw the Player, create a Player Object Type, place one instance on the Map, and use Logic to move it in four directions.

When this milestone is complete, the Player will move around each Location, stop at solid walls, and travel automatically through matching exits.

Draw the Player sprite

Objects use Sprites rather than Tiles. Tiles are fixed at 8 by 8 pixels, while a Sprite can be larger and can include a mask that allows the room background to show around its shape.

  1. Open the Sprites editor.

  2. Add a Sprite Sheet.

  3. Open its properties and name it Characters.

  4. Set the Sprite Width and Height to 16 pixels.

  5. Enable the Mask flag.

  6. Add one Sprite to the sheet.

  7. Draw a simple top-down character in the 16-by-16 grid.

  8. Use the mask tool to mask the area around the character.

The artwork does not need to be elaborate. A clear body, head, and one small directional detail are enough. We will use a single image for now so that we can concentrate on movement.

Player Sprite Sheet
Player Sprite

Create the Player Object Type

The Sprite is only the picture. An Object Type combines one or more Sprites with animations, default drawing properties, and Logic.

  1. Open the Object Types editor.

  2. Add an Object Type.

  3. Open its properties and name it Player.

  4. Select Mask as its Draw Mode.

  5. Set its default Ink to Magenta and enable Bright.

  6. Leave its Paper colour unset so that the room background can show through the mask.

  7. In the Animation tab, select the Characters Sprite Sheet.

  8. Add an animation.

  9. Add the Player sprite as the animation’s first frame.

This tutorial uses one animation frame for the initial controller. Directional animations can be added later without changing the movement Logic.

Player Object Properties
Player Animation

Add Player variables

Variables give Logic somewhere to store numbers. Every instance of an Object Type receives its own values for the variables defined by that type.

Open the Player’s Logic tab. In the Variables panel, add these three variables:

Variable Purpose Initial value

speed

Number of pixels moved on each update

2

facing_x

Horizontal part of the last direction faced

0

facing_y

Vertical part of the last direction faced

1

Enable Initialised for each variable. The initial values will be set on the Player Object Instance after we place it on the Map.

facing_x = 0 and facing_y = 1 mean that the Player begins facing down. These two variables will be used when we add projectiles.

Player Variables

Place a Player instance on the Map

The Player Object Type defines what a Player looks like and what every Player can do. We still need an Object Instance that actually exists in the game.

  1. Open the Map editor.

  2. Find Player in the Assets panel.

  3. Drag it into Entrance Yard.

  4. Place it on an ordinary Floor area, clear of walls, decorations, and gems.

  5. Open the Player instance properties.

  6. Keep its name as Player.

  7. Enable these flags:

    • Player

    • Collide Bg

    • Visible

    • Active

  8. Set speed to 2.

  9. Set facing_x to 0.

  10. Set facing_y to 1.

  11. Accept the changes.

The Player flag tells Kwyll that this object controls the current Location. When it crosses an open edge, Kwyll looks for a neighbouring Location on the Map and moves the game there.

The Collide Bg flag makes movement respect the collision settings on our Wall tile. Visible draws the Sprite, while Active allows the instance to take part in object interactions later.

Player On Map
Player Instance Properties

Build four-way movement

Return to the Player Object Type and open its Logic tab.

Kwyll Logic is made from Nodes. White connections control the order in which actions happen. Coloured value connections carry numbers or references between Nodes.

Right-click an empty part of the graph, or press kbd:[Shift+A], to open the node menu. Typing part of a Node’s name is usually the quickest way to find it.

Read the controller

  1. Add an Always trigger.

  2. Add a Controller Input Node and keep its Mode set to Pressed.

  3. Set its directional values as follows:

    • Left: -1

    • Right: 1

    • Up: -1

    • Down: 1

Always starts a flow on every game update. Controller Input produces zero for a direction that is not pressed and the configured value for a direction that is pressed.

Using negative values for Left and Up matches Kwyll’s coordinate system: X increases to the right, and Y increases downwards.

Player Controller Input

Calculate horizontal movement

  1. Add a Math Node and set its Operator to Add.

  2. Connect Controller Left to its A input.

  3. Connect Controller Right to its B input.

  4. Add a Get Variable Node and select speed.

  5. Add another Math Node and set its Operator to Multiply.

  6. Connect the result of Left plus Right to one input of the Multiply Node.

  7. Connect speed to its other input.

  8. Add an Object Position Node.

  9. Add a third Math Node and set its Operator to Add.

  10. Connect the current X output of Object Position to one input.

  11. Connect the multiplied movement amount to the other input.

The first addition gives a horizontal direction:

  • Left produces -1.

  • Right produces 1.

  • Neither direction produces 0.

  • Pressing both directions also produces 0.

Multiplying by speed converts that direction into the number of pixels to move. Adding it to the current X position produces the requested new X position.

Player Horizontal Logic

Calculate vertical movement

Build the same calculation for Y:

  1. Add a Math Node set to Add.

  2. Connect Controller Up and Down to its inputs.

  3. Add a Math Node set to Multiply.

  4. Connect the Up-plus-Down result and the same speed value to it.

  5. Add a final Math Node set to Add.

  6. Connect the current Y output of Object Position and the multiplied vertical movement to it.

This produces the requested new Y position. Up subtracts speed and Down adds it.

Player Xy Logic

Move the Player

  1. Add a Move Object Node.

  2. Connect the new X calculation to its X input.

  3. Connect the new Y calculation to its Y input.

  4. Leave Ref unconnected. In Object Logic, an unconnected Ref means the current Object Instance.

  5. Connect the white Flow Out port of Always to the Flow In port of Move Object.

The value calculations do not need white flow connections. They are evaluated when Move Object needs their results.

Player Move Logic

Save the project and test it in Preview.

The Player should:

  • move left, right, up, and down;

  • stop when it reaches a Wall tile; and

  • remain still when no direction is pressed.

Try pressing Left and Right together, then Up and Down together. The opposing values should cancel, leaving no movement on that axis.

Player Moving

Remember the facing direction

When we add projectiles, the game will need to know which way the Player last faced. We will record that now while the controller values are already familiar.

Create a separate Always flow. Add another Controller Input Node using Mode Pressed, with the same directional values as before.

Build a chain of four If Nodes:

  1. Test whether Left is not equal to 0.

    • On True, set facing_x to -1, then set facing_y to 0.

  2. From the first False output, test whether Right is not equal to 0.

    • On True, set facing_x to 1, then set facing_y to 0.

  3. From that False output, test whether Up is not equal to 0.

    • On True, set facing_x to 0, then set facing_y to -1.

  4. From that False output, test whether Down is not equal to 0.

    • On True, set facing_x to 0, then set facing_y to 1.

Connect the second Always trigger to the first If. Connect each False flow to the next test. Each True branch ends after setting both variables.

Checking the directions in this order also gives diagonal input a predictable facing direction: horizontal input takes priority over vertical input.

Player Facing Logic

Test movement between Locations

Save and run Preview again.

  1. Move down through the opening in Entrance Yard.

  2. Confirm that the game changes to Stone Corridor.

  3. Move right from Stone Corridor into the Store Room.

  4. Move up from the Store Room into Upper Yard.

  5. Move down to return to the Store Room.

The Player should appear at the corresponding edge of the new Location and continue to respond to the controller. No explicit change-room Logic is required: the Player flag and the arrangement of Locations on the Map handle this automatically.

Also test the closed edges. Walking into a Wall should stop the Player rather than changing Location.

Player Changed Location

Milestone 2 checklist

Before continuing, confirm that:

  • the Characters Sprite Sheet is 16 by 16 and has a mask;

  • the Player Object Type uses Mask draw mode and has one animation frame;

  • Player Logic defines speed, facing_x, and facing_y;

  • one Player Object Instance exists on the Map in Entrance Yard;

  • the instance has Player, Collide Bg, Visible, and Active enabled;

  • the Player moves in all four directions;

  • Wall collision stops the Player;

  • the Player can travel through all three connected pairs of Locations; and

  • the facing variables retain the last direction pressed.

Save another copy as gem_patrol_milestone_2.tres.

Milestone 3: Collect and remember gems

In this milestone, the gems will become part of the game rather than just part of the scenery. The Player will react to the Gem tile’s Type, remove the tile, play a sound, and increase the counter at the top of the Screen.

We will also record the coordinates of every collected gem in the current Location. When the Player returns, Room Logic will clear those tiles again so that collected gems do not reappear.

This mechanic uses four different places where Kwyll can run Logic:

Logic Responsibility

Player Object

Detect the Gem tile and begin the collection flow

Room

Remember which gem coordinates were collected in each Location

Game

Keep the total number of collected gems

Screen

Display the total in the GemCount Instrument

This may sound like several moving parts, but each part has one small, well-defined job. We will build and test them one at a time.

Create the collection sound

  1. Open the Sound editor.

  2. Open the Beep FX section.

  3. Add a sound effect and name it Collect.

  4. Add a short, bright block or note sequence.

  5. Use the play control to listen to it.

Keep the effect brief. It may play several times in quick succession while the Player moves through a group of gems.

Collect Sound

Detect a Gem tile

Open the Player Object Type and return to its Logic graph. Find an empty area away from the movement flows.

  1. Add a Touch Tile trigger.

  2. Add an If Node.

  3. Connect the Type output of Touch Tile to A on the If.

  4. Leave B set to 1.

  5. Set the Comparison to Equal.

  6. Connect the white Flow Out of Touch Tile to the Flow In of If.

The Gem tile was given Type 1 in Milestone 1. Touch Tile runs when the Player’s bounding rectangle touches a tile with a non-zero Type, and the If ensures that this flow handles only gems.

Touch Gem Logic

Remove the touched Gem

  1. Add a Beep FX Node and select Collect.

  2. Connect the True flow of the If to Beep FX.

  3. Add a Set Tile Node after Beep FX.

  4. Connect X from Touch Tile to X on Set Tile.

  5. Connect Y from Touch Tile to Y on Set Tile.

  6. Set Tile to 3, the index of our Decorative floor tile.

The gems in our rooms were painted over Decorative floor areas, so replacing a gem with tile 3 reveals the surface that was visually beneath it. If your Decorative floor has a different index, use that index instead.

The Gem disappears immediately because its tilemap cell is replaced with Decorative floor. The replacement also prevents the collection flow from running again on the following update: Floor has Type 0, so Touch Tile no longer triggers for that cell.

Remove Gem Logic

Save and test the game now. Touching a gem should play the sound and replace the gem with Floor. The counter will still show 0, and the gem will return after leaving and re-entering the Location. Both behaviours are expected at this stage.

Gem Removed Preview

Send collection messages

We now need to tell two other parts of the project what happened:

  • Game Logic needs to increase the total.

  • Room Logic needs the tile’s X and Y coordinates.

We will use message ID 1 to mean “a gem was collected” in all four Logic areas.

  1. Add a Message Global Logic Node after Set Tile.

  2. Set its ID to 1.

  3. Set P1 to 1, the amount to add to the total.

  4. Add a Message Location Node after Message Global Logic.

  5. Set its ID to 1.

  6. Connect X from Touch Tile to P1 on Message Location.

  7. Connect Y from Touch Tile to P2 on Message Location.

  8. Add a Current Location Node.

  9. Connect its location reference output to Location on Message Location.

Message Location needs an explicit Location reference because this flow is running on the Player Object, not in Room Logic. Current Location provides the Location the Player is presently exploring.

The complete white flow should now be:

Touch Tile -> If Type is 1 -> Beep FX -> Set Tile
           -> Message Global Logic -> Message Location
Player Collection Logic

Store the total in Game Logic

Open the Game Logic editor.

  1. Add an initialised variable named gems.

  2. Give it an initial value of 0.

  3. Add an On Message trigger.

  4. Set its ID to 1.

  5. Give it the descriptive name Gem collected if the Node provides a name field.

  6. Add a Change Variable Node and select gems.

  7. Connect P1 from On Message to By on Change Variable.

  8. Connect the white flow from On Message to Change Variable.

Each collection message sends 1 in P1, so Change Variable increases the total by one.

Game Gem Total Logic

Send the new total to the Screen

Still in Game Logic:

  1. Add a Get Variable Node and select gems.

  2. Add a Message Screen Node after Change Variable.

  3. Select the Game Screen.

  4. Set its ID to 1.

  5. Connect the gems value to P1 on Message Screen.

  6. Connect the Flow Out of Change Variable to the Flow In of Message Screen.

The message is sent after the variable changes, so P1 contains the new total.

Game To Screen Logic

Update the GemCount Instrument

Open the Game Screen and select its Logic tab.

  1. Add an On Message trigger.

  2. Set its ID to 1.

  3. Give it the descriptive name Update gem counter if available.

  4. Add a Set Instrument Node.

  5. Select GemCount as the Instrument.

  6. Connect P1 from On Message to Value on Set Instrument.

  7. Connect the white flow from On Message to Set Instrument.

Screen Logic does not own the total. It simply displays the value supplied by Game Logic.

Update Gem Instrument Logic

Save and test again. Collect two gems and check that GemCount changes from 0 to 1, then to 2.

Gem Count Updated

Give Room Logic somewhere to remember gems

We will build the persistence Logic on Yard, then let the other two Room Types share it.

Open the Yard Room Type and select its Logic tab. Add these initialised variables:

Variable Kind Purpose

collected_positions

Array of length 16

Stores up to eight X/Y coordinate pairs

num_collected

Integer

Stores how many coordinate pairs are in use

Set every initial value to 0.

Each gem requires two array entries: one for X and one for Y. An array length of 16 therefore has room for eight gems in one Location, which is enough for the rooms in this tutorial.

Room Collection Variables

Record a collected position

In Yard Logic:

  1. Add an On Message trigger with ID 1.

  2. Give it the descriptive name Remember collected gem if available.

  3. Add a Get Variable Node and select num_collected.

  4. Add a Math Node set to Multiply, with B set to 2.

  5. Connect num_collected to A.

The result is the first free array index. For example, if two gems have already been collected, num_collected is 2 and the next pair begins at index 4.

  1. Add a Set Variable Node and select collected_positions.

  2. Connect P1 from On Message to Value.

  3. Connect the multiplication result to Index.

  4. Connect the white flow from On Message to this Set Variable.

This stores the X coordinate.

  1. Add another Math Node set to Add, with B set to 1.

  2. Connect the multiplication result to A.

  3. Add a second Set Variable Node for collected_positions.

  4. Connect P2 from On Message to Value.

  5. Connect the plus-one result to Index.

  6. Connect the first Set Variable flow to the second.

This stores the Y coordinate in the following array entry.

  1. Add a Change Variable Node after the second Set Variable.

  2. Select num_collected and set By to 1.

The count is increased only after both coordinates have been stored.

Remember Gem Position Logic

Remove remembered gems when entering a Location

Still in Yard Logic:

  1. Add a Room Entered trigger.

  2. Add a Repeat Node.

  3. Connect Room Entered to Repeat.

  4. Set Start to 0 and Step to 1.

  5. Add a Get Variable Node for num_collected.

  6. Connect its output to Until on Repeat.

The Repeat body runs once for every remembered gem.

  1. Add a Math Node set to Multiply, with B set to 2.

  2. Connect Index from Repeat to A.

  3. Add a Get Variable Node for collected_positions.

  4. Connect the multiplication result to its Index.

This reads the stored X coordinate.

  1. Add a Math Node set to Add, with B set to 1.

  2. Connect the multiplication result to A.

  3. Add a second Get Variable Node for collected_positions.

  4. Connect the plus-one result to its Index.

This reads the stored Y coordinate.

  1. Add a Set Tile Node.

  2. Set Tile to the Decorative floor tile index, 3 in this project.

  3. Connect the first array value to X.

  4. Connect the second array value to Y.

  5. Connect Body from Repeat to the Flow In of Set Tile.

Leave the Flow Out of Set Tile unconnected. When the body ends, Repeat advances its Index and begins the next iteration.

Restore Collected Gems Logic

Share the persistence Logic with every Room Type

The persistence flow does not depend on the layout of Yard. Corridor and Store Room can reuse it.

  1. Select the Corridor Room Type and open its Logic tab.

  2. In Use Logic From, choose Yard.

  3. Select the Store Room Room Type.

  4. In Use Logic From, choose Yard.

All three Room Types now use the same Room Logic and variable declarations. Each Location still receives its own values. In particular, Entrance Yard and Upper Yard share the Yard Room Type and its Logic, but their collected_positions arrays remain independent.

Shared Room Logic

Open the Map and inspect the properties of all four Locations. Confirm that each has:

  • a collected_positions array containing sixteen zeroes; and

  • num_collected set to 0.

Location Collection Defaults

Test collection persistence

Save and run Preview. Use this test route:

  1. Collect one gem in Entrance Yard.

  2. Note the value in GemCount.

  3. Move down into Stone Corridor.

  4. Return to Entrance Yard.

  5. Confirm that the collected gem is still gone.

  6. Move through Stone Corridor and Store Room to Upper Yard.

  7. Confirm that Upper Yard’s gems are still present even though it uses the same Yard Room Type.

  8. Collect a gem in Upper Yard.

  9. Return to Entrance Yard and confirm that each Yard remembers its own collected positions.

The total in GemCount should continue increasing across all Locations.

In Preview’s Data panel, expand the Locations and compare num_collected for Entrance Yard and Upper Yard. Their different values make the per-Location state visible.

Yard Locations Independent

How the collection message travels

When the Player touches a gem, the information follows two paths:

Player -> Game Logic -> Game Screen -> GemCount
   |
   +----> Current Location -> stored X/Y position

The first path manages a value that belongs to the whole game. The second stores state belonging to one Location. This division is one of the key principles of organising Logic in Kwyll.

Milestone 3 checklist

Before continuing, confirm that:

  • touching a Gem tile plays Collect;

  • the touched tile changes to Floor;

  • each gem increases the Game Logic gems variable once;

  • GemCount displays the updated total;

  • Yard defines collected_positions[16] and num_collected;

  • Corridor and Store Room use Yard’s Room Logic;

  • returning to a Location does not restore collected gems;

  • the two Yard Locations remember their state independently; and

  • the gem total continues across Location changes.

Save another copy as gem_patrol_milestone_3.tres.

Milestone 4: Add moving enemies

In this milestone, we will create one Enemy Object Type and use it twice. One Enemy instance will patrol horizontally in Stone Corridor, while another will patrol vertically in the Store Room.

Both instances will share the same Sprite, animation, and Logic. Different initial variable values will give each one its own movement direction. This is the same Object Type and Object Instance relationship that we introduced with the Player, but the Enemies will be Room Objects rather than a Map Object.

Draw the Enemy sprite

  1. Open the Sprites editor.

  2. Select the Characters Sprite Sheet.

  3. Add a second Sprite.

  4. Draw a simple enemy in the 16-by-16 grid.

  5. Add a mask around its shape.

Make the Enemy visually distinct from the Player. A pointed, angular, or one-eyed design works well at this size.

Enemy Sprite

Create the Enemy Object Type

  1. Open the Object Types editor.

  2. Add an Object Type and name it Enemy.

  3. Set its Draw Mode to Mask.

  4. Set its default Ink to Red and enable Bright.

  5. Leave Paper unset.

  6. Select the Characters Sprite Sheet.

  7. Add one animation with the Enemy sprite as its first frame.

Enemy Object Properties
Enemy Animation

Add Enemy movement variables

Open the Enemy’s Logic tab and add three initialised integer variables:

Variable Purpose Example value

speed

Pixels moved on each update

1

move_x

Horizontal direction

-1, 0, or 1

move_y

Vertical direction

-1, 0, or 1

The Logic will multiply each direction by speed. A zero direction means no movement on that axis.

Enemy Variables

Build the Enemy movement flow

The calculation is a smaller version of the Player movement graph. There is no Controller Input because the Enemy gets its direction from variables.

  1. Add an Always trigger.

  2. Add an Object Position Node.

  3. Add a Get Variable Node for speed.

  4. Add a Get Variable Node for move_x.

  5. Add a Math Node set to Multiply.

  6. Connect move_x and speed to its inputs.

  7. Add another Math Node set to Add.

  8. Connect current X from Object Position and the multiplied horizontal movement to its inputs.

This produces the requested new X position.

  1. Add a Get Variable Node for move_y.

  2. Add a second Math Node set to Multiply.

  3. Connect move_y and the same speed value to its inputs.

  4. Add another Math Node set to Add.

  5. Connect current Y and the multiplied vertical movement to its inputs.

This produces the requested new Y position.

  1. Add a Move Object Node.

  2. Connect the two final results to its X and Y inputs.

  3. Leave Ref unconnected so that the current Enemy instance is moved.

  4. Connect Always to Move Object with a white flow connection.

Enemy Move Logic

Reverse direction at a wall

The Enemy instances will have Collide Bg enabled. When Move Object tries to move one into a solid Wall tile, Kwyll prevents the movement and runs the Enemy’s Collided trigger.

We will reverse both direction variables. One of them is zero for each Enemy, and zero multiplied by -1 remains zero.

  1. Add a Collided trigger in another area of the Enemy graph.

  2. Add a Get Variable Node for move_x.

  3. Add a Math Node set to Multiply, with B set to -1.

  4. Connect move_x to A.

  5. Add a Set Variable Node for move_x.

  6. Connect the multiplication result to Value.

  7. Connect Collided to this Set Variable.

  8. Add a Get Variable Node for move_y.

  9. Add another Math Node set to Multiply, with B set to -1.

  10. Connect move_y to A.

  11. Add a Set Variable Node for move_y.

  12. Connect the second multiplication result to Value.

  13. Connect the first Set Variable flow to the second.

The Sides output of Collided is not needed for this simple patrol. A more advanced controller could inspect it and reverse only the axis that hit a wall.

Enemy Reverse Logic

Place a horizontal Enemy in Corridor

Open the Corridor Room Type and select its Layout tab.

  1. Select the Objects section of the Assets panel.

  2. Drag Enemy into the room.

  3. Place it in a horizontal passage that has solid Wall tiles at both ends.

  4. Open the Enemy instance properties.

  5. Keep its name as Enemy.

  6. Enable Collide Bg, Visible, and Active.

  7. Leave Player disabled.

  8. Set speed to 1.

  9. Set move_x to 1.

  10. Set move_y to 0.

  11. Accept the changes.

This is a Room Object. It exists only while Stone Corridor—the Location using the Corridor Room Type—is current.

Do not place it directly in the exit leading to the Store Room. Room Objects do not use the Player’s automatic Map navigation, so the patrol should be bounded by walls inside its Room Type.

Corridor Enemy
Corridor Enemy Properties

Place a vertical Enemy in Store Room

Select the Store Room Room Type.

  1. Drag another Enemy into the room.

  2. Place it in a vertical aisle with solid Wall tiles above and below it.

  3. Keep it clear of the opening to Upper Yard and any gem tile.

  4. Open its properties.

  5. Enable Collide Bg, Visible, and Active.

  6. Leave Player disabled.

  7. Set speed to 1.

  8. Set move_x to 0.

  9. Set move_y to 1.

  10. Accept the changes.

This second instance uses the same Enemy Logic but moves vertically because its variable values are different.

Store Room Enemy
Store Enemy Properties

Test the patrols

Save and run Preview.

  1. Move from Entrance Yard into Stone Corridor.

  2. Watch the first Enemy travel horizontally.

  3. Wait until it touches each end of its passage and confirm that it reverses.

  4. Continue into the Store Room.

  5. Watch the second Enemy travel vertically and reverse at its walls.

  6. Leave a Location and return to it.

The Enemy should be recreated from its Room Object data when its Location becomes active again. It may restart from its initial position and direction; unlike collected-gem state, we are not recording patrol progress.

The Enemies do not damage the Player yet. They can overlap it because we have not configured object intersection behaviour. That will be added alongside projectiles in the next milestone.

In Preview’s Data panel, expand the current Room’s Objects section. The live Enemy instance should show its position and its speed, move_x, and move_y values. Watch the appropriate direction change between 1 and -1 when it reaches a wall.

Corridor Enemy Moving
Store Enemy Moving

What we reused

One Enemy Object Type now supplies:

  • one shared Sprite and animation;

  • one movement graph;

  • one collision-response graph; and

  • three variable declarations.

Each Room Object Instance supplies only its own position, flags, and initial variable values. This is a useful Kwyll pattern: put common behaviour on the Object Type and put the differences on its instances.

Milestone 4 checklist

Before continuing, confirm that:

  • the Enemy Object Type has a masked 16-by-16 Sprite;

  • Enemy Logic defines speed, move_x, and move_y;

  • one Enemy Room Object exists in Corridor;

  • one Enemy Room Object exists in Store Room;

  • the Corridor Enemy moves horizontally;

  • the Store Room Enemy moves vertically;

  • both patrols reverse when they collide with walls;

  • neither Enemy can escape through a Location opening; and

  • leaving and returning safely recreates the Room Object patrol.

Save another copy as gem_patrol_milestone_4.tres.

Milestone 5: Fire projectiles and destroy enemies

This milestone adds the final main mechanic. Pressing Fire will create a Projectile at the Player’s position. It will travel in the direction stored in facing_x and facing_y, disappear when it hits a wall or leaves the Screen, and defeat an Enemy when the two objects intersect.

Projectiles will be Dynamic Objects. Unlike the Player Map Object and the Enemy Room Objects, they are created while the game is running with Spawn Object and removed with Kill Object.

Create the combat sounds

The default Kwyll project already contains a Fire Beep FX that is suitable for the Projectile. We will reuse it rather than creating another one.

  1. Open the Sound editor.

  2. Select Fire and use the play control to hear the existing effect.

  3. Add one new Beep FX named Enemy Hit.

  4. Give it a slightly lower or noisier character than Fire and Collect.

  5. Preview all three effects and make sure they are easy to distinguish.

Combat Sounds

Draw the Projectile sprite

The Projectile can be smaller than the Player and Enemy.

  1. Open the Sprites editor.

  2. Add a Sprite Sheet named Projectiles.

  3. Set its Sprite Width and Height to 8 pixels.

  4. Enable its Mask flag.

  5. Add one Sprite.

  6. Draw a small bolt, star, or pellet.

  7. Mask the unused pixels around it.

Projectile Sprite

Create the Projectile Object Type

  1. Add an Object Type named Projectile.

  2. Set its Draw Mode to Mask.

  3. Set its default Ink to Yellow and enable Bright.

  4. Leave Paper unset.

  5. Select the Projectiles Sprite Sheet.

  6. Add one animation containing the Projectile sprite.

Add these initialised variables to Projectile Logic:

Variable Purpose Initial value

speed

Pixels moved on each update

0

direction_x

Horizontal direction supplied by the Player

0

direction_y

Vertical direction supplied by the Player

0

All three values begin at zero. Dynamic Objects do not take the configured instance defaults used by placed Map and Room Objects, so the Player will send the movement values immediately after spawning the Projectile.

Projectile Object
Projectile Variables

Receive the firing direction

In Projectile Logic:

  1. Add an On Message trigger with ID 1.

  2. Give it the descriptive name Set direction if available.

  3. Add a Set Variable Node for direction_x.

  4. Connect P1 from On Message to Value.

  5. Add a Set Variable Node for direction_y.

  6. Connect P2 from On Message to Value.

  7. Add a Set Variable Node for speed and set Value to 4.

  8. Connect the white flow through all three Set Variable Nodes.

The Player will use message ID 1 when initialising a newly spawned Projectile. P1 carries facing_x, while P2 carries facing_y.

Projectile Direction Message

Move the Projectile

Build a movement flow in Projectile Logic:

  1. Add an Always trigger and an Object Position Node.

  2. Get speed and direction_x.

  3. Multiply direction_x by speed.

  4. Add the result to the current X position.

  5. Get direction_y.

  6. Multiply direction_y by speed.

  7. Add the result to the current Y position.

  8. Add a Move Object Node.

  9. Connect the calculated X and Y positions.

  10. Leave Ref unconnected.

  11. Connect Always to Move Object.

This is the same movement pattern used by the Enemy. Reusing a familiar pattern is often easier and safer than inventing a different solution for every Object Type.

Projectile Move Logic

Remove Projectiles that hit walls

Add a Collided trigger and connect it directly to a Kill Object Node. Leave Ref unconnected so that the current Projectile is removed.

Kill Object works here because the Projectile was created dynamically. It cannot remove the Enemy Room Objects.

Projectile Wall Cleanup

Remove Projectiles that leave the Screen

A Projectile might travel through an open exit instead of hitting a wall. We do not want unseen Dynamic Objects to accumulate.

  1. Add an Is On Screen? Node after Move Object.

  2. Connect the Flow Out of Move Object to its Flow In.

  3. Leave Ref unconnected.

  4. Connect the False flow to another Kill Object Node.

  5. Leave the True flow unconnected.

The Projectile continues to exist while it is on the current Screen. As soon as its position is outside, it is removed.

Projectile Cleanup Logic

Configure Enemy intersection layers

Kwyll uses intersection layers to avoid testing every object against every other object. We only need Projectiles to test against Enemies.

Open the Enemy instance properties in both Corridor and Store Room. For each Enemy:

  1. Set On Layers to 1.

  2. Set Checks Layers to 2, the Projectile layer.

  3. Keep Collide Bg, Visible, and Active enabled.

The two Enemy instances must use the same layer configuration.

There is no separate Intersect Objects flag on an Object Instance. Whether an intersection is registered is determined by the On Layers and Checks Layers settings.

Enemy Intersection Layers

Fire from the Player

Open Player Logic and find another empty area.

  1. Add an Always trigger.

  2. Add a Controller Input Node.

  3. Set its Mode to Just Pressed.

  4. Set Jump/Fire to 1.

  5. Add an If Node.

  6. Connect Jump/Fire to A.

  7. Set B to 0 and Comparison to Not Equal.

  8. Connect Always to the If.

Using Just Pressed creates one Projectile for each press. Using Pressed would attempt to create a new Projectile on every update while Fire was held.

Player Fire Input

Spawn the Projectile

Before adding the spawn flow, allow Dynamic Objects in the project:
  1. Open Project Settings using the gear icon.

  2. In the General settings, set Max Dynamic Objects to 5.

  3. Leave Max Tracked Intersections at its default value.

  4. Accept the changes.

A new project defaults Max Dynamic Objects to 0. If it remains zero, every Spawn Object attempt follows the Fail output and no Projectile appears.

Dynamic Object Limit
Now build the spawn flow:
  1. Add a Beep FX Node and select Fire.

  2. Connect the True flow of the If to Beep FX.

  3. Add an Object Position Node.

  4. Add a Spawn Object Node after Beep FX.

  5. Select Projectile as its Object Definition.

  6. Connect current X from Object Position to X on Spawn Object.

  7. Connect current Y to Y on Spawn Object.

  8. Leave Plane at 0.

The Projectile begins at the Player’s map position. Because we will configure it to check only the Enemy layer, overlapping the Player at the moment of creation does not count as a hit.

Spawn Object has Success and Fail flows. The Fail flow runs if the project has reached its Max Dynamic Objects limit. We can leave Fail unconnected in this introductory game; a larger project could play a different sound or reuse an existing Projectile.

Spawn Projectile Logic

Configure the spawned Object

Dynamic Objects are created Visible and Active, but the Projectile also needs background collision and intersection settings.

  1. Add a Set Object Flags Node to the Success flow of Spawn Object.

  2. Connect Spawn Ref to Ref on Set Object Flags.

  3. Set Collide Bg to On.

  4. Leave Visible and Active On.

  5. Set On Layers to 2, the Projectile layer.

  6. Set Checks Layers to 1, the Enemy layer.

Use the three-state flag controls carefully: On explicitly enables a flag, Off disables it, and Leave preserves its existing value.

Projectile Spawn Flags

Send the facing values

  1. Add a Message Object Node after Set Object Flags.

  2. Connect Spawn Ref to its Ref input.

  3. Set its ID to 1.

  4. Add Get Variable Nodes for facing_x and facing_y.

  5. Connect facing_x to P1 on Message Object.

  6. Connect facing_y to P2.

  7. Connect the Flow Out of Set Object Flags to Message Object.

The complete success path now creates the Projectile, configures its flags and layers, and tells it which way to move.

Player Fire Logic

Save and test before adding Enemy hits.

The Player should fire one Projectile per press. Test all four facing directions. Each Projectile should travel until it hits a wall or leaves through an exit, then disappear.

In Preview’s Data panel, expand Dynamic Objects while a Projectile is visible. Its direction values should match the Player’s facing values.

Projectile In Preview

Let the Projectile recognise an Enemy

In Projectile Logic:

  1. Add an Object Hit trigger.

  2. Add an Is Type? Node.

  3. Connect Ref from Object Hit to Ref on Is Type?.

  4. Select Enemy as the Object Definition.

  5. Connect the white Flow Out of Object Hit to Is Type?.

This protects the combat flow from reacting to any other Object Type that might later share an intersection layer.

Projectile Enemy Check

Tell the Enemy it was hit

  1. Add a Message Object Node to the True flow of Is Type?.

  2. Connect Ref from Object Hit to Ref on Message Object.

  3. Set its ID to 2.

  4. Add a Kill Object Node after Message Object.

  5. Leave Ref unconnected so that it removes the current Projectile.

Message ID 2 means “the Enemy was hit”. The Projectile tells the Enemy first, then removes itself.

Projectile Hit Logic

Defeat the Enemy Room Object

Room Objects cannot be removed with Kill Object; that Node is specifically for Dynamic Objects. Instead, the Enemy will respond to message ID 2 by turning off the flags that make it visible and active.

Open Enemy Logic:

  1. Add an On Message trigger with ID 2.

  2. Give it the descriptive name Hit by projectile if available.

  3. Add a Beep FX Node and select Enemy Hit.

  4. Add a Set Object Flags Node.

  5. Leave Ref unconnected so that it changes the current Enemy instance.

  6. Set Collide Bg to Off.

  7. Set Visible to Off.

  8. Set Active to Off.

  9. Leave the layer values unchanged.

  10. Connect the white flow from On Message through Beep FX to Set Object Flags.

Once inactive and invisible, the Room Object behaves as a defeated Enemy.

Enemy Defeated Logic

Test the complete combat loop

Save and run Preview.

  1. Enter Stone Corridor.

  2. Face towards the moving Enemy.

  3. Press Fire once.

  4. Confirm that the Projectile travels in the stored facing direction.

  5. When it overlaps the Enemy, confirm that both disappear and Enemy Hit plays.

  6. Continue into the Store Room and defeat its Enemy.

  7. Fire into a Wall and confirm that the Projectile disappears.

  8. Fire through an open exit and confirm that the Projectile is cleaned up after leaving the Screen.

  9. Leave a defeated Enemy’s Location and return.

The Enemy should remain defeated. A Room Object is stored in the Room Type’s definition, and changes to its flags persist until the game is reset. Leaving and re-entering a Location does not create a fresh copy of it.

This also matters when reusing Room Types. A Room Object belongs to the Room Type, not to an individual Location. If two Locations use the same Room Type, disabling an Enemy this way makes it disappear from both Locations.

If your game should restore its Enemies whenever the player enters a room, add a Room Entered trigger to the room’s Logic and connect it to a Set Object Flags Node for each Enemy. Set Visible and Active to On, and restore any other flags or variables that the Enemy needs. If reused Locations should remember separate defeated states, store that state in per-Location variables, as we did for collected gems, and use Room Entered to apply the correct state.

Projectile Near Enemy
Enemy Defeated Preview

Object roles in the finished template

Object Kind Lifetime

Player

Map Object

Exists throughout the game and moves between Locations

Enemy

Room Object

Stored in its Room Type; changes persist and are shared by Locations that reuse that Room Type

Projectile

Dynamic Object

Created on demand and killed on impact or when off-screen

These three lifetimes are worth understanding. Choosing the appropriate kind of Object Instance keeps both Logic and resource use manageable.

Milestone 5 checklist

Before continuing, confirm that:

  • the existing Fire effect and the new Enemy Hit effect sound distinct;

  • Projectile is an 8-by-8 masked Object Type;

  • Max Dynamic Objects is set to 5;

  • Fire creates one Projectile per press;

  • the Projectile receives facing_x and facing_y and sets its speed to 4;

  • it travels in all four directions;

  • wall collision kills it;

  • leaving the Screen kills it;

  • each Enemy is On Layers 1 and Checks Layers 2;

  • each Projectile is On Layers 2 and Checks Layers 1;

  • hitting an Enemy disables the Enemy and kills the Projectile;

  • both Room Object Enemies can be defeated; and

  • returning to the Location leaves the Enemy defeated.

Save another copy as gem_patrol_milestone_5.tres.

Next: Final testing and ways to continue

The main game template is now complete. This final milestone is a chance to test the whole route and review how its parts fit together.

Milestone 6: Test and review the finished template

Play the complete route

Save the project, open Preview, and play from Entrance Yard to Store Room. Try to test features together rather than one at a time:

  1. Collect the gem in Entrance Yard and check that the Gems Instrument increases.

  2. Move down into Stone Corridor, defeat the Enemy, and collect its gem.

  3. Continue right into Store Room and test its Enemy and gem.

  4. Travel up into Upper Yard and collect the final gem.

  5. Return to every Location and confirm that collected gems stay collected.

  6. Check that enemies patrol, reverse at walls, and can be hit from all four directions.

  7. Fire at walls and through exits to confirm that unused Projectiles are removed.

If something goes wrong, use Preview’s Data and Debugger panels to inspect the current values and active Objects. Testing after each small change is usually faster than trying to diagnose several changes at once.

What you have built

Gem Patrol is deliberately small, but it uses the main ideas that larger Kwyll projects are built from:

  • A Screen arranges the game window and the Gems Instrument.

  • Tiles draw the rooms and carry collision and collection information.

  • Three Room Types define reusable room layouts.

  • Four Locations place those rooms on the Map and connect them.

  • The Player is a persistent Map Object.

  • Enemies are Room Objects belonging to individual rooms.

  • Projectiles are temporary Dynamic Objects created by Logic.

  • Sounds give immediate feedback for collecting, firing, and hitting.

  • Logic connects input, movement, messages, collisions, variables, and changes to the game world.

The gem system also demonstrates an especially useful principle: store lasting game state separately from what is currently drawn. The tile changes immediately when collected, while a per-Location variable records that change so the room can restore the correct state when the player returns.

Keep experimenting

This project is a template rather than a complete game. Good next steps include:

  • restore enemies on Room Entered, or remember independent defeated states with per-Location variables using the same pattern as collected gems;

  • give the player lives and make touching an enemy reduce them;

  • add a second enemy behaviour or vary its speed;

  • animate the Player, Enemy, Gem, or Projectile;

  • add more Locations that reuse the existing Room Types;

  • create a locked exit that opens after all four gems are collected; or

  • add a title Screen and a simple win Screen.

Make one change at a time and test it. Reusing a familiar project while experimenting is one of the easiest ways to learn what Kwyll’s Nodes and editors can do.

Final checklist

Before calling the project finished, confirm that:

  • all four Locations are connected and reachable;

  • the Player moves and fires in four directions;

  • the Gems Instrument displays the correct total;

  • collected gems remain collected after revisiting a Location;

  • both Enemies move and can be defeated;

  • Projectiles are removed after hitting an Enemy, a wall, or leaving the Screen; and

  • the project saves and starts correctly in Preview.

Save the finished project as gem_patrol_complete.tres, or keep your current gem_patrol.tres as the version you continue to extend.

You have now built a small playable Kwyll project from an empty starting point. More importantly, you have used the same project structure and Logic patterns that you can carry into your own games.