Introduction: Continued: Mountain Survival

Continued from the previous instructables! We just left off with...

Step 1: Populator: Populator Manager

Now that we have the four populators created and added to our Game Manager gameobject, we need a manger to execute the code inside each populator. Inside the populator is a set of commands that spawn different objects. For example...

  • PopulateWorld(): This function uses every populator in the list to spawn their game objects. This is usually used at the start of the game isn't really used anywhere else.
  • ResetDynamicObject(): This function resets every objects that are dubbed "Dynamic." Dynamic objects are objects that the player can interact with. This is usually needed when the game has to resupply the player.
  • DestroyWorld(): This function does what it says on the tin, it destroys every game object that was spawned using the populators. This isn't really used but I included it just in case if I ever needed.
  • ChangeDifficultyLevel(): The zombie populator and crate populator's stats are determined by a number called DifficultyLevel.

Every function simply uses the references we created early in the script. They call the populator's function to populate the world, and for the populators that need a list of randomly generated positions, we just give those functions their own functions that generate random positions.

And also notice how the doesn't do a lot of executing code. The only code it executes is PopulateWorld() at start to spawn game objects and then sets Difficultylevel at 1. (Picture 1) is the finished result.

This is the manager of the populators we have on our Game Manager empty gameobject.

usingSystem.Collections;
usingSystem.Collections.Generic;
usingUnityEngine;
publicclassPopulator_Manager : MonoBehaviour
{
//IMPORTANT: Dynamic means objects that can be interacted with (Zombies, Crates, etc.) Static means objects that can't be interacted with like grass or trees
//Here we have the four populators we created. We create a reference to each one so we can easily access them.
publicZombie_Populatorthis_Zombie_Populator;
publicEnvironment_Populatorthis_Environment_Populator;
publicCrate_Populatorthis_Crate_Populator;
publicDetail_Populatorthis_Detail_Populator;
//This number determines how some of the populator works
publicintDifficultyLevel;
voidStart(){
PopulateWorld();
DifficultyLevel=1;
}
//This function goes through every populator and calls the function responsible for placing their respective gameobjects.
//Notice how in the Zombie and Enviornment Populator, we feed in their GetSpawnVectors() and SpawnVectors() repsectively, which
//satisfies the need for a random list of positions for those populators
publicvoidPopulateWorld(){
this_Zombie_Populator.PopulateWorld(this_Zombie_Populator.GetSpawnVectors());
this_Environment_Populator.Populate_World(this_Environment_Populator.SpawnVectors());
this_Crate_Populator.Crate_Populate();
this_Detail_Populator.Spawn_Objects();
}
//Destroys all dynamic objects and respawns them.
publicvoidResetDynamicObjects(){
this_Zombie_Populator.DestroyAllSpawnedObjects();
this_Crate_Populator.DestroyAllObjects();
this_Zombie_Populator.PopulateWorld(this_Zombie_Populator.GetSpawnVectors());
this_Crate_Populator.Crate_Populate();
}
publicvoidResetWorld(){
this_Zombie_Populator.DestroyAllSpawnedObjects();
this_Crate_Populator.DestroyAllObjects();
this_Environment_Populator.DestroyGameObjectsSpawned();
this_Detail_Populator.Destroy_Objects();
this_Environment_Populator.Populate_World(this_Environment_Populator.SpawnVectors());
this_Zombie_Populator.PopulateWorld(this_Zombie_Populator.GetSpawnVectors());
this_Crate_Populator.Crate_Populate();
this_Detail_Populator.Spawn_Objects();
}
publicvoidDestroyWorld(){
this_Zombie_Populator.DestroyAllSpawnedObjects();
this_Crate_Populator.DestroyAllObjects();
this_Environment_Populator.DestroyGameObjectsSpawned();
this_Detail_Populator.Destroy_Objects();
}
publicvoidChangeDifficultyLevel(intNewLevel){
//Prevents Modifiying the populator stats if the same difficulty level
if(DifficultyLevel!=NewLevel){
DifficultyLevel=NewLevel;
this_Zombie_Populator.PackNumber*=DifficultyLevel;
this_Crate_Populator.CratesToSpawn*=DifficultyLevel;
}
}
}

