This article is part of a series.
Prev: «How To Make a Roguelike: #4 The Player Next: How To Make a Roguelike: #6 Entity Interactions »
In the previous article we introduced Amethyst which provides the means to handle game entities easily. Using it we added the Player to our game, but it is not doing much, so let’s teach them how to move around the cave!
Kicking our World into Motion
We’ve had a World class for quite some time now, but it is rather static, meaning that we don’t
have the means to evolve it over time. Let’s add now a function to it which will enable us to
update it whenever the player presses a button:
// Add this to World.kt
import org.hexworks.zircon.api.screen.Screen
import org.hexworks.zircon.api.uievent.UIEvent
fun update(screen: Screen, uiEvent: UIEvent, game: Game) { // 1
engine.executeTurn(GameContext( // 2
world = this,
screen = screen, // 3
uiEvent = uiEvent, // 4
player = game.player)) // 5
}
In order for this to work we also need to specify the type of our Engine a bit more clearly in World:
private val engine: TurnBasedEngine<GameContext> = Engine.create()
Here we
- Create the function
updatewhich takes all the necessary objects as parameters - We use the context object which we created before to update the engine. If you were wondering before why this class will be necessary now you know: a
Contextobject holds all the information which might be necessary to update the entity objects within our world - We pass the screen because we’ll be using it to display dialogs and similar things
- We’ll inspect the
UIEventto determine what the user wants to do (like moving around). We’re usingUIEventinstead ofKeyboardEventhere because it is possible that at some time we also want to use mouse events. - Adding the player entity to the context is not mandatory, but since we use it almost everywhere this little optimization will make our life easier.
Now we have to modify our PlayView to update our world whenever the user presses a key:
import org.hexworks.zircon.api.uievent.KeyboardEventType
// Add this to init in PlayView.kt
screen.handleKeyboardEvents(KeyboardEventType.KEY_PRESSED) {event, _ ->
game.world.update(screen, event, game)
Processed
}
You could argue that this code:
game.world.updateis an antipattern (also called a Train Wreck) and should be discouraged. I agree and we could have added anupdatemethod to theGameclass itself but note that hereGameis just a data structure holding theWorldand thePlayerand nothing else, so we’ll leave it as it is.
Congratulations! With these changes the world is updated whenever the player presses a button! The only thing which is left to do is to add some systems which handle the different aspects of input handling.
Enabling Movement
Let’s break down what do we want to do when a key is pressed:
- We accept the keys
W,A,SandDas movement keys (up, left, down, right respectively) - We move the player in the proper direction when either of those keys are pressed
- We scroll the view (if necessary) when the player is moved
All of the above steps can be encapsulated into their own system in the SEA model (just like in
a traditional ECS model) so let’s create an InputReceiver, a Movable and a CameraMover System.
“Why don’t we just write a
switchand move the player/character by hand instead of splitting it into multiple operations?” you might ask. The reason is that by having systems which are completely oblivious of the outside world and just do their own job we enable a lot of interesting features to be implemented with ease. One of those things is having a demo AI component which sends the proper messages to the player entity instead of using UI inputs. Here is a great talk by Brian Bucklew, the creator of Caves of Qud where he explains these concepts in depth and why they are useful.
As you might have seen in the previous article systems are either Facets (enable the outside world to interact with our entity)
or Behaviors (interact with the world on their own). In this case Movable and CameraMover are Facets and InputReceiver is
a Behavior.
A Behavior is straightforward: whenever the world gets updated it calls update on all of its entities, which in turn call update on all of their Behaviors. Facets work a bit differently. They accept Messages.
Let’s create the MoveTo message which holds all the data which is necessary to move our player. Amethyst supplies a Message interface for us:
interface Message<C : Context> {
val context: C
val source: Entity<EntityType, C>
}
This seems pretty straightforward. We have a context object (remember the GameContext we added before?) which has all the data we might need to perform actions and a source. source holds a reference to the entity where the Message is sent from. This handy feature lets entity objects respond to messages without maintaining a reference to the callee.
As with GameEntity before, we’re going to create a typealias for Message as well, which substitutes our GameContext object in place of the C type parameter:
// Add this to the TypeAliases.kt file which we have created before
import org.hexworks.amethyst.api.Message
typealias GameMessage = Message<GameContext>
Let’s put the MoveTo message in a new package named messages:
package com.example.cavesofzircon.messages
import com.example.cavesofzircon.extensions.GameEntity
import com.example.cavesofzircon.extensions.GameMessage
import com.example.cavesofzircon.world.GameContext
import org.hexworks.amethyst.api.entity.EntityType
import org.hexworks.zircon.api.data.Position3D
data class MoveTo(
override val context: GameContext,
override val source: GameEntity<EntityType>,
val position: Position3D
) : GameMessage
All Messages have 2 mandatory parameters: context and source. We only add position here which holds the position where we want to move the player. As this class indicates, the Movable System will be oblivious to inputs and other things, it only cares about moving an entity.
The class which knows about all entity objects is the World, so let’s add the logic of moving
one to it:
// Add this to World.kt
fun moveEntity(entity: GameEntity<EntityType>, position: Position3D): Boolean { // 1
var success = false // 2
val oldBlock = fetchBlockAt(entity.position)
val newBlock = fetchBlockAt(position) // 3
if (bothBlocksPresent(oldBlock, newBlock)) { // 4
success = true // 5
oldBlock.get().removeEntity(entity)
entity.position = position
newBlock.get().addEntity(entity)
}
return success // 6
}
private fun bothBlocksPresent(oldBlock: Maybe<GameBlock>, newBlock: Maybe<GameBlock>) = // 7
oldBlock.isPresent && newBlock.isPresent
A quick refresher: the
mapoperation on theMaybewhich is returned fromfetchBlockAtwill only run the lambda we pass to it if there is aBlockat the given position.
The operation is rather simple. What we do here is:
- We pass the
entitywe want to move and thepositionwhere we want to move it. - We create a
successvariable which holds aBooleanvalue representing whether the operation was successful - We fetch both blocks
- We only proceed if both blocks are present
- In that case
successis true - Then we return
success - This is an example of giving a name to a logical operation. In this case it is very simple but sometimes logical operations become very complex and it makes sense to give them a name like this (“both blocks present?”) so they are easy to reason about.
Then our Movable class will be used to wire these things together:
package com.example.cavesofzircon.systems
import com.example.cavesofzircon.messages.MoveTo
import com.example.cavesofzircon.world.GameContext
import org.hexworks.amethyst.api.Consumed
import org.hexworks.amethyst.api.Pass
import org.hexworks.amethyst.api.Response
import org.hexworks.amethyst.api.base.BaseFacet
object Movable : BaseFacet<GameContext, MoveTo>(MoveTo::class) { // 1
override suspend fun receive(message: MoveTo): Response {
val (context, entity, position) = message // 2
val world = context.world
var result: Response = Pass // 3
if (world.moveEntity(entity, position)) { // 4
result = Consumed // 5
}
return result
}
}
This system is very simple but it is useful for learning how to write Facets:
Hey, what’s
PassandConsumed? Why do we have to return anything? Good question! When anEntityreceives aMessageit tries to send the given message to itsFacets in order. EachFacethas to return aResponse. There are 3 kinds:Pass,ConsumedandMessageResponse. If we returnPass, the loop continues and the entity tries the nextFacet. If we returnConsumed, the loop stops.MessageResponseis special, we can return a new message using it and the entity will continue the loop using the newMessage! This is useful for implementing complex interactions between entities like the one described in this video.
- A
Facetaccepts only a specific messagek so we have to indicate that we only handleMoveTo. - This funky
(context, entity, position)code is called Destructuring. This might be familiar for Python folks and what it does is that it unpacks the values from an object which supports it. So writing this:val (context, entity, position) = myObjis the equivalent of writing this:
val context = myObj.context val entity = myObj.entity val position = myObj.position - Here we say that we’ll return
Passas a default - Then we check whether moving the entity was successful or not (remember the
successreturn value?) - And if it was, we tell Amethyst, that we
ConsumedtheMessage - Finally we return the
result
Handling Player Input
Now we know how to move entities, but we don’t know how to handle input. Let’s write a simple Behavior which we can add to our player entity for this purpose:
package com.example.cavesofzircon.systems
import com.example.cavesofzircon.extensions.GameEntity
import com.example.cavesofzircon.extensions.position
import com.example.cavesofzircon.messages.MoveTo
import com.example.cavesofzircon.world.GameContext
import org.hexworks.amethyst.api.base.BaseBehavior
import org.hexworks.amethyst.api.entity.Entity
import org.hexworks.amethyst.api.entity.EntityType
import org.hexworks.zircon.api.uievent.KeyCode
import org.hexworks.zircon.api.uievent.KeyboardEvent
object InputReceiver : BaseBehavior<GameContext>() {
override suspend fun update(entity: Entity<EntityType, GameContext>, context: GameContext): Boolean {
val (_, _, uiEvent, player) = context // 1
val currentPos = player.position
if (uiEvent is KeyboardEvent) { // 2
val newPosition = when (uiEvent.code) { // 3
KeyCode.KEY_W -> currentPos.withRelativeY(-1)
KeyCode.KEY_A -> currentPos.withRelativeX(-1)
KeyCode.KEY_S -> currentPos.withRelativeY(1)
KeyCode.KEY_D -> currentPos.withRelativeX(1)
else -> {
currentPos // 4
}
}
player.receiveMessage(MoveTo(context, player, newPosition)) // 5
}
return true
}
}
InputReceiver is pretty simple, it just checks for WASD, and acts accordingly:
- We destructure our context object so its properties are easy to access. Destructuring is positional, so here
_means that we don’t care about that specific property. - We only want
KeyboardEvents for now so we check with theisoperator. This is similar as theinstanceofoperator in Java but a bit more useful. - We use
whenwhich is similar toswitchin Java to check which key was pressed. Zircon has aKeyCodefor all keys which can be pressed.whenin Kotlin is also an expression, and not a statement so it returns a value. We can change it into ournewPositionvariable. - If some key is pressed other than
WASD, then we just return the current position, so no movement will happen - We receive the
MoveTomessage on our player here.
Now with our Movable and InputReceiver systems in place we should be able to augment the player entity to be able to move around in the world! Let’s do that by modifying the player creation code in our EntityFactory:
// new imports
import com.example.cavesofzircon.systems.InputReceiver
import com.example.cavesofzircon.systems.Movable
fun newPlayer() = newGameEntityOfType(Player) {
attributes(EntityPosition(), EntityTile(GameTileRepository.PLAYER))
behaviors(InputReceiver)
facets(Movable)
}
Now if we run this code this is what we’ll see:

