Introduction: Mountain Survival

A game about surviving on a mountain where you have to fend off zombies, hunger, and thirst. When I originally created the idea for the game, there were a couple of design goals I had to meet for the game to be complete, then I would polish.

  1. The game would be a top-down shooter
  2. The game would have a hunger and thirst system, where if they get below a certain level, things happen
  3. A day-system where the player would sleep and the difficulty would increase
  4. Randomly spawned objects in the world like zombies, food, and guns

This Instructables will be going over the development of those goals. This instructable also assumes you have basic programming knowledge.

The game can be downloaded and played from this step. Go to this link, download the file, and put it on your desktop. Make sure all the files are in a single folder. Then use the application file inside named Mountain Survival. Have fun!

https://drive.google.com/drive/folders/1u_QSDtH3XS...

IMPORTANT: Due to a bug in how the code snippets, all the spaces in the code has been deleted. To look at the code properly, click on the orange link above every snippet!

Step 1: Install Game Development Software

The software necessary to develop the Mountain Survival were...

  1. Unity Game Engine
  2. Visual Code

Both can be downloaded and installed from their respective websites....

  1. Unity:

https://unity3d.com/get-unity/download

2. Visual Studio Code

https://code.visualstudio.com/download

Step 2: Set Up Visual Studio Code

Before you can start developing using Unity and Visual Studio Code, you have configure VS Code to play nice with Unity.

Installing Extensions

Visual Studio Code uses an extension system to add additional functionality, in our case, Unity Support. On the left hand bar, click the bottom square. Search up and install the extensions...

  1. C# by Microsoft
  2. Debugger for Unity by Unity Technologies

Step 3: Set Up Unity Game Engine

To start using, we have to create a new 3D project (Gif 1).

  1. Click on New
  2. Name your project
  3. Make sure to your project is using the 3D Template
  4. Click Create New Project

You should come up on the main Unity Screen (Picture 2).

Step 4: An Explanation of GameObjects and Components

GameObjects

Every object in the Unity Game is considered a GameObject. Every GameObject has a position, rotation, and scale in the world. Additionally, every GameObject can have components added on to it. (Picture 1)

Components

Components are modifiers/extra data that can be attached to GameObjects to add on additional behaviours, like playing audio or even a custom 3D model (Picture 2). In the pictures case, the GameObject's components give it a 3D Model (Mesh Filter, Mesh Rendered) and gives it a collider to interact physically with other objects (Box Collider). For right now, you don't have to know what every component does, as this project only uses a handful of them.

The properties of every GameObject/component can be manipulated through code, allowing us to develop game systems.

You can create gameobjects, including a list of primitive gameobjects like cubes, by right clicking on the hierarchy to the left and click on the gameobject you need (Gif 3).

Step 5: Creating the Game Playspace

Before we get into any coding, we have to make the world the player will move around in. First, we create a cube gameobject, scaling it to 100 in the x and z-direction, giving us a ground (Gif 1). Then we give it a color by giving it a material with a brown color (Gif 2).

Step 6: Design Goal: Creating a Top-Down Shooter

Top-Down shooters are games where the camera is positioned above the character, following them around from a bird's eye view. Along with that, the character clicks on the screen where they want the character to shoot.

  1. First we create a cube and then add a rigidbody component and a charactercontroller component.
  2. From there we move the camera above the player, making sure to make it orthographic.
  3. Finally, we get into scripting our player.

Step 7: Creating a Top Down Shooter: Creating Player Movement

Now we get into scripting. We create a script in the asset manager (bottom panel) by right-clicking, and clicking on script, naming it Player_Movement. We create a charactercontroller component, player_manager component, and a vector3. When we create objects in C#, we are essentially creating a reference to a component in the game world. It lets us control the components we see in the editor through code. (Picture 1).

  1. Character controller provides different functions for player movement
  2. Player_Manager is a component providing different information on the player. We'll get into this later, but for right now, we'll be fine.

Because every object we create in the script is just a reference, the Player Gameobject must have those components attached so that the script has a component to reference. Because we made created those components in our script, you can see the editor the components you need (Picture 2). So, because you put a charactercontroller component controller on your Player gameobject, you can drag and drop it into the Player_Movement script (Gif 3).

And That's Player Movement Done! Now we need to make our PlayerManager Script to give Player_Movement a stat for current speed.

Step 8: Creating a Stat System: Variables in Player_Manager

The Script can be downloaded from instructables.

Now we need to make a new script for our stat system. First, we make a script named Player_Manager. This script will keep the stats of the player like hunger, thirst. The script will also update them over time, including a degradation over time.

The first thing we do is create three variables for each stat (hunger, water, health, speed) (Picture 1). These are the values at the start of the game, so to use them, we have to make new variables that are set to the beginning values, then other scripts can use them (Picture 2). The [HideInInspector] makes the variable invisible to the editor, so you can't change it in the editor.

Finally we create different variables for degradation. Make sure to check the picture to get more info (Picture 3).

Step 9: Creating a Stat System: Writing Code to Be Executed

Now that we have the variables created, now we have to write the code that will execute when the game is running.

Coroutines

Coroutines are an important tool that we use alot in making our game. To make a long story short, they let us run time dependant code. For more explanation, check the official unity documentation. https://docs.unity3d.com/Manual/Coroutines.html

In our case, we use our coroutine to run the code, then wait for a certain amount of seconds. The amount of seconds depends on what the variable, degrade_tick_rate (Picture 1).

void Start()

As I mentioned before, all the code in Start() is run at the start of the object. We set the current stat variables to the beginning variables. Along with that, we start the coroutine to start the stat degradation (Picture 2).

void LateUpdate()

In other scripts, you might have seen the function Update(). The difference between Update and LateUpdate is that LateUpdate is ran after Update. In our script, we check if the player health is or is less than 0, making them die if they meet that condition (Picture 3).

Camera

We also make sure to store the player's camera in the script so we manipulate its properties. We make sure to store different variables for its zoom property (Picture 4).

Event

A UnityEvent is a way for other scripts to execute code depending on what happens in this script. Check the official unity documentation for more details, but for this script, we're using it to let other scripts know that the player died. To use UnityEvents, we have to import UnityEngine.Events (Picture 5/6).

Once you have everything in the script, all you have to do is drag the script onto the player object in the hierarchy, adding the Player_Management script to the Player. From there, all you have to do is to set all the stat variables. Make sure to set the Player_Manager reference in the player_movement script you attached.

And with that, movement of the player is done! If everything is in order, you should be able to move the player around. Picture (7) what the script looks like in the editor.

Step 10: OOP: Object Orientated Programming in Our Game

Object Orientated Programming, in the most simplest terms, allows us to take code and represent it as "objects." Every one of these objects has code inside that can be called, they have their own data, functions. This in itself is really important and useful. An example would be a database with students.

Object Student{
 string Name;
 int Grade;
}

In OOP, every student can be represented by this object, so you don't have to write different code for every student. You could get their name, grade, or tell the object to show you their picture. All you would have to do is make a new Student object with the correct data for each student.

Inheritance

Inheritance is another important aspect that we make use of. Inheritance essentially allows one object to take data from a "parent" object. A classic example is a hierarchy of animal objects.

Object Animal{
speak();
walk();
}

Object Dog: Animal{
speak(){
	Say("BARK!")
	}
}

Object Bird: Animal{
speak(){
	Say("Squak!")
	}
}

Both Bird and Dog objects "inherit" from the Animal Object, meaning they they all share the lineage of the Animal Object. This is useful when we make the "parent" Animal Object force its children to implement certain functions. In this case, the speak() function is being forced to be implemented, meaning we can expect every child of Animal to have that function. Which means...

Polymorphism

When we know that a function is in every object, we can reliably call that function without any unforeseen consequences. Inherited objects also have a special property that we can make use of. Because they all inherit from a single object, we can make a list of the parent object while having the children of that object in the list. It's easier to see in a diagram so..