Step 2: Day System: Goals and Solutions

Goals

Now that we're done with the populator system, some of you may asking how the DifficultyLevel. As the game goes on, we need the difficulty of the game to increase. And that brings up two problems we have to make solutions for.

  1. How do we increase difficulty
  2. How do we do that over time

Solutions

The solutions I came up were created using the populator system.

  • We'll increase difficulty by increasing the number of zombies and crates so that the player has more enemies but more supplies to use.
  • We'll have a day system where the player can sleep after all the zombies are killed on the map, adding onto a number we'll create which will track the days. At certain days, we'll increase the difficulty of the game by changing the DifficultyLevel number.

First we'll start with the sleeping system.

Step 3: Day System: Homestead Clickable

So our first problem: Letting the player essentially "sleep" and pass the days. When they do this the game will increase the difficulty and reset the dynamic objects in the world. So we allow the player to sleep by having an object they can click on to reset the dynamic objects and reset the world!

  • We make the script inherit from ClickableParent so we can the player can click on it
  • We make a DayCountManager reference in the script. We'll cover this later, but for right, all you need to know is that we call it to go tell the populations to update.
  • We fade the screen to black then call a function in the DayCountManager reference.
  • We then fade the screen back.

The script is pretty filled with UI references and fading to black, but for right now, all you need to know is that the script, when clicked, calls on DayCountManager. I will touch on UI on a later section, but right now, I'll go on to the DayCount Manager.

This is the script that handles the player going to sleep. Alot of is dealing with the fade which I'll cover later.

usingSystem.Collections;
usingSystem.Collections.Generic;
usingUnityEngine;
usingUnityEngine.UI;
publicclassSleep_Clickable : ClickableParent
{
//DaycountManager is the manager that handles specifically the days system
publicDayCountManagerDayManager;
publicPopulator_ManagerPopManager;
publicTextDayDisplay;
publicTextDifficultyDisplay;
publicImageFadeImage;
publicfloatMinimumDistance;
publicAudioClipClip;
privateAudioSourceSource;
privatefloatcurrentAlphaOfImage;
voidStart(){
Source=GameObject.Find("Player").GetComponent<AudioSource>();
FadeImage.color=newColor(FadeImage.color.r, FadeImage.color.g, FadeImage.color.b, 0);
currentAlphaOfImage=0;
DayDisplay.enabled=false;
DifficultyDisplay.enabled=false;
}
publicoverridevoidOnClick(Vector3ClickPosition){
if(Vector3.Distance(ClickPosition, transform.position) <=MinimumDistance)
Source.PlayOneShot(Clip);
StartCoroutine(Fade());
}
IEnumeratorFade(){
while(FadeImage.color.a<1){
currentAlphaOfImage+=0.1f;
FadeImage.color=FadeImage.color=newColor(FadeImage.color.r, FadeImage.color.g, FadeImage.color.b, currentAlphaOfImage);
yieldreturnnewWaitForSeconds(0.01f);
}
DayManager.NextDay();
DayDisplay.text="Day: "+DayManager.CurrentDay.ToString();
DayDisplay.enabled=true;
DifficultyDisplay.text="Difficulty Level: "+PopManager.DifficultyLevel.ToString();
DifficultyDisplay.enabled=true;
yieldreturnnewWaitForSeconds(5.0f);
DifficultyDisplay.enabled=false;
DayDisplay.enabled=false;
while(FadeImage.color.a>0){
currentAlphaOfImage-=0.1f;
FadeImage.color=FadeImage.color=newColor(FadeImage.color.r, FadeImage.color.g, FadeImage.color.b, currentAlphaOfImage);
yieldreturnnewWaitForSeconds(0.01f);
}
currentAlphaOfImage=0;
FadeImage.color=FadeImage.color=newColor(FadeImage.color.r, FadeImage.color.g, FadeImage.color.b, currentAlphaOfImage);
StopCoroutine(Fade());
}
}

Step 4: Day System: Day Count Manager

We also have to implement a increasing difficulty stem. We do this using our DayCountManager, which does...

  • Adds to the current day
  • Resets the crates and zombies in the world
  • Checks if the current day is between a threshold and changes difficulty

