How Goblinson Crusoe Works (The TL;DR version)

Published February 11, 2011
Advertisement
As per yckx's request, I'm writing a little bit about how Goblinson Crusoe is made.

[subheading]The Core Library (C++)[/subheading]

GC is actually a Lua runtime environment running a default Lua script specified in the build process as startup.lua. If no default script is specified, then the executable goes to a command-line Lua interpreter. The interpreter is based off the default Lua command-line interpreter, but with C++ extensions for my own library of stuff built into it.

Included in the C++ library are things such as the core system, which handles creating a window with an OpenGL context (via SFML) and handles querying for events from the event queue. There is also a boost::shared_ptr-based resource management system that handles loading and caching requested assets. There is also a set of modules, CIsoSceneChunk (which manages an isometric scene) and CIsoView (which handles rendering an isometric scene chunk). All of my noise modules, adapters, etc... are also implemented as C++ exposed to Lua; basically, anything that I feel needs to be in C++ for performance or convenience reasons, such as the picking groups which I rewrote as C++ this very morning. (Early, early morning).

C++ modules are exposed via tolua, which makes it very easy to generate the bindings. I also make extensive use of the simple Lua class setup described here . This convention is not strictly necessary, of course, but I do find it convenient to build managers and components as "classes".


[subheading]The Kernel (Main Loop) (Lua)[/subheading]


The heart of the game is the Lua class CoreKernel, found in kernel.lua. It's based on your typical interpolation-based fixed-timestep game loop: Gather input and process, perform logic updates according to the specified update-rate, perform interpolations for rendering, and render. Here is the loop:

[spoiler]


CoreKernel=class(function(a)
a.reset_loop=false
a.running=false
a.oldtime=0
a.logicupdatecounter=0
a.logicstep=0
a.curtime=0
a.framedelta=0
a.percentwithintick=0
a.statecontext=nil

a.logframerate=false
end)

function CoreKernel:addFrameTime(elapsed_time, percent_within_tick)
if self.statecontext~=nil then
self.statecontext:addFrameTime(elapsed_time, percent_within_tick)
end
interface.cursormanager:addElapsedTime(elapsed_time)
end

function CoreKernel:updateLogic()
if self.statecontext~=nil then
self.statecontext:updateLogic()
end
end

function CoreKernel:handleEvent(event)
end
if self.statecontext~=nil then
self.statecontext:handleEvent(event)
end
end

function CoreKernel:render()
if self.statecontext~=nil then
self.statecontext:render()
end
end

function CoreKernel:endLoop()
self.running=false
end

function CoreKernel:resetLoop()
self.reset_loop=true
end

function CoreKernel:screenshot()
core_system:screenshot("screen.jpg")
end

function CoreKernel:run(updates_per_sec, startingstate)
self.statecontext=startingstate
if self.statecontext==nil then
print("Invalid starting state; exiting kernel.")
return
end

self.reset_loop=false
self.running=true
self.oldtime=core_system:getTime()
self.logicupdatecounter=0
self.logicstep=1.0/updates_per_sec
self.purgeevents=false
event=sf.Event()

local thistime=0
local timesincelastshow=0
local framecount=0
local totaltime=0

while(self.running) do
if(self.reset_loop) then
-- Reset loop after performing some long processing action, in order to keep from "jumping"
self.oldtime=core_system:getTime()
self.logicupdatecounter=0
self.reset_loop=false
self.purgeevents=true
end

-- Process input
while (core_system:getEvent(event)) do
if self.purgeevents==false then self:handleEvent(event) end
if self.running==false then return end
end
self.purgeevents=false

self.curtime=core_system:getTime()
self.framedelta=(self.curtime-self.oldtime)
totaltime=totaltime+self.framedelta
thistime=thistime+self.framedelta

-- Update logic

self.logicupdatecounter = self.logicupdatecounter+self.framedelta
while(self.logicupdatecounter >= self.logicstep) do
-- Update logic
self:updateLogic()
self.logicupdatecounter = self.logicupdatecounter - self.logicstep