List ListofAnimals = new List(); A numbered list of Animal Objects

ListofAnimals.Add(new Bird()); We added a bird object, not a animal object

ListofAnimals.Add(new Dog()); We added a dog object, not an animal object

foreach(Animal x in ListofAnimals){   This is just a loop to go through every object in the list
	print(x.speak());
}

Alright that example is pretty loaded so I'll explain it here if you don't understand. First we create a list of the Animal Object. The special part is the fact that we can add a Bird or Dog Object to that list, even when they're not Animal Objects. We can add them because they inherited from the Animal Object. And because we know that every object that inherits from the Animal Object has a speak() function (each function doesnt have to specifically do the same thing, just have the same name), we can call it without knowing the specific type of object; all we know is that they inherit from animal and that they have a speak(). Think about it like your calling a person by their last name. Multiple people in that family have that name, and they all answer to that last name.This is really useful in game development because it allows us to develop complex behaviours without worrying about how its going to interact with every other potential objects. And this leads us to...

Step 11: OOP: Entity_Parent, Players, and Zombies

Using OOP, we make our Player_Manager and our Zombie script inherit from the same object to let us use a set of standard functions during gameplay. In this case, we make the zombie script and player_manager script inherit from...

Entity_Parent (Picture 1)

(Make sure to click on the picture to see more info!)

We make this script with abstract functions like Losehealth() and Die() to force inheritors of the script to implement these functions. This lets us, in gameplay, just check if a gameobject has a Entity_Parent type object. And because of Polymorphism, if the gameobject has a Player_Manager, or Zombie script, it'll let us run their LoseHealth() and Die() functions. But notice how the abstract functions don't have anything in them. Abstract forces inheritors to implement the classes, but lets them have free reign on what they do. In this case, we create a Entity_Parent parent class for any object meant to be hurt or damaged during gameplay to inherit from.

Player_Manager (Picture 2)

Notice how that at the top of Player_Manager class, instead of having Player_Manager : Monobehaviour, it instead has Player_Manager : Entity_Parent. This means PLayer_Manager has inherited from Entity_Manager, meaning it must implement LoseHealth() and Die(). (Picture 2) What code it has in those functions doesn't matter, all that matters is that the function exists and is ready to call.

Enemy_Zombie (Picture 3)

Notice how the code inside the functions are different between the inheritors. That's completely fine under OOP, and lets us make each object unique within a defined structure.

Step 12: Creating Enemies: Zombie's Movement

When I developed the game, zombies were the first real challenge in terms of programming. The challenge was to create an enemy that would follow the player around, do damage when they were close, and act sporadically.

First Attempt

Initially, the behaviour was to just find the player's position and move directly towards the position. Check the picture for more information (Picture 1), but for right now, all you need to know is that it didn't work because the zombies would converge into the same spot by the player, which isn't very sporadic.

Solution

The solution I came up with is to have the zombies move toward a random point near the player instead of moving towards the player directly. The behaviour would follow this list...

  1. Find the Player's position through the form of a Vector3, which is a data structure with three numbers representing x,y,z.
  2. Generate a random position around that Vector3.
  3. Move towards that position.
  4. Occasionally recalculate that position to keep the zombies sporadic.

Step 13: Creating Enemies: Creating Zombies's Movements

Finding the Player's Position

Remember how I when you create a gameObject, or rigidbody, or charactercontroller, or anything of that nature, you have to drag it in in the editor? Well that's not the only way to get the gameobject or component in your script. Instead, you can tell your script to find the gameobject in the game (Picture 1). In Start() we use GameObject.Find to get the gameobject in the game with the name "Player" and set the GameObject Player we created to it (Picture 2).

Generating a Random Position around the player

Now that we have the Player gameobject in our script, we can use it to generate a random position around the player. The way we generate the random position is by adding or subtracting a randomly generated number from the x component and the z component of the player vector3. We create a function that gives us (the term is returns) a Vector3. We make two float variables, which store decials, and set them to the Player's x and z position and add Random.Range, which returns a random number between negative DistanceOfEngagement and Positive DistanceofEngagement (Picture 3). DistancOfEngagement is a float variable we create to let us easily edit the distance the zombies are from the player (Picture 4). We then make a new Vector3 with its x and z components set to the new ones we just created, and we make the function give that Vector3.

And with that, we now have a randomly generated position around the player. Next we need movement.

Step 14: Creating Enemies: Moving the Zombie and Recalculating

Now that we have the random position around the player, we can start moving the zombies toward the player.

Recalculating the Player's Random Position

Before we move towards the player, we should recalculate the random position around the player. We do this by creating a Vector3 holding the position the zombie should move towards, and recalculating occasionally. First we define a Vector3 currentPlayerEnangeVector.

Occasionally Recalculating

The need to occasionally do something is a perfect for a coroutine, which lets us execute code based on time. We define a variable float TimeTillRecalc for how long the code should wait until it recalculates that random position. Now we define the coroutine Recalculate. (Picture 1). We loop the code forever using for(;;). We set PlayerEnangeVector to GetPlayerNext() which gives us a random position around the player. And then we tell the code to wait TimeTillRecalc number of seconds. And with that, our coroutine is done. Now all we have to do is start the coroutine in Start() to make the script recalculate the vector every TimeTillRecalc seconds (Picture 2).

Moving the Player

Now that we have the position that the zombie should be moving towards, now we actually need to tell the script to move towards the position (Picture 3). In LateUpdate() (which executes after Update()), we check if the script is closer to the player than a variable we define float LossDistance, which defines how close the player has to get before the zombies start chasing. If they are close enough, we set the script's position to Vector3.MoveTowards which moves the object towards a position using a variable float step, which is defined by the speed we set times time.deltaTime which makes the speed per second.

And with that, the zombie will start moving towards the player! (Gif 1)

Step 15: Creating Enemies: Attacking the Player

Attacking the Player

Attacking the player will fall under this behavior loop.

  1. Check if the player is in attack range
  2. If they are, attack every x seconds and do damage by making the player lose health

Checking if the player is in range

Because zombies can't exactly use guns, all they can do is hit with their arms, which is very close range. We define a variable float range which is the range of the zombie. We also define a variable float attackrate which is the number of seconds between every attack. Then we define a function CheckAttack() to hold all our code for attacking, which will be ran every frame.

CheckAttack()

We check if the player is within range with the player using Vector3.Distance which finds the distance between two vector3s. Then we add time.deltaTime to a variable to keep track of how many seconds pass since the zombie has come into range.

Attack rate

Once the zombie is in range, we add on time.deltaTime to a variable to keep track of how many seconds since the player has come into range. If that variable reaches or goes above the attack rate we attack the player and reset the variable. Because this function is called every frame, we do this cycle over and over, until the zombie leaves the range of the player and sets the variable to 0.

Using Entity_Parent and OOP to do damage

Remember the step with all the OOP and Entity_Parent? Well this is where it comes in handy. Instead of getting the Player_Manager class and subtracting health directly, all we do is get Entity_Parent, which Player_Manager inherits from, and calls the losehealth function to damage the player.

And with that, all we have to do is call CheckAttack() in Update() to get the zombie to start attacking.

Notice how once the zombie gets close, your health starts to decrease. (Gif 1)

While the whole zombie script is now done, we'll go over the rest in a later part involving the weapon system. For now, we'll move on to the Interaction System.

Step 16: Player Interaction: Goals

By this step, the only thing player's can do is move around and avoid zombies. But that's not very fun. To add on another layer of gameplay, we give the player the ability to right click on certain objects in the game to do something. In some cases to get more items, or replenish their water or hunger stats. The results should look like (Gif 1).

The goal is to

  • Let the Player Right Click objects to do stuff
  • Create objects for the player to right click

First we'll start with getting the player to right click.

Step 17: Player Interaction: Raycasting

What is Raycasting?

