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
object
s which are added to a regular class. They work in a similar way as thestatic
keyword in Java, you can call functions and access properties on them by using their parent class. Here we can invokecreate
in 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 GameBlock
s 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
occupier
will return the first entity which has theBlockOccupier
flag or an emptyMaybe
if 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
Attribute
s. 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,
S
andT
.S
is theEntityType
of thesource
,T
is theEntityType
of thetarget
. This will be useful later on as we’ll see. - We save the reference to
target
in allEntityAction
s - The
component1
,component2
…componentN
methods implement destructuring in Kotlin. Since destructuring is positional as we’ve seen previously by implementing thecomponent*
functions we can control how anEntityAction
can be destructured. In our case with these 3 operator functions we can destructure anyEntityAction
s 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
Attribute
is capable of holding classes of any kind ofEntityAction
. We usevararg
here which is similar to how varargs work in Java: we can create theEntityActions
object 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 actualEntityAction
objects because each time we perform an action a newEntityAction
has to be created. So you can think aboutactions
here as templates. - This function can be used to create the actual
EntityAction
objects by using the givencontext
,source
andtarget
- 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
EntityAction
it 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 calltryActionsOn
on any of our entities! It is also asuspend
fun because thereceiveMessage
function 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
/target
combination - And we then send the message to the
target
for immediate processing and if the message isConsumed
it means that - We can break out of the
forEach
block.
There are some interesting things going on here. First we call
createActionsFor
onit
. What is this thing? When we create a lambda if it only has one parameter we can refer to it using the implicitit
name. Read more about it (pun intended) here. Also what’s this funkyreturn@forEach
? This construct allows us to break out of theforEach
loop. 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.