So, despite the lack of updates I've been working on this quietly. Unfortunately recent progress has been entirely on the game logic and architecture, rather than anything readily screenshotable, hence no real drive to blog.
But here I am, writing a new post. This can only mean one thing: rambling, waffly code-oriented braindump!
My current obsession is the structure of entities, going right back to basics. Entity is a deliberately ambiguous term, which I'm taking to mean any gameplay-relevant object that can be affected by the actions of agents in the game. Doors, monsters, AI, buffs... many things potentially fall under this umbrella.
I tried splitting the potential abilities of an entity into traits/mixins:
abstract class EntityAbility
trait HasPosition extends EntityAbility {
val position : Vector
val blocksTravel : Boolean
}
trait HasHealth extends EntityAbility {
val currentHealth : Int
val maxHealth : Int
}
abstract class Entity extends EntityAbility {
val id : Int
}
class Character(val id:Int) extends Entity with HasPosition with HasHealth {
val (position,blocksTravel) = ((2,2),true)
val (currentHealth,maxHealth) = (100,100)
}
This is nicer in many ways than a traditional hierarchy, allowing for more code reuse and less chance that required functionality will come with a bundle of meaningless state.
Unfortunately there are also some interesting problems with this approach. Mainly they concern the immutable update step. If only the position (say) is changed in a given timestep, objects can be dealt with as instances of the
HasPosition trait. However this trait has no concept of the entity that it's being mixed into, and so cannot instantiate a copy of the entity with an updated position. Dropping the requirement for immutable world state allows for this easily, but that's no
fun.
In addition, information about capabilities of an entity are defined at compile time, bound up in the type system. There's no way to define these things in a data file read at runtime. Whilst this isn't as big a concern for me (this is a small game and only needs a handful of basic types to define most of the game) it is an unfortunate restriction.
Another approach is to use actual composition...
sealed abstract class Position
final case class HasPosition( position: Vector, block: Boolean ) extends Position
final object NonPositional extends Position
sealed abstract class Health
final case class HasHealth( current: Int, max: Int ) extends Health
final object Invulnerable extends Health
final case class Entity( id: Int, position: Position, health: Health )
def move( e: Entity, newPos: Vector ) = e.position match {
case HasPosition( _, b ) => Entity( e.id, HasPosition(newPos,b), e.health )
case NonPositional => error("can't move nonpositional entities")
}
Well, changing entities becomes trivial (and supports using a Builder pattern or helper methods to remove those ugly constructor calls everywhere, plus enabling runtime construction of new entity 'types') but I end up having pattern matching all over the place. When you're operating on several components that becomes very messy, very quickly. There's also the issue that there seem to be vastly
more potential runtime errors visible in the code, even if in practise I always check that a given component is of the correct type the compiler will whinge if pattern matching is not exhaustive (and correctly so).
I've fiddled with some other approaches such as conventional inheritance and a slightly COM-like abomination, but so far the above two feel most natural in Scala. I can't help but think that I'm missing some obvious alternatives though, as neither is particularly elegant when implementing common operations. Clearly I'm doing something wrong...