Raycasting in its simplest form is shooting a line from a point in space in a direction, and getting info from the gameobject it hits. This is really useful for stuff like shooting, getting information, or in our case, interacting with objects. Check the official unity tutorial for more information.

https://unity3d.com/learn/tutorials/topics/physics...

How do we use Raycasting?

We use raycasting through three main components.

  • Defining a Ray variable, which defines the ray itself (where it starts, which direction it goes)
  • Defining a Raycasthit variable, which holds information about where the raycast hits
  • using Physics.Raycast which uses the variables above
Ray exampleray = new Ray();  RaycastHit Ray_Hit; Physics.Raycast(exampleray, out Ray_Hit); 

Raycasting is really important and is used quite a bit in our project, so if you're confused, I encourage you to look at the Unity tutorial or read through the following steps carefully. But before we get into Player Interaction with right clicking, we need to go over how we use OOP in creating Player Interaction.

Step 18: Player Interaction: OOP and Reusing Code

Let's Pretend...

Let's pretend that OOP didn't exist. In that case, every time an object was clicked we'd have to write custom code to handle it. Every single clickable object would have their own code amounting to same thing; they would all handle the player clicking on them. This doesn't even include actual object behavior like replenishing stats or healing the player. Rewriting code to do the same thing is a waste of time, and that's why OOP exists.

How OOP makes Player Interaction a breeze!

Instead of writing code to handle how each and every object handles the player rightclicking on them, we'll just make a script to be a parent to all clickable objects.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public abstract class ClickableParent : MonoBehaviour {

	public abstract void OnClick(Vector3 ClickerPosition);

}

The code is so small that it isn't worth taking a picture. Notice how the class is abstract, along with the function. This means every class inheriting from ClickableParent must have an OnClick(Vector3 ClickPosition) function. If an object wants to be clicked on, they have to inherit from ClickableParent. Now, we can just use their OnClick() because we know they have to have one!

Step 19: Player Interaction: Letting the Player Right Click

To handle the player right clicking on the screen, we create this script that will be attached to the player object. This script needs two things...

  • It needs a Camera gameobject to handle the player clicking on the screen
  • It needs the Player gameobject for the Vector3 needed for OnClick.

We just create these and drag them in in the editor, so they aren't really a hassle.

For the case of Player Interaction, focus on ClickOn(). We create a function to check if the player has clicked on the right mouse button. If they have...

  • Create a ray using a built in Camera function, Camera.ScreenPointToRay, which takes the mouse position and gives a ray shooting out from the camera in that direction
  • Create a RaycastHit variable to hold information.
  • Use Physics.Raycast to shoot out the ray and fill our RaycastHit variable with information.
  • Now here's the fun part!

Every RaycastHit variable holds the gameobject the ray hit. And with that, we can get any component on using GetComponent. We create a ClickableParent in our script and set it to the ClickableParent on the gameobject in RaycastHit. Then we use OnClick() . Do we know what code that OnClick() holds? No! And that's the beauty of OOP. We then run ClickOn() in update() to check if they player has right clicked every frame.

But what if the object doesn't have a clickable parent? It's not an issue because the script will just move on and won't do anything (Gif 1). Notice how when I click on the ground an error pops up, but it doesn't stop the game.

How the game handles the player clicking on the screen

usingSystem.Collections;
usingSystem.Collections.Generic;
usingUnityEngine;
publicclassPlayer_Cursor : MonoBehaviour {
publicCameramain_Camera;
publicGameObjectPlayer;
// Update is called once per frame
voidStart(){
Cursor.visible=true; Bydefault, theplayercan'tseetheircursor. Sowejustmakeitvisible.
}
voidUpdate () {
ClickOn();
}
voidClickOn()
{
if (Input.GetMouseButtonUp(1))
{
Raycamera_Ray=main_Camera.ScreenPointToRay(Input.mousePosition);
RaycastHithit_Ray;
Physics.Raycast(camera_Ray, outhit_Ray);
ClickableParentC=hit_Ray.collider.gameObject.GetComponent();
C.OnClick(Player.transform.position);
}
}
publicstaticVector3GetPositionClick(CameraC) //returns the point where the raycast hit on the ground
{
Raycamera_Ray=C.ScreenPointToRay(Input.mousePosition);
RaycastHithit;
Physics.Raycast(camera_Ray, outhit);
return (hit.point);
}
view rawPlayer_Cursor.cs hosted with ❤ by GitHub

Step 20: Player Interaction: Creating Items

Now that we have have the ability to click on objects, we now need objects to click! Now we have to make...

Items that do stuff, that also inherit from ClickableParent

To make an item, all we have to do is make a script and make it inherit from ClickableParent to allow it to be clicked. The first example of this bottom step replenishes the player's food stat.

Notice how...

  • We create a Player_Manager and use GameObject.Find("Player").GetComponent() so we can manipulate the data inside.
  • We implement our OnClick() function with the same input data, but with it's own code inside. In this case, the code is using the Player_Manager script it created beforehand to replenish the player's hunger.
    • We check if the distance between the player and the item is smaller than the Minimum pickup distance of the item
    • We add onto the Player_Manager's hunger stat
    • We delete the object using Destroy(gameObject)

The next example at the bottom of this script replenishes the player's water stat.

Notice how...

  • The structure between this script and the previous script are almost identical, yet the code inside that structure is different
  • Both implement OnClick()

Essentially, the way Items work is this.

  1. Use a ray to detect which object the player is clicking.
  2. If that object inherits from ClickableParent, bring it in using GetComponent (Polymorphism). If not, just move on.
  3. Using that ClickableParent in our script, use OnClick() and use the behavior inside that object.

With that, all we have to once we're finished with the scripts is add them onto any object just like any other component (Gif 1).

And with that, our item system is done! This system allows easy addition of items because the only thing we'd have to do is to inherit from Clickable Parent and put in new behavior in OnClick().

This script is an item script that replenishes the player's food and then deletes itself.

usingSystem.Collections;
usingSystem.Collections.Generic;
usingUnityEngine;
[System.Serializable]
publicclassFoodScript : ClickableParent {
publicstringName;
publicfloatfood_Value;
publicfloatMMinimumDistance;
privatePlayer_ManagerMaster_Manager;
publicAudioSourceSource;
publicAudioClipClip;
voidStart () {
Master_Manager=GameObject.Find("Player").GetComponent<Player_Manager>();
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;
Source.PlayOneShot(Clip);
Destroy(gameObject);
}
}
}
view rawFoodScript.cs hosted with ❤ by GitHub

This script is similar to the food script. It replenishes the player's water stat and then it deletes itself.

usingSystem.Collections;
usingSystem.Collections.Generic;
usingUnityEngine;
publicclassWaterScript : ClickableParent {
publicstringName;
publicfloatwater_Value;
publicfloatMinimumDistance;
privatePlayer_ManagerMaster_Manager;
publicAudioClipClip;
publicAudioSourceSource;
voidStart () {
Master_Manager=GameObject.Find("Player").GetComponent<Player_Manager>();
Source=GameObject.Find("Player").GetComponent<AudioSource>();
}
publicoverridevoidOnClick(Vector3ClickPosition)
{
if(Vector3.Distance(ClickPosition, transform.position) <=MinimumDistance){
Master_Manager.current_Water_Level+=water_Value;
Source.PlayOneShot(Clip);
Destroy(gameObject);
}
}
}
view rawWaterScript.cs hosted with ❤ by GitHub

Step 21: Weapon System: Goals

As of this step, the game has items and zombies. Sure, the player will probably have loads of fun running around and picking up items, but we should let them go on the offensive. So how should the weapon system in the game work? My design goals were

  1. The player should be able to click on the ground and do damage in a line in that direction
  2. The player should have a limited amount of ammo so that they aren't able to just kill every zombie on screen
  3. The player should be able to pick up new weapons or ammo

