Jump to content

GameStates with CStateManager

Averice

1,236 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 1
      DPI scaling and the 2D drawing and GUI system were an issue I was a bit concerned about, but I think I have it worked out. This all goes back to the multi-monitor support that I designed back in September. Part of that system allows you to retrieve the DPI scale for each display. This gives you another piece of information in addition to the raw screen resolution. The display scale gives you a percentage value the user expects to see vector graphics at, with 100% being what you would expect with a regular HD monitor. If we scale our GUI elements and font sizes by the display scale we can adjust for screens with any pixel density.
      This shot shows 1920x1080 fullscreen with DPI scaling set to 100%:

      Here we see the same resolution, with scaling set to 125%:

      And this is with scaling set to 150%:

      The effect of this is that if the player is using a 4K, 8K, or any other type of monitor, your game can display finely detailed text at the correct size the user expects to see. It also means that user interfaces can be rendered at any resolution for VR.
    • By Josh in Josh's Dev Blog 2
      Previously I talked about the technical details of hardware tessellation and what it took to make it truly useful. In this article I will talk about some of the implications of this feature and the more advanced ramifications of baking tessellation into Turbo Game Engine as a first-class feature in the 
      Although hardware tessellation has been around for a few years, we don't see it used in games that often. There are two big problems that need to be overcome.
      We need a way to prevent cracks from appearing along edges. We need to display a consistent density of triangles on the screen. Too many polygons is a big problem. I think these issues are the reason you don't really see much use of tessellation in games, even today. However, I think my research this week has created new technology that will allow us to make use of tessellation as an every-day feature in our new Vulkan renderer.
      Per-Vertex Displacement Scale
      Because tessellation displaces vertices, any discrepancy in the distance or direction of the displacement, or any difference in the way neighboring polygons are subdivided, will result in cracks appearing in the mesh.

      To prevent unwanted cracks in mesh geometry I added a per-vertex displacement scale value. I packed this value into the w component of the vertex position, which was not being used. When the displacement strength is set to zero along the edges the cracks disappear:

      Segmented Primitives
      With the ability to control displacement on a per-vertex level, I set about implementing more advanced model primitives. The basic idea is to split up faces so that the edge vertices can have their displacement scale set to zero to eliminate cracks. I started with a segmented plane. This is a patch of triangles with a user-defined size and resolution. The outer-most vertices have a displacement value of 0 and the inner vertices have a displacement of 1. When tessellation is applied to the plane the effect fades out as it reaches the edges of the primitive:

      I then used this formula to create a more advanced box primitive. Along the seam where the edges of each face meet, the displacement smoothly fades out to prevent cracks from appearing.

      The same idea was applied to make segmented cylinders and cones, with displacement disabled along the seams.


      Finally, a new QuadSphere primitive was created using the box formula, and then normalizing each vertex position. This warps the vertices into a round shape, creating a sphere without the texture warping that spherical mapping creates.

      It's amazing how tessellation and displacement can make these simple shapes look amazing. Here is the full list of available commands:
      shared_ptr<Model> CreateBox(shared_ptr<World> world, const float width = 1.0); shared_ptr<Model> CreateBox(shared_ptr<World> world, const float width, const float height, const float depth, const int xsegs = 1, const int ysegs = 1); shared_ptr<Model> CreateSphere(shared_ptr<World> world, const float radius = 0.5, const int segments = 16); shared_ptr<Model> CreateCone(shared_ptr<World> world, const float radius = 0.5, const float height = 1.0, const int segments = 16, const int heightsegs = 1, const int capsegs = 1); shared_ptr<Model> CreateCylinder(shared_ptr<World> world, const float radius = 0.5, const float height=1.0, const int sides = 16, const int heightsegs = 1, const int capsegs = 1); shared_ptr<Model> CreatePlane(shared_ptr<World> world, cnst float width=1, const float height=1, const int xsegs = 1, const int ysegs = 1); shared_ptr<Model> CreateQuadSphere(shared_ptr<World> world, const float radius = 0.5, const int segments = 8); Edge Normals
      I experimented a bit with edges and got some interesting results. If you round the corner by setting the vertex normal to point diagonally, a rounded edge appears.

      If you extend the displacement scale beyond 1.0 you can get a harder extended edge.

      This is something I will experiment with more. I think CSG brush smooth groups could be used to make some really nice level geometry.
      Screen-space Tessellation LOD
      I created an LOD calculation formula that attempts to segment polygons into a target size in screen space. This provides a more uniform distribution of tessellated polygons, regardless of the original geometry. Below are two cylinders created with different segmentation settings, with tessellation disabled:

      And now here are the same meshes with tessellation applied. Although the less-segmented cylinder has more stretched triangles, they both are made up of triangles about the same size.

      Because the calculation works with screen-space coordinates, objects will automatically adjust resolution with distance. Here are two identical cylinders at different distances.

      You can see they have roughly the same distribution of polygons, which is what we want. The same amount of detail will be used to show off displaced edges at any distance.

      We can even set a threshold for the minimum vertex displacement in screen space and use that to eliminate tessellation inside an object and only display extra triangles along the edges.

      This allows you to simply set a target polygon size in screen space without adjusting any per-mesh properties. This method could have prevented the problems Crysis 2 had with polygon density. This also solves the problem that prevented me from using tessellation for terrain. The per-mesh tessellation settings I worked on a couple days ago will be removed since it is not needed.
      Parallax Mapping Fallback
      Finally, I added a simple parallax mapping fallback that gets used when tessellation is disabled. This makes an inexpensive option for low-end machines that still conveys displacement.

      Next I am going to try processing some models that were not designed for tessellation and see if I can use tessellation to add geometric detail to low-poly models without any cracks or artifacts.
    • By Josh in Josh's Dev Blog 0
      For finer control over what 2D elements appear on what camera, I have implemented a system of "Sprite Layers". Here's how it works:
      A sprite layer is created in a world. Sprites are created in a layer. Layers are attached to a camera (in the same world). The reason the sprite layer is linked to the world is because the render tweening operates on a per-world basis, and it works with the sprite system just like the entity system. In fact, the rendering thread uses the same RenderNode class for both.
      I have basic GUI functionality working now. A GUI can be created directly on a window and use the OS drawing commands, or it can be created on a sprite layer and rendered with 3D graphics. The first method is how I plan to make the new editor user interface, while the second is quite flexible. The most common usage will be to create a sprite layer, attach it to the main camera, and add a GUI to appear in-game. However, you can just as easily attach a sprite layer to a camera that has a texture render target, and make the GUI appear in-game on a panel in 3D. Because of these different usages, you must manually insert events like mouse movements into the GUI in order for it to process them:
      while true do local event = GetEvent() if event.id == EVENT_NONE then break end if event.id == EVENT_MOUSE_DOWN or event.id == EVENT_MOUSE_MOVE or event.id == EVENT_MOUSE_UP or event.id == EVENT_KEY_DOWN or event.id == EVENT_KEY_UP then gui:ProcessEvent(event) end end You could also input your own events from the mouse position to create interactive surfaces, like in games like DOOM and Soma. Or you can render the GUI to a texture and interact with it by feeding in input from VR controllers.

      Because the new 2D drawing system uses persistent objects instead of drawing commands the code to display elements has changed quite a lot. Here is my current button script. I implemented a system of abstract GUI "rectangles" the script can create and modify. If the GUI is attached to a sprite layer these get translated into sprites, and if it is attached directly to a window they get translated into system drawing commands. Note that the AddTextRect doesn't even allow you to access the widget text directly because the widget text is stored in a wstring, which supports Unicode characters but is not supported by Lua.
      --Default values widget.pushed=false widget.hovered=false widget.textindent=4 widget.checkboxsize=14 widget.checkboxindent=5 widget.radius=3 widget.textcolor = Vec4(1,1,1,1) widget.bordercolor = Vec4(0,0,0,0) widget.hoverbordercolor = Vec4(51/255,151/255,1) widget.backgroundcolor = Vec4(0.2,0.2,0.2,1) function widget:MouseEnter(x,y) self.hovered = true self:Redraw() end function widget:MouseLeave(x,y) self.hovered = false self:Redraw() end function widget:MouseDown(button,x,y) if button == MOUSE_LEFT then self.pushed=true self:Redraw() end end function widget:MouseUp(button,x,y) if button == MOUSE_LEFT then self.pushed = false if self.hovered then EmitEvent(EVENT_WIDGET_ACTION,self) end self:Redraw() end end function widget:OK() EmitEvent(EVENT_WIDGET_ACTION,self) end function widget:KeyDown(keycode) if keycode == KEY_ENTER then EmitEvent(EVENT_WIDGET_ACTION,self) self:Redraw() end end function widget:Start() --Background self:AddRect(self.position, self.size, self.backgroundcolor, false, self.radius) --Border if self.hovered == true then self:AddRect(self.position, self.size, self.hoverbordercolor, true, self.radius) else self:AddRect(self.position, self.size, self.bordercolor, true, self.radius) end --Text if self.pushed == true then self:AddTextRect(self.position + iVec2(1,1), self.size, self.textcolor, TEXT_CENTER + TEXT_MIDDLE) else self:AddTextRect(self.position, self.size, self.textcolor, TEXT_CENTER + TEXT_MIDDLE) end end function widget:Draw() --Update position and size self.primitives[1].position = self.position self.primitives[1].size = self.size self.primitives[2].position = self.position self.primitives[2].size = self.size self.primitives[3].size = self.size --Update the border color based on the current hover state if self.hovered == true then self.primitives[2].color = self.hoverbordercolor else self.primitives[2].color = self.bordercolor end --Offset the text when button is pressed if self.pushed == true then self.primitives[3].position = self.position + iVec2(1,1) else self.primitives[3].position = self.position end end This is arguably harder to use than the Leadwerks 4 system, but it gives you advanced capabilities and better performance that the previous design did not allow.
×
×
  • Create New...