-- Debugging: Log framerate+mem usage at intervals
if self.logframerate then
if timesincelastshow==0 then
timesincelastshow=400
console_print("("..math.floor(thistime)..") FPS: "..math.floor(framecount/totaltime).." Lua mem: "..math.floor(collectgarbage("count")))
framecount=0
totaltime=0

else
timesincelastshow=timesincelastshow-1
end
end
if self.reset_loop then self.logicupdatecounter=0 end
end

-- Render

self.percentwithintick = self.logicupdatecounter / self.logicstep

-- Add frame time to update animations and interpolate positions
self:addFrameTime(self.framedelta, self.percentwithintick)
core_system:clear()
self:render()
if(interface.gui_window) then
interface.gui_window:draw(self.percentwithintick)
end
-- Flip the page
core_system:render()

--collectgarbage()
self.oldtime=self.curtime
framecount=framecount+1
end
end

[/spoiler]

Once the various systems are initialized in startup.lua (window created, world created, kernel created, interface created, etc...) then control is turned over to CoreKernel until the application exits.

CoreKernel accepts the setting of a local reference to a StateContext. A StateContext is a Lua class that provides three methods: handleInput(), updateLogic(), and addFrameTime(). By changing the current StateContext, I can implement different states of the game: main menu, gameplay, etc...

In the case of Goblinson Crusoe, I have a World object which encapsulates an isometric world, including the various picking managers, resource managers, etc... The World object acts as a StateContext for the kernel, so that when it is set as the state context then we are in gameplay mode: the world is drawn, input is handled, objects are updated, etc...

[subheading]The Objects (Lua)[/subheading]

Now, the way I implement objects is based on the whole component/entity methodology that seems to be in vogue. Objects in the game, whether they are trees or rocks to be harvested, the player, projectiles fired at enemies, the enemies themselves, etc... are all basically the same base type of object. This base object implements a couple methods: addComponent(), which will add a new behavioral component to the object to help define its attributes, and handleMessage(), which will pass a message/event, along with an optional list of arguments, to the object. Objects also maintain positional state (current position + last position) as well as a GUID unique to the object. Here is the source to BaseObject:

[spoiler]


Object=class(function(a, guid)
a.components=LinkedList()
a.guid=guid
a.pos={x=0, y=0, z=0}
a.lastpos={x=0, y=0, z=0}

end)

function Object:SetObjectLogicalPosition(owner,args)
self.pos.x=args.x
self.pos.y=args.y
self.pos.z=args.z
self.lastpos.x=args.x
self.lastpos.y=args.y
self.lastpos.z=args.z
end

function Object:UpdateObjectLogicalPosition(owner,args)
self.lastpos.x=self.pos.x
self.lastpos.y=self.pos.y
self.lastpos.z=self.pos.z
self.pos.x=args.x
self.pos.y=args.y
self.pos.z=args.z
end

function Object:UpdateObjectLogicalPositionFix(owner,args)
self.lastpos.x=self.pos.x
self.lastpos.y=self.pos.y
self.lastpos.z=self.pos.z
end

function Object:AddFrameTime(owner,args)
local t=args.percent_within_tick
local xvis=self.lastpos.x + t*(self.pos.x-self.lastpos.x)
local yvis=self.lastpos.y + t*(self.pos.y-self.lastpos.y)
local zvis=self.lastpos.z + t*(self.pos.z-self.lastpos.z)
self:handleMessage("UpdateObjectVisualPosition", {x=xvis, y=yvis, z=zvis})
end

function Object.addComponent(self,comp)
self.components:push_back(comp)
end

function Object.removeComponent(self, comp)
self.components:remove(comp)
end

function Object.handleMessage(self, msg, args)
-- First, see if the object itself wants to handle it
if self[msg]~=nil then self[msg](self,self,args) end
-- Now, pass it on to components
local p=self.components.list
local k
while p~=nil do
k=p.value
if k[msg]~=nil then
k[msg](k,self,args)

