*sigh* I'm sorry, this project was a failure. I went ahead and rewrote the game into states to allow for correct input handling and animation that was homogenous for both players and enemies. On Thursday I received work for my research project which kept me busy well into Sunday, resulting in little to no further updates on the game. It was going well, I suppose, but this project is now officially a
failure.
I completed development yesterday, using an illegal eighth day, to boot the game back into a playable state. Here's the changes:
1. StatesThere is now a turn-based system baked into the game. We transition from the Main Menu into the PlayerTurnState. We remain there until the user enters a command at which point we execute that command and transition into the EnemyState.
2. AnimationEvery Input is called an InputRequest. It contains all information necessary to create an animation out of it, so my toAnimation(InputRequest) method does just that. From there, an AnimationPlayer queues and plays all pending animations remaining for a given state. For example, if the player moves left, then a MoveRequest is created, turned into a MoveAnimation and then played. After it's done, the EnemyTurnState is invoked.
I like this solution because it uses what I believe is called Continuation Passing Style. It's a functional idiom whose counterpart can be best explained with the Command Pattern. The Command Pattern is just a wrapper because free-floating functions usually aren't allowed and thus everything must be wrapped in a class.
This is all a little theoretical, here is some code:
We have our InputRequests:
trait InputRequest {
fun initialize()
fun execute()
fun isValid(): Boolean
}
fun initialize(initializer: () -> InputRequest): () -> InputRequest {
return {
val request = initializer()
request.initialize()
request
}
}
Which is used to initialize my keymap:
inputRequests = mapOf(
configuration.moveUp to initialize { MoveRequest(level, player, Direction.North) },
configuration.moveDown to initialize { MoveRequest(level, player, Direction.South) },
configuration.moveLeft to initialize { MoveRequest(level, player, Direction.West) },
configuration.moveRight to initialize { MoveRequest(level, player, Direction.East) },
configuration.toggleWallUp to initialize { ToggleWallRequest(level, player, Direction.North) },
configuration.toggleWallDown to initialize { ToggleWallRequest(level, player, Direction.South) },
configuration.toggleWallLeft to initialize { ToggleWallRequest(level, player, Direction.West) },
configuration.toggleWallRight to initialize { ToggleWallRequest(level, player, Direction.East) },
configuration.shoot to initialize { ShootRequest(level, player, enemies) }
)
and then acted on whenever an input is received:
override fun update(gameContainer: GameContainer?,
game: StateBasedGame?,
delta: Int) {
if (!renderer.hasAnimationsPlaying()) {
renderer.onAllAnimationsFinished { game!!.enterState(EnemyTurnState.ID) }
enteredInputs(gameContainer!!).filter { it.isValid() }
.forEach { renderer.play(toAnimation(it)) }
}
renderer.update()
}
private fun enteredInputs(gameContainer: GameContainer): List<InputRequest> {
val isKeyPressed = { (key: String) -> inputController.isKeyPressed(gameContainer, key) }
return inputRequests.entrySet()
.filter { isKeyPressed(it.getKey()) }
.map { it.getValue()() }
}
In this last snippet you can see how the code is glued together. It's pretty terse, so I'll take it step-by-step.
if (!renderer.hasAnimationsPlaying()) {
// ...
}
A State is split into two sub-states: accepting input and animating. If we're not animating, we're accepting input and vice-versa. When we enter a state, then we first wait for input, and need to make sure that all further calls are not accepting input.
renderer.onAllAnimationsFinished { game!!.enterState(EnemyTurnState.ID) }
onAllAnimationsFinished is a function which accepts a function- Higher Order Functions is the concept. I'm telling the renderer what to do when all animations have finished. This inversion of control, my PlayerTurnState telling the Renderer to do, not letting the Renderer decide what happens, is called Continuation Passing Style. A difficult name for a simple concept
enteredInputs(gameContainer!!).filter { it.isValid() }
.forEach { renderer.play(toAnimation(it)) }
I poll the keyboard for all relevant inputs (the keys in the map) and make sure that all entered commands are valid. For all valid commands I queue an animation to be played.
That's it
This lets me handle a multitude of commands- right now we have shooting, moving and toggling walls and floors. I wasn't able to implement enemies yet, even though they ARE in the code. Well, a very primitive version of them.
3. ShootingMyes, that's possible now. Not much to say, it's very basic and there are no enemies yet.
4. ConclusionI learned
a lot. As I said in the beginning, this was my first roguelike and, to be honest, my first real attempt at programming a game. No other game came as far as this one- even though this one didn't even come that far! However, for as ashamed as I am in the bitter defeat I've suffered here, it was great fun. I'll participate again and with all of this new knowledge and these new tools, I'll be able to create a better game in less time. Fate permitting, I won't be pulled off the contest again, but who knows
As usual, all code can be found on Github:
https://github.com/SoulBeaver/7DRLThat's my time, take care guys! Congratulations to all of you that finished, what great entries!