Entities in SeriousEngine are classified by their type (enum RenderType, declared in <Engine\Entities\Entity.h). Type of an entity defines the way it is rendered and the way that it collides and interacts physically with other entities in the world.
Each entity must declare its type as the first step of initialization in its Main() procedure.
Initialized by calling InitAsModel().
Each model entity has a CModelObject which it can obtain a pointer to by calling GetModelObject(). Usually, model is set by calling SetModel() with the component number of the desired model file and its texture by calling SetModelMainTexture() with the component number of the desired texture file. If you desire to set the model's specular, reflection or bump textures, add attachments or similar, use GetModelObject() and then modify the model object manually.
Model entities use their collision boxes edited in Serious Modeler for collision.
Initialized by calling InitAsEditorModel().
Editor models are similar to ordinary model entities in every way, except that they are only rendered in Serious Editor and only if 'Render Editor Models' flag is turned on for the particular view.
Beware that they can collide with other entities in game, even though they will be invisible. You will usually want to declare them as immaterial, so that they do not collide with other entities.
Initialized by calling InitAsBrush().
Each brush entity is accompanied by one CBrush. Initially, the brush will be empty and level editor will then create it in the Serious Editor. In the entity source code, programmer really does not need to access the brush at all. All of its appearance settings are set by the level editor. Programmer just sets its collision and physics settings and defines its behavior.
Initialized by calling InitAsFieldBrush().
Field brushes are same as brushes, just that they do not have any textures and are only rendered as green translucent objects. Level editors define its geometry only, they cannot texture them. Their geometry is then used to detect if some entity has entered the field. When that happens, the field entity receives an event, and it can react on it. They are rendered only in Serious Editor and only if 'Render Fields' flag is turned on for the particular view.
Initialized by calling InitAsVoid().
Voids are used when you need to create a parallel thread for some AI purposes. They are nevered rendered, they do not collide, they cannot move, cannot emit light, cannot make sound etc. They are made such to reduce the performance impact on the rendering and physics algorithms, but to still be present in the world.
The base class of the entire hierarchy of entity classes is CEntity. Simplest entities can be derived from that class, but they won't be able to move and execute any other AI procedures except the initialization procedure Main().
Examples of such entities are lights and markers.
CLiveEntity derives from CEntity and extends with ability to be damaged. It tracks its own health and reports death event when the health reaches zero. Note that even the base class CEntity receives damage, by the ReceiveDamageFunction(), but its default behavior is to ignore it.
CRationalEntity derives from CLiveEntity and extends with ability execute AI procedures using its own state stack.
CMovableEntity derives from CRationalEntity and extends with a physics engine based on movement requests. IMPORTANT: You should never derive directly from this class. Use either CMovableModelEntity or CMovableBrushEntity.
Entities derived from this class must have EPF_MOVABLE physics flag set in order to work properly. Then, you can call following functions to make it move:
To get useful information about the entity's direction (if you want to have 6DOF movement and variable gravity support) use:
CMovableModelEntity is a CMovableEntity of render type RT_MODEL. It has additional ability to change its collision box using following functions:
CMovableModelEntity is a CMovableEntity of render type RT_BRUSH.
CPlayerEntity is a CMovableModelEntity used for players and spectators.
You must override ApplyAction() and Disconnect() functions.
ApplyAction() will be called each tick with a new action packet that the entity should apply to its movement.
Disconnect() will be called just for notification that the client was disconnected. The entity should die and/or destroy itself.
Entities are dynamic inside a world and they can be constructed and deleted on the fly, both in the SeriousEditor and in the game. Since other entities and various structures inside the engine and the game can have pointers to an entity, it would be unsafe to just delete it at any time. To prevent invalid address accesses, a reference counting scheme is used in the entity management.
Each entity has an internal counter which counts the number of times it is referenced by a pointer. Normally, the counter is at least equal to one. This means that the entity is referenced by the world's container of existing entities. It shows the entity's intent to exist. As other entities or some code in engine and/or game make pointers to it, the counter can increase and decrease.
When the entity wants to cease existing, it calls its Destroy() function. It is then marked as deleted (ENF_DELETED), as much memory as possible is freed (deleting its model object, brush, etc.), its properties are adjusted so that it is neither rendered nor interacting physically, and its counter is decremented by one. This way it uses as little resources as possible, while it still exists in memory in case that there are pointers to it.
If the entity's reference count reaches zero, the entity is really deleted.
Usual scenarios are like a missile having a pointer to its owner and the owner gets deleted while the missle still flies, and is trying to send some event to the owner or similar. The owner stays in memory while the missile lives. Or if a monster has a pointer to the player it is attacking, and some other monster kills the player. The player never really is removed since there is a pointer to it. Such situation is resolved by the fact that on Destroy(), ENF_ALIVE is also cleared, so the monster will cease attacking the player, since he/she is already dead. As the monster deduces that the player is dead, and sets its pointer to null, the player can be released from its zombie state and freed from memory.
To prevent circular references, Destroy() function clears all eventual pointers to other entities.
The reference counting uses a special class CEntityPointer (4 bytes in size, just the pointer itself), which implements the standard reference counting algorithms. You must make sure you do not keep any non-temporary ordinary pointers to entities (CEntity *), since they do not take care of reference counts. You do not have to worry much about that, since Ecc will not allow you to declare any CEntity* member variables, only CEntityPointer are allowed.
Some entity settings and physics and collision behavior is primarily defined with three sets of flags: base entity flags (just flags), physics flags and collision flags. They are all defined in "Engine/Entities/Entity.h".
The flags can be accessed and modified using SetFlags(), GetFlags(), SetPhysicsFlags(), GetPhysicsFlags(), SetCollisionFlags() and GetCollisionFlags().
1) Base flags define various properties. Some of them are used by the engine internally, while others can be set to impose a specific property.
Following flags are meaningful for changing:
Set this for brushes which should define worlds spatial classification by sectors. Those are the base of the architecture, as opposed to moving brushes (doors and similar) and detail brushes (like columns etc.) which you do not want to be zoning.
Engine sets this for entities that were destroyed, but are still kept in memory while someone holds a pointer to them.
Set if the entity is currently a living being.
Physical ray-casts will pass through this object.
Set this for models that you want to cast shadows on shadow maps, instead dynamic shadows. Used for immovable models like statues and light-holders.
A brush or model marked this way will be rendered on background, using the background viewer entity as a viewer.
Entity marked this way cannot be moved in editor without special allowance from the user (clicking a toolbar button). This is used to prevent editors from accidentially moving entire buildings around.
Set for a model entity if it should render particles.
The entity is marked as invisible for AI purposes. It is entirely up to the custom ES code to take care about that. Does not influence rendering at all.
A moving brush that causes automatic shadow recalculation as it moves. This one can be slow on large worlds, use with care.
If this set, the entity is notified when level is changed.
If this is set, the entity must be carried to the new level when level is changed.
2) Collision flags define which other entities this one will collide with. The collision flags are divided in three groups: 'is', 'test' and 'pass'. Each group has identical layout of 10 bits each completely customizable by the programmer. Engine does not assign any particular meaning to any of the bits inside the group. When two entities (A and B) might collide, the following algorithm (in pseudo code) is applied:
if (!(isA&testB) || !(isB&testA))
do not test at all
if ((isA&passB) || (isB&passA))
test and report if passed one through another
test and apply collision response
By proper assignment of is, test and pass flags, you can adjust which entities collide which, and which pass through.
Usual combinations are that magical projectiles can pass through one another, and the players and creatures can pass through items etc. Magical projectiles can pass through physical projectiles, while physical projectiles cannot pass through one another etc.
3) Physics flags define collision response and general physical properties of the entity. This includes how entity is infuenced by gravity (rotated and/or translated), how it reacts if its movement is blocked and how the entity breathes.
Here is a snippet from the header file:
Following flags are meaningful for changing:
If set, the entity always turns to be feet-down.
Without this flag, entity levitates/flies.
If this is not set, the entity cannot be pushed by brushes.
Sticky feet entities always fall to the nearest polygons. They can walk on the walls like spiders.
Set if rotation and translation are synchronized. Used for moving brushes which must not be allowed to continue turning if they cannot translate when blocked.
Set if entity is translated absolute and not relative to its position. Use for moving brushes.
Set if entity can change its speed immediately. Used for moving brushes.
- EPF_HASLUNGS and EPF_HASGILLS
Defines entity's way of breathing. Entity that has both lungs and gills will be amphibious, but can still choke in vacuum. Note that an entity which has neither is considered unaerobes, and can newer choke/drown.
Must set this if derived from CMovableEntity.
Entities are not damaged when hitting this one.
Desired rotation can be reduced by sector contents (like water).
Flags in the EPF_ONBLOCK_MASK decide the collision response:
- EPF_ONBLOCK_STOP stop moving
- EPF_ONBLOCK_SLIDE slide along wall
- EPF_ONBLOCK_CLIMBORSLIDE clim up a stair or slide along
- EPF_ONBLOCK_BOUNCE bounce off
- EPF_ONBLOCK_PUSH push the obstacle
Each entity (even the voids) have its placement in the world. The placement defines its position and orientation. The placement is primarily kept as a CPlacement3D (consisting of a FLOAT3D for position and an ANGLE3D - a set of Euler angles - for orientation). A copy of the orientation is also kept as a rotation matrix. The placement also influences the entity's linking in the spatial classification structures in the world.
The spatial classification must not be corrupted, or the engine might malfunction. Therefore, you must never change the placement manually. Use SetPlacement(), GetPlacement() and GetRotationMatrix() to access the placement data. To teleport entity from one place to another use Teleport() instead of SetPlacement().
The GetLerpedPlacement() function is used by the engine's rendered to calculate placement of an entity in-between two ticks. That is for an effect called lerping (from LERP=Linear intERPolation), and is used to smooth out annoying visual effects of unstable frame rate and inconsistency between physics/AI ticks and rendering frame rate. It is already implemented in CMovableEntity, but you can override it if you provide your own movement code.
An entity can inflict damage to another entity by calling InflictDirectDamage(). Be sure to provide all the needed parameters. If you do not supply the hit point and direction it won't interfere with the engines capability of tracking the entity's health, but the other entity's AI won't have exact info.
InflictDirectDamage() inflicts damage to all entities within given range.
Here are some damage types declared in the MovableEntity.es:
You can add you own enums as you like above the value of 100.
One of the most important facts you have to keep in mind is that the code of an entity is not executed only when the game is running. It is also executed in the SeriousEditor, when the entity is initialized or changed.
SeriousEditor is based on the principle of 'frozen time'. While a level editor is working on a world in SeriousEditor, the world is frozen in the moment of zero-time. A '.wld' file can in fact be thought about as a save game at the zero-time, with no players added. The process of game starting involves loading that save-game to the server and adding players to it.
When a new entity is added to the world, it is initialized in the following way:
1) The entity is constructed and added to the world's container of entities.
2) The entity's Main() procedure is called.
3) Main() procedure initializes the entity's appearance and settings and continues running the AI. After this point, the Main() will usually return, or will try to start behaving.
4a) If it returns (for simple non-rational entities, like lights), it will never continue to execute any procedures, and will only execute overriden virtual functions.
4b) If it starts behaving (for rational entities, like enemies), it must ultimately, at some point, wait for some time interval, or for the next event. When it does so, it is put on a waiting queue. Since the time is frozen, that's it. It will continue executing only after the game starts.
5) After the Main() returns, or waits, the engine initializes other parameters, like spatial classification and collision, and precaches its components.
At the initialization, the entity usually sets its model, textures, description, etc. to reflect its properties, which are initially set to defaults.
When any of the properties is changed, the entity is reinitialized. The process of reinitialization goes as follows:
1) The entity is ended (but not destructed!). That means that its cached data like spatial classification, collision etc., is flushed, and its model object, if any, is deleted.
2) The property (or properties) is/are changed.
3) The entity is initialized again, as described above, just that it doesn't need to be constructed again, since it already exists.
Entity AI in SeriousEngine is based on states and events.
Events are sent to entities by the engine e.g. when two entities touch, when an entity is damaged, when levels are changed, at certain moments in time programmed by the entities themselves, etc. Also entities can send events to each other at will.
An event is a class derived from base class CEntityEvent. It contains the event code and additional event data. Event code describes the type of event and is must have unique value for each event type. Event data is stored in the class and is different for each event type.
For example, ETouch, the event sent to an entity when it touches another entity, has its unique code EVENTCODE_ETouch with value 0x00010004. The value is derived from the number of the entity source file where the event is declared (CMovableEntity - number 1), and the number of the event in the file (4). The data that ETouch holds is a pointer to the touched entity, a flag telling which of the two entities moved when the collision occured, and a plane of collision.
You do not need to care about the interns of an event type. You just declare it similarly to any other structure in an .es file, and the Ecc generates the code and supporting functions. More about it in EntitySyntax.
Each entity has ability to receive events using SendEvent() function. When SendEvent() is called for an entity, the event is stored in a global event queue together with a reference to the entity which is to receive it. The stored events are handled in the FIFO order.
The most primitive way an entity can handle events is using its HandleEvent() function. It can test the event type to see if it is interested in the event and then cast it to the proper event class to examine its data. This is use, for example, by CLight in SeriousSam which handle some specific events in a simple way by changing their color, but they never perform any complicated AI or behavior.
If an entity is supposed to have a complicated behavior, it executes the behavior via procedures.
Each entity class has both functions and procedures.
The differences between procedures and functions is following:
1) Stack they are running on
Functions execute on the ordinary call stack of the main application thread.
Procedures, on the other hand, execute on the state stack of the entity they were called for. Each entity derived from CRationalEntity has its own state stack.
Functions always have to return, so that the engine can continue working.
State stacks enable virtual parallelism of procedures, so the procedures do not have to return. When a procedure waits for a timer or for an event, it is just stored on the top of the state stack for that entity, and the engine continues to run.
3) Syntax constructs
Functions are ordinary C++ functions and can be either overrides of virtual functions from base entity classes declared in the engine, or can be written independently by the entity programmer for some custom uses. Virtual overriding is used to provide some facilities like custom rendering, custom physics, interaction with the SeriousEditor etc. Custom used functions can be used as usual in C++, for code simplification, communication between different entity classes etc.
Procedures are similar to C++ functions, and can contain most of the C++ constructs, plus they can contain .es specific AI-related syntax constructs. Those include waiting for timer and/or event, specific event handling and routing, calling or jumping to other procedures etc.
State stack is kept separately for each entity derived from CRationalEntity. It is just a stack of integers, each integer representing one state.
Entities derived from CRationalEntity can execute procedures, wait for time delays, and react on events using their hierarchy of states on stack. In fact, each state on the state stack represents one procedure on the call stack of procedures.
When Ecc compiles procedures, it divides each of them in parts, along the lines of specific waiting constructs. Each such part is one state.
For example, take a look at this simple procedure:
// run for two seconds
// start running animation
// wait a bit
// stop running animation
It is divided in three parts, one before the autowait, one is the wait intself, one is the part after the autowait. Each state has its integer code, automatically assigned by the Ecc, and that state code represents that part of the procedure on the stack.
When the procedure is executed, initially, the first state is on the top of stack. That state is simple, it just starts the animation, and proceeds to the second state.
The first state which was on the top of stack, is now replaced with the second state. Since this state waits, the timer is set and this entity runs no more AI for now, it is placed on waiting.
When the timer expires, an ETimer event is sent to the entity. The state at the top of the stack handles the event and skips over to the next state. Now the third state replaces it on the top of stack.
The third state just sets new animation and returns from the procedure. On return, current state is popped off the stack, and the control is given to the procedure below (if any) that called this one. If there are no more states below on the stack, this entity has finished its AI, and will never run again (that doesn't usually happen, unless the entity destroys itself).
The state stack provides calling and jumping facilities.
If a procedure executes call() construct calling another procedure, the state code of that other procedure is pushed on the stack above the current one, and the new procedure takes the control. The caller procedure is in the call() event handler and can handle any events that the callee doesn't handle or explicitly ignore.
If a procedure executes jump() construct jumping to another procedure, the state code of that other procedure replaces current procedure's state code on the stack. When (and if) the new procedure returns, it will return to the one below on the stack, which is the one that called the original procedure.
If a procedure executes return, its state code on top of stack is popped off, and the procedure below (if any) will continue executing.
Call, jump and return can carry over some event, which will be immediately handled by the target procedure, without beeing put on the event queue.
If the calling procedure traps some event that the callee has not handled, it can decide that it wants to abort the call. Then the callee and its eventual deeper callees are cleaned off from the stack, and the caller continues running.
This concept of stack machine with hierarchical event handling enables programmer to modularize an entity's AI in two axes: horizontally, for sequential execution and vertically for task abstraction. An example of such usage would be calling a procedure that will do something (e.g. just run around), and trapping any eventual events which might mean that you have to do something else (like damage event meaning you should attack the one that attacked you, etc.).
More on that in EntitySyntax.
To simplify loading, saving and editing of entities, SeriousEngine uses a concept of properties. Entity properties are member variables which have additional support for editing in SeriousEditor, automatic loading/saving and copying etc.
You just declare each property with its type, identifier, name, default value etc in the .es file. The engine then handles the serialization tasks, and editing works automatically.
In SeriousEditor, level editor can see those properties that you declare as visible, and sees them by their names. You can also add keyboard shortcuts to the properties. The properties are edited according to their type. For example, COLOR properties are edited with color edit controls with HSV and RGB sliders, CEntityPointer properties can be edited by pointing to the target entity, RANGE properties are visible as spheres in space which can be stretched in 3D etc.
Each entity class can also contain components like models, textures, sounds and other classes. The fact that a class has a component means that when some entity of that class is loaded, the component should be loaded also, since the entity might need it. Each class holds only one pointer to the cached component to speed up and simplify the loading.
The exact caching method can be controlled with console variable 'gam_iPrecachePolicy' which can be set to following values:
- precache_NONE - do not precache components until needed. Makes slowdowns in game.
- precache_ALL - precache all components of all classes in the world when the game starts. If some classes have much more components then they use, this may lead to unneeded memory consumption. E.g. all beheaded soldiers are one class in Serious Sam, but each of the has their own components. Even if only one is present on a world, all components will be loaded.
- precache_SMART - let each entity tell which components it wants. This is done by overriding Precache() virtual function. In the Precache(), an entity should examine its properties and call following functions to precache the components it will need:
- precache_PARANOIA - same as precache_ALL, but if some component cannot be loaded, an error is reported.