end
p=p.next
end
end

function Object.handleRequest(self,msg,args)
local p=self.components.list
local k
local retvalue=LinkedList()
--First, see if object wants to return a request
if self[msg]~=nil then retvalue:push_back(self[msg](self,self,args)) end
while p~=nil do
k=p.value
if k[msg]~=nil then
retvalue:push_back(k[msg](k,self,args))
end
p=p.next
end
return retvalue
end

[/spoiler]

addComponent() adds an instance of a component class to an internal list of components. When handleMessage() is called upon the object, the list is iterated and the message is passed to all components in the list. If a component does not implement a function to listen for a specific message, then it is silently ignored by that component. There is also a function, handleRequest(), which acts similarly to handleMessage(), but which will return a list of results. This is used for queries, and I am unsure on whether or not it is actually needed, as I haven't really used it much.

When handleMessage()/handleRequest() are called, the object will look at each component, and check to see if the component has a function that has the same name as the message that is being passed, and if so it calls this function with the passed argument table.

The basic functionality provided by the BaseObject class is this:

1) It handles the messages SetObjectLogicalPosition, UpdateObjectLogicalPosition, and UpdateObjectLogicalPositionFix
The object maintains a current position and a last position, which are interpolated during AddFrameTime from a parameter, percent_within_tick, to provide smooth frame transitions as the object moves, rather than letting the object suddenly "jump" to its new position after taking a step. For all logical purposes, current time is considered to be the actual position of the object, but for picking purposes and rendering purposes, the interpolated visual position is used.

2) It generates UpdateObjectVisualPosition messages to itself in response to AddFrameTime, after performing the aforementioned interpolation. This tells all interested components that the object's visual (ie, on-screen) location has changed.


[subheading]The World[/subheading]

So the world maintains a list of Objects and provides functions to obtain a reference to an Object given its GUID. This is useful, of course, since another object may keep a GUID of the object locally, then call into the World to get the actual Object form the GUID. And if the object has in the meantime been killed, then the World will let the caller know so it can be handled gracefully, rather than blindly trying to act on an object that should be dead. The world also handles the creation and killing of objects in a sensible manner, implementing a Kill List of objects that should be destroyed, and purging those objects at the end of each update cycle.

The main working points of the loop are the updateLogic() and addFrameTime() functions. This is where the magic happens. The World keeps 2 other lists of objects, one for objects interested in getting UpdateLogic messages and another for objects interested in getting AddFrameTime messages, and these two functions act to pass these messages to the lists of interested objects. AddFrameTime messages are passed with a parameter, elapsed_time, to specify how much time has passed since the last time AddFrameTime was called, and is used to advance animations and do other time-dependent tasks. It is also passed a percent_within_tick parameter which is used to interpolate current and last positions and generate UpdateObjectVisualPositon messages. UpdateLogic is called a fixed number of times per second, and is used to move objects around, make decisions, do attacks, and all the other fun game stuff.

Now, the interesting thing about a system like this is that the behavior of an object is determined completely by the components you add to the object. A plain object just sits there, silently passing on messages that are just as silently being ignored. But you add components that can listen for those messages, and suddenly you have a complex system.

[subheading]Object Examples and Usages[/subheading]

For an example, let's consider a tree in GC. A tree is a harvestable resource. It has a visual, non-animated presence in the world, and when you click on it it will disappear (possibly with some animated effect, like the ubiquitous smoke burst) and in its place will appear a Stack of Wood Planks that the player can pick up. So how do we go about creating a tree in GC?

1) We create the base object. This gives us a GUID and an entry in the World's global table of objects. Henceforth, until the object is destroyed, we can obtain a reference to the object via the GUID.