The Day Count Manager doesn't actually execute any code. The code inside is called by the Sleep_Clickable script when the player clicks on it. Essentially, the script order goes like this

  • Sleep Clickable calls on DayCountManager when clicked on
  • DayCountManager calls the PopulatorManager to change the DifficultyLevel
  • The PopulatorManager calls on each of the populations inside the script, using DifficultyLevel to change different numbers to spawn

And with that, every one of our initial design goals is complete! Now is finishing touches, starting with audio.

This DaycountManager takes care of changing the DifficultyLevel variable.

usingSystem.Collections;
usingSystem.Collections.Generic;
usingUnityEngine;
publicclassDayCountManager : MonoBehaviour
{
//This is the current day. We set it at 1 at the start
[HideInInspector]
publicintCurrentDay;
//We make a Populator_Manager reference so we can drag the Populator_Manager and use its difficulty function
publicPopulator_ManagerPopulator;
voidStart(){
CurrentDay=1;
}
voidUpdate(){
Debug.Log("Day:"+CurrentDay);
}
publicvoidNextDay(){
//The previous script, SleepClickable, calls the NextDay() function to move to the next day
//When it does this, it checks if it's between a certain set of numbers, and then we change the
//Difficulty Level accordingly
if(CurrentDay>1&&CurrentDay<=3){
Populator.ChangeDifficultyLevel(2);
}
if(CurrentDay>3&&CurrentDay<=10){
Populator.ChangeDifficultyLevel(3);
}
//We then call the Populator to reset the Dynamic Objects in the world
//Reseting the zombies and the crates in the world
Populator.ResetDynamicObjects();
//We then add 1 to the current day
CurrentDay+=1; //Add a day to the counter
}
}

Step 5: Audio: Making Noise!

If you've played the game up to this point, you'll notice something. Or, more accurately, you won't notice anything, as there is no sound. Now, we need to add sound. Audio is made up of three components in Unity

  • An Audio listener, which acts as a pair of ears that the player listens through
  • Audio Clips are files that contain sounds. They are treated as assets like prefabs. All the audio in the game was pulled from Freesound.org, which provides free sounds for games.
  • Audio Sources which use Audio Clips to act as an emitter of sound. Audio Sources contain the commands to actually play the sounds.

The way the sounds is implemented in the game is a bit more complicated but I'll go through the two main methods.

  • Having an audio source on every object and playing clips from there. This makes sounds realistic as they became fainter and quieter as you move away from them.
  • Having an audio source on the player and getting that component, playing audio through that. While this makes sounds less realistic, it allows us to play audio when the game object has to be immediately destroyed.

First we'll start with the first method, having the audio source on the game object.

Step 6: Audio: Audiosource on the Game Object

The first method is to have the AudioSource emit sound from the game objects. Because of the way audio components are built, this lets us have realistic sound out of the box. An example of gameobjects with audio sources on them would be zombies.

  • Zombies snarling will be playing while they're still alive, so we don't have to worry about the audio being cut off
  • Because of that, we can reliably use the first method.
  • Look below at the zombie code. A majority of the code has been removed but I've left in the parts that matter to audio.

With the script below, all we would need to do is to attach an AudioSource component to the zombie game object, and then find audio clips and drag them in into the script in the editor (Picture 1).

This is the script for the zombie. For this section, I've cut out most of the script to only include the audio portions.

usingSystem.Collections;
usingSystem.Collections.Generic;
usingUnityEngine;
publicclassEnemy_Zombie : Entity_Parent {
//We create reference to the AudioSource that we can drag in because the zombies are a prefab
publicAudioSourceSource;
//This is the Audioclip of the zombie dying. Audio clips are made by downloading sounds from the internet and dragging them into the asset window
publicAudioClipDieClip;
//This is the clip for a zombie snarling. It plays periodically.
publicAudioClipSnarlClip;
//This is the time between the zombies snarling
publicfloatAudioInterval;
voidStart () {
StartCoroutine(PlayGrowl());
}
// Update is called once per frame
voidUpdate () {
CheckAttack();
if(current_Health_Level<=0)
{
//PlayOneShot is a command from the AudioSource Component which lets us play any audio clip we feed into the function
Source.PlayOneShot(DieClip);
Die();
}
}
//Here we define a coroutine that will use PlayOneShot to play the snarlclips every AudioInterval seconds with an addtional random range
IEnumeratorPlayGrowl(){
for(;;){
Source.PlayOneShot(SnarlClip);
yieldreturnnewWaitForSeconds(AudioInterval+Random.Range(0, 10.0f));
}
}
}
view rawEnemy_Zombie.cs hosted with ❤ by GitHub

