This article is part of a series.
Prev: «How To Make a Roguelike: #5 Exploring the Cave Next: How To Make a Roguelike: #7 Stationary Monsters »
In the previous article we added cave exploration and scrolling, but there is not much going on between the entities of our world. Let’s improve on that by enabling the player to interact with the world with its own Entity Actions!
A World of Entities
In a traditional Entity Component System almost all things can be entities. This is also true with Amethyst. In our case there are two things in the world right now: floors and walls. Both of them are just simple blocks in our game world. We don’t want to interact with floors yet, but let’s make wall an entity!
Our GameBlock already supports adding entities to it, so let’s upgrade it a bit so that we can create one with an initial entity:
// add this to the GameBlock class
companion object {
fun createWith(entity: GameEntity<EntityType>) = GameBlock(
currentEntities = mutableListOf(entity)
)
}
A quick refresher: companion objects are singleton
objects which are added to a regular class. They work in a similar way as thestatickeyword in Java, you can call functions and access properties on them by using their parent class. Here we can invokecreatein the following way:GameBlock.create().
Now if we want a wall entity we need to create a type for it first:
// put this in EntityTypes.kt
object Wall : BaseEntityType(
name = "wall"
)
Apart from a type a wall also has a position and an attribute which tells us that it occupies a block (eg: you can’t just move into that block with the player). We already have EntityPosition so let’s create our first flag attribute, BlockOccupier:
package com.example.cavesofzircon.attributes.flags
import org.hexworks.amethyst.api.base.BaseAttribute
object BlockOccupier : BaseAttribute()
This is an object because we will only ever need a single instance of it. BlockOccupier works as a flag: if an entity has it the block is occupied, otherwise it is not. This will enable us to add walls, creatures and other things to our dungeon which can’t occupy the same space.
It is also useful to add an extension property to our EntityExtensions so that we can easily determine for all of our entities whether they occupy a block or not:
import com.example.cavesofzircon.attributes.flags.BlockOccupier
// put this in EntityExtensions.kt
val AnyGameEntity.occupiesBlock: Boolean
get() = findAttribute(BlockOccupier::class).isPresent
Remember that BlockOccupier is just a flag so it is enough that we check for its presence.
We know that we’ll check our GameBlocks for occupiers so let’s start using the occupiesBlock property right away in it:
import org.hexworks.cobalt.datatypes.Maybe
import com.example.cavesofzircon.extensions.occupiesBlock
// add these extension properties to GameBlock
val occupier: Maybe<GameEntity<EntityType>>
get() = Maybe.ofNullable(currentEntities.firstOrNull { it.occupiesBlock }) // 1
val isOccupied: Boolean
get() = occupier.isPresent // 2
occupierwill return the first entity which has theBlockOccupierflag or an emptyMaybeif there is none- Note how we tell whether a block is occupied by checking for the presence of an
occupier
You might ask why are we creating these extension properties instead of just checking for the presence or absence of
Attributes. The answer is that by having these very expressive properties we can write code which is really easy to read and interpret as you’ll see later.
Now creating a wall entity is pretty straightforward:
import com.example.cavesofzircon.attributes.types.Wall
import com.example.cavesofzircon.attributes.flags.BlockOccupier
// put this function to EntityFactory.kt
fun newWall() = newGameEntityOfType(Wall) {
attributes(
EntityPosition(),
BlockOccupier,
EntityTile(GameTileRepository.WALL)
)
}
Now to start using this we just make our GameBlockFactory create the wall blocks with the new factory method:
// put this in GameBlockFactory
fun wall() = GameBlock.createWith(EntityFactory.newWall())
There is one thing we need to fix. In the WorldBuilder’s smoothing algorithm we check whether a block is a floor. This will no longer work because the defaultTile of a GameBlock is now always a floor tile so let’s change it to use isEmptyFloor instead:
// change this in WorldBuilder#smooth
if (block.isEmptyFloor) {
floors++
} else rocks++
Wait, what happened to our
defaultTile? Well, the thing is that when we demolish a wall we want to see the default tile in its place which is a floor in our case. Previously we did not have the ability to do this, but now we’re going to implement it.
If we run the program now there won’t be any visible changes to gameplay. The reason is that although we added the BlockOccupier flag to our game it is not used by anything…yet. Let’s take a look at how can we go about implementing the interaction between entities.
Adding Entity Interactions
Let’s stop for a second and think about how this works in most roguelike games. When the player is idle nothing happens since updating the game world is bound to player movement. The usual solution is that whenever we try to move into a new tile the game tries to figure out what to do. If it is an enemy creature we attack it. If it is an empty tile we move into it. It is also possible to move off of a ledge in which case the player usually suffers some damage or dies. To sum it all up these are the steps we want to perform when the player presses a movement key:
- Check what’s in the block where we want to move
- Take a look at what we are able to do
- Try each one on the target block and see what happens
Let’s see how we can go about implementing this. We know that entities communicate with each other by passing messages. So an EntityAction can be implemented as such:
package com.example.cavesofzircon.messages
import com.example.cavesofzircon.extensions.GameEntity
import com.example.cavesofzircon.extensions.GameMessage
import org.hexworks.amethyst.api.entity.EntityType
interface EntityAction<S : EntityType, T : EntityType> : GameMessage { // 1
val target: GameEntity<T> // 2
operator fun component1() = context // 3
operator fun component2() = source
operator fun component3() = target
}
Our EntityAction is different from a regular GameMessage in a way that it also has a target. So an EntityAction represents source trying to perform an action on target.
- We have two generic type parameters,
SandT.Sis theEntityTypeof thesource,Tis theEntityTypeof thetarget. This will be useful later on as we’ll see. - We save the reference to
targetin allEntityActions - The
component1,component2…componentNmethods implement destructuring in Kotlin. Since destructuring is positional as we’ve seen previously by implementing thecomponent*functions we can control how anEntityActioncan be destructured. In our case with these 3 operator functions we can destructure anyEntityActions like this:val (context, source, target) = entityAction
Kotlin supports operator overloading! This means that things like
+and-can be overloaded just like in C++. You can read more about this here.
Let’s add an actual EntityAction to our project now! Our current problem is that we can walk through walls, so let’s solve that problem by adding a Dig action:
package com.example.cavesofzircon.messages
import com.example.cavesofzircon.extensions.GameEntity
import com.example.cavesofzircon.world.GameContext
import org.hexworks.amethyst.api.entity.EntityType
data class Dig(
override val context: GameContext,
override val source: GameEntity<EntityType>,
override val target: GameEntity<EntityType>
) : EntityAction<EntityType, EntityType>
This is rather simple. Here we supply EntityType as a type parameter for both source and target because we don’t want to limit who can dig and what can be digged out.
Now that we can create actions for our entities we need to enable them using it. In the SEA model this means that we need a new attribute that can be added to entity objects which can perform actions:
package com.example.cavesofzircon.attributes
import com.example.cavesofzircon.extensions.GameEntity
import com.example.cavesofzircon.messages.EntityAction
import com.example.cavesofzircon.world.GameContext
import org.hexworks.amethyst.api.base.BaseAttribute
import org.hexworks.amethyst.api.entity.EntityType
import kotlin.reflect.KClass
class EntityActions(
private vararg val actions: KClass<out EntityAction<out EntityType, out EntityType>> // 1
) : BaseAttribute() {
fun createActionsFor( // 2
context: GameContext,
source: GameEntity<EntityType>,
target: GameEntity<EntityType>
): Iterable<EntityAction<out EntityType, out EntityType>> {
return actions.map {
try {
it.constructors.first().call(context, source, target) // 3
} catch (e: Exception) { // 4
throw IllegalArgumentException("Can't create EntityAction. Does it have the proper constructor?")
}
}
}
}
- This
Attributeis capable of holding classes of any kind ofEntityAction. We usevararghere which is similar to how varargs work in Java: we can create theEntityActionsobject with any number of constructor parameters like this:EntityActions(Dig::class, Look::class). We need to use the class objects (KClass) here instead of the actualEntityActionobjects because each time we perform an action a newEntityActionhas to be created. So you can think aboutactionshere as templates. - This function can be used to create the actual
EntityActionobjects by using the givencontext,sourceandtarget - When we create the actions we just call the first constructor of the class and hope for the best. There is no built-in way in Kotlin (nor in Java) to make sure that a class has a specific constructor in compile time so that’s why
- We catch any exceptions and rethrow them here stating that the operation failed. We just have
to remember that whenever we create an
EntityActionit has a constructor for the 3 mandatory fields.
Now let’s add a convenience function for actually trying the actions of an entity:
import com.example.cavesofzircon.world.GameContext
import org.hexworks.amethyst.api.Response
import org.hexworks.amethyst.api.Pass
import com.example.cavesofzircon.attributes.EntityActions
import org.hexworks.amethyst.api.Consumed
// put this in EntityExtensions.kt
suspend fun AnyGameEntity.tryActionsOn(context: GameContext, target: AnyGameEntity): Response { // 1
var result: Response = Pass
findAttributeOrNull(EntityActions::class)?.let { // 2
it.createActionsFor(context, this, target).forEach { action -> // 3
if (target.receiveMessage(action) is Consumed) { // 4
result = Consumed
return@forEach // 5
}
}
}
return result
}
- We define this function as an extension function on
AnyGameEntity. This means that from now on we can calltryActionsOnon any of our entities! It is also asuspendfun because thereceiveMessagefunction we call later is also a suspending function. Suspending is part of the Kotlin Coroutines API and it is a deep topic. We’re not going to cover it here as we don’t take advantage of it, but you can read up on the topic here - We can only try the actions of an entity which has at least one, so we try to find the attribute.
- if we find the attribute we just create the actions for our
context/source/targetcombination - And we then send the message to the
targetfor immediate processing and if the message isConsumedit means that - We can break out of the
forEachblock.
There are some interesting things going on here. First we call
createActionsForonit. What is this thing? When we create a lambda if it only has one parameter we can refer to it using the implicititname. Read more about it (pun intended) here. Also what’s this funkyreturn@forEach? This construct allows us to break out of theforEachloop. You can read more about this feature here.
Digging out Walls
Now that we have created a mechanism for our entities to perform actions on each other let’s put it to good use. We already have an action we can use (Dig), so we need to create the corresponding facet: Diggable. When we dig a block out we want to remove it from the World so let’s add this function to our World class:
// add this to the World class
fun removeEntity(entity: Entity<EntityType, GameContext>) {
fetchBlockAt(entity.position).map {
it.removeEntity(entity)
}
engine.removeEntity(entity)
entity.position = Position3D.unknown()
}
and then Diggable will look like this:
package com.example.cavesofzircon.systems
import com.example.cavesofzircon.messages.Dig
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 Diggable : BaseFacet<GameContext, Dig>(Dig::class) {
override suspend fun receive(message: Dig): Response {
val (context, _, target) = message
context.world.removeEntity(target)
return Consumed
}
}
Now if we add the Dig action to our player entity:
import com.example.cavesofzircon.messages.Dig
import com.example.cavesofzircon.attributes.EntityActions
// update this method in EntityFactory
fun newPlayer() = newGameEntityOfType(Player) {
attributes(
EntityPosition(),
EntityTile(GameTileRepository.PLAYER),
EntityActions(Dig::class)
)
behaviors(InputReceiver)
facets(Movable, CameraMover)
}
and make the walls Diggable:
import com.example.cavesofzircon.systems.Diggable
// update newWall in EntityFactory with this
fun newWall() = newGameEntityOfType(Wall) {
attributes(
EntityPosition(),
BlockOccupier,
EntityTile(GameTileRepository.WALL))
facets(Diggable)
}
and start the program we can still move through walls because we haven’t added support for entity actions in our Movable facet. If you recall we’ve figured out before that entity actions are bound to player movement in most roguelike games, so let’s modify Movable now, to fix this:
import org.hexworks.cavesofzircon.extensions.tryActionsOn
import org.hexworks.cobalt.datatypes.extensions.map
world.fetchBlockAtOrNull(position)?.let { block -> // 1
if (block.isOccupied) {
result = entity.tryActionsOn(context, block.occupier.get()) // 2
} else {
if (world.moveEntity(entity, position)) { // 3
result = Consumed
if (entity.type == Player) {
result = MessageResponse(MoveCamera(
context = context,
source = entity,
previousPosition = previousPosition
))
}
}
}
}
What changed is that
- We will only do anything if there is a block at the given position. It is possible that there are no blocks at the edge of the map for example (if we want to move off the map)
- If the block is occupied we try our actions on the block
- Otherwise we do what we were doing before
What this will do is that if we bump into something which occupies the block the player will try to dig it out, otherwise we’ll just move to that block!
Now if you start this up you’ll see slower player movement but no walls, only floors. Why is that? The problem is that in our GameBlock we introduced a bug. All entity changes require us to call updateContent but we forgot to take into account the case when there is an entity added to a block when it is being created! The solution is a simple init block in GameBlock:
init {
updateContent()
}
Now we can start up our game and see the new changes in action:

Wow, this is awesome!
Conclusion
In this article we’ve learned how entities can interact with each other so now there is nothing stopping us from adding new ways for changing our world that enriches the player experience. We’ve also learned how everything can be an entity and why this is very useful.
In the next article we’ll add a new kind of entity: monsters!
Until then go forth and kode on!
The code of this article can be found in commit #6.
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.