The Weapon System should like look like (Gif 1). The player clicks on the ground to shoot at the zombies, and he picks up the weapons on the ground to replenish ammo.

First we'll start with goal 1, clicking on the ground and doing damage...

Step 22: Weapon Systems: Clicking the Ground to Do Damage

Our first design goal: When the player clicks on the ground do damage in a line in that direction. So, we have to...

Find where the player is clicking on the ground Get the direction from the player to that point Do damage to zombies in the line that's drawn from the player to the point 1. Finding where the player clicked on the ground Finding where the player clicked is relatively easy with the code we already created. The code we used earlier

Ray camera_Ray = main_Camera.ScreenPointToRay(Input.mousePosition); 
RaycastHit hit_Ray;            
Physics.Raycast(camera_Ray, out hit_Ray); 


This information on where the player is clicking on the ground is already in hit_Ray,we just need to access it and use it. The code we use to find the point on the groundwhere the player clicked is this.

public static Vector3 GetPositionClick(Camera C) //returns the point where the raycast hit on the ground { Ray camera_Ray = C.ScreenPointToRay(Input.mousePosition); RaycastHit hit; Physics.Raycast(camera_Ray, out hit); return (hit.point);

} We create a ray from the camera like before, create a RaycastHit variable, and use Physics.Raycast to fill the RaycastHit with information. But, unlike before, we're not getting a component from the RaycastHit, we're getting the point where it hit. We do by accessing RaycastHit.point, which is a Vector3. So this function takes in a camera, and gives you the point on the ground.

As a sidenote, notice how the function is static. This function isn't actually inside our weapon script, its in the previously shown Player_Cursor script. But static allows functions to be called without actually creating the object that holds that function. The reason we don't use statics all the time is because how statics don't need an object to be called, we lose the individuality of every object because the function is universal to every object.

2. Get the direction from the player to that point public Ray GetBulletDirection() //Uses Player_Cursor function and the transform.position of the player

Vector3 currentMouseRay = Player_Cursor.GetPositionClick(GameObject.Find("Main Camera").GetComponent());         
Ray Ray = new Ray(transform.position, currentMouseRay - transform.position);         return (Ray); 

Inside our Weapon_Parent script we make a function that gives a ray. This ray will be pointing in the direction of the point on the ground from the player.

We create a Vector3 that holds the point on the ground We then create a new ray that starts at the player's position (transform.position) and goes in the direction of the point (currentMouseRay - transform.position). This is vector math, and to be frank, I don't understand it, but I know the resulting Vector3 gives us the right direction. We then make the function return the Ray. 3. Doing Damage to Zombies in that Ray void

OnFire()    

