|
|
(3 intermediate revisions by 2 users not shown) |
Line 1: |
Line 1: |
− | This library complements [[MiddleClass]] by adding states to its objects.
| + | #REDIRECT [[stateful.lua]] |
− | | |
− | 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 [http://love2d.org/forum/viewtopic.php?f=5&t=1039 PÄSSION] lib.
| |
− | | |
− | ==Table of Contents==
| |
− | | |
− | # [[MindState#Questions|Questions]]
| |
− | # [[MindState#Requirements|Requirements]]
| |
− | # [[MindState#Main_Features|Main Features]]
| |
− | # [[MindState#Basic_states_and_callbacks|Basic states and callbacks]]
| |
− | ## [[MindState#Another_Example:_Game_controller|Another Example: Game controller]]
| |
− | # [[MindState#State_Inheritance|State Inheritance]]
| |
− | # [[MindState#Stackable_States|Stackable States]]
| |
− | # [[MindState#Source_Code|Source Code]] | |
− | # [[MindState#Advanced_Features|Advanced Features]]
| |
− | # [[MindState#Advanced Examples|Advanced Examples]]
| |
− | | |
− | ==Questions==
| |
− | Please refer to the [http://love2d.org/forum/viewtopic.php?f=5&t=1053 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
| |
− | | |
− | ==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).
| |
− | <source lang="lua">
| |
− | 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.
| |
− | ]]
| |
− | </source>
| |
− | | |
− | ===Another Example: 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:
| |
− | <source lang="lua">
| |
− | 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
| |
− | </source>
| |
− | 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.
| |
− | <source lang="lua">
| |
− | require 'Game.lua'
| |
− | game = Game:new()
| |
− | --[[ output:
| |
− | Creating global game variables
| |
− | Creating the options menu buttons
| |
− | ]]
| |
− | </source>
| |
− | 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)
| |
− | <source lang="lua">
| |
− | game:gotoState('OptionsMenu')
| |
− | --[[ output:
| |
− | Destroying the main menu buttons
| |
− | Creating the main menu buttons
| |
− | ]]
| |
− | </source>
| |
− | Similarly, one can change the state back to 'MainMenu', or go to the 'Play' state.
| |
− | | |
− | ==State Inheritance==
| |
− | States are inherited conveniently.
| |
− | | |
− | <source lang="lua">
| |
− | | |
− | ------------------- 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
| |
− | </source>
| |
− | | |
− | Now, an example of use:
| |
− | <source lang="lua">
| |
− | 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!
| |
− | ]]
| |
− | </source>
| |
− | | |
− | 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==
| |
− | | |
− | Stateful Objects have an internal pile, in which they can "push" and "pop" states.
| |
− | | |
− | When an object has more than one state on its pile, the methods are searched this way:
| |
− | 1. First, look for the method on the state at the top of the pile
| |
− | 2. Then look for the method on the second state
| |
− | ...
| |
− | N. When there are no states left to look for, look on the class definition.
| |
− | N+1. Look on the superclasses.
| |
− | | |
− | In order to use the pile, one has to use the pushState and popState methods (as opposed to gotoState).
| |
− | | |
− | In addition to the aforementioned enterState and exitState callbacks, when using the pile there are 4 additional ones: pausedState, pushedState, poppedState and continuedState.
| |
− | | |
− | Let's say that an object is on state 'A', and we push state 'B'. Assuming that all the callbacks were defined for states A and B, they would get called on this order:
| |
− | | |
− | <source lang="lua">
| |
− | -- object is initially on state 'A'
| |
− | object:pushState('B')
| |
− | -- A.pausedState(object)
| |
− | -- B.pushedState(object)
| |
− | -- B.enterState(object)
| |
− | </source>
| |
− | | |
− | Similarly, when the state is popped from the object, this is what happens:
| |
− | <source lang="lua">
| |
− | -- object state pile is [A,B]
| |
− | object:popState()
| |
− | -- B.exitState(object)
| |
− | -- B.poppedState(object)
| |
− | -- A.continuedState(object)
| |
− | </source>
| |
− | | |
− | Note that popState accepts a state name as a parameter. If included, the state is removed from the pile, even if it is not on the top. However, this will trigger 'continuedState' on the state at the top of the pile, so be careful!
| |
− | <source lang="lua">
| |
− | -- object state pile is [A,B,C]
| |
− | object:popState('B')
| |
− | -- B.exitState(object)
| |
− | -- B.poppedState(object)
| |
− | -- C.continuedState(object)
| |
− | -- the state pile is now [A,C]
| |
− | </source>
| |
− | | |
− | ===Example===
| |
− | | |
− | Let's say that you have a Soldier class that can be Running and Firing at the same time. You can use the following example as a reference.
| |
− | | |
− | Note that we're using enterState and exitState callbacks here- you could use the other callbacks too.
| |
− | <source lang="lua">
| |
− | Soldier = class('Soldier', StatefulObject)
| |
− | function Soldier:initialize()
| |
− | super.initialize(self)
| |
− | end
| |
− | function Soldier:draw()
| |
− | print('yes, captain?')
| |
− | end
| |
− | function Soldier:status()
| |
− | print('in static position')
| |
− | end
| |
− | | |
− | Firing = Soldier:addState('Firing')
| |
− | function Firing:enterState()
| |
− | print('weapon ready')
| |
− | end
| |
− | function Firing:draw()
| |
− | print('pew pew!')
| |
− | end
| |
− | | |
− | Running = Soldier:addState('Running')
| |
− | function Running:draw()
| |
− | print('1,2,1,2')
| |
− | end
| |
− | function Running:status()
| |
− | print('go go go!')
| |
− | end
| |
− | function Running:exitState()
| |
− | print('resting my feet')
| |
− | end
| |
− | | |
− | frank = Soldier:new()
| |
− | frank:draw() -- yes, captain?
| |
− | frank:status() -- in static position
| |
− | | |
− | frank:pushState('Running')
| |
− | frank:draw() -- 1,2,1,2
| |
− | frank:status() -- go go go!
| |
− | | |
− | frank:pushState('Firing') -- weapon ready
| |
− | frank:draw() -- pew pew!
| |
− | frank:status() -- go go go!
| |
− | | |
− | frank:popState('Running') -- resting my feet
| |
− | frank:draw() -- pew pew!
| |
− | frank:status() -- in static position
| |
− | | |
− | frank:popState()
| |
− | frank:draw() -- yes, captain?
| |
− | frank:status() -- in static position
| |
− | | |
− | </source>
| |
− | | |
− | ==Source Code==
| |
− | MindState can be found on the [http://github.com/kikito/middleclass-extras MiddleClass Extras github page]
| |
− | * Here's a link to the latest [http://github.com/kikito/middleclass-extras/raw/master/MindState.lua raw file]
| |
− | * And here you have a [http://github.com/kikito/middleclass-extras/blob/master/MindState.lua 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).
| |
− | | |
− | <source lang="lua">
| |
− | 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
| |
− | ...
| |
− | ]]
| |
− | </source>
| |
− | | |
− | On this example how the AngrySilent state inherits the getStatus function from Angry, and then redefines askWhy.
| |
− | | |
− | == See also ==
| |
− | | |
− | * [[Libraries]]
| |
− | | |
− | {{#set:LOVE Version=0.6.1}}
| |
− | {{#set:Description=Stateful information complement for [[MiddleClass]].}}
| |
− | [[Category:Libraries]]
| |