John Manard III, Fiona Soetrisno, Mitchell Kanazawa, Bob Loth
OVERVIEW
Starfall is a sand-surfing collection game developed using C++ and OpenGL. Race over dunes to
collect star fragments that power your escape. Hurry before the moon sets though, or you'll be stuck
in dark. As you collect star fragments, drone helpers will join you and cheer you on in your endeavor.
USER GUIDE
LUNA, the main character, is controlled using W/A/S/D and LEFT SHIFT. W - Move Forwards S - Move Backwards A - Turn Left D - Turn Right LEFT SHIFT - Increase downwards force The shift key should be used only when you are sure you are travelling on a downhill slope,
otherwise it will slow you down. A tip for new players is to only focus on going straight forward and go to star fragments only a
little bit out of reach.
Gameplay Video
Inspiration
Concept Art
Features
Player Movement
Luna moves with the slopes of the terrain! Holding shift launches them down slopes, but beware
because that same downward momentum can slow them down while going uphill. Try to pick up speed going
down slopes to collect stars as fast as you can.
Terrain
The dunes themselves are procedurally generated and infinite, giving you plenty of space to zoom
over the slopes. It also is tessellated
allowing for better visuals close to the camera while saving on rendering overhead faraway. Hold z to
see the meshes making up all the objects.
The shape of the dunes is defined using a clever composite of sine and cosine functions to generate
the repeating soft hills. The mesh of the terrain moves and rotates with the player, provided infinite
terrain with a still relatively small amount of vertices needed. The dune shape function is evaluated
at world space coordinates, meaning that the displacement is stable despite the mesh moving with the
player.
Particle Systems
The particle system used initialized several vertex array objects, with a few vertex buffer objects
per VAO, with different semi-randomized characteristics. Then, at program setup, all particle data was
buffered onto the GPU, and the particle system class kept their references as static class members. A
newly-created particle system would select the next vertex array object, and a random starting
position into the vertex buffers, in order to get randomized characteristics without buffering data
every frame. There was no sorting done on any of the particle systems, instead glDepthMask was set to
disabled, and alpha values were adjusted to minimize any potential color saturation caused by
stacking.
The particle systems activated upon collecting a star fragment were smaller points, indexed into a
buffer that was generated with randomized normals, and used a constant Perlin noise, added to a
higher-weighted stacked Perlin noise that adjusted the point's vertex position along the direction of
the random normal. This was animated using log(1 + the particle system's total time), and faded out
with an alpha value that decreased with total time, to create a starburst effect with a faster
particle speed at first that tapered off and faded out.The particle systems activated upon exceeding a certain velocity were done by using a sprite sheet,
precomputing the texture-space coordinates of each image in the sprite sheet and storing into a lookup
table, and indexing into the sprite sheet table based on the total lifetime of the particle system. A
particle system that managed one particle was spawned under the player, with a transform component
that locked it at the player's position upon particle system spawn. The alpha values faded in and out
over the lifetime of the particle system, as well as with the speed the player was travelling when the
system was spawned.
Sound
Sound was all done through Miniaudio's high-level API. All sounds were loaded from files at startup.
The soundtrack played at the start of the game, and the sound effects were played upon collecting a
star fragment. 100 instances of the sound effect were loaded and iterated through to ensure the sound
effects could play simultaneously. The soundtrack's main key was B minor, and a vector of integers
representing the half steps of the B natural minor scale was used to adjust the pitch of the base
sound effect if additional star fragments were collected in a period of 1s. The formula for adjusting
a pitch in Hz up n half steps is pitch' = pitch * 2 ^ (n/12).
View Frustum Culling
View frustum culling was done individually on star fragments and particle systems, as they were the
only two types of objects that would ever potentially be out of frame. The star fragments were culled
with the radius obtained from their Collision component, and because of the uncertainty of the
animated particle system's total radius at a given point in time, a value of 5 was used instead, as a
wide estimate.
Entity-Component system
The entity-component system used a ComponentManager class, with a vector of GameObjects and a hashmap
of vectors of Components, one vector for each first-level component. A GameObject is a name, and a
hashmap of indices of any components that belong to it, and Components were inherited from a base
Component class, with virtual Init and Update functions, as well as lifetime status boolean values.
First-level derived components included Movement, Transform, Collision, Renderer, Collect, Particle,
and DroneManager, which were used as interfaces to concrete classes with object-specific
implementations.Initializing a game object involved creating derived components, and passing them to AddGameObject,
which would place all of the components into the first empty location in the respective vector, which
was implemented by maintaining an atomic priority queue for each vector to avoid vector resizing and
race conditions, and creating and storing a GameObject that stored its indices. Destroying a
GameObject would involve calling RemoveGameObject, which marked all of the components located at the
vector and index referenced by the GameObject as killed, and pushed their index to the respective
atomic priority queue. This would allow the vector index to be used again, eventually overwriting the
location when a component to be added popped the index off the queue.Communication between components belonging to the same object took place through initializing shared
pointers to components that needed to communicate with each other in the Init method, and the Update
method was called every frame on active Components during the render loop. Because no components of
the same type needed to communicate with each other, we could process their updates in parallel
stages, using C++17's std::execution, or start an update or creation asynchronously and process it
during unrelated updates using std::future.While setting up the project in this way took longer than expected, I think it paid off by the end of
the quarter. We were largely able to write new functionality by inheriting from a number of the
first-level components, add a resulting game object, and the rest would take care of itself.
Shading
Shading the star fragments and LUNA used a 3-component microfacet BRDF: a geometric shadowing
function(GSF), a normal distribution function(NDF), and a fresnel function(F). The geometric shadowing
function used was the Kelemen approximation of the Cook-Torrance GSF, the NDF used was the GGX
microfacet NDF, and the Fresnel function was the Schlick's approximation function, with a few
adjustments to add tunable values.
Character and Camera Animation
Each body part of the character is split up into individually animated objects. Each one has a
desired orientation based on the input of the player and moves towards its desired orientation by
slerping between its existing orientation and its goal quaternion. The player's head turns in the
direction it wants to go and the player's body leans and rolls with the direction of motion it is
accelerating in. Additionally, if you press ALT, you can see the eyes blinking with a scaled ellipse
function in the fragment shader.The camera is positioned behind the player and changes its distance and FOV based on the speed of the
player. It smooths to its desired position, and has several different positions based on the state of
the character. It looks directly at the player and follows the character as it travels through the
sand. The camera also collides with the terrain to prevent the terrain from blocking the view of the
player.
Post Processing
Bloom is implemented using a modern technique used in engines like Unity. The pass takes the high
valued colors above a threshhold, and then downsamples down to an extremely low resolution, and then
recursively upsamples back to the base resolution while adding each pass together back into the final
image.The game is rendered in high dynamic range and then tonemapped using ACES tonemapping (see
references).Camera based motion blur was implemented, but the effect didn't give the feeling of speed we wanted,
so we used a radial blur that started outside of the center of the screen that scales its strength
with the player speed. This gives a similar effect as motion blur, but only on moving forwards and not
the rotation of the camera. This effect gave our game the feeling of speed we were looking for.
Normal Mapping and Other Mappings
The main character is normal mapped and has several other maps used for lighting. Since we are using
physically based rendering, we used a metallic map and a roughness map in calculating the shading, in
addition to the normal and base texture map of the character.