Still working on this, on and off.
Current version: 0.3.2Fully remade the "track" engine.
Technically, it's now called "scene" as it's the entire scene
Prepare yourself for a lot of information!
(there is a screenshot at the end)The entire thing is split into "projects" (I'm using Visual Studio). This allows me to do all of the scene work in one and build it as a static library. Then, it can be used by multiple other projects. For example, the actual game and a track editor (which still needs to be made).
I'll be mainly discussing the "Scene" project/library here as that's the bit I've been working on (and is the "interesting" part as there's no game or anything yet).
The Scene class is the actual drawable scene and is the thing that does all the projections and putting everything together. It holds a pointer to scene definition classes so that they can be switched out (separately, if required).
The scene definition classes are:
Track, Marks, Objects and Sprites.
Track is the road, mainly.
That is, it defines its length, its curves and hills, its colours as well the horizon/sky/background textures to use.
A track is defined by "pieces". There are two types of pieces: Standard and Pattern. Most pieces are Standard.
Standard piece is a basic, singular piece that occurs only once. A pattern piece allows a repeatable pattern to be created.
Track stores these pieces and Scene interpolates the points it needs from the pieces as it needs them.
A StandardPiece stores a position (start distance along track), a length (distance from position), and then an initial and final value. The value is determined by the type of StandardPiece. The start value is used at the beginning of the piece (at its position) and the final value is used at the end of the piece (position + length). At any distance between those two distances, the value is interpolated. The interpolation type can be specified for each piece. Types are: none, linear and ease. Linear and Ease interpolate the start and final value based on linear and easing interpolation. None ignore the final value and uses only the start value.
The two most obvious and most important are bendPieces and slopePieces. These define the curves and hills of the track. They are both of type StandardPiece.
For the following examples, we will assume that the piece's position is 100 and its length is 100. This piece would then be in effect from 100 up to 200.
Examples of bend pieces:
A curve could have an initial value of 0.2 (a slight curve to the right). The curve would have interpolation of None and any final value is not used. This creates a simple, constant curve (all points on this piece would be a bend value of 0.2)
A curve could have an initial value of 0.2 (slight curve to the right) and a final value of 0 (straight ahead). The curve would have a bend value of 0.2 at distance 100, a value of 0 at distance 200 and a value of 0.1 at distance 150. This type connects a right bend (of 0.2) to a straight road (0).
Examples of slope pieces:
A hill could have an initial value of 0.2 (slight incline). The hill would have interpolation of None and any final value is not used. This creates a simple, constant incline (all points on this piece would be a slope value of 0.2)
A hill could have an initial value of 0.2 (slight incline) and a final value of 0 (flat). The hill would have a slope value of 0.2 at distance 100, a value of 0 at distance 200 and value of 0.1 at distance 150. This type connects an incline (of 0.2) to a flat road (0).
Similar pieces exist for the colour of the road, the colour of the ground (roadside), the colours of the ground on the background and the colours in the sky gradient, and even the texture IDs of the sky, ground (background), and reverse sky and ground (this can be used to change these colours and textures dependant on distance on the track).
A Pattern Piece is slightly more complex.
It has a (start) position as with StandardPiece. It also has the interpolation type.
However, instead of a single initial value and a single final value, it has a vector for each. It then stores a vector of "nodes", which basically just store which value from the vectors to use and how long for. The piece also stores how many types the entire pattern of nodes should be repeated. The length of this piece, then, is one higher than the number of repeats, multiplied by the length of all of the nodes.
The PatternPiece is used for road and ground (roadside) colours. This allows repeating colour patterns including the simple alternating shades as per the "usual" road-based game.
Using these pieces, a track can be built simply and Scene can create the final track image from that. Bend and Slope pieces can overlap; the values are simply added together where multiple pieces overlap.
Marks are the marks on the road (or anywhere on the ground).
A Mark is defined as one or more Quads. These quads are two-dimensional as they are flat on the road.
Each mark has its own position, colour and scale, which applies to all quads in that mark.
Each quad stores the four points of a quad - a polygon with four vertices, as well its own colour and an positional offset. The polygon
must be convex but, of course, concave polygons can be created by using multiple quads.
Quads are stored separately (in their own vector) and the Marks store a vector of pointers to quads. This allows the same quads to be used in different marks.
Marks, when drawn, are clipped to each segment of the road and drawn in pieces, one or two per segment that contains it. For example, a simple stripe that stretches for 6 segments will be split into 6 stripes - one for each segment.
Marks and Quads are stored in their original form. They are split in real-time each frame. This, however, can be time-consuming so Marks can be "baked". Baking splits all of the marks' quads and stores the split quads and which segment it should be displayed with. This allows much quicker calculations.
Objects are very similar to Marks. They also store multiple quads, which are split for each segment. They also allow baking. The only real difference is that the quads in Objects are three-dimensional. That is, they can also have height/vertical offset (y) whereas Marks can only have horizontal offset (x) and distance (z).
Sprites are also quite similar to both Marks and Objects but are a lot more simple. They are "billboard" quads that are specified by width and height only. However, their position is three-dimensional, allowing "floating" sprites. Sprites do have one feature that objects and marks do not - textures.
Sprites are used for all 2D textured sprite graphics in the scene. They can be static (roadside scenery etc.) or dynamic (vehicles etc.).
Sprites does not contain only the Scene Sprites described above; it also includes Sky Sprites that allow sprites to be drawn on the background as it anchors to the background "plate". They are always drawn after all of the background and before all of the scene (track, marks, objects and scene sprites).
There can be a lot of
Marks,
Objects and
Sprites for a track so each cycle, a vector of pointers of each are prepared; these pointers are all of the items that are within the visible range of the scene. Then, for each segment, the vector of pointers is then consulted to see which ones are in that segment. This significantly reduces the amount of testing.
The
Scene itself only really has camera controls plus a few set-up things. The set-up things are, of course, setting which Track, Marks, Objects and Sprites to use as well as setting which texture resources to use. There are two textures: the main scene texture and the background texture. It also stores a TextureSheet for each, which is basically just the texture rects for each sprite/image in the texture.
There is also a function to get a predefined view, which provides a full window view or half window views in the top or bottom half - useful for one player and two player set-ups!
The rest of the controls are camera controls. Things like: position (3D), aspect ratio, where the vanishing point is, the camera depth (effective depth of the scene) - I think I will use this as an effect
.
One other feature added to this version is reversible direction. That is, the track can be drawn from the camera as if it's facing towards the beginning of the track instead of the end. One use for this is a flipped view to display a mirror.
Of course, there is one other method: update(), which brings Scene up to date based on its camera's current position and using the current state of the Track, Marks, Objects and Sprites.
Scene's update sets the range of segments that will be drawn and then updates the projections for each. It then updates the background vertex array before updating the main scene vertex array.
The background vertex array consists of multiple quads:
the sky base (skyTopColor),
the sky gradient - top part (skyTopColor to skyMiddleColor),
the sky gradient - bottom part (skyMiddleColor to skyBottomColor),
the skyline - texture above horizon,
the ground block (groundBlockColor - colour below horizon),
the groundline - texture below horizon,
all Sky Sprites.
The Scene vertex array consists of, for each segment:
a ground segment (both road sides at once - a horizontal bar),
a road segment,
all baked marks' quads for the segment,
all unbaked marks' quads clipped to the segment,
all baked objects' quads for the segment,
all unbaked objects' quads clipped to the segment,
all sprites within the segment.
First, it calculates a maximum number of vertices required to draw the scene.
It starts with the number of segment multiplied by 8 (one quad for the ground and one quad for the road)
It sets the range of Marks, Objects and Sprites so that they can prepare the ones in range and then adds 4 for each of their quad in the segment.
Note that, at this point, any unbaked Marks or Objects will be split into segments and stored as quads - similar to baked quads.
During the projection updates earlier, if a segment is facing away from the camera, it marks it as invisible and adds 8 (4 for the ground, 4 for the road) to a counter called vertexReduction, as these won't be drawn.
The vertex array is then resized to the maximum number of vertices required minus vertexReduction.
vertexReduction is reset to 0 as there may be more reductions to follow...
Then, quads are added...
For each segment - in reverse order (see
Painter's Algorithm):
if the segment was not previously as invisible, the ground and road quads are added,
loops through all baked marks' quads within visible range to see if they're for the current segment. If it is, it will add that quad otherwise it will add 4 to vertexReduction,
similarly loops through all marks' dynamically split quads and adds quads or 4 to vertexReduction,
loops through all baked objects' quads within visible range to see if they're for the current segment. If it is, it will add the quad only the top of the quad is above the top of the hill/ground at this point otherwise it will add 4 to vertexReduction,
similarly loops through all objects' dynamically split quads and adds quads or 4 to vertexReduction if behind a hill,
loops through all sprites in range to see which ones are in the current segment. For each one that is, if the sprite's boolean "isActive" is true, the sprite's quad will be added unless its position is on the wrong side of the camera or its top is below the hill in which case 4 will be added to vertexReduction. If isActive is false, vertexReduction gets another 4.
"Adding a quad" in this case consists of using an iterator pointing to the vertex array (technically, it's a vector of vertices) and modifying the current and following 3 vertices for the quad and then moving the iterator forward by four. When a quad that was prepared for doesn't get added, this leaves unused vertices at the end of the vertex array. vertexReduction keeps track of how many are unused.
That's the update. All that is left is to set the view to whichever is needed and the draw it. The draw function draws the background vertex array first using the background texture and then draws the scene vertex array using the scene texture. When it draws the scene "vector" of vertices, it uses the pointer overload for draw that takes a size and passes the size of the vector minus vertexReduction, which stops it from drawing all the unused vertices at the end:
target.draw(m_vertices.data(), m_vertices.size() - m_vertexReduction, primitiveType, states);
The app itself that I am currently using to develop the Scene will be a starting point for both the game and the track editor. Currently, it allows movement of the camera, independant movement of the single car sprite including flipping its direction, modification of the camera depth and vanishing point, and changing setup things like: toggling camera direction, car visibility, framerate limit, pixelation and cycling window mode (window, fullscreen, fullscreen window). Cycling window mode skips fullscreen mode if its not a valid mode.
You can also toggle a "feature" that "wobbles" the Marks and Objects (different sine wave side to side) but this only works for unbaked Marks and Objects.
I have a dilemma/question.I can't decide on whether Faux Car should have a pixelated display or not:Click images for full-sized versions.