Norsevar

Summer 2022 Repeat Portfolio

Tags: Collaborative Project, GD4, Norsevar, Red Axes, Repeat

Importance of props in storytelling

According to Mark Brown from Game Maker's Toolkit, level design can convey three different messages :

  1. The feelings of the character (e.g. in God of War (2018), when Kratos is looking for Atreus, he's surrounded by rocky cliffs, the vision is blocked by a thick fog and he's being slowed down by a really narrow passage. This passage tries to convey a sense of panic as he's frantically looking for his son)
  2. Defining the player identity (e.g. in Hitman (2016), the level developers rely on the player's understanding of social behaviours to let them understand where they can or cannot go under certain conditions)
  3. Help the understanding of the world

This third point is where props are involved, as it falls under environmental storytelling. Mark defines it as a "small, optional, self-contained vignette", which requires the player to use their deductive reasoning, making them an active participant in the storytelling.

Environmental storytelling

Norsevar already had a handful of points of interest which prompted a short remark from Erik, the protagonist, when interacted with. Those remarks were entirely disconnected from the narrative and added nothing to the game. I decided to keep those elements and rewrite the attached dialogues to make them fit with the story of the game and Erik's personnality. I also asked Erik's voice actor to record new voice lines to accompany those dialogues.

Listed below are all points of interest and both the old and new dialogues.

—————————————————————————————————————

1. Axe and shield.png

Point of interest : Axe and shield

Location : Start of the level, next to the player

Triggers : Trying to progress without grabbing both / Grabbing both items

Old dialogue : I should not leave without my Axe and Shield! / Now i'm prepared to go!

New dialogue : Can't leave my gear there ! / Now I can return to the village.

—————————————————————————————————————

Point of interest : Ritual circle

Location : Clearing

Trigger : Finishing the first fight

Old dialogue : Who performs those rituals ?

New dialogue : Hmm… What in Hel is that ritual ?

—————————————————————————————————————

Point of interest : Lakeside dock

Location : Lakeside by the clearing

Trigger : Finishing the first fight

Old dialogue : I can see the village from here

New dialogue : That's odd, I can see no smoke.

—————————————————————————————————————

Point of interest : Sign

Location : Clearing

Trigger : Finishing the first fight

Old dialogue : Must be a sign

New dialogue : Beware, avalanches... Least of my problems, I need to get to the village.

—————————————————————————————————————

Point of interest : Comment

Location : Valley

Trigger : Finishing the second fight

Old dialogue : Why are all these Animals going Feral?

New dialogue : By the Gods, what’s taken over them ?

—————————————————————————————————————

Point of interest : Fireplace

Location : Village

Trigger : Finishing the third fight

Old dialogue : Cozy

New dialogue : Still burning. They must be around here somewhere…

—————————————————————————————————————

Point of interest : House

Location : Village

Trigger : Finishing the third fight

Old dialogue : It's Locked

New dialogue : This seems to be locked.

—————————————————————————————————————

Point of interest : Fisherman's house

Location : Village

Trigger : Finishing the third fight

Old dialogue : Looks like the Fisher's House

New dialogue : Nobody here either… As fresh as usual though…

—————————————————————————————————————

Point of interest : Oceanside dock

Location : Oceanside by the village

Trigger : Finishing the third fight

Old dialogue : I'd be up for another Raid...

New dialogue : They don’t seem to have run to the seas. Sailing blue in the barren waves…

—————————————————————————————————————

Point of interest : Chief's House

Location : Village

Trigger : Finishing the third fight

Old dialogue : Looks like the Warrior's House

New dialogue : The Chief’s not here either… Evil must be afoot…

—————————————————————————————————————

Point of interest : Law Rock

Location : Village

Trigger : Finishing the third fight

Old dialogue : Here's their Law Rock

New dialogue : It’s too quiet…

—————————————————————————————————————

Point of interest : Blacksmith's forge

Location : Village

Trigger : Finishing the third fight

Old dialogue : I could use a better weapon!

New dialogue : Gunvor’s not hammering away… Now something is definitely wrong.

—————————————————————————————————————

Point of interest : Fox (1)

Location : Next to the player's spawn

Trigger : None

Old dialogue : Want me to follow you?

New dialogue : Hm ?

—————————————————————————————————————

Point of interest : Fox (2)

Location : Between the spawn and the clearing

Trigger : Completing Fox (1)

Old dialogue : What are you trying to show me?

New dialogue : You again ?

—————————————————————————————————————

Point of interest : Fox (3)

Location : Between the clearing and the valley

Trigger : Completing Fox (2)

Old dialogue : Keep safe, little Fox

New dialogue : You’re rather brave, for a fox…

—————————————————————————————————————

Point of interest : Fox (4)

Location : Between the valley and the village

Trigger : Completing Fox (3)

Old dialogue : Where are you leading me?

New dialogue : Heh, brave and reckless it is… Wait, what’s that ?

—————————————————————————————————————

Point of interest : Fox (5)

Location : Village

Trigger : Completing Fox (4)

Old dialogue : Don't leave me!!

New dialogue : Oh no, you’re not leaving in the forest before I get answers.

 

Traditional storytelling in Norsevar

Besides environmental storytelling, there are also elements pertaining to more traditional storytelling, most notably regarding exposition. As there are only two levels at the moment, being the first demo level and the hub where the player will respawn after a death during playthroughs, I will talk about what narrative elements are contained in both.

On the technical side of things, I rely on four main functions to play dialogues :

  • PlaySound(ENorseGameEvent) raises an event at the player's position, which plays that sound. It can be run as a Coroutine.
  • PlayDialogue(ENorseGameEvent) plays a dialogue voice line.
  • PlayVoiceLine(ENorseGameEvent) plays a "one-liner".
  • PlayRandomVoiceLine(ENorseGameEvent[], {float}) picks a random voice line from a given list and plays it. The optional float parameter allows for no voice line to play sometimes.

DialogueVoiceLines - All-purpose functions.png

General functions used for playing voice lines

Level 1 - Game demo

The first level stands out from the rest, as the player will only play through it once. With that in mind, I wrote dialogues specifically for that level in order to introduce some key plot points. Erik will remark on the oddity of being attacked by wolves and snakes, even in the village. Drawing a comparison with the Hero's Journey, this first level would represent the first two steps, the Ordinary World and the Call to Adventure, as Erik's normal life whilst returning to the village gets disrupted through repeated wolf and snake attacks.

In order to trigger those dialogues, I altered my old methods for making Erik say a random line of dialogue when a combat starts or ends so that he would say new dialogues exclusively in this level. In the level itself, I added three box colliders to coincide with the three main fights, and gave them a tag related to each fight, so that the game would use the correct line, whilst still maintaining the random voice lines during other fights.

Dialogue areas.png

Bounds of the dialogue areas (left to right : Fight 1, 2 and 3)

 

DialogueVoiceLines - OnEnterCombatVoiceLine.png

OnEnterCombatVoiceLine

 

DialogueVoiceLines - OnExitCombatVoiceLine.png

OnExitCombatVoiceLine

Level 2 - Hub

In a hypothetically complete version of the game, the player would first arrive in this level after dying during their very first run. As they wake up, the Foreknowing God Heimdall greets them telepathically, explaining to Erik what's going on and Loki's involvement in this, and concluding with Erik deciding to go after Loki. The parallel with the Hero's Journey continues, with the Refusal of the Call corresponding with Erik's first unravelled death : he tried to answer the call whilst in the comfort of his ordinary life, but he needs to become better and stronger to be able to progress. Meeting the Mentor happens as Erik wakes back up and is introduced to Heimdall, who explains to him the powers at play and the stakes. At this point, Erik could refuse the call deliberately, but he choses to face Loki, and is therefore Crossing the Threshold.

For those dialogues, I have simply placed a box collider around the player's starting point, and attached both a scriptable object which holds the number of times the player died, and a listener which, once the player leaves that box, plays either the Resurrection dialogue if this is the player's first death, or a random taunt from Loki for any subsequent death. Once the player leaves the box collider, it deactivates itself so that the player cannot trigger the dialogue multiple times. The Resurrection dialogue consists of a chain of events triggered one after the other with a short delay between lines. I tried using the dialogue system instead, but it felt clunky and unintuitive, especially for players using a controller. What's more, Erik's voice lines didn't seem to play as dialogue responses.

Respawn area.png

Bounds of the starting area

 

DialogueVoiceLines - ResurrectionDialogue.png

ResurrectionDialogue (all routines are the same, they just play the corresponding voice line and wait for its entire duration)

 

DialogueVoiceLines - OnRespawnVoiceLine.png

OnRespawnVoiceLine

Voice line manipulation in FMOD

For the voice lines, I asked some friends to record the dialogues I had written. Whilst the quality was pretty good, I wanted to add a bit more identity to the audio files. That's why I, with a bit of help from Simmone to explain how some of the technical features work, altered those voice lines in FMOD.

Erik (voiced by Léo V.)

The protagonist has quite a few voice lines, all spoken in one of three main environments :

  1. A forest (first section of the first level, hub and all run levels)
  2. The mountains (second section of the first level)
  3. The village (third section of the first level)

To reflect those environments, I associated a different level of reverb for each. The village has the least reverb, followed by the forest, and the mountains has the most reverb. Some of the dialogues could easily be assigned a level of reverb, as they are only spoken in a single environment. Others, on the other hand, can be uttered in any of those three environments. For that purpose, I created a global parameter called Environment and paired a level of reverb for each possible value. Then, I added three box colliders in the first level to determine the bounds of each environment, which I paired with FMOD Studio Global Parameter Triggers to alter the value of Environment relative to the player's position in the first level. Those aren't needed in subsequent levels, as from the hub onwards, Erik only progresses through a forest.

Environment areas.png

Bounds of the Environment areas (left to right : Forest, Mountains and Village)

 

Erik settings - Forest.png

FMOD settings - Erik (Forest)

 

Erik settings - Mountains.png

FMOD settings - Erik (Mountains)

 

Erik settings - Village.png

FMOD settings - Erik (Village)

Heimdall (voiced by Liam M.)

The Foreknowing God is only present in an introduction dialogue with Erik after the player's first death, explaining the situation to Erik. I wanted Heimdall to have a deep, booming voice, so I adjusted the pitch, shifting it down to get it deep enough without sounding fake. I also gave him a lot of reverb, to emulate the fact that he's communicating with Erik through telepathy, and to make him sound even more grand. When combined with him speaking in rhymes, it creates a figure that's grandiose and noble, which is exactly how my interpretation of Heimdall would look like.

Heimdall settings.png

FMOD settings - Heimdall

Loki (voiced by Rei S.)

The God of Mischief is only ever heard when Erik is saved by Heimdall's magic more than once, taunting the player to incentivize them to try again. The overall idea was to make Loki sound malicious, while also including elements reminiscing of their shapeshifting abilities. For that purpose, I added a delay to their voice lines to make it echo around the player, as if there were multiple Lokis taunting you. I also added the same reverb as for Heimdall to mimic the effect of telepathy for Loki as well.

Loki settings.png

FMOD settings - Loki

 

Example voice line : Erik (Forest)

Download Erik_forest.mp3 [0.05MB]
Details

Example voice line : Erik (Mountains)

Download Erik_mountains.mp3 [0.03MB]
Details

Example voice line : Erik (Village)

Download Erik_village.mp3 [0.11MB]
Details

Resurrection dialogue : Erik and Heimdall

Download Resurrection.mp3 [1.46MB]
Details

Example voice line : Loki

Download Loki.mp3 [0.06MB]
Details

Demonstration : Voice lines in level 1

Download Level1_demo.mp4 [252.87MB]
Details

Introduction to Procedural Level Generation

Being a roguelite, Norsevar could benefit from having randomly generated levels, as it would mean that less time needs to be allocated to level design, which can be used to work on other features. As I worked on this, I have broken this task into several, smaller pieces, before assembling them back together to create a complex generator.

Whilst working on the Procedural Level Generator, I have worked on the following components :

  • Procedural Layout Generation, using evolutionary computing (abandoned)
  • Procedural Terrain Generation, using layered Perlin noise applied to a plane
  • Random Object Placement, using Poisson Disc Sampling
  • Random Prop Placement, using Wave Function Collapse
  • Procedural Tree Generation, using a modified hexagonal tile generator
  • Procedural Rock Generation, using layered Perlin noise applied to a sphere

Procedural Layout Generation - Research

Details

Procedural Layout Generation

Data structures

As suggested by the research paper, a Room as a rectangular mesh holds four integer values : the coordinates of the top-left corner (x ; y), and the room's length and width. The class also contains methods to detect if two rooms overlap, to find the bounds of the overlap, to calculate the area of said overlap, to find the combined area of two rooms, to build the mesh corresponding to that room, as well as a delegate methods to allow for mutations.

 As Evolutionary Computing is closely related to genetics, some terminology is drawn over, including the notion of a Chromosome. As a data structure, it only holds a list of rooms (the genes) and their meshes. It also has methods to calculate the overall area, the number of narrow and tiny rooms, add a room, and build a mesh from overlapping the rooms on top of one another.

Evolutionary Computing

Evolutionary Computing designs a family of optimization algorithms which draw inspiration to the theory of Evolution, and uses a population of chromosomes to determine the most optimized (or fittest) outcome. Here, it relies on two key operations :

  • Crossover is when a child is created from picking genes at random from either parent.
  • Mutation is when a random chromosome's gene gets randomly shuffled.

Crossover and Mutation.png

Visual representation of crossover and mutation

 

Genetics - Crossover.png

C# implementation of the Crossover operation

 

Genetics - Mutation.png

C# implementation of the Mutation operation

In the context of Evolutionary Computing, a Population is comprised of several chromosomes and relies on a fitness function to determine the "viability" of each chromosome. A population starts with fully randomized chromosomes. Then, every time the population evolves, the following happens :

  1. The fitness function is used to assign a fitness rating to each chromosome.
  2. A new generation is created by operating a crossover between the two fittest chromosomes.
  3. This new generation is then mutated.

The idea is that, after enough generations have passed, the fitness rating would converse towards a certain value, meaning the fittest chromosome would be the absolutely ideal candidate. Once applied to the concept of room generation, several fitness functions can be considered, and three in particular were implemented :

  • Maximizing the area, where the fitness rating simply equals the area of the room generated by the chromosome.
  • Minimizing the area, where the fitness rating is inversely proportional to the room's surface area.
  • Penalizing the area, where the fitness rating gets negatively impacted if the room has "narrow" or "tiny" sections (sections that are 1-unit wide or have a surface area of 1 respectively).

Population - Fitness functions.png

C# implementation of the three fitness functions.

Results

I have developed a function that can build a rectangular Room mesh based on the values held by the object, which works well. When implementing a similar method for chromosomes, I encountered a major issue, as vertices could get duplicated, which would cause problems with triangles. To solve this, I decided to turn each room into a grid of mesh squares, each containing two isosceles triangles and their indices to create a square with sides that are 1-unit long. With this, I was able to use HashSets to hold all vertices and squares, whilst being able to create a single mesh.

However, the function used to calculate the area doesn't seem to work properly, and gives vastly incorrect errors (including 0 and negative values). After struggling on this for a while, I decided to move on and return to it later. Once I implemented the other elements of the level generator, I ended up not needing a layout generator.

Chromosome - Area.png

The non-functional Area function, based on a solution suggested on StackExchange

Procedural Terrain Generation

Most of the explanations provided here come from Sebastian Lague's video series on the subject. What's more, he incorrectly spelled persistence as persistance. If the wrong spelling shows up in my code, that is why and I have missed it during refactoring.

How does it work ?

Unlike regular noise, which provides independently generated random values, Perlin noise uses a pseudo-random appearance to generate coherent noise which looks like a gradient. However, Perlin noise alone creates curves that are way too smooth to emulate realistic terrain. To remedy this, we layer multiple levels of noise on top of one another, let's call them octaves. As Perlin noise is a function, it has a frequency and an amplitude. To scale each octave, we introduce a variable called lacunarity, which controls the increase in frequency of subsequent octaves, being more and more important the more octaves are used. However, the intensity of each octave should be inversely proportional to its frequency. That's why we introduce a second variable, persistence, which controls the decrease in amplitude of the octaves. Lacunarity is always greater than 1, whilst persistence is always between 0 and 1.

With that in mind and assuming that indexing starts at 0, for the nth octave, we have

  • frequency = lacunarityn
  • amplitude = persistencen

The greater n is, the larger the frequency and the smaller the amplitude.

Visual summary of  the theory behind Procedural Terrain Generation (by Sebastian Lague)

With these two parameters, we can implement a 3D mesh generator, as Unity has a built-in function to evaluate the Perlin noise value at any given point.

Data structures

In order to make this reusable, several data structures are used to hold key settings.

First, Noise Settings are used to define key values for generating the noise map. Persistence and lacunarity are included, alongside the following parameters :

  • scale (float) : Used to adjust the overall height of the terrain
  • octaves (integer) : Determines the level of detail in the noise map
  • seed (integer) : Used in conjunction with a pseudo random number generator in order to be able to recreate a terrain
  • offset (Vector2) : Used to move within a given terrain smoothly

Noise - NoiseSettings.png

Noise Settings class

Second, Height Map Settings are used to turn the noise map into a height map. It includes the aforementioned noise settings, as well as :

  • useFalloff (boolean) : If set to true, a falloff map will be laid on top of the noise map, which will create an island-like terrain
  • heightMultiplier (float) : Determines the influence of the height curve
  • heightCurve (Animation Curve) : Used to alter the scale of each height value

HeightMapSettings.png

Height Map Settings class

Third, Mesh Settings are used to determine elements more specific to the terrain mesh, including :

  • meshScale (float) : Used to enlarge or shrink a mesh
  • useFlatShading (boolean) : If set to true, flat shading will be used
  • chunkSizeIndex (integer) : Determines the size* of the chunks
  • fSChunkSizeIndex (integer) : Determines the size* of the chunks when flat shading is used

* due to technicalities in how the mesh is constructed, the size of a chunk can only be set to certain values

MeshSettings.png

Mesh Settings class

Finally Texture Data is used to store information regarding the texture applied to the terrain. It only contains an array of data structures used to store information about each Layer of terrain. A Layer is composed of the following :

  • texture (Texture2D) : The layer's texture
  • tint (Color) : Used to give a specific tint to the texture
  • tintStrength (float) : Determines the intensity of the tint
  • startHeight (float) : The layer will apply to all points that are at this height or higher
  • blendStrength (float) : Used to merge layers together, for a smoother look
  • textureScale (float) : Scales the texture

TextureData - Layer.png

Layer class

Note : I used to have multiple textures, based on shaders covered by Sebastian Lague in his terrain generation series. The first only used colours, the second only used textures, and the third was using tinted textures. As he coded his using SRP, some techniques were not available to me, as I was using URP. Therefore, I tried to recreate those in ShaderGraph to the best of my ability. Later, I went back and reused the shader he designed for the planet generator, as it was also using tinted textures. I kept the old code in as an indicator of the change, but commented it out.

TextureData - TextureData (1).pngTextureData - TextureData (2).png

Texture Data class

Results

Terrain Generator.gif

Whilst the original terrain generator allows for infinite world generation, this feature is not used in the level generator, as a single chunk is more than enough for a single level. In order to create randomized terrain, a random seed and offset are picked. The falloff map was used in order to define the limits of the stage (coloured in tan in the renders above). Various shades of white are used to mimic the appearance of snow, although I'm don't know whether the snow shader used in the main game could be used here as well. Below are the values for each set of settings.

Terrain Height Map.png

Height Map settings

 

Terrain Mesh.png

Mesh settings

 

Terrain Texture.png

Texture settings

Procedural Prop Placement

Poisson Disc Sampling

Poisson Disc Sampling is an algorithm used to randomly place objects within a given area where all objects at more than a given distance apart from each other. The number of points placed is random, but depends on the minimal distance. The lower the radius, the denser the point population. The code below mostly comes from Sebastian Lague, with some refactoring from me in order to conform to my coding practices.

PoissonDiscSampling - GeneratePoints.pngPoissonDiscSampling - Sub-methods.png

C# implementation of Poisson Disc Sampling (by Sebastian Lague)

In order to reuse parameters, I bundled them all as Sampling Settings. These include :

  • radius (float) : The minimal distance between any two points
  • regionCentre (Vector2) : The centre of the sampling region
  • regionSize (Vector2) : The size of the sampling region on the X and Z axis
  • rejectionSamples (integer) : How many times the algorithm will attempt to place a point before moving on

Sampling settings also include a method to sample points and return a list of all those points.

Sampling Settings.png

Sampling Settings class

Wave Function Collapse

Wave Function Collapse is an algorithm used to solve a grid where each possible value a tile can take is set by an ensemble of constraints. For instance, in sudoku, each cell of the grid can take a digit between 1 and 9, with the constraint being "Any given digit can only appear once in any given row, column, and box". This algorithm is named as such because, when you collapse the value of a cell (for instance, you write a 9 in a cell of a sudoku grid), the constraints will be applied to nearby cells and alter the number of possible values they can take (e.g. all cells in the same row, column or box as the collapsed cell cannot be a 9). And altering the possible values of a cell might affect other nearby cells, creating a ripple effect across the entire grid like a wave.

I did not find a Unity implementation, so I made my own. For starters, I created a generic Data Grid type based on a video by Code Monkey to hold any type of data in a 2D array, whilst also supporting a debug display and mouse interactivity.

DataGrid (1).pngDataGrid (2).png

Data Grid class

Then, in order to emulate the placement of props, I decided to create an object to describe a Constrained Tile, using solely adjacency constraints. The default assertion is that a tile can always be adjacent to itself. For the purpose of the generator, I created Air, Tree, and Rock tiles.

Constrained Tile.png

Constrained Tile class

Finally, in order to properly implement Wave Function Collapse, I combined the previous two classes and created a Collapsing Grid, which also holds a 2D array of possible values for each cell along a list of each possible value any cell can take. It also contains the method SetAndCollapse(int, int, ConstrainedTile), which collapses a cell, then applies the Wave Function Collapse algorithm to adjacent cells, repeated until no affected cell is left. Finally, there is also the Generate() method, which collapses random cells until the entire grid is set.

Collapsing Grid (1).pngCollapsing Grid (2).pngCollapsing Grid (3).png

Collapsing Grid class

Procedural Prop Placement

By combining the two previous methods (which I dubbed Poisson Collapse), it is possible to randomly place props across a given area. The Poisson Collapse algorithm works as follows :

  1. Points are placed using Poisson Disc Sampling
  2. A random value is given to each point using Wave Function Collapse, converting the point's position (Vector3) into grid coordinates (pair of integers)
  3. If the given value is that of an air tile, nothing else happens. Otherwise, a prop of the corresponding type (tree or rock) is generated.

Poisson Collapse (1).pngPoisson Collapse (2).png

Poisson Collapse class

Results

Procedural Prop Placement.gif

The Poisson Collapse algorithm works as intended, placing random points in a given area and associating them with a type of prop. Air tiles were implemented in order to give a bit more breathing room and make it feel more like an actual forest with more open areas instead of being constantly surrounded by rocks and trees. Below are the sampling settings and tile types used.

Prop Placement Sampling Settings.png

Prop Placement sampling settings

 

Air Tile.png

Air tile (skipped tile)

 

Rock Tile.png

Rock tile

 

Tree Tile.png

Tree tile

Prop Generation

When I tried to search for props to use in Norsevar's file, I couldn't find the trees and rocks that were used. That's why I decided to make generators for both of them. To create those, I repurposed tutorials on the Internet in order to create what I wanted to build. The tree generator was built from a modified hex grid generator, and the rock generator is in reality a planet generator with specific settings to make it look like rocks.

Tree Generator

Implementation

When I started working on this generator, I determined that, with the time I had, my best course of action was to create low-poly trees by stacking cones on top of one another. Creating a cone was a complex task in itself, and I struggled for some time. My original idea was to first build a regular polygon as the base and use the generated vertices to link them together and with the top vertex to create the faces. I was able to create that first function as BuildCirclePoints(Vector2, float, int), which returned a list of all points on the base. After that, I got stuck, unsure of what to make of those points.

ConeGenerator - BuildCirclePoints.png

Code of BuildCirclePoints

After a while, I watched a video by Game Dev Guide about hexagonal grids and how to create one in Unity. Looking at the code from the tutorial, I realised that I could easily alter it to make it work with any number of sides, as well as turn it into a regular pyramid. I created a class to hold all the essential values to build a Regular Pyramid, and altered the original code to make use of that class.

RegularPyramid.png

Regular Pyramid class

The original code made use of a data structure to store mesh data for each Face, so I implemented that as well.

Face.png

Face structure

And with all those changes implemented, the hexagon cell generator was turned into a "low-poly cone" generator.

ConeGenerator (1).pngConeGenerator (2).pngConeGenerator (3).png

Cone generation

Now that I had a working cone generator, the next step was to use those to create a tree. My plan was to have a large tall cone as the trunk and stack smaller, thinner cones on top to serve as foliage. To be able to generate different-looking trees, I created a class called Tree Settings to hold the extrema for possible values each parameter a Regular Pyramid can take, alongside methods to get a random value.

Tree Settings.png

Tree Settings class

Then, I created a class to serve as a Tree Generator. It holds settings for both the trunk and foliage, as well as other extrema for the height of the lowest piece of foliage, the interval between two pieces of foliage, as well as the number of pieces of foliage on one tree. Finally, it also holds a value called shrink factor, used to make each piece of foliage slightly smaller than the one below it.

When I first wrote GenerateTree(), I placed the trunk first, followed by the foliage. However, this led to some trees having the trunk popping above the foliage. To fix this, the foliage is now placed first, the trunk can then be placed and its size can be adjusted if needed.

Tree Generator (1).pngTree Generator (2).pngTree Generator (3).pngTree Generator (4).png

Tree Generator class

Results

Tree Generator.gif

The generator can create randomized trees. When adding it to the level generator, I had to tweak certain parameters, as the trees were too big.

Tree Generator.png

Tree Generator settings

Rock Generator

Implementation

The rock generator works similarly to the terrain generator, making use of layered Perlin noise to create irregular surfaces. Here, it is applied to a sphere in order to create a planet. I used a tutorial series by Sebastian Lague, but the implementation of Perlin noise that was used is the one from the libnoise-dotnet library.

Procedurally generating a planet relies on two sets of settings. The first is a set of Shape Settings, holding the radius and a list of noise layers. Each Noise Layer holds a boolean to choose whether to use the first layer as a mask, as well as some noise settings.

Shape Settings.png

Shape Settings class

Noise Settings allow to choose between a Simple or Rigid Noise Filter, alongside the following settings :

  • strength (float) : Intensity of the noise
  • numLayers (integer) : How many layers of Perlin noise are overlaid
  • baseRoughness and roughness (float) : Lacunarity of the noise
  • persistence (float) : Persistence of the noise
  • centre (Vector3) : Point at which the Perlin noise is evaluated
  • minValue (float) : Minimal threshold for the noise to be registered, can provide some flat areas
  • weightMultiplier (float) : Intensity of the ridges (Rigid Noise Filter only)

Noise Settings.png

Noise Settings class

Both Simple Noise Filter and Rigid Noise Filter inherit from the Noise Filter interface, which only features the Evaluate(Vector3) function. The Simple Noise Filter simply evaluates the noise value at a given point, like how the terrain generator handled it. The Rigid Noise Filter, on the other hand, tries to create more extreme ridges with steeper elevation. In order to make it simpler to create, a Noise Filter Factory was created to create the apt noise filter depending on the Noise Settings.

INoiseFilter.png

Noise Filter interface

 

Simple Noise Filter.png

Simple Noise Filter class

 

Rigid Noise Filter.png

Rigid Noise Filter class

 

Noise Filter Factory.png

Noise Filter Factory class

The second of settings used are Colour Settings. They hold a material (using a specific shader made in Shader Graph), a set of Biome Colour Settings, as well as a Gradient used to paint the oceans. Biome Colour Settings hold a list of biomes, a set of Noise Settings, values for noise offset and strength, as well as the blend amount. A Biome is composed of a gradient, a tint, a starting height, and a value to set the strength of the tint.

Color Settings.png

Colour Settings class

 

Rock Shader.png

Rock Shader shader graph

With those settings, it is possible to turn a unit cube into a unit sphere, then turn that sphere into a more complicated planet. In order to construct the unit sphere, each cube face is turned into a Rock Face by calculating the coordinates of each vertex relative to the centre, then normalizing it to turn the cube into a sphere.

Rock Face (1).pngRock Face (2).pngRock Face (3).png

Rock Face class

In order to calculate the elevation of a given point on the unit sphere, a separate class is used as a Shape Generator. It can calculate the elevation, both scaled and unscaled. Scaled elevation is used to set the position of the vertices, whereas unscaled elevation is used to set the UVs of the mesh. Then, once the meshes are constructed, Colour Settings are used by a Colour Generator to update the rock's texture.

Shape Generator.png

Shape Generator class

 

Color Generator (1).pngColor Generator (2).png

Colour Generator class

With all those components, all that's needed is to attach a final script to a Game Object in order to turn it into a Rock. The initial code made it possible to generate the rock in the Editor, but I added the option to generate it through a method call, and even to randomize the rock's appearance.

Rock (1).pngRock (2).pngRock (3).png

Rock class

Finally, I created one more class to serve as a Rock Generator, which randomizes the radius of each generated rock.

Rock Generator.png

Rock Generator class

Results

Rock Generator.gif

Finding the ideal settings was achieved through trial and error. The radius and centre of noise settings weren't taken into consideration, as the scripts already randomize those two. Similarly to what happened with the tree generator, the maximum radius had to be tuned down as it was too big for the generated terrain otherwise.

Rock Shape Settings.png

Rock shape settings

 

Rock Color Settings.png

Rock colour settings

Procedural Level Generation

With all the parts ready, all that's left is to combine them in order to create the level generator. First, the Prop Placer needs to be adjusted to fit the Terrain Generator.

3D Prop Placement

Forest Generation Prop Placement.gif

In order to fit the generated terrain, different settings are used, with similar results. In Poisson Collapse, only the Skipped Tile, Sampling Settings and Wave Function Collapse possible values were altered, using the objects below.

Forest Generation Sampling.png

Forest Generation sampling settings

 

Forest Air Tile.png

Forest Air tile (skipped tile)

 

Forest Rock Tile.png

Forest Rock tile

 

Forest Tree Tile.png

Forest Tree tile

To adjust the Prop Placer to a 3D space, the height of the props needs to be adjusted and remove props on terrain that's too low, as they would be out of bounds.

Prop Placer 3D.png

Prop Placer 3D class

Now, the final step was to attach the 3D Prop Placer to the Procedural Terrain Generation to create the Forest Generator. In order to randomize the terrain, a random seed and offset are picked on generation.

Forest Generator.png

Forest Generator class

Results

Level Generator.gif

The level generator is now functional. Had I more time, I would've liked to work on the following features :

  • Adding the player character, to see how the core gameplay would work with the generated levels
  • Implementing the snow shader from the base game
  • Add invisible walls to limit where the player can go
  • Add randomized enemy spawners, possibly with some way of scaling difficulty
  • Attaching the level generator to the run handler from the base game
  • Using the props used in the base game instead of the generated ones

Summer 2022 Repeat Journal RSS

Week 2 : 27/06 - 03/07

Planned work

This week, I have to work on

  • Research procedural terrain generation (est. 4h)
  • Implement procedural terrain generation (est. 4h)
  • Research and implement a blizzard effect when a fight starts (est. 4h)
  • Research Poisson Disc Sampling (est. 2h)
  • Research Poisson Disc Sampling (est. 2h)

Update 1 : 27/06

I watched Sebastian Lague's introduction video to Procedural Terrain Generation (link) and updated the portfolio to talk about the failed attempt at layout generation and explain how terrain generation works (4h).

Update 2 : 28/06

I started implementing Sebastian Lague's Procedural Terrain Generation (4h). Somehow, I hadn't taken into account the duration of the video tutorials whilst estimating the time each task would take, meaning this would have to span over multiple days. Other than that, I haven't encountered any issues.

Update 3 : 29/06

I continued the Procedural Terrain Generation implementation (6h). This took longer than expected as the tutorials were using the Standard Render Pipeline, meaning the shader code using Cg didn't work with the Universal Render Pipeline. To remedy this, I had to spend time recreating the shaders in ShaderGraph. Fortunately, I found a thread on the Unity Forum started by someone who followed this exact tutorial and encountered the same issue. I also added triplanar mapping on a separate shader. Later during refactoring, the generation stopped working for me. I lost a lot of time trying to fix it before realizing one of the values was set to zero. Once that was set to a positive value, it worked fine again.

Update 4 : 30/06

I finished implementing Procedural Terrain Generation, cleaned up the code, re-enabled the possibility to use a falloff map and added a shader which uses multiple textures and tints to paint the terrain based on the height of the various layers (6h). I didn't encounter any major issues.

Update 5 : 01/07

I researched and implemented Poisson Disc Sampling based on a tutorial by Sebastian Lague (4h). I also implemented a scriptable object to store sampling settings. After doing a bit of research, I couldn't find a way to create a blizzard effect, so I'll put this task off for later.

Week 1 : 20/06 - 26/06

Planned work

This week, I have to work on

  • Research how to tell a story through the decor and find appropriate props (est. 6h)
  • Implement said props in the first level (est. 6h)
  • Research Hotline Miami-inspired procedural level generation (est. 4h)
  • Implement Hotline Miami-inspired procedural level generation (est. 4h)

From here on, I'll use the following format to convey  :

  • Green represents narrative-related tasks
  • Olive represents technical tasks
  • Estimated and actual spent time are in bold

Update 1 : 20/06

I watched a pair of videos by Game Maker's Toolkit and Extra Credits on storytelling, which gave me the insight I needed to figure out the theory of telling a story through its decor (4h), relying on environmental storytelling to help build the lore and the characters.

Update 2 : 21/06

I decided to take the points of interest (POI) which were already implemented in the first level by other team members and inject plot relevance in them. In that optic, I took each POI and wrote new dialogues spoken by Erik when interacting with them (4h). I have also contacted Erik's voice actor so that he could record voice lines for those new reactions. This will also help make the game more uniform by having all lines of dialogue spoken instead of having some just being displayed lines of text.

Once I'll have enough time to do so, I also plan to add a Credits button in the menu to actually give credit to the three voice actors of Erik, Heimdall and Loki. While the latter two's dialogues still aren't used in the game, this is something which I intend to remedy if I find the time to do so.

Circling back to the environmental storytelling, once the current POIs are all repurposed, I'll have some family and friends play the level and collect their feedback. Based on that, I'll decided whether to include more POIs or not.

Update 3 : 22/06

I carried on with yesterday's work and replaced the old POI dialogues with the new ones (1h). From here on, this task will have to be put on stand-by until I can get the voice lines recorded and feedback from ensuing play-testing.

That's why I moved on to the next task, procedural level generation. The first step was research, so I studied a paper by J. A. Brown et al. about Procedural Content Generation of Level Layouts for Hotline Miami (3h). Procedural level generation being a dropped feature, I have already read through this paper a handful of times, so technical concepts relating to evolutionary computing are not foreign and I was able to extract the information I needed relatively easily. I have good hopes that I'll be able to wrap up the procedural level generation by tomorrow, although I might need to tweak the generator to fit the following features I intend to add.

Update 4 : 23/06

I continued studying the paper and established a general data structure which I will use to create the level layout generator (4h). It took longer than expected because of fatigue, but I'm still on track with my planning.

Update 5 : 24/06

I started turning the data structure I established into usable code for the generator (4h). Before moving on to the evolutionary computing part, I wanted to make sure the mesh generation worked as intended first. Creating a grid mesh from a room five-tuple was easy enough, even if I had to rely on code by Catlike Coding to help me get it working. Turning a chromosome into many meshes that are cut to remove the overlap, however, proved to be a more difficult task.

Removing the unnecessary vertices is the easy part, you only have to look out for those on the edge of the "mask" which need to stay even if they are shared.

Removing the unnecessary triangles, however, is another can of worms, as it relies in indices, which change the moment you alter the number of vertices. My solution was to rely on "squares" (basically, two adjacent triangles which form a square) to identify which can stay and which should go.

As of today, I have encountered two problems :

  1. The mesh cutting doesn't work properly for rooms with a placement type of U ;
  2. The function which is supposed to calculate a room's surface area doesn't return the proper value.

I have no idea so far how to fix this, but I'll come back to it tomorrow and will hopefully find a solution.

Update 6 : 26/06 - Weekend work and reflection

I decided to work on the project this weekend as I don't want to fall back too much on my work planning. I continued working on procedural level layout generation on Saturday (6h) and Sunday (8h). I encountered even more issues as I was working with non-rectangular meshes, which was a struggle to generate. Even once I managed to generate those, the evolutionary computing part decided to put up a fight and refused to cooperate. If I can't manage to get it working by tonight, I'll move on and find another way to create a non-rectangular level layout.

As this week concludes, it's now time to reflect on what I've accomplished.

What I had to do Research how to tell a story through the decor and find appropriate props
Implement said props in the first level Research Hotline Miami-inspired procedural level generation Implement Hotline Miami-inspired procedural level generation
Estimated time 6 hours 6 hours 4 hours 4 hours
What I did Learnt about environmental storytelling and the importance of using the decor to tell the player about the lore

Replaced all written dialogues with newer, more pertinent ones

Commissioned Eric's voice actor to record more voice lines

Reread the paper

Took notes and wrote the bases for the code

Implemented the system as explained in the paper

Added a system to display the layout as a mesh

Time spent 4 hours 5 hours 7 hours 18 hours
Why I had to do it As it stands, the narrative isn't palpable at all within the game, which needs to be fixed The game only has one level, I want to add a procedural level generator, step-by-step
Problems encountered None

I hadn't planned new voice lines, so this task will have to continue in parallel until I can complete it for good

  • implement the voice lines
  • gather feedback from people who don't know about the game
  • react accordingly
Fatigue

The mesh generator didn't work properly

The function to calculate a room's surface didn't work

The mesh cutting didn't work properly

The evolutionary computing didn't work properly

Reflection This was really educative I should plan a bit further ahead from now on, to avoid being taken aback   I should be a tad less overconfident about my ability to make something work. At least I am certain to have tried everything before moving on
  • Research how to tell a story through the decor and find appropriate props (est. 6h)

As it stands, the narrative isn't conveyed in the game, which needs to be fixed. To solve this, I watched videos and learnt about environmental storytelling and the importance of using the decor to let the player know about the lore. This took me 4h, and I encountered no major roadblocks for this.

  • Implement said props in the first level (est. 6h)

As it stands, the narrative isn't conveyed in the game, which needs to be fixed. To remedy this, I have replaced most written dialogues with more pertinent ones, and I also contacted Erik's voice actor to record more voice lines. This took me roughly 5h, and it posed no major problem. However, as I hadn't planned for new voice lines, it added more tasks to do, which means I'll have to continue working on this task in parallel to the rest until it's finished (wait for the new voice lines to be recorded, implement them, get the level play-tested to see if the narrative is conveyed better and react accordingly).

  • Research Hotline Miami-inspired procedural level generation (est. 4h)

The game only has one level, so I want to add a procedural level generator, doing so step-by-step. For starters, I reread the paper I found earlier this year about procedural level layout generation and established the bases for the code. This took me 7h, mostly due to fatigue, but the time I saved on the narrative tasks meant it didn't lose me any time for this week.

  • Implement Hotline Miami-inspired procedural level generation (est. 4h)

The game only has one level, so I want to add a procedural level generator, doing so step-by-step. To get the level layout generator working, I implemented the system as explained in the paper, as well as another generator to turn the layout into a usable mesh. This took me a staggering 18h, as I encountered issues every single step of the way : The mesh cutter didn't work properly, the area calculating function returned incorrect values, it took me a day and a half to get the more complex mesh generator working, and the evolutionary computing part doesn't seem to work with the mesh generator. This made me feel a tad less overconfident in my ability to make something work, and it took me most of my weekend for nothing, but at least I know I have tried everything before abandoning the feature and moving on.

All in all, I had planned to work 20h this week, but ended up working 34h, mostly due to the implementation of a complex feature for which I couldn't find any pre-made solution on the Internet.

Details

Useful general links

Mahara skin made by my teammate Paul Bichler.

GitHub repository

Useful technical links

Procedural Terrain Generation by Sebastian Lague

Poisson Disc Sampling by Sebastian Lague

Wave Function Collapse :

Hexagon Grid Generation by Game Dev Guide

Procedural Planet Generation by Sebastian Lague