-
-
Notifications
You must be signed in to change notification settings - Fork 60
Engine layer
The in-game engine layer provides various functionality, which can be grouped as follows:
- Entity management
- Rendering (map, sprites, particles)
- Collision detection and movement functions
- Physics
But before we get into more details on these, let's first define some basics.
Aside from a few exceptions, the canonical unit for positions, sizes etc. in RigelEngine is tiles. A single tile is made of 8 by 8 pixels in Duke Nukem II. The game map (or world) is constructed out of tiles, and all objects in the game are always aligned to the corresponding grid of 8x8 pixel blocks. It's not possible for an object (aside from particles) to be positioned outside of this grid. Therefore, storing all positions and sizes as tiles makes sense.
There are three coordinate systems in RigelEngine: World, local, and screen.
World coordinates are relative to the entire map, with 0,0
being at the top-left of the map. The maximum x and y depend on the size of the map. Depending on the camera position, a world coordinate can be on-screen or off-screen.
Local coordinates are relative to an object. To turn a local coordinate into a world coordinate, the object's world position is added to the local coordinate.
Screen coordinates are relative to the screen, meaning 0,0
is always at the top-left of the screen, and 31,19
is bottom-right (in non-widescreen mode). To convert from world to screen coordinates, the camera position is subtracted from a world position.
The following picture illustrates these different coordinate systems:
TODO
Thanks to the EntityX library, entity management is mostly taken care of already by instantiating an entityx::EntityManager
. This class allows creating and destroying entities, as well as iterating over all entities that have a certain set of components.
Aside from particles and the world/map, everything in RigelEngine is represented by an entity. The engine layer provides various components which can be added to entities, in order to give basic functionality.
The most commonly used components are WorldPosition
and BoundingBox
. The former locates an entity inside the world (as the name suggests), by giving it a position (x and y) in the world coordinate system. The latter gives the entity a size, by defining a rectangle (in object space). The bounding box also defines how an entity collides with the world and other entities (see below).
Note that the rectangle's bottom-left corner corresponds to the entity's world position by default. The rectangle can also define an offset, to adjust that. The following image illustrates position and bounding box:
The left-most image shows a sprite with a position and no bounding box. The sprite has a size of 4x5 tiles. Since the position refers to the bottom-left tile, positioning this sprite at world coordinates 10, 14
would result in covering the rectangle starting at 10, 10
and ending at 13,14
.
The middle image shows an additional bounding box, which is made smaller than the sprite (Duke's protruding weapon doesn't participate in collision detection, therefore it makes sense to reduce the bounding box in size). This bounding box is defined as a rectangle at local coordinate 0, 0
with a size of 3, 5
, resulting in no offset from the position - the position matches with the lower left corner of the bounding box.
Finally, the right-most image shows a sprite where the bounding box is offset from the position. In this case, the box is placed at local coordinate 0, -6
to give it an offset, and has a size of 4, 4
.
'Orientation' refers to whether an entity is facing left or right. Sprite drawing can be configured to take orientation into account, in order to draw a different sprite frame based on the entity's orientation (see Sprite rendering). Orientation can be converted into a movement value, to influence direction of movement (see Movement). Aside from that, behavior controller code might use the orientation component for various purposes, like determining which direction to fire shots in, or whether an enemy can see the player. These are outside of the responsibility of the engine layer, though.
In the original Duke Nukem II, most enemies (and other objects) are only active when on screen. This was most likely a performance optimization at the time, because fewer objects to process meant less CPU time was required. But it has gameplay purposes as well, for example hidden items that only fall down if the player looks up etc. However, some objects are always active, and some are always active once they have been on screen once.
In RigelEngine, all of this is provided by the Active
and ActivationSettings
components. The former will be assigned or removed each frame, depending on whether the entity should currently be active. This is implemented by the Entity Activation System (markActiveEntities
function). Other systems can use the presence or absence of the Active
component to decide if they want to process an entity or not.
By default, entities will be active only when on screen. Assigning a ActivationSettings
component can override this. It has a Policy
member, which specifies when the entity should be active: Always, when on screen, or always after it has been on screen once.
Most entities will be destroyed at some point. In some cases, this depends on a specific condition: Projectiles, for example, disappear once they are not visible on screen anymore. Water drops disappear when touching the ground. Explosion effects disappear once their animation has finished playing.
To automate these relatively common cases, the engine provides a AutoDestroy
component. An entity which features this component will automatically be destroyed by the engine if one of the conditions specified in the component becomes true. Currently, the available conditions are:
-
OnWorldCollision
: Entity will be destroyed if it collides with the world. Requires the entity to be physics-enabled (i.e., have aMovingBody
component) in order to have an effect. -
OnLeavingActiveRegion
: Entity will be destroyed as soon as it doesn't have anActive
component anymore. Note that if you create an entity in the middle of a frame, and you'd like it to have this condition, you need to assign anActive
component manually, as otherwise, the entity would be destroyed immediately at the end of the current frame. -
OnTimeOutElapses
: Entity will be destroyed once it has existed for the specified number of frames.
There are three different types of visual elements making up a RigelEngine scene: Map, sprites, and particles.
The map is drawn by the MapRenderer
class. It consists of three layers, with sprites positioned between and above those layers. The layers are:
- Parallax background (aka backdrop)
- Background layer
- Foreground layer
Sprites can appear between background and foreground, or on top of the foreground.
The map is not represented by entities, but by a 2-dimensional array of tile indices. The MapRenderer
operates directly on that grid. The appearance of the map depends on the tile set, which is given to the MapRenderer
in form of a texture. Tile indices are used to choose a sub-section of that texture when displaying a tile.
Tile debris, i.e. parts of the map exploding, is drawn by the MapRenderer
as well.
To make an entity appear visually, it needs to have a Sprite
component attached, in addition to a world position. A bounding box is not mandatory for rendering.
There are two types of collision in RigelEngine: Entity/Entity and Entity/World. At the moment, the engine layer only provides a mechanism for the latter. The former can easily be done by testing the bounding boxes of both entities for intersection.
In Duke Nukem II, the map data itself doesn't define which parts of the world are solid and which ones allow objects to pass through. Instead, this is defined as part of the tile set. For each tile index in the tile set, a set of attributes defines which sides of the tile are solid (among other properties). A tile can be fully solid, only solid on top (allow passing through from below, but not from above) etc. The following picture shows the various possible combinations:
TODO image
Since the map is a grid of tile indices, we can retrieve the solidity information for each tile by looking up the tile index' attributes.
Given a bounding box and a direction, we can then determine whether the bounding box is currently touching a solid part of the world:
TODO image
Note: There is no mechanism to determine if an entity is currently stuck inside a solid part of the map. Collision detection assumes that the object to be tested for collision is in a valid location, meaning it doesn't intersect any solid parts of the map. Consequently, collision detection always has to be performed before moving an entity (unless having the entity move through walls is desired).
In RigelEngine, this functionality is implemented in the CollisionChecker
class.
In addition to the map itself, entities can also act as solid parts of the world, when they have the SolidBody
component. This is mainly to support sliding doors and elevators, which are not part of the map, but solid.
The CollisionChecker
automatically takes any SolidBody
entities into account.
Entities can be physics-enabled by assigning a MovingBody
component. This makes it possible to have the entity move automatically in a given direction at a given speed (including collision detection), and applies gravity to the entity.
To make an entity move, the mVelocity
member of the MovingBody
component needs to be set accordingly.
Gravity can be enabled and disabled by setting the mGravityAffected
member.
Collision checking can be disabled by setting mIgnoreCollisions
to true
. Regardless of this setting, physics-enabled entities will generate a CollidedWithWorld
event on collision with the world.
Physics-enabled entities will also get pushed by conveyor belts.
For cases where a fixed velocity vector is insufficient to describe the desired motion, a MovementSequence
component can be assigned. This contains a list of velocities, which will be used in sequence, one value per frame. Once the list has been exhausted, the entity will either stop moving, or keep moving with the last set velocity value (depending on configuration).
TODO: Provide example