{         

Ray BulletDirection = GetBulletDirection();         
RaycastHit info;         
Physics.Raycast(BulletDirection, out info);         
Source.PlayOneShot(Weapon_Data.FireSound);         
if (info.collider.gameObject.GetComponent() != null)             
	info.collider.gameObject.GetComponent().LoseHealth(Weapon_Data.Damage);
}

} First we get the ray pointing in the direction from the player to the point on the ground by setting Ray BulletDirection to GetBulletDirection() which gives us the Ray. We then do what we usually do to get information using Rays using Physics.Raycast. (For right now ignore the Source.PlayOneShot) We then check if the object inherits from Entity_Parent, which is the parent class for objects that can take damage ( if(info.collider.gameObject.GetComponent() != null)

If the object does have an object that inherits from Entity_Parent, we use LoseHealth() to inflict damage depending on the current weapon. With the code here, we can do damage to zombies. But this code won't work without two other things.

Step 23: Weapons Systems: Creating Different Weapons

Now that we have the code to do damage to zombies, we need to make weapons with different stats for the player to use. How do we create different weapons which have all the same type of stats but different stats...

Scriptable Objects!

You probably thought I was going to say an object, but in this Scriptable Objects are more useful. They allow us to easily contain data and edit it from the editor. The difference between these and classes is that they primarily containdata, which is perfect for us because all we need is stats like how much ammo a weapon has or the damage it does.

Notice how...

  • We inherit from ScriptableObject, which allows us to create this as an asset (Weapon_Template : ScriptableObject)
  • [CreateAssetMenu...] allows us to right click on the asset menu and create a new Weapon_Template Scriptable Object (Gif 1).
  • THERE IS NO CODE; JUST VARIABLES. The purpose of ScriptableObjects is to be containers of data that can be accessed by code.

Now that we have this scriptable object, we can just create new weapons by doing what we did in (Gif 1) and filling out the numbers. (Picture 1) is an example of weapon data fully filled out.

This is the template for all weapons. Every weapon in the game has these stats.

usingSystem.Collections;
usingSystem.Collections.Generic;
usingUnityEngine;
[CreateAssetMenu(fileName="Weapon_Template", menuName="Weapons/Template", order=1)]
publicclassWeapon_Template : ScriptableObject {
publicfloatDamage;
publicfloatRateOfFire;
publicKeyCodekey;
publicintStartingAmmo;
publicAudioClipFireSound;
publicAudioClipEmptySound;
}

Step 24: Weapons Systems: Using Different Weapons

To start using the weapon data we have in our Weapon_Template ScriptableObject, all we do is create a Weapon_Template object in our script and and set it to an existing Weapon_Template Object. It's similar to the way we attached other components like GameObjects or CharacterControllers (Gif 1). This let's use change weapon stats really easily since all we need to do is switch Weapon_Template objects.

Now turn your attention to the usage of the Weapon_Template object in Weapon_Parent script.

<p>info.collider.gameObject.GetComponent<entity_parent>().LoseHealth(Weapon_Data.Damage);</entity_parent></p>
<p>currentAmmo = Weapon_Data.StartingAmmo;</p>

We're using the Weapon_Template to inform Weapon_Parent on what kind of stats the current weapon should have. We do this because every weapon behaves the same (Using rays), so all they need is different data. They don't need different code. In this case, we use the integer StartingAmmo in the Weapon_Template to give the player a certain amount of ammo in the beginning, and we use it tell Weapon_Parent how much damage to do to the zombies.

Weapon_Parent.cs

usingSystem.Collections;
usingSystem.Collections.Generic;
usingUnityEngine;
[RequireComponent(typeof(AudioSource))]
publicclassWeapon_Parent : MonoBehaviour {
publicAudioSourceSource;
//Player weapon uses the Weapon_Template Scriptable object to get data for the weapon
publicWeapon_TemplateWeapon_Data;
publicboolFireEnabled;
[HideInInspector]
publicintcurrentAmmo;
voidStart () {
currentAmmo=Weapon_Data.StartingAmmo;
FireEnabled=true;
}
// Update is called once per frame
voidUpdate () {
if(Input.GetKeyUp(Weapon_Data.key) &&FireEnabled){
if(currentAmmo>0){
OnFire();
currentAmmo-=1;
}
if(currentAmmo<=0){
Source.PlayOneShot(Weapon_Data.EmptySound);
}
}
}
voidOnFire()
{
RayBulletDirection=GetBulletDirection();
RaycastHitinfo;
Physics.Raycast(BulletDirection, outinfo);
Debug.DrawRay(BulletDirection.origin, BulletDirection.direction*10, Color.blue, .5f);
Source.PlayOneShot(Weapon_Data.FireSound);
if (info.collider.gameObject.GetComponent<Entity_Parent>() !=null)
info.collider.gameObject.GetComponent<Entity_Parent>().LoseHealth(Weapon_Data.Damage);
}
///<summary>
/// Returns a ray extending from the current Player's position to the position the cursor is hovering over.
///summary>
///<returns>returns>
publicRayGetBulletDirection() //Uses Player_Cursor function and the transform.position of the player
{
Vector3currentMouseRay=Player_Cursor.GetPositionClick(GameObject.Find("Main Camera").GetComponent<Camera>());
RayRay=newRay(transform.position, currentMouseRay-transform.position);
return (Ray);
}
publicvoidPickUpWeapon(Weapon_TemplatenewWeaponData) //Changes the current weapon data
{
if(newWeaponData==Weapon_Data) //Adds ammo to current ammo if the same weapon
{
currentAmmo+=newWeaponData.StartingAmmo;
}
else//Changes ammo if a different weapon
{
Weapon_Data=newWeaponData;
currentAmmo=newWeaponData.StartingAmmo;
}
}
publicvoidChangeFireStatus(boolNewStatus){
FireEnabled=NewStatus;
}
}
view rawWeapon_Parent.cs hosted with ❤ by GitHub

Step 25: Weapon Systems: Ammo

The rest of the Weapon_Parent is pretty simple. We create the variable public int currentAmmo, which tells us how much ammo the player has left.

<p>if(Input.GetKeyUp(Weapon_Data.key) && FireEnabled){<br>            if(currentAmmo > 0){
                OnFire();
                currentAmmo -= 1;
            }
            if(currentAmmo <= 0){
                Source.PlayOneShot(Weapon_Data.EmptySound);
            }
        }</p>

We check first if the player has pressed the key to shoot (Weapon_Data.key) and if firing is enabled (FireEnabled).

  • Then we check if the current ammo is greater than. If they do then we'll shoot and subtract one.
  • If they don't have ammo, then play a sound.

And that's it to give the player the ability to defend themselves! But right now, the only way for players to get weapons (Weapon_Template data) is for us to drag it in from the asset menu...

Step 26: Weapon Systems: Weapon Pickups

Now that we have the system to change weapon data, now we need to make an in-game way to let the player change weapons.

Initally, I thought of an inventory system, where player's can change weapons through a menu. However, I believed that A. that would slow down the game too much. B. it would be confusing. So instead, I settled on..

A. The Player would right click on the weapons like any other item to equip them

B. If he already had that weapon, just pick up the ammo.

C. If he doesn't have that weapon, then set the Weapon Data in Weapon_Parent to the new weapon.


Creating Weapon Pickups

Creating weapons that can be clicked on and equipped is easy using the ClickableParent script we made earlier. We inherit from that script and implement OnClick(). We also make sure to create a Weapon_Template object in our script so we can tell it what kind of weapon to equip the player with when they pick it up with public (Weapon_Template thisWeaponData;). Other than that, it is similar to the other items we created earlier (The wonders of OOP!).

Using Weapon Pickups

If you look back at the Weapon_Parent script, you can notice a public function near the bottom named PickUpWeapon. Essentially, it takes in a Weapon_Template object containing data on a weapon, and changes the current weapon's stats to match. And that is what our Weapon_Pickup script does. When it is clicked...

  1. It finds the Player's object
  2. It brings in the Weapon_Parent script using GetComponent<> and calls that function, giving it the Weapon_Template that it has.
  3. From there, the Weapon_Parent function takes over. If it doesn't match the current Weapon_Template, it'll change the stats to match the new one. If it does match, It'll simply add on ammo.

And that's it! Now we have the weapon script. Now what we do is create a gameobject, and attach this script on to, making sure to give it a Weapon_Template so it has data to give (Gif 1).

And now we're done with weapons! After exploring how we implemented weapons, weapon data, and weapon pick ups, we're ready to head to our next destination, modifiers!

This is the script for weapons that are on the ground

usingSystem.Collections;
usingSystem.Collections.Generic;
usingUnityEngine;
publicclassWeapon_Pickup : ClickableParent {
publicWeapon_TemplatethisWeaponData;
publicfloatMinimumDistance;
publicoverridevoidOnClick(Vector3ClickPosition)
{
if(Vector3.Distance(transform.position, ClickPosition) <=MinimumDistance){
//Set the current player's weapon data to match the weapon on the ground
GameObject.Find("Player").GetComponent<Weapon_Parent>().PickUpWeapon(thisWeaponData);
Destroy(gameObject);
}
}
}
view rawWeapon_Pickup.cs hosted with ❤ by GitHub

Step 27: Modifiers: Goals

Our Stat System: What is it good for!

Remember when we created our stat system? As of this step, all it does is have two numbers representing hunger and thirst incrementally go down. But if you remember even further back, you'll remember that one of my overarching design goals were to have a stat system that does stuff when certain thresholds are met.

Goals

So what would have to be done to make this a reality?

  • Effects happening would be dependent on certain thresholds
  • These effects could affect other stats like speed
  • These effects, or modifiers, should be easily editable and addable

Initial Solutions

Initially I believed I could hold all this information in one script (Code Snippet 1). However this presents issues, mainly in readability and file size. Ideally, code should be easily readable and should be as small as possible. Functionality of code should be split up into different pieces (OOP). And having every modifier in a single script isn't very readable, nor tidy.

Best Solution

The solution I came to was to have modifiers as their own objects, with a modifier manager to oversee their execution. Essentially...

The Modifier Manager would contain a copy of every modifier, and would handle using those modifiers. First we'll start with the modifiers themselves.

1. This was the old implementation of the modifier system

publicvoidCheckStatsForDebuffs() //
{
Sluggishness();
ShortSightedness();
}
//Debuffs and Buffs
privatevoidSluggishness() //Every modifier was a function in the Player_Manager Script
{
if (current_Hunger_Level<90) //Inside every function they had their threshold check, in this case checking if
{ //if hunger is below 90
current_Speed=Max_Speed*.5f;
}
else
{
current_Speed=Max_Speed;
}
}
privatevoidShortSightedness() //This is another modifier
{
if (current_Water_Level<90)
{
Player_Camera.orthographicSize=2.5f;
}
else
{
Player_Camera.orthographicSize=5;
}
}
}

Step 28: Modifiers: Modifier Objects

Modifier Template

Every modifier essentially takes in a Player_Manager, the script that holds the player's stats, and changes the numbers in that script depending on if a condition is met. However, the modifier itself doesn't run its own code. That's up to the upcoming Modifier_Manager to do. All Modifiers themselves hold is code to be executed.

Every modifier that we'll be making will inherit from Modifier_Template. Modifier_Template is made up of 4 primary components

  1. public abstract float Attribute: This is a float number that every modifier must have. This determines by what number amount the modifier should change.
  2. public abstract void AttributeChange(Player_Manager Player): This is where the numbers of the Player_Manager are actually changed.
  3. public abstract void EndChange(Player_Manager Player): Some modfiers, like the one below, won't automatically stop even after they should stop. Without this function, the player would still move slow because there isn't anything to tell the script to set it back. So this function resets Player_Manager's stats back to normal.
  4. public abstract bool ConditionMet(Player_Manager Player): This is where when the modifier should run is determined. The function takes in a Player_Manager as usual, and then checks a number inside that Player_Manager. If they number meets a certain condition, say being below a certain number, then the function will say the modifier should run.

Check below for more information! Attached to this step are the files, so I encourage you to open them u in Visual Code and explore them!

Before we can start running these modifiers, we have to create our modifier manager....

This is the parent class of all modifiers. Every modifier we make will inherit from this class. The second file is an example of a completed modifier.

