Skip to content

Engine layer

Nikolai Wuttke edited this page Sep 8, 2019 · 10 revisions

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.

Units and coordinate systems

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

Entity management

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.

Position and bounding box

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:

Example image showing sprite 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

'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.

Orientation example image

Active state and activation policy

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.

Life-time control

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 a MovingBody component) in order to have an effect.
  • OnLeavingActiveRegion: Entity will be destroyed as soon as it doesn't have an Active 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 an Active 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.

Rendering

There are three different types of visual elements making up a RigelEngine scene: Map, sprites, and particles.

Map rendering

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.

Sprite rendering

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.

Orientation adjustment

Virtual and real frames

Automated animation

Collision detection and movement

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.

Collision with the world

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.

Solid Body entities

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.

Movement

Physics

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.

Movement sequence

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