2) We add an IsoStaticSceneEntityComponent to it, specifying the name of the graphic to load. This creates an actual CIsoSceneEntity object (part of the C++ module for isometric world scenes) with the graphic asset associated with it and loaded into a texture. A static scene entity is non-animated (ie, doesn't process AddFrameTime messages to update animations). We can set the scene entity's scale, position, etc... via exposed methods in the CIsoSceneEntity class. This component listens for UpdateObjectVisualPosition messages and will set the position of the scene entity in response. Thus, any time the object's visual position is updated, this component will update the visual representation in the scene entity. OF course, trees don't move around so this message is ignored, but it is there if we do want to make use of it. (Walking trees, anyone?)

3) We add another IsoStaticSceneEntity component to represent the object's shadow. Again, this component listens for UpdateObjectVisualPosition to set the position of the shadow scene entity.

4) We want the tree to be pickable, ie we want the user to be able to point the mouse at it and right-click to harvest it. So we add a PickingComponent. When creating the PickingComponent, we specify the GUID of the object (since the picking group will return GUIDs when pick() is called, just in case the referenced objects at some point are destroyed), as well as the picking group to which the object is to be added (currently I have enemygroup, itemgroup, and usableobjectgroup). Resources are classified as usableobjects, ie they are objects in the world that you right click on to use for some effect, rather than to attack or to pick up and add to inventory. So we specify the usableobject picking group. Lastly, we specify the size of the picking rectangle that defines the object's size in the picking area, in order to perform the actual line->triangle intersection tests.

The PickingComponent also listens for UpdateObjectVisualPosition messages, and will update the object's picking data to reflect changes.

5) We want the tree to have a floating name above its head when the mouse hovers over, so we add a NameAboveHeadComponent, and specify the name to display as well as the color of the text, and how high above the object we should display it. This component listens for a couple special messages: HoverOver and UnHoverOver, which will show and hide the floating text, respectively. It also listens for UpdateObjectVisualPosition to know where the object's current visual position is in order to place the floating text accordingly.

6) Finally, we want to make the tree actually harvestable, so we add a HarvestableResourceComponent. When specifying this component, we pass parameters to determine which resource type to drop when harvested, and which special effect to spawn to indicate the harvesting action. This component will listen for a UseWorldItem message, and in response it will do a sequence of actions. First, it will spawn an item at its current location, of the type specified for drop-type when the component was created. Second, it will spawn a special effect of the type specified. And third, it will pass to the tree object a KillObject message, which makes the object self destruct, removing it from the world and all groups, deleting components and freeing resources.

And there we have a tree resource. In the handleEvent() function of the StateContext, the mouse position is queried and reverse-projected into the world. The reverse-projected coordinates are passed to the various picking groups in order of enemy->item->usable, and if at any step along the way an object is picked, then that object is passed a HoverOver message, while any previously picked object from last time around is given an UnHoverOver message. This has the result of hiding the floating nameplate of the previously picked object, and showing the nameplate of the currently picked object.

Then, if the right mouse button is clicked, we do another pick() against the picking groups, and if a result turns up then we pass an appropriate message to the picked object. In the case of the Tree, we pass the UseWorldItem message, which will trigger the destruction of the tree and the spawning of a Stack of Wood Planks. In the case of an object in the enemy group, we would pass a TargetEnemy message. An object in the item group would get a PickUpItem message.

So altogether, the aggregate of these components forms a tree resource. Take any away, and you change its behavior. The picking component lets you select it, the harvest component lets you harvest it, and the nameplate component gives you a visual indicator of the resource type.


All objects in the game are built up this way. Some more examples:

* Projectiles will have a ProjectileComponent (to handle moving the projectile, testing for collisions, spawning the "explode" effect, etc...), an AnimatedSceneEntityComponent (to animate the fireball or whatever as it moves), and so forth.

* A treasure chest will have an AnimatedEntityComponent (to handle animating the opening/closing lid, etc...), a StorageChestComponent (to handle the inventory management and listen for the UseWorldItem message to display its contents), a NameAboveHeadComponent, a PickingComponent (usableobject group), etc...

