Difference between revisions of "MindState"
(→Main Features) |
(→Source Code) |
||
Line 240: | Line 240: | ||
Finally, the Goblin class, also a subclass of Monster, defines how Goblins behave: They react cowardly and enter a new state called 'RunningAway' as soon as they get attacked. | Finally, the Goblin class, also a subclass of Monster, defines how Goblins behave: They react cowardly and enter a new state called 'RunningAway' as soon as they get attacked. | ||
+ | |||
+ | ===Stackable States=== | ||
==Source Code== | ==Source Code== |
Revision as of 22:24, 23 March 2010
This library complements MiddleClass by adding states to its objects.
What this library tries to accomplish is the creation of objects that act differently depending on their state.
The code is reasonably commented, so you might want to employ some time in reading through it, in order to gain insight of Lua's amazing flexibility.
MindState was developed as a key part of the PÄSSION lib.
Contents
Questions
Please refer to the forum post if you have any questions/issues/bugfixes.
Requirementes
- MiddleClass to be installed and available through require('MiddleClass.lua')
Main Features
- StatefulObject is the root class for objects with stateful infomation (derived from MiddleClass' root Object)
- Subclasses of StatefulObject have a class method called addState. It creates new states.
- States redefine functions: Instances on a state will use the state's methods instead of the methods defined on the class.
- An instance can "change its state" via the method instance:gotoState('stateName') or instance:gotoState(nil)
- There are two callbacks called onEnterState and onExitState, called when instances enter or exit a State
- States are be inherited by subclasses (if a parent class has states, then the subclass has the same states, and can modify them)
- States are "Stackable"; an object can be on several states at a given moment.
In addition to these main features, the interface is complemented with:
- Class.states and instance.states contain the list of all the states available for the class/instance.
- instance.currentState returns the current state (or nil).
- state.name returns an identifying string
Examples
Basic states and callbacks
Let's say you have an Enemy class that defines only one method, called "speak". It also defines two states, "Alive" and "Dying", and in those states the method speak() is redefined.
Also, there are enterState and exitState callbacks that do stuff when an enemy enters/exits those states (note that these callbacks are not mandatory at all).
require 'MindState.lua'
Enemy = class('Enemy', StatefulObject)
function Enemy:speak()
print("Well, you can tell by the way I use my walk, I'm a woman's man, no time to talk.")
end
local Alive = Enemy:addState('Alive')
function Alive:speak()
print("Ah, ha, ha, ha staying alive, staying alive!")
end
local Dying = Enemy:addState('Dying')
function Dying:enterState()
print("I've got a bad feeling about this...")
end
function Dying:speak()
print("I am dying! Noooooooo!")
end
function Dying:exitState()
print("Few! It seems I did not die after all.")
end
local robin = Enemy:new()
robin:speak()
robin:gotoState('Alive')
robin:speak()
robin:gotoState('Dying')
robin:speak()
robin:gotoState(nil)
robin:speak()
--[[ output:
Well, you can tell by the way I use my walk, I'm a woman's man, no time to talk.
Ah-ha-ha-ha staying alive, staying alive!
I've got a bad feeling about this...
I am dying! Noooooooo!
Few! It seems I did not die after all.
Well, you can tell by the way I use my walk, I'm a woman's man, no time to talk.
]]
Game controller
Typically, games start with a Main Menu screen. Then they have an options screen (maybe with more screens inside it for input, gameplay, display and sound). Then they have the "real" game. After the "game over" screen the game returns to the Main Menu.
This can be modelled very easily with MindState. For example, this would be the Game.lua file:
require 'MindState.lua'
Game = class('Game', StatefulObject)
function Game:initialize()
super.initialize(self)
print('Creating global game variables')
self:gotoState('MainMenu')
end
local MainMenu = Game:addState('MainMenu')
function MainMenu:enterState()
print('Creating the main menu buttons')
end
function MainMenu:exitState()
print('Destroying the main menu buttons')
end
local OptionsMenu = Game:addState('OptionsMenu')
function OptionsMenu:enterState()
print('Creating the options menu buttons')
end
function OptionsMenu:exitState()
print('Destroying the options menu buttons')
end
local Play = Game:addState('Play')
function Play:enterState()
print('Creating player, world and enemies')
end
function OptionsMenu:exitState()
print('Destroying the player, world and enemies')
end
You can then use this Game class by instantiating a global variable (we'll call it "game"). You can create it, for example, on main.lua.
require 'Game.lua'
game = Game:new()
--[[ output:
Creating global game variables
Creating the options menu buttons
]]
It is up to you to create the buttons however you want. At some point, some of those buttons will contain the following call (or equivalent)
game:gotoState('OptionsMenu')
--[[ output:
Destroying the main menu buttons
Creating the main menu buttons
]]
Similarly, one can change the state back to 'MainMenu', or go to the 'Play' state.
State Inheritance
States are inherited conveniently.
------------------- Monster.lua:
require 'MindState.lua'
Monster = class('Monster', StatefulObject)
function Monster:initialize()
super.initialize(self)
self:gotoState('Idle')
end
local Idle = Monster:addState('Idle')
function Idle:update(dt)
print('I am a bored')
end
local Attacked = Monster:addState('Attacked')
function Attacked:update()
print('I am being attacked!')
end
------------------- Troll.lua
require 'Monster.lua'
Troll = class('Troll', Monster)
function Troll:initialize()
super.initialize(self)
print('Created Troll')
end
local Stone = Monster:addState('Stone')
function Stone:enterState()
print('I am turning into stone!')
end
function Attacked:update()
print('I am all granite now')
end
------------------- Goblin.lua
require 'Monster.lua'
function Goblin:initialize()
super.initialize(self)
print('Created Goblin')
end
Goblin = class('Goblin', Monster)
-- Goblins are coward and run away if attacked
function Goblin.states.Attacked:enterState(dt)
print('I am being attacked! I have to run away fast!')
self:gotoState('RunningAway')
end
local RunningAway = Goblin:addState('RunningAway')
function RunningAway:update(dt)
print('Run, run, run!')
end
Now, an example of use:
require('Troll.lua')
require('Goblin.lua')
local terrence = Troll:new()
terrence:update()
terrence:gotoState('Attacked')
terrence:update()
terrence:gotoState('Stone')
terrence:update()
local gob = Goblin:new()
gob:update()
gob:gotoState('Attacked')
gob:update()
--[[ output:
Created Troll
I am bored
I am being attacked!
I am turning into stone!
I am all granite now
Created Goblin
I am bored
I am being attacked! I have to run away fast!
Run, run, run!
]]
On the previous code, the Monster class has two states, "Idle" and "Attacked". Idle is set by default on the constructor.
On another file, we define a Troll class as a subclass of Monster. Troll inherits all the states from Monster (it can be also Attacked and Idle). But it adds an additional state, called 'Stone'.
Finally, the Goblin class, also a subclass of Monster, defines how Goblins behave: They react cowardly and enter a new state called 'RunningAway' as soon as they get attacked.
Stackable States
Source Code
MindState is bundled with MiddleClass, which means that it can be found on the MiddleClass google code project page.
- Here's a link to the latest raw file
- And here you have a syntax highlighted version
Advanced Features
- States can 'copy' (inherit from) other states.
- States work with mixins
- States are classes, so if needed they can be initialized on the class initialize() constructor
Advanced Examples
State "Copying"
In reality, States themselves are classes. Nothing stops you from creating a State that is a subclass of another state. This way, it will inherit its methods (which you can then override). The addState method takes a second parameter for specifying a superclass (by default it is Object).
require('MindState.lua')
GirlFriend = class('GirlFriend', StatefulObject)
function GirlFriend:getStatus()
print("I'm bored. Entertain me")
end
local Angry = GirlFriend:addState('Angry')
function Angry:getStatus()
print("I'm angry with you")
end
function Angry:askWhy()
print("You should know why")
end
local AngrySilent = GirlFriend:addState('AngrySilent', Angry) -- AngrySilent "copies" Angry (in reality it is a subclass of Angry)
function AngrySilent:askWhy()
print("...")
end
gf = GirlFriend:new()
gf:getStatus()
gf:gotoState('Angry')
gf:getStatus()
gf:askWhy()
gf:gotoState('AngrySilent')
gf:getStatus()
gf:askWhy()
--[[ output:
I'm bored. Entertain me.
I'm angry with you
You should know why
I'm angry with you
...
]]
On this example how the AngrySilent state inherits the getStatus function from Angry, and then redefines askWhy.