There are some problems though. First, we can move through walls. This is not a problem if we’re ghosts, but in our case this is not what we want. Second, the camera doesn’t follow the player. This is not a problem if the world is so small that it fits on our screen, but we want something bigger. Let’s address the second issue first:
Scrolling the World
Let’s start by making our world bigger to demonstrate the problem. Open the GameConfig class and modify WORLD_SIZE to this:
val WORLD_SIZE = Size3D.create(WINDOW_WIDTH * 2, WINDOW_HEIGHT * 2 , DUNGEON_LEVELS)
It is not too big but will do for demonstrational purposes:

Oops. We can move out of the world! Let’s fix this by adding the CameraMover facet to our player entity. For this we’re going to create a new message, MoveCamera:
package com.example.cavesofzircon.messages
import com.example.cavesofzircon.extensions.GameEntity
import com.example.cavesofzircon.extensions.GameMessage
import com.example.cavesofzircon.world.GameContext
import org.hexworks.amethyst.api.entity.EntityType
import org.hexworks.zircon.api.data.Position3D
data class MoveCamera(
override val context: GameContext,
override val source: GameEntity<EntityType>,
val previousPosition: Position3D
) : GameMessage
Using this the CameraMover facet can be implemented. The logic might seem a bit tricky, but I’m going to explain below:
package com.example.cavesofzircon.systems
import com.example.cavesofzircon.extensions.position
import com.example.cavesofzircon.messages.MoveCamera
import com.example.cavesofzircon.world.GameContext
import org.hexworks.amethyst.api.Consumed
import org.hexworks.amethyst.api.Response
import org.hexworks.amethyst.api.base.BaseFacet
object CameraMover : BaseFacet<GameContext, MoveCamera>(MoveCamera::class) {
override suspend fun receive(message: MoveCamera): Response {
val (context, source, previousPosition) = message
val world = context.world
val screenPos = source.position - world.visibleOffset // 1
val halfHeight = world.visibleSize.yLength / 2 // 2
val halfWidth = world.visibleSize.xLength / 2
val currentPosition = source.position
when { // 3
previousPosition.y > currentPosition.y && screenPos.y < halfHeight -> {
world.scrollOneBackward()
}
previousPosition.y < currentPosition.y && screenPos.y > halfHeight -> {
world.scrollOneForward()
}
previousPosition.x > currentPosition.x && screenPos.x < halfWidth -> {
world.scrollOneLeft()
}
previousPosition.x < currentPosition.x && screenPos.x > halfWidth -> {
world.scrollOneRight()
}
}
return Consumed
}
}
- The player’s position on the screen can be calculated by subtracting the
World’svisibleOffsetfrom the player’s position. ThevisibleOffsetis the top left position of the visible part of theWorldrelative to the top left corner of the wholeWorld(which is0, 0). - We calculate the center position of the visible part of the world here
- And we only move the camera if we moved in a certain direction (left for example) and the
Entity’s position on the screen is left of the middle position. The logic is the same for all directions, but we use the correspondingxorycoordinate.
Now if we start the application nothing happens yet, because no one sends the MoveCamera message! Where should we put it? One option is to put it in the InputReceiver, but a better solution is to use the MessageResponse functionality which is provided by Amethyst to return a new Message when we move the player. Why is it better this way? Because the Movable system has knowledge of all the related information (position and direction) and moving an entity is closely related to moving the camera so it maintains the cohesion of the code. Let’s see what we need to change in Movable:
// add these to the imports
import com.example.cavesofzircon.attributes.types.Player
import com.example.cavesofzircon.extensions.position
import com.example.cavesofzircon.messages.MoveCamera
import org.hexworks.amethyst.api.MessageResponse
override suspend fun receive(message: MoveTo): Response {
val (context, entity, position) = message
val world = context.world
val previousPosition = entity.position // 1
var result: Response = Pass
if (world.moveEntity(entity, position)) {
result = if (entity.type == Player) { // 2
MessageResponse(MoveCamera( // 3
context = context,
source = entity,
previousPosition = previousPosition
))
} else Consumed // 4
}
return result
}
- First, we save the previous position before we change it
- If the move was successful and the entity we moved is the player
- We return the
MessageResponse - Otherwise we keep the
Consumedresponse
Now if we add CameraMover to our player:
// add this to the imports
import com.example.cavesofzircon.systems.CameraMover
fun newPlayer() = newGameEntityOfType(Player) {
attributes(EntityPosition(), EntityTile(GameTileRepository.PLAYER))
behaviors(InputReceiver)
facets(Movable, CameraMover)
}
and run the program we see proper scrolling:

Conclusion
We’ve covered a lot of ground in this article. We added life to our world by making it updatable and we also added player interaction with the keyboard, moving and camera scrolling!
In the next article we’ll take a look at how entities can interact with each other. This will also involve fixing our problem of being able to move through walls. Exciting!
Until then go forth and kode on!
The code of this article can be found in commit #5.
Do you have questions? Ask us on our Discord Server.
If you like what we do and want to support us consider becoming a Patron.