After some thought, I think the following scheme would be easy enough to implement and almost as flexible as can be.
Just as a quick recap to make sure the terminology is clear to everyone. An
Entity is a "thing" in our game world. In the microbe stage, that would include the player's microbe, AI controlled organisms, compounds floating around, obstacles placed throughout the game world, and so on. An entity has a unique id and is made up of several components (see below). The entity's component makeup can change during the entity's lifetime.
A
Component is a piece of data with a very narrow purpose. Examples include a
TransformComponent that holds the position, the rotation and the scale of its entity. A
CollisionComponent would hold the entity's collision mesh (and other collision related stuff) for use by the physics engine. A
RenderableComponent contains all the information needed by the graphics engine to render the entity. The list goes on.
Now to the scripts. To make scripts flexible enough, they need to be able to do several things. I'll assume we'll use Lua.
Retrieving an entity and its componentsThat's easy enough. We need a global manager for the entities anyway, so Lua only needs a global function or table to get an entity by its id, something like this:
- Code:
-
playerEntity = getEntity("player")
The getEntity function is a global and takes an id, returning the entity with that id. If no entity with that id was found, it returns nil, a kind of "non-value" in Lua.
Accessing an entity's components could look like this:
- Code:
-
playerTransform = playerEntity["transform"]
The entity returned by getEntity is actually a table (Lua's almost-omnipotent data structure), "holding" all the components for easy retrieval. By the way, the above line could just as well be
- Code:
-
playerTransform = playerEntity.transform
It's the same to Lua.
Manipulating a component's dataWe could go several routes here, API-wise. Setting an entity's x position could work like one of these lines:
- Code:
-
playerTransform.x = 12.34 -- Option A
playerTransform:setX(12.34) -- Option B
playerTransform:setPosition(12.34, playerTransform.y, playerTransform.z) -- Option C
I'd be partial to option C as it allows (or maybe even forces) a script author to set all coordinates at once. Option A has a nice syntax, though. Maybe a hybrid of both would be ideal.
Removing components or whole entities from the game worldEasy:
- Code:
-
playerEntity:destroy()
-- or
playerTransform:destroy()
When calling these functions, C++ will handle the removal. The above example, however, exposes a small problem. When the playerEntity is destroyed, all its components should be destroyed as well. So what happens if we still use a component (or an entity)
after it has been destroyed?
Doing stuff like this in C++ would almost inevitably lead to a crash if the programmer is not diligent about watching the objects' lifetimes like a hawk. In Lua, we might be able to get away with a simple error message like "Called function on destroyed object, on line 58". While not ideal, it's certainly better than just crashing to the desktop.
(Note: this error behaviour will have to be implemented first, it doesn't come automatic with Lua).
Adding components or whole entities to the game worldThis section will be a bit fuzzy, mainly because we may want to think about how to handle it in C++ first. Of course, in C++, we can always just create the necessary objects, plug them together in the right way and give the complete entity to the entity manager. But some entities may require dozens of components to work correctly. Other entities might be a lot simpler to build, but will require lots of tweaking to make them look good or behave the way we want them, requiring a recompilation for small adjustments.
So, ideally, we should be able to "describe" entities in a text file or other convenient format and the game loads those files to find out how to construct a given entity. I propose to use Lua tables for that. Then a definition for a "bouncy block of cheese" entity could look like this:
- Code:
-
local CheeseEntity = {
collision = {
mesh = "meshes/cube.mesh",
bounciness = 3.1,
},
transform = {
scale = {0.1, 0.1, 0.15}
},
render = {
mesh = "meshes/cube.mesh",
texture = "textures/cheese.png"
},
}
-- Register the blueprint with a name
registerEntityBluePrint("Cheese", CheeseEntity)
Then, when we want to spawn cheese from a script:
- Code:
-
cheeseEntity = spawnEntity("Cheese")
Instead of a string identifier, we could also directly pass the CheeseEntitiy table to spawnEntity. Useful for entities that are used rarely.
Reacting to third-party changes in a component's dataThis is where it gets complicated and the implementation of this feature will have far reaching consequences for both the C++ and Lua side. As noted in my first post in this thread, it's important to be able to react to certain events, like collision with another entity, position changes, and so on. It's important to note here that the components themselves should be as dumb as possible. The CollisionComponent, for example, can't "know" about other entities in the vicinity, so why would it know about any collisions occuring? That's the job of the physics system.
So, how do we approach this? I suggest using "signals". A signal is a list of callbacks that are called when the signal is emitted. The callbacks receive relevant information for that signal as arguments.
Note: a signal is distinct from a so-called "event", which are usually handled centrally by some global event manager. Signals are more localized and direct.
The idea is to give each component a set of appropriate signals. The CollisionComponent would get a sig_collision signal, carrying the entity id, the collision point, the force, and maybe some more. From Lua, we could use it like this:
- Code:
-
-- Define a function to handle a collision
function onPlayerCollision(otherId, point, force)
if force > 5.0 then
-- Ouch
playerEntity:destroy()
elseif force > 2.0
-- Less ouch
playerEntity.health:damage(10)
end
end
-- Connect the signal
playerEntity.collision.sig_collision:connect(onPlayerCollision)
Now each time the sig_collision signal is emitted, the onPlayerCollision function is called, giving the script the opportunity to react to the collision and damage the player or outright kill them for their carelessness.
So, who would be responsible for emitting the signals? As always, it depends. The collision signal certainly should be emitted by the physics system. A sig_moved signal, on the other hand, could be handled directly by the TransformComponent when someone sets the position.
Care would have to be taken to make sure that no cicular calls occur, i.e. setting the position from within the sig_moved signal. But I'm optimistic we can deal with that.
Nimbal, why do you always write so much text?!What do you mean with "so mu... oh. Sorry about that.