Step 7: Audio: Audio Sources NOT on the Game Object

The second method is to have the game object find the audio source on the player and then use that audio source. This has the main advantage of not being tied down to a game object. Because Audio Components are deleted along with the rest of the GameObject, we can't use them. So, instead, we...

  • Get told to proceed with input
  • Use GameObject.find and find the Player GameObject so the
  • Use that source to Play(OneShot)

An example of this method would be food items. Because food items are destroyed immediately afterwards, we need to use an external audio source. We get the Audio Source on the player, and play the audio through that source.

However, the audio clip is still assigned to the food object in the editor, like before. (Picture 1) Every object uses these two methods. So in summary...

  • Every object that needs to make noise has a clip in their script
  • If the object makes sound without being destroyed, it will have an AudioSource on the object
  • If the objects makes sound and then is destroyed, it finds the AudioSource on the player and uses that

Now the audio is done, and for the final touch for the game, UI and Player Death!

Food items use the method I described above for audio because they're destroyed immediately as their clicked; we need to use on external audio source.

usingSystem.Collections;
usingSystem.Collections.Generic;
usingUnityEngine;
[System.Serializable]
publicclassFoodScript : ClickableParent {
publicAudioSourceSource;
publicAudioClipClip;
voidStart () {
//when the gameobject is created, we find the audiosource on the player so we can use that source
Source=GameObject.Find("Player").GetComponent<AudioSource>();
}
publicoverridevoidOnClick(Vector3Clickposition)
{
Debug.Log("Clicked "+Name+", Distance: "+Vector3.Distance(Clickposition, transform.position));
if(Vector3.Distance(Clickposition, transform.position) <=MMinimumDistance){
Master_Manager.current_Hunger_Level+=food_Value;
//Because we're calling Destroy right after playing the audio, we need to use an audiosource on on the gameobject
//Otherwise, the audio would be interrupted when its destroyed
Source.PlayOneShot(Clip);
Destroy(gameObject);
}
}
}
view rawFoodScript.cs hosted with ❤ by GitHub

Step 8: User Interface: Goals and Explanation

User Interface Goals

The game is fun, but now the issue is that the player doesn't have any information. The player can't see their health, the amount of ammo, etc. When we create our UI, we need to make sure to create...

  • Gameplay UI showing Health, ammo, and other stat
  • A main menu

Explanation of the UI

The UI system in Unity has its own separate "package" in the script, which means we'll have to use the "using" command to bring it in. The way it works in the editor is through a canvas system.

  1. The highest "layer" is the canvas object, which has the other individual UI elements inside it. By controlling the canvas, you can control things like the scale, visibility, etc of the UI elements inside it.
  2. The layer below the canvas object is where the individual UI elements reside. Elements like text, image, etc are here and can be controlled individually.

UI elements themselves can be treated like game objects, so the previous procedure to get game objects into scripts can be used for UI (Dragging them in using the editor, GetComponent<>)

In (Picture 1), MasterCanvas is the Canvas, which holds the EventSystem and another Canvas which itself holds the gameplay UI.

(Gif 1) is a demonstration of what the UI will look and act like.

(Picture 2) is what we'll be making in the next slide, making the gameplay UI.

Step 9: User Interface: Gameplay UI

Gameplay UI

The gameplay UI needs to include text that displays current health, water, hunger, and ammo. Because UI elements can be treated as game objects, we can create UI elements by right clicking on the hierarchy and going down to UI (Gif 1).

  • We create four text elements under a single canvas, making sure to name them so we can easily use them later. We adjust their positions, sizes and scaling so that they're the appropriate size.
  • We create the script above and attach it to our GameManager object. We drag in the text we just made and drag in the Player object into the script so the script has information.

