Jump to content

GameStates with CStateManager

Averice

1,280 views

I released an OOP class sytem for leadwerks last time this time I'm going to post my StateManager class which I use to control all the gamestates for the game I am working on.

 

If you don't know what a Statemanager is or what my interpretation of one is I'll run through it quickly.

Generally speaking it's just a stack full of 'states' each state has a list of functions with identical names but different internal functionality, the game will call these functions but ONLY for the active state, say your game has a Draw function but you want a splash screen with a Draw function, you'll separate these into different States for the stack, now when your splash state is active only the splash's Draw function will be called. State stacks almost always run [L]ast n [F]irst [O]ut, so the last state you push onto the stack will be the one the game starts using, so to have a splash screen -> menu -> game order in your stack you would push the game onto the stack first, the menu second and the splash last so that the splash is the first thing the end user sees.

 

Enough ramblings let me post some code.

 

statemanager.lua

requires the class scripts.

class "CStateManager";

function CStateManager:Init()
self.States = {}
end

function CStateManager:Push(state, init)
if( ctype(state) == "CState" ) then
self.States[#self.States+1] = state;
if( init ) then
if( self.States[#self.States-1] and self.States[#self.States-1].isinit ) then
self.States[#self.States-1]:Shutdown();
self.States[#self.States-1].isinit = false;
end
self.States[#self.States]:Init(App.context);
self.States[#self.States].isinit = true;
end
else
print("StateManager: CStateManager.Push expected CState got: "..ctype(state));
end
end

function CStateManager:InitCurrentState()
if( self.States[1] and not self.States[#self.States].isinit ) then
self.States[#self.States]:Init(App.context);
self.States[#self.States].isinit = true;
end
end

function CStateManager:Pop()
if( self.States[1] ) then
if( self.States[#self.States].isinit ) then
self.States[#self.States].isinit = false;
self.States[#self.States]:Shutdown();
end
local oldState = self.States[#self.States];
self.States[#self.States] = nil;
self:InitCurrentState();
return oldState;
end
print("StateManager: Called CStateManager.Pop with empty stack");
end

function CStateManager:GetAll()
return self.States
end

function CStateManager:GetActive()
if( self.States[1] and self.States[#self.States].isinit ) then
return self.States[#self.States];
end
print("StateManager: Called CStateManager.GetActive with no running states");
end

function CStateManager:Pause(state)
if( ctype(state) == "CState" ) then
state.paused = true;
end
end

function CStateManager:Resume(state)
if( ctype(state) == "CState" ) then
state.paused = false;
end
end

function CStateManager:IsPaused(state)
if( ctype(state) == "CState" ) then
return state.paused;
end
end

function CStateManager:Call(func, ...)
if( self.States[1] and self.States[#self.States].isinit and not self.States[#self.States].paused ) then
if( self.States[#self.States][func] ) then
self.States[#self.States][func](self.States[#self.States], ...);
end
end
end

 

state.lua

-- Tiny file this one. really just a declaration and a nilfix file.

class "CState";

function CState:Init()
end

function CState:Shutdown()
end

 

Example useage.


-- Our splash screen.
SplashScreen = new "CState"

function SplashScreen:Init()
self.Something = "HELLO";
end

function SplashScreen:Think()
self.Something = self.Something.."O";
end
function SplashScreen:Draw()
App.context:DrawText(self.Something, 100, 100);
end

-- Now something else.
Random = new "CState"

function Random:Draw()
App.context:DrawText("Second State", 100, 200);
end

 

-- Now in our main file to initialize out statemanager and load our states.

function App:Start()
StateManager = new "CStateManager";
StateManager:Push(Random); -- Remember this goes before the splash so we see it AFTER the splash
StateManager:Push(SplashScreen, true); the true means we want to initialize this, as it's the last state being pushed we may aswell tell the statemanager we are ready to begin.
end

function App:Loop()
StateManager:Call("Think") -- Can name your functions anything, Init and Shutdown are always the same though.
StateManager:Call("Draw", "some", "arguments", "here", "if", "you", "want");
end

 

To remove the current state from the stack and initialize the next, we use StateManager:Pop();

I hope people get some use out of this, and I hope I've explained it nice enough.



2 Comments


Recommended Comments

Instead of:

 

SplashScreen = new "CState"

 

wouldn't you want:

 

class "SplashScreen" : extends "CState"

 

since that's sort of the point to why you made the extends function? Just curious why you didn't go that route?

 

Also I notice you are a semicolon guy. They aren't needed when each statement is on a separate line, but guessing you come from C++ and just a habit or you have some plans for them?

Share this comment


Link to comment

SplashScreen is an instance of CState not a new derivative class, I know semi colons aren't needed just a habit that I don't see the need in breaking.

 

I try to make these modules self contained, so any errors are reported to the user instead of crashing the game, CStateManager checks if a CState is being pushed onto the stack, if it's not a CState it isn't allowed. The inheritance in my class module is used to have base classes with inherited values, whereas the SplashScreen in this example is just a class instance with modified public methods ( I know they're all public being Lua since we can't privatize them without a workaround with the standard class metatable )

 

It would still work extending instead of instancing but most states will not be even remotely similar so inheriting values we won't use wouldn't be wise.

Share this comment


Link to comment

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.

Guest
Add a comment...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

  • Blog Entries

    • By Josh in Josh's Dev Blog 0
      I've restructured the plugin SDK for our new engine and created a new repository on Github here:
      https://github.com/Leadwerks/PluginSDK
      The GMF2 format will only be used as an internal data transfer protocol for model loader plugins. Our main supported file format will be GLTF.
      As of now, the plugin system can be used to write texture loaders for different file formats, model loaders, or to modify behavior of particles in the new particle system. The FreeImage texture loader has been moved out of the core engine and into a plugin so you no longer have to include the FreeImage DLL unless you want to use it. The main supported texture format will be DDS, but the FreeImage plugin supports many common image file formats.
    • By Admin in Leadwerks Company Blog 2
      The GMF2 file format provides the fastest possible load times for 3D models. A preliminary specification and SDK for loading and saving files in the GMF2 file format is now available on GitHub here:
      A Quake 3 MD3 model loader is included as an example.
    • By Josh in Josh's Dev Blog 2
      The Leadwerks 5 beta will soon be updated with particle emitters and an example particle system plugin. Previously, I showed some impressive results with physically interactive particles that collide with and exert forces on the environment. I decided to use the plugin system for controlling particle behavior, as this offers the best performance and can be run on the physics thread. 
      A particle system plugin uses some predefined structures and functions to modify the behavior of particles when they are emitted or as they are updated. This allows for unlimited features to be added to the particle system, because anything you want can be added with a plugin. A system for sending settings to the plugin will be implemented in the future so you can adjust the plugin settings and see the results. The default particle settings and features will probably stay pretty barebones and I will just use the plugin system to add any advanced functionality since it is so flexible.
      void EmitParticle(ParticleModifier* mod, ParticleSystem* particlesystem, Particle* particle) { if (mod->emissionshape == EMISSION_SHAPE_BOX) { particle->position[0] = Random(-mod->area[0], mod->area[0]); particle->position[1] = Random(-mod->area[1], mod->area[1]); particle->position[2] = Random(-mod->area[2], mod->area[2]); } else if (mod->emissionshape == EMISSION_SHAPE_CYLINDER) { particle->position[0] = Random(-mod->area[0], mod->area[0]); particle->position[1] = Random(-mod->area[1], mod->area[1]); particle->position[2] = Random(-mod->area[2], mod->area[2]); auto l = sqrt(particle->position[0] * particle->position[0] + particle->position[1] * particle->position[1] + particle->position[2] * particle->position[2]); if (l > 0.0f) { particle->position[0] /= l; particle->position[1] /= l; particle->position[2] /= l; } } particle->position[0] += particlesystem->matrix[12]; particle->position[1] += particlesystem->matrix[13]; particle->position[2] += particlesystem->matrix[14]; } There are three other new Lua examples included. Coroutines.lua shows how a sequence of actions can be added to an entity before the game starts, and the actions will be executed in order:
      --Create model local model = CreateBox(world) --Add some behaviors to be executed in order model:AddCoroutine(MoveToPoint, Vec3(3,0,0), 2) model:AddCoroutine(MoveToPoint, Vec3(-3,0,0), 2) model:AddCoroutine(MoveToPoint, Vec3(0,0,0), 2) --Main loop while window:Closed() == false do world:Update() world:Render(framebuffer) end This is great for setting up cut scenes or other sequences of events.
      An example showing how to enable tessellation is also included. Tessellation is now a per-camera setting.
      camera:SetTessellation(10) The number you input is the size in pixels of the tessellated primitives. Use zero to disable tessellation. Tessellation is disabled by default on all cameras.
      Finally, an example showing how to use a texture loader plugin is included. All you have to do is load the plugin and after that textures can be loaded in VTF format:
      local vtfloader = LoadPlugin("Plugins/VTF.dll") local tex = LoadTexture("Materials/wall01.vtf")  
×
×
  • Create New...