usingSystem.Collections;
usingSystem.Collections.Generic;
usingUnityEngine;
//Notice how this class doesnn't inherit from Monobehaviour. This means we can't add this to gameobjects
publicabstractclassModifier_Template
{
//By making all of these abstract, we force every modifier to have these functions
publicabstractfloatAttribute { get; set; } //This is a property. We're essentially force every child to have a float number variabled named Attribute
//This attribute property usage depends on the modifier, but generally, it tells the modifier by how much to change a stat in Player_Manager
publicabstractvoidAttributeChange(Player_ManagerPlayer); //Attribute Change is the function that changes a stat in Player_Manager;
publicabstractvoidEndChange(Player_ManagerPlayer); //EndChange is the function that resets the Player's stats once the modifier is over
publicabstractboolConditionMet(Player_ManagerPlayer); //A bool is a data type with only true false. In this case, this function returns whether the
//Should be run or not depending on the hunger/water
}
usingSystem.Collections;
usingSystem.Collections.Generic;
usingUnityEngine;
publicclassSluggishness : Modifier_Template//We inherit from the Template above using the colon
{
publicSluggishness(floatValue){ //This something new! Here it what is known as a constructor. A constructor can be thought
Attribute=Value; //of as a sort of function that is called when a new object is created.
} //In this case, we're setting the Attribute property below to the number we pass in when we create the object
privatefloatattribute; //Here we define a private float variable which holds our attribute number.
publicoverride float Attribute //Because Attribute is abstract, we implement using the override keyword. In this case, we
{ //we define it as a standard property
get
{
returnattribute;
}
set
{
attribute=value;
}
}
publicoverridevoidAttributeChange(Player_ManagerPlayer) //Attribute change is called by the modifier manager to actually
{ //Change the stat in Player_Manager. By having it in its own
Player.current_Speed=Player.Max_Speed*attribute; //Function, we can easily change what the modifier does without
} //changing the rest of the modifier
publicoverridevoidEndChange(Player_ManagerPlayer) //For some modifiers like this, if we didn't have a function to revert
{ //The Player_Manager stat, the stat would stay the same even if it the
Player.current_Speed=Player.Max_Speed; //Modifier stopped working. In this case, it reverts the player's speed
} //Back to their normal speed.
publicoverrideboolConditionMet(Player_ManagerPlayer) //This function determines whether the modifier should be run. It takes in
{ //The Player_Manager and checks a certain stat, returning (True) if
if(Player.current_Hunger_Level<60) //the modifier should be ran or (false) if it shouldn't.
{
return (true);
}
else
{
return (false);
}
}
}
view rawSluggishness.cs hosted with ❤ by GitHub

Step 29: Modifiers: Modifier Manager

The Modifier Manager

The Modifier Manager oversees the execution of the modifiers. It does by...

  1. It keeps a list of modifiers inside the script by creating the every modifier object and adding it to a list
  2. Every couple of seconds, we go through the list of modifiers
    1. We use each modifier's code to check if it meets the condition
      1. If it does, we use that modifier's AttributeChange() to change the Player_Manager's numbers
      2. If it doesn't, we use that modifier's EndChange() to reset the Player_Manager's numbers.
    2. We then loop back

The Modifier Manager goes through this loop periodically. The Modifier Manager is useful because it doesn't require the modifiers themselves to be in the code, unlike the previous solution. This keeps the code tidy, and the modifiers tidy as well. Instead of having it all in the script, we split it up among different files (Picture 1).

And with the Modifier Manager done, the modifiers should be working! Notice in (Gif 1) how when our hunger goes below 50, our health begins to decrease. That's the modifiers working!

The Modifer Manager is the script that takes every modifier and runs the four components inside each modifier.

usingSystem.Collections;
usingSystem.Collections.Generic;
usingUnityEngine;
publicclassModifier_Manager : MonoBehaviour
{
publicPlayer_ManagerPlayer; //Every modifier needs a Player_Mangager, so we create one here instead of getting it for each modifier
publicList<Modifier_Template>Debuffs=newList<Modifier_Template>(); //We create a list of modifiers
//Which holds the list of modifier we can go through
//and execute their code.
publicfloatTickrate; //The modifiers are run every number of seconds, so we create this variable to allow greater control
voidStart()
{
///We add every modifier that will be run to the previously created list. They're created with the new keyword, making sure to put in a float variable in the constrcutor
Debuffs.Add(newSluggishness(0.5f));
Debuffs.Add(newShortSightedness(2.5f));
Debuffs.Add(newStarvation(5));
Debuffs.Add(newRichness(5.0f));
Player=GetComponent<Player_Manager>();
StartCoroutine(UpdateModifiers());
}
//Using a coroutine, we run through every modifier every Tickrate number of seconds, running the code inside each component.
IEnumeratorUpdateModifiers() //TODO:Get the system to restore regular stats after a certain amount of time
{
for (; ;) //This loops infinitely
{
foreach (Modifier_TemplatexinDebuffs) //We go through every Modifier_Template object in the list, naming it x in the loop
{ //This lets us go through a list, executing code
if (x.ConditionMet(Player)) //We check if the condition for the modifier is met
{
x.AttributeChange(Player); //If it, we execute the code to affect the Player_Manager.
}
if(!x.ConditionMet(Player)) //If the condition for the modifier isn't met
{
x.EndChange(Player); //We end the change
}
}
yieldreturnnewWaitForSeconds(Tickrate); //We then wait Tickrate seconds
}
}
}

Step 30: Creating Crates: Goals

Crates: Getting Items to Players

After creating the weapon pickup system, I ran into a problem. I had the weapon pickup system working, but I needed a way to get the items to randomly spawn on the ground. Spawning them directly on the ground would be fine, but it'd be pretty boring. I came to the solution of crates! Crates explode into several different weapons after being clicked on, giving the player a source of new weapons. It should look like (Gif 1).

Crates: Final Design

I settled on this design for crates...

  • They have a list of objects inside that they can spawn
  • They spawn a random number of random objects from that list
  • They spawn randomly around the map

So let's get to our first design goal for crates, having a list of objects!

Step 31: Creating Crates: an Explanation of Prefabs

Prefabs, short for Prefabrication

Before we get into how crates work, we need to go over how prefabs work. Before this step, all we've been doing is creating game objects and dragging our scripts onto them. But let's say we needed to copy that gameobject with its components. No big deal, just find it and copy it. But what if we have to do it 100 times? 200? That's where prefabs come on.

Prefabs let us save gameobjects as assets in our asset menu after we add our components. An example can be seen in (GIf 1).

  • We create a cube gameobject
  • Add on our food script
  • Then fill in the information
  • We then drag it into the asset menu at the bottom, making it a prefab.

Now that this object is a prefab, we can use it in scripts to spawn copies of it.

HOWEVER! Code used inside prefabs have to follow an important rule. That rule is that any components the scripts inside the prefab accesses have to be brought in during gameplay. That can't be brought in using the editior.

Player_Manager Player;

void Start(){
   print(Player.current_health);
}

This won't work inside a prefab because the Player_Manager's reference has to be dragged in through the editior.

Player_Manager Player;<


void Start(){       

GameObject.Find("Player").GetComponent<Player>();

print(Player.current_health);
}


This will work because we're getting the Player_Manager component in Start() as opposed to getting it through dragging it in the editor. Now that we have

Step 32: Creating Crates: How Crates Themselves Work

We use ClickableParent to easily let implement player clicking on the crate. This makes implement OnClick(), which in this case...

1. We run through a loop a certain number of times defined by us (public int NumberOfItemstoSpawn)

2. We select a random item from a list of items (public List ItemsInside = new List()). This list can contain any prefab that we'd want to spawn in a crate.

2a. We copy and paste that item at the crates position (Instantiate())

2b. We set a new gameobject to equal that previously created object (GameObject x = Instantiate...)

2c. We add a tiny explosion force to add more flare. (x.GetComponent().AddExplosionForce(10, transform.position, 5);)

2d. We go back to the start of the loop

3. Once we're done with the loop, we destroy the crate

With the code below, the crate in the editor will look like (Picture 1).

NumberofItemstoSpawn and MinimumDistance are pretty self-explanatory, but what is size for ItemsInside?

Size is how many different prefabs the crate can spawn. Notice in (Gif 1) how we change it to 2 and drag in some prefabs. But then we change it to 3, drag in another, and then change it back to 2. Using our list system, and randomly selecting from that list, we can easily add or subtract items to the crates. And that's how crates work! At the end we drag in the crate once we're done to make the crate a prefab so it can be used in this upcoming part, Populators!

This is the code that handles Crates. Notice how it inherits from ClickableParent, giving it the ability to be clicked on.