* An enemy will have a CombatantComponent, CreatureControllerComponent, AnimatedEntityComponent, PickingComponent (enemy group), NameAboveHeadComponent, VitalsComponent (to track life and food levels), a MapMarkerComponent (to show a dot on the minimap if the enemy is being tracked), a CharacterPortraitComponent (to show the portrait+vitals when selected), a FloatingCombatTextComponent (to display scrolling damage/healing numbers above its head) and so forth.

It is the aggregate of all these smaller behaviors that create the whole entity that is a projectile, or a tree, or a goblin. Since no object is locked into any given set of behaviors, it is entirely possible to create an object, add an AnimatedEntity, Combatant, CreatureController, Picking (usableobject group), NameAboveHead, and StorageChest components, and end up with an object that looks like a goblin, and wanders around like a goblin, but which, when picked (right clicked), will show the interface of a storage chest and allow you to store and retrieve items. It's all in the components you add. Want a tree that when harvested will shoot the harvester with a poison spike? There's a component for that. Want the scene camera to follow that Goblin Storage Chest around? Give it a CameraControlComponent, which will listen for UpdateObjectVisualPosition and set the scene camera accordingly. (That's how the player object does it).

Some more examples:

The VitalsComponent tracks life and food levels, and listens for special messages related to life and food level handling including, but not limited to, ModifyLife and ModifyFood. These messages will raise or lower levels based on the amount argument, and are the chief mechanism by which damage is applied or food-cost is applied. If VitalsComponent receives a ModifyLife message and does the appropriate modification, then sees that the life level has dropped below the critical level (10%), then it will generate a LifeLevelLow message. Now, say the player has equipped a VitalsWarning component. This component will listen for LifeLevelLow (among other things) and in response it will trigger a FloatingCombatText("Life low!!!") message. This, of course, is listened for by the FloatingCombatText component, which will add a scrolling text warning ("Life low!!!") above the player's head. In an enemy object, you could instead have another component listen for LifeLevelLow, and have that message trigger a Flee response. Or a Berserk response. Or whatever is appropriate for the enemy type. And if it listens for the LifeLevelDepleted message the VitalsComponent will send when life drops to 0, the creature will know when it's time to die.

The FloatingCombatTextComponent itself will also listen for ModifyLife messages, and if the amount is negative it will display the value in a floating red number, while if the amount is positive it will show the amount in blue. Thus, by adding a FloatingCombatTextComponent to any object, you "magically" get floating numbers above that object whenever it takes damage or uses food. Magic!

So that's a basic rundown. It sounds kind of complicated, but in essence it is really simple once you wrap your head around it.
Previous Entry Bug squashin'
Next Entry Combat and Goals
2 likes 4 comments

Comments

yckx
Awesome sauce. Thanks!
February 12, 2011 01:01 AM
yckx
Wow. This is enlightening stuff. And more detail than I'd hoped for, which is great. I'm going to have to process what you've written up, and then take another look at my game. You've helped me solidify some of my more nebulous thoughts on what I want to use Lua for, and how to approach it.

Thanks again.
February 12, 2011 01:48 AM
Mike Bossy
It's neat to see where other people decide to put functionality, in the scripting layer or native code. You definitely do more in script than I do. Makes me want to re-evaluate where I do things. I don't think there's necessarily a complete "right" way to do it. More of a personal preference type thing.
February 12, 2011 08:39 PM
JTippetts
Yeah, it's mostly personal preference. I used to implement the kernel loop in C++ and just call into Lua via various hooks, but over time I've migrated to doing it this way, for no particular reason. I do think I take a little bit of a framerate hit by doing so much in script, but considering the genre here I don't think it makes too big a difference. And by keeping the objects and components in script, I can definitely prototype them a great deal more quickly.
February 13, 2011 12:08 AM
You must log in to join the conversation.
Don't have a GameDev.net account? Sign up!
Profile
Author
Advertisement
Advertisement