Once the information and text has been dragged into the script, the text should start updating with the current numbers of Weapon_Parent and Player_Manager.

Now, we need to create our main menu.

This script goes on the canvas that holds the gameplay UI elements

usingSystem.Collections;
usingSystem.Collections.Generic;
usingUnityEngine;
usingUnityEngine.UI;
publicclassUIManager : MonoBehaviour {
publicTextHealthText;
publicTextWaterText;
publicTextHungerText;
publicTextAmmoText;
publicPlayer_ManagerPlayer_Stats_Source;
publicWeapon_ParentPlayer_Weapon_Stats_Source;
voidStart () {
}
// Update is called once per frame
voidUpdate () {
HealthText.text="Health:"+Player_Stats_Source.current_Health_Level.ToString();
WaterText.text="Water:"+Player_Stats_Source.current_Water_Level.ToString();
HungerText.text="Hunger:"+Player_Stats_Source.current_Hunger_Level.ToString();
AmmoText.text="Ammo: "+Player_Weapon_Stats_Source.currentAmmo.ToString();
}
}
view rawUIManager.cs hosted with ❤ by GitHub

Step 10: User Interface: Main Menu

Creating Main Menus

Unity game worlds are saved in the format of scenes. Scenes hold everything we've created so far (Player, populators, homestead, etc.) The first time saving in the editor, Unity would have prompted you to create one. Now there are two approaches to creating a main menu.

  • Create a separate portion in the scene we already have and just teleport the camera.
  • Create a new scene with the main menu.

While these two methods have advantages and disadvantages, the latter is useful in this case. In the first case, we would have to pause the populators and zombies from moving, and implement code. However, in the second method, we can use another package like UnityEngine.UI, UnityEngine.SceneManagement.

Buttons

Before we get into the Main Menu, we need to understand how buttons work. Buttons themselves don't have any code in them. Buttons, in the editor, can use any public function in any script. So, in this case, we create a script that contains two functions (Picture 1).

Creating the Scene

We create a new scene, and then make a cube game object identical to the ground in the game scene. We drag in some prefabs, and then position the camera. From there, we create UI elements like the Buttons, editing them until we like how they look. We make sure to go into the editor and select the game object with the script below and assign the appropriate function. At the end, it should look like (Picture 2).

The script for the Main Menu.

usingSystem.Collections;
usingSystem.Collections.Generic;
usingUnityEngine;
//This package contains the commands for loading different scenes
usingUnityEngine.SceneManagement;
publicclassMainMenuScript : MonoBehaviour
{
//This function starts the game by loading the scene with all our gameobjects
publicvoidStartGame(){
StartCoroutine(StartLoadingScene());
}
//This function quiets the game by calling Application.Quit()
publicvoidQuitGame(){
Application.Quit();
}
//This is a coroutine. While how it works is somewhat vague, what it does is pauses the game until the scene is done loading
//This makes performance in terms of computing much easier
IEnumeratorStartLoadingScene(){
AsyncOperationasyncLoad=SceneManager.LoadSceneAsync("TestScene");
while(!asyncLoad.isDone){
yieldreturnnull;
}
StopCoroutine(StartLoadingScene());
}
}

Step 11: Conclusion

Finished!

And that's it! We have accomplished every goal in the game. To sum up...

  • We created a top-down shooter using our weapon system and using recasting
  • We created a randomly generated map using our popular system
  • We create a day system where the player can sleep and increase difficulty level
  • And we created a day a hunger and thirst system where the player has different things happen to them once they get below a certain threshold

Credit

Here I just wanna go over some software and credit some other places for different things that were really useful making the game.

Unity - The game engine the entire game is built in. Without Unity, the game wouldn't even exist.

Visual Code - The tool used to write the code. The brains of the project.

Blender - A 3D Modeling program. I didn't cover it in the project as the 3D modeling would fall more under art and not programming, which is what the instructables is primarily aimed toward.

Dafont.com - The website source for the 1942 font used in the game.

Freesound.org - The website which all the audio in the game is sourced from. Awesome resource for any game developer or any content creator in general.