usingSystem.Collections;
usingSystem.Collections.Generic;
usingUnityEngine;
publicclassCrate : ClickableParent
{
publicList<GameObject>ItemsInside=newList<GameObject>();
publicintNumberOfItemsToSpawn;
publicfloatMinimumDistance;
publicAudioClipClip;
publicAudioSourceSource;
voidStart(){
Source=GameObject.Find("Player").GetComponent<AudioSource>();
}
publicoverridevoidOnClick(Vector3ClickPosition)
{
if(Vector3.Distance(ClickPosition, transform.position) <=MinimumDistance){
for (inti=0; i<NumberOfItemsToSpawn; i++)
{
//Pick a random object from ItemsInside list and spawn it, adding a tiny explosion force to make it satisfying
GameObjectx=Instantiate(ItemsInside[Random.Range(0, ItemsInside.Count)], transform.position, Quaternion.identity) asGameObject;
x.GetComponent<Rigidbody>().AddExplosionForce(10, transform.position, 5);
}
Source.PlayOneShot(Clip);
Destroy(gameObject);
}
}
}
view rawCrate.cs hosted with ❤ by GitHub

Step 33: Populators: Goals

As of this step, two of our four design goals have been met! So far...

  • We've created a top down shooter with our weapon system and our player movement we created earlier
  • We've created a stat system that affects gameplay using our modifiers, food items, and stat system we created earlier.

But now we're moving on to our third design goal, randomly spawned items. We have our items to spawn (food items, crates, weapons), but we need code to actually spawn them. We learned how to spawn items using prefabs and instantiate() in our earlier. So we need to create some goals for the populator system.

  • Flexible, and easily expandable, so more items could be created and added on
  • Randomly spawns items across the Gameworld
  • Different types of spawning for different types of items
  • Easy to use

Step 34: Populator: Solutions

Initial Solutions

Intially I thought I could use the standard OOP approach. Every populator would have the same code and then they would all be put into a list under a populator_parent class and a populator manager class would handle that. But in this case, OOP is more of a hassle.

OOP is not and should be treated as a one size fits all solution. When objectsare all similaror all havesimilar functionality, thenthey can all inherit from a single parent and useOOP. But if every object is special, and specialized, there isn't a need to use OOP. And in this case, every populator is specialized, so there's no need for OOP.

Finished Solution

Finally, I settled on creating different populators for the different types of objects.

  1. A populator for environmental objects (grass, rocks)
  2. A popuator for food and water items
  3. A populator for crates
  4. A populator for zombies
  5. A populator manager which would handle using the populators

Due to the amount of populators, every step will be a quick run-down on how the populator works along with a code snippet with deeper explanation.

Step 35: Populator: Environmental Populator

First is our Environment populator, which handles spawning the grass and other enviornmental objects in our game. It works by using a randomly generated black and white noise map to tell the populator where to put objects.If you look closely at (Picture 1) and (Picture 2), you can see that the white of Picture 2 matches with the grass in Picture 1.

Essentially the script...

  • Goes through every pixel a 100x100 randomly generated noise map such as the one above
  • It checks if it is above a certain darkness.
  • If it is, spawn an environment object from our list of environmental objects there using the coordinates of the picture will correspond with coordinates in the game
  • Keep track of how densely the objects are spawned, using a variable to only spawn objects every certain number of pixels
  • Make sure to add every object to a list so we can manipulate them later

(Gif 1) is how we attach the populator to the GameManager Object. We create an empty gameobject to act as a GameManager which holds scripts that control the game as a whole, such as UI, Game Overs, etc. Then we drag our populator script on to it, filling out the information, making sure specify how many environmental game objects there are to spawn, inserting the prefabs, and filling out the rest of the information.

(Picture 3) is how the populator script is setup in our actual game.

Environment_Populator handles the placing of grass among other enviornmental objects.

usingSystem.Collections;
usingSystem.Collections.Generic;
usingUnityEngine;
publicclassEnvironment_Populator : MonoBehaviour {
publicList<GameObject>EnvironmentSpawnables=newList<GameObject>();
publicTexture2DSpawnMap;
publicintSpread;
publicfloatScale;
publicList<GameObject>GameObjectsSpawned=newList<GameObject>();
publicList<Vector3>SpawnVectors()
{
intz=0; ;
List<Vector3>returnList=newList<Vector3>();
for (intx=0; x<SpawnMap.width; x++)
{
for (inty=0; y<SpawnMap.height; y++)
{
z++;
if (SpawnMap.GetPixel(x, y).grayscale>=0.6&&SpawnMap.GetPixel(x,y).grayscale<=1)
{
if(z>=Spread)
{
returnList.Add(newVector3(x/Scale, 0, y/Scale));
z=0;
}
}
elseif (SpawnMap.GetPixel(x, y).grayscale>0.3&&SpawnMap.GetPixel(x, y).grayscale<0.6)
{
if(Random.Range(0,1) ==1)
{
if(z>=Spread)
{
returnList.Add(newVector3(x/Scale, 0, y/Scale));
z=0;
}
}
}
}
}
return (returnList);
}
publicvoidPopulate_World(List<Vector3>SpawnVectors)
{
foreach (Vector3xinSpawnVectors)
{
if(Random.Range(0,1f) > .01) //Makes Berry bushes more rare than just shrubs TODO: Implement an easier way to determine random chances for the plants
{
GameObjectz=Instantiate(EnvironmentSpawnables[0], x, Quaternion.Euler(0, Random.Range(0, 360.0f), 0)) asGameObject;
GameObjectsSpawned.Add(z);
}
else
{
GameObjectz=Instantiate(EnvironmentSpawnables[Random.Range(1, EnvironmentSpawnables.Count)], x, Quaternion.Euler(0, Random.Range(0, 360.0f), 0)) asGameObject;
GameObjectsSpawned.Add(z);
}
}
}
publicvoidDestroyGameObjectsSpawned(){
foreach (GameObjectxinGameObjectsSpawned){
DestroyImmediate(x);
}
}
}

Step 36: Populator: Detail Populator

Second is our Detail populator. The purpose of the detail populator is to place objects that are placed in a even, random distribution around the map instead of using a noise map. Instead, we just randomly generate positions around the map and use those as spawn positions. It should look like (Picture 1) Essentially...

  • We have two numbers representing the upper bounds of the range
  • We generate random numbers between 0 and those upperbounds, creating a vector3 position using those numbers as the x and z coordinates
  • We add those to a list and do this several times
  • We go through the list and spawn a randomly selected object from our list at those positions

Similar to before, we just drag it on to the GameManager empty object, so it's a bit redundant to show you again. However, (Picture 2) is how the Detail_Populator looks in our game.

This populator places object that should be placed in a uniform, random distribution, unlike the Environment populator which uses a noise map.

usingSystem.Collections;
usingSystem.Collections.Generic;
usingUnityEngine;
publicclassDetail_Populator : MonoBehaviour
{
//These two variables control how far the objects could potentially be placed.
//Ex. If these two variables were set to 20, the the farthest an object could be placed is at (20,0,20)
publicfloatEndRangeX;
publicfloatEndRangeY;
//Because this populator doesn't use a noise map to spawn objects, we need to specify how many objects the populator has to spawn
publicintNumberofObjectsToSpawn;
//This is the list of prefab/gameobjects the populator could spawn
publicList<GameObject>spawnGameObjects=newList<GameObject>();
//We make a list to manipulate the gameobjects we have spawned. We make sure to hide it in the editor because there's no point in showing it
[HideInInspector]
publicList<GameObject>spawnedGameObjects=newList<GameObject>();
//Similar to the environment populator, we define a function that gives us a list of Vector3s positions
publicList<Vector3>GetSpawnVectors(){
//we make a list to add on Vector3s for
List<Vector3>NewList=newList<Vector3>();
//We make a loop that will iterate depending on how many objects we want to spawn
for(inti=0; i<NumberofObjectsToSpawn; i++){
//We create a new vector with the x and z components having a random number between 0 and the preivously specified upper bounds
//We then add that Vector3 to the list
NewList.Add(newVector3(Random.Range(0, EndRangeX), 0, Random.Range(0, EndRangeY)));
}
//We make the function give that list of Vector3
return (NewList);
}
//This is the function that actually spawns our gameobjects
publicvoidSpawn_Objects(){
//We create a vector3 list and set it to GetSpawnVectors() which gives us the random positions
List<Vector3>SpawnVectors=GetSpawnVectors();
//We go through that list, setting the Vector3 we're on as x
foreach (Vector3xinSpawnVectors){
//Similar to before, we create a gameobject and set it to a copied randomly selected object from that list, and then add it to a list.
GameObjecty=Instantiate(spawnGameObjects[Random.Range(0, spawnGameObjects.Count)], x, Quaternion.Euler(0, Random.Range(0,360), 0)) asGameObject;
spawnedGameObjects.Add(y);
}
}
publicvoidDestroy_Objects(){
foreach(GameObjectxinspawnedGameObjects){
DestroyImmediate(x);
}
}
}

