Dungeon 163
I’m dismissing my desire to convert to mesh for animation. Let’s get that Mimic hiding.
If all my entities had multi-frame sprite sheets for their graphics, a switch to mesh might make sense. I think it would reduce total lines of code and number of classes, and it is supposedly more efficient. However, that’s not the case. The case is that all but one of my monsters have separate images, and only a few of them. We could still make them all into mesh, but some of the advantages would be lost.
And, perhaps more to the point, the user/player would see no benefit at all. So, I think I’ll write an article or a few about mesh, at some point, and I’ll try to push it to the same level of capability that we have here, just to get a bit of a comparison between the approaches.
Structurally, I imagine that there would be the same bits and pieces, except for the slicing and lookup objects, which might be eliminated, or nearly so. Anyway, for now, that effort is deferred and set aside.
The Mimic
To refresh our memories, here’s the deal with the Mimic. It sits quietly, unmoving, looking like a treasure chest. When the player messes with it, it wakes up and surprise! you’ve got trouble.
Our mimic has six sprite sheets, each with ten frames:
- hide - changing from chest to monster. Reversed, it would change back.
- idle - just standing around fidgeting.
- walk - moving
- attack - he attac
- hit - he has been hit and reacts
- dead - he’s down!
I envision these being used in a pretty obvious way.
- The Mimic will sit still, showing only a single chest view from the “hide” animation, just like the real ones (which are now using one of the Mimic’s frames).
- When the player moves onto the Mimic’s tile, it will run the “hide” animation once, turning into a monster.
- This should give the player time to move away from the Mimic. If she moves far enough, the Mimic will go into “idle”.
- If the player is within some range to be determined, the Mimic will “walk” toward her.
- If the player and woke Mimic are in adjacent squares, there will be battle. When the Mimic attacks, we want it to run its “attack” animation once for each attack.
- If the Mimic takes damage, run the “hurt” animation once.
- If the Mimic is defeated, run the “death” animation once, and stop in the final position.
I’m not sure that all of these are currently possible with existing capabilities. In particular, we’ll need some new Animator methods to run an animation once and then move to another, and to run a single frame, and to run once and stop at the end. (I think we have that one.) We’ll see as we go.
Let’s begin at the beginning:
Mimic Hiding
When we create a Mimic, we work from this table:
m = {name="Mimic", level=1, health={10,10}, speed={10,10}, strength=10, facing=-1, strategy=MimicMonsterStrategy,
attackVerbs={"bites", "chomps", "gnaws"},
dead={"mdead01","mdead02","mdead03","mdead04","mdead05","mdead06","mdead07","mdead08","mdead09","mdead10"},
hit={"mhurt01", "mhurt02", "mhurt03", "mhurt04", "mhurt05", "mhurt06", "mhurt07", "mhurt08", "mhurt09", "mhurt10"},
moving={"mwalk01", "mwalk02", "mwalk03", "mwalk04", "mwalk05", "mwalk06", "mwalk07", "mwalk08", "mwalk09", "mwalk10"},
attack={"mattack01", "mattack02", "mattack03", "mattack04", "mattack05", "mattack06", "mattack07", "mattack08", "mattack09", "mattack10"},
idle={"midle01", "midle02", "midle03", "midle04", "midle05", "midle06", "midle07", "midle08", "midle09", "midle10"},
hide={"mhide01", "mhide02", "mhide03", "mhide04", "mhide05", "mhide06", "mhide07", "mhide08", "mhide09", "mhide10"},
}
The MimicMonsterStrategy looks like this so far:
function MimicMonsterStrategy:init(monster)
self.monster = monster
end
function MimicMonsterStrategy:execute(dungeon)
local method = self:selectMove(self.monster:manhattanDistanceFromPlayer())
self.monster[method](self.monster, dungeon)
end
function MimicMonsterStrategy:selectMove(range)
if range > 2 then
self.monster:setAnimation("moving")
return "basicMoveRandomly"
else
self.monster:setAnimation("attack")
return "basicMoveTowardPlayer"
end
end
This is just a stopgap so that they’ll wander more or less like other monsters. So far, a monster gets one strategy and sticks with it. I’d like to continue that, but we could always jam in a new strategy if that seemed preferable.
Preferable? Well, consider the Mimic plan. Most of the time it just sits there. It only gets active if the Player bothers it. So there is at least one pair of states associated with the Mimic, passive vs active.
Dithering …
I’m trying to figure out where to pick up this tangle of ideas that is the Mimic. I guess I’ll just go with a state flag. Call it monster.awake. It’ll be uninitialized. When the Mimic wakes up, we’ll set it to true.
Try this:
function MimicMonsterStrategy:selectMove(range)
if not awake then
return "basicDoNotMove"
end
if range > 2 then
self.monster:setAnimation("moving")
return "basicMoveRandomly"
else
self.monster:setAnimation("attack")
return "basicMoveTowardPlayer"
end
end
I’m definitely just following my nose here. I’m supposing that we have to return a move, so I’ve made up a new one, basicDoNotMove
.
function Monster:basicDoNotMove(dungeon)
-- this method intentionally blank
end
Now I’ll jigger the system to create only Mimics and see what they’re doing.
As expected. The mimic isn’t changing position, but it is of course running its default animation. That’s a bit of a problem, because of this:
function Monster:initAnimations()
local mtEntry = self.mtEntry
self.animator = Animator(mtEntry)
self:setAnimation("moving")
end
One possibility is to rig up monster tables so that they can specify the animation to start. Another might be to handle it in the Strategy. Let’s try that: it sets animations anyway.
function MimicMonsterStrategy:selectMove(range)
if not awake then
self.monster:setAnimation("hide")
return "basicDoNotMove"
end
if range > 2 then
self.monster:setAnimation("moving")
return "basicMoveRandomly"
else
self.monster:setAnimation("attack")
return "basicMoveTowardPlayer"
end
end
That does run the hide animation, but that cycles from chest to monster. Looks good, but not what we want:
I note that the Mimic is at a larger scale than the other chests. We’ll deal with that in due time.
Our Animator understands two methods for cycling animations:
function Animator:cycle(name)
self.animation = Animation:cycle(self:getAnimation(name))
end
function Animator:oneShot(name)
self.animation = Animation:oneShot(self:getAnimation(name))
end
And the Animation only has two step types:
function Animation:cycleStep()
self.index = self.index+1
if self.index > #self.frames then
self.index = 1
end
end
function Animation:oneShotStep()
self.index = self.index + 1
if self.index > #self.frames then
self.index = #self.frames
end
end
Let’s give them a new capability. I’ll call it firstFrame
.
function Animator:firstFrame(name)
self.animation = Animation:firstFrame(self:getAnimation(name))
end
function Animation:firstFrame(frames)
return Animation(frames, self.firstFrameStep)
end
function Animation:firstFrameStep()
return self.frames[1]
end
And now we’ll need to call a method on monster and implement it:
function MimicMonsterStrategy:selectMove(range)
if not awake then
self.monster:firstFrameAnimation("hide")
return "basicDoNotMove"
end
function Monster:setFirstFrameAnimation(name)
self.animator:firstFrame(name)
end
Noticed while I was in Monster that the animation calls start with set
. Decided to follow that rule. Change my call from Strategy:
function MimicMonsterStrategy:selectMove(range)
if not awake then
self.monster:setFirstFrameAnimation("hide")
return "basicDoNotMove"
end
Now, unless I missed, the Mimic should stay in chest mode forever.
And there it is. Nice. It’s a bit around the horn, isn’t it, to call to the monster and then back to the animator, but unless we build a weird net of who knows whom, I think this is best.
We now have another issue to write down: don’t display the Mimic sheet unless it’s awake. That’ll be a bit tricky, because the Mimic isn’t a separate class, so it can’t override the sheet easily. We’ll figure that out in due time. Due time is not now.
Now to wake him up.
Who starts an interaction, the mover or the resident? I can never remember.
function TileArbiter:moveTo()
local entry = self:tableEntry(self.resident,self.mover)
local action = entry.action
if action then action(self.mover,self.resident) end
local result = entry.moveTo(self)
return result
end
TileArbiter decides what happens.
t[Monster][Player] = {moveTo=TileArbiter.refuseMoveIfResidentAlive, action=Player.startActionWithMonster}
So the Player will get the message first. What does she do?
function Player:startActionWithMonster(aMonster)
self.runner:initiateCombatBetween(self,aMonster)
end
Ah, this is irritating. We don’t want to initiate combat with a Mimic yet. We don’t know it’s a mimic. I think for now we’ll hack:
function Player:startActionWithMonster(aMonster)
if aMonster:name() == "Mimic" and aMonster.awake then
self.runner:initiateCombatBetween(self,aMonster)
else
aMonster.awake = true
end
end
I’m not loving that but once we get the thing wired up we’ll see if we can improve it. Make it run, then make it right.
Yeah well that’s not even close. Let’s try again.
function Player:startActionWithMonster(aMonster)
local name = aMonster:name()
if name ~= "Mimic" or (name == "Mimic" and aMonster.awake) then
self.runner:initiateCombatBetween(self,aMonster)
else
aMonster.awake = true
end
end
This is horrid. But we might be able to make it work. I sort of wish I had a commit backing me up.
The strategy is wrong, it was checking a naked global.
function MimicMonsterStrategy:selectMove(range)
if not self.monster.awake then
self.monster:setFirstFrameAnimation("hide")
return "basicDoNotMove"
end
if range > 2 then
self.monster:setAnimation("moving")
return "basicMoveRandomly"
else
self.monster:setAnimation("attack")
return "basicMoveTowardPlayer"
end
end
OK. This actually “works”. When Princess tries to step on a Mimic it immediately attacks, and it follows her relentlessly. We want to give her a chance to get away.
Let’s try this. We’ll keep a move counter in the strategy, and we’ll idle for three moves, giving her a change to get away.
function MimicMonsterStrategy:init(monster)
self.monster = monster
self.moveCount = 3
end
function MimicMonsterStrategy:selectMove(range)
if not self.monster.awake then
self.monster:setFirstFrameAnimation("hide")
return "basicDoNotMove"
end
if self.moveCount == 3 then
self.monster:setAnimation("idle")
end
self.moveCount = self.moveCount - 1
if self.moveCount <= 0 then
if range > 2 then
self.monster:setAnimation("idle")
return "basicDoNotMove"
else
self.monster:setAnimation("attack")
return "basicMoveTowardPlayer"
end
else
return "basicDoNotMove"
end
end
I think this might do it. It nearly does. I forgot to run the unhide. We can do that where moveCount is three, but we need a new method, run once and then go to another animation. First let’s just do the unhide.
A quick refactoring to get a method in Monster:
function Monster:setDeathAnimation()
self:setOneShotAnimation("dead")
end
function Monster:setOneShotAnimation(name)
self.animator:oneShot(name)
end
And now:
function MimicMonsterStrategy:selectMove(range)
if not self.monster.awake then
self.monster:setFirstFrameAnimation("hide")
return "basicDoNotMove"
end
if self.moveCount == 3 then
self.monster:setOneShotAnimation("hide")
end
self.moveCount = self.moveCount - 1
if self.moveCount <= 0 then
if range > 2 then
self.monster:setAnimation("idle")
return "basicDoNotMove"
else
self.monster:setAnimation("attack")
return "basicMoveTowardPlayer"
end
else
return "basicDoNotMove"
end
end
This looks pretty decent:
We approach the mimic and try to step on it. It wakes up. It just stands there: it’s waiting for three moves. Our spec wants it to drop into idle right away. We move away three squares and the Mimic idles. We approach and it gets angry, chasing us. Because of the way the turns work out, we get the first attack. It bites and graws at us. In the end we drop it and it falls to the floor defeated.
Not bad at all. I’ll take it for today. Commit: Mimic hides, idles, dies.
Not bad. 40 net lines of code. Four tabs touched, Animator, Monster, MonsterStrategy, and Player.
The code, however, is full of conditionals. I don’t like the Player having to check whether she’s dealing with a Mimic, and we’re seeing a solid indication that our strategy could benefit from a better implementation than a bunch of if statements.
So the code isn’t right, but it does work. I’d like to see the Mimic walking toward the player using its walk animation, and only doing the biting when it’s actually attacking. We can get part way there, I think:
function MimicMonsterStrategy:selectMove(range)
if not self.monster.awake then
self.monster:setFirstFrameAnimation("hide")
return "basicDoNotMove"
end
if self.moveCount == 3 then
self.monster:setOneShotAnimation("hide")
end
self.moveCount = self.moveCount - 1
if self.moveCount <= 0 then
if range > 2 then
self.monster:setAnimation("idle")
return "basicDoNotMove"
else
-- see below v
self.monster:setAnimation("moving")
return "basicMoveTowardPlayer"
end
else
return "basicDoNotMove"
end
end
Now it’ll do its walking animation but it will never do the bitey one.
I think it drops into idle too soon. Maybe it should be a bit more interested. But that’s all tuning, and ideally it’ll wait until we make this code better.
We’ll work on that tomorrow. Today, turn on the other monsters and commit: Mimic walk but he no attac.
Let’s sum up.
Summary
Well, there’s good news and bad news. Or, I guess I’d say, good news and mediocre news.
The good news is that we have the mimic running a single frame animation, a one-shot animation, and a moving and dropping animation. We’ll surely be able to trigger the attack one and probably the hit one as well.
This will make us want more monsters that can do all this stuff. I’m gonna need a kickstarter.
The not so good news is that the code is messy and a bit invasive. In particular, knowledge of the situation is spread all over. The Princess changes her behavior based on checking the type of the monster. That’s not good design.
And our simple approach to strategy, asking a couple of questions and picking a move isn’t panning out well here. We need a more intelligent strategy for this kind of monster.
I’m feeling tempted to subclass Monster to get the Mimic, then override a bit of behavior. That’s not a strategy that my colleagues think well of. Maybe we’ll plug in something special, that’s usually more socially acceptable.
Bottom line, though, in a couple of hours we’ve got a much more interesting Mimic and that’s a good thing.
See you next time!