Step 37: Populator: Crate Populator

Third is our crate populator. The purpose of the crate populator is to spawn crates around the map using the spawn map. The way the crate populator works is sort of a combination between the Environment Populator and the Detail Populator. It uses the noise map, while also specifying how many objects to spawn. The end result should like (Picture 1). Essentially, what we're doing...

  • We generate a random position using the width and height of a noisemap as our upperbounds
  • We check if the corresponding pixel on the noise map is white
  • If it is white, add that positon to a list. Otherwise, keep going until we get a position that is in white
  • Keep doing this until a certain number of positions are added to the list
  • Go through that list of positions and spawn crates at those positions

(Picture 2)is what our script looks like after being attached to our GameManager object.

The Crate Populator handles placing crates around the map.

usingSystem.Collections;
usingSystem.Collections.Generic;
usingUnityEngine;
publicclassCrate_Populator : MonoBehaviour
{
publicList<GameObject>CrateSpawnables=newList<GameObject>();
publicintCratesToSpawn;
publicfloatScale;
publicTexture2DSpawnMap;
publicList<GameObject>SpawnedGameObjects=newList<GameObject>();
publicvoidCrate_Populate()
{
List<Vector3>CrateSpawnVectors=SpawnVectors();
foreach (Vector3xinCrateSpawnVectors)
{
GameObjectz=Instantiate(CrateSpawnables[Random.Range(0, CrateSpawnables.Count)], x, Quaternion.Euler(0, Random.Range(0, 360), 0)) asGameObject;
SpawnedGameObjects.Add(z);
}
}
publicList<Vector3>SpawnVectors()
{
List<Vector3>returnList=newList<Vector3>();
for (inti=0; i<CratesToSpawn;)
{
intx=Random.Range(0, SpawnMap.width);
inty=Random.Range(0, SpawnMap.height);
if(SpawnMap.GetPixel(x,y).grayscale<0.1f)
{
returnList.Add(newVector3(x/Scale, 0, y/Scale));
i++;
}
}
return (returnList);
}
publicvoidDestroyAllObjects(){
foreach (GameObjectxinSpawnedGameObjects){
DestroyImmediate(x);
}
}
}

Step 38: Populator: Zombies

Our fourth, and probably most complicated populator, is the zombie populator. Our zombie populator handles placing the player's enemies. However, unlike the rest of the populators, the objects this populator is placing have to be like zombies. So, this populator places the zombies in packs, like in (Picture 1). Essentially...

  • Generate a random position inside a square defined by two positions representing the lower and upper bound. Do this several times depending on a variable in our script, keeping these positions in a list.

  • Go through that list using each entry to generate random positions around those entries. Add these random positions to a list, which we'll be using.

  • We use the list in the step above and spawn zombie gameobjects at every one of those positions. We make sure to add all these objects to a list so we can manipulate them later.

Like the rest of the populators, we just add this to the GameManager empty object. We also make sure to drag in our zombie prefab. The populator looks like (Picture 2) in our game.


Due to an error in instructables, I can't make this one any bigger. Please follow the link to see the the rest!

https://www.instructables.com/id/Continued-Mountain-Survival

This populator places the zombies in the game world.

usingSystem.Collections;
usingSystem.Collections.Generic;
usingUnityEngine;
publicclassZombie_Populator : MonoBehaviour {
//This determines how many packs of zombies will be spawned IE; how many clusters around the map, not how many zombie gameobjects
publicintPackNumber;
//This determines how many zombie gameobjects are in each pack of zombies
publicintPackSize;
//The number of zombie gameobjects in each pack can be given a random variance to give variety
publicintPackSizeVariance;
//When the random positions for the packs are generated, this number determines how far a zombie could be spawned from the pack center
publicfloatPackSizeDistance;
//This is the lower bound of the spawn area for zombies. If the spawn area is a square, this can be thought as the position of the bottom left corner
publicVector3StartVector;
//This is the lower bound of the spawn area for zombies. If the spawn area is a square, this can be thought as the position of the upper right corner
publicVector3End;
//Because there's only one zombie gameobject, we don't need to make a list of gameobjects. We just need to make a gameobject for us to drag in our zombie prefab.
publicGameObjectZombieGameObject;
//This is the list of the objects we have spawned already
privateList<GameObject>SpawnedGameObjects=newList<GameObject>();
//We define a function that gives us a list of random positions in the spawn area defined by the upper/lower bounds. Every position represents the center of a zombie pack.
publicList<Vector3>GetSpawnVectors(){
List<Vector3>returnList=newList<Vector3>();
//We create a loop with the number of packs so we can generate that number of pack centers
for (inti=0; i<PackNumber; i++)
{
//We generate a random number between the x and z components of the upper/lower bounds, effectively giving us the x and z components of a random position in those bounds
floatx=Random.Range(StartVector.x, End.x);
floatz=Random.Range(StartVector.z, End.z);
//We then use those numbers to create a new Vector3 and add it to our list which we return below
returnList.Add(newVector3(x, 0, z));
}
return (returnList);
}
//We define a function that gives us the positions of the individual zombie gameobjects in the packs. This is the function we'll be using
//to actually spawn our gameobjects
//It takes in a Vector3 position and gives a list of randomly generated positions around that position
publicList<Vector3>GeneratePack(Vector3Location){
List<Vector3>PackSpawns=newList<Vector3>();
//We create a loop with the number of zombies in a pack + an additional random amount between 0 and PackSizeVariance
for (inti=0; i<PackSize+Random.Range(0, PackSizeVariance); i++)
{
//We create a new Vector3 that has the x and z components of the inputed Vector3 Location modified by adding on a
//random number between -PacksizeDistance and Packsize distance. This let the zombies spawn around the location in a circle
//We then add it to the list we return below
PackSpawns.Add(newVector3(Location.x+Random.Range(-PackSizeDistance, PackSizeDistance), 0, Location.z+Random.Range(-PackSizeDistance, PackSizeDistance)));
}
return (PackSpawns);
}
//This function actually places the zombie gameobjects in the gameworld. The function takes in a list of positions to generate packs around.
publicvoidPopulateWorld(List<Vector3>SpawnVectors){
//We go through the list the function takes in, calling the entry we're on x
foreach (Vector3xinSpawnVectors)
{
//We then go through the list generated by GeneratePack() when we pass in Vector3 x.
//We are essentially going through the list above, generating the random positions around each entry,
//And then spawning the game objects
foreach (Vector3yinGeneratePack(x))
{
GameObjectz=Instantiate(ZombieGameObject, y, Quaternion.identity) asGameObject;
SpawnedGameObjects.Add(z);
}
}
}
publicvoidDestroyAllSpawnedObjects(){
foreach (GameObjectxinSpawnedGameObjects){
DestroyImmediate(x);
}}
}