Ever wanted to create a game like Subnautica in Unity? Check out Part 1 of our guide here, where we go through how to set up a player controller and some player stats.
A link to a package containing the project files of this tutorial series can also be found at the end of this article, exclusive to Patreon subscribers only.
1. Setting Up
First of all, we should set up the project scene.
a. Import all assets
In this project, we will be using various different free assets. For today, we will be using the following assets.
Create a folder called Imported Assets in your Assets folder to hold all your imported assets. Put the AQUAS lite resources there. Then, you can create a folder there called Models and put the Mixamo model there.
b. Set up the terrain
In a later episode, we will be learning how to set up your own custom terrain for your game. So, for now, we will be using the default terrain from the demo scene of the AQUAS lite assets. You can copy the terrain game object and the water game object to the sample scene of unity, and work there.
c. Create the Player Character
Now to create our player GameObject. there are a few things we need to do.
- Create a capsule GameObject. Name it “Player”.
- Add a Rigidbody component to it.
- Make the camera a child of the Player GameObject.
- Drag the Mixamo model into the scene and make it a child of the Player as well.
- Position the Mixamo model so that the model stands neatly at the bottom of the capsule.
- Adjust the position of the camera, and adjust the camera properties as you see fit.
- Delete the Mesh Filter and Mesh Renderer on the player.
2. Player Movement — Looking Around
Now that we have the terrain and a player GameObject, we will be setting up the player controls. To start, we will make the player be able to look around.
a. Create a script
Create a new folder called Scripts, and in there create a folder called Player. Then, we can create the script PlayerController
[Assets/Scripts/Player]. We will start by creating a function that allows the player to look around. Here are the steps.
- Cache the Transform for better performance — in future episodes, we will be doing a lot of optimisation!
- Create two variables to store the input from the mouse movement
- Create the function
LookAround()
to allow the player to look around - Lock the cursor to the center of the screen
- Add a debug function to unlock the cursor
PlayerController.cs
using System.Collections; using System.Collections.Generic; using UnityEngine; public class PlayerController : MonoBehaviour { //reference the transform Transform t; [Header("Player Rotation")] //mouse input float rotationX; float rotationY; // Start is called before the first frame update void Start() { t = transform; Cursor.lockState = CursorLockMode.Locked; //Debug.Log(Space.World); } // Update is called once per frame void Update() { LookAround(); //debug function if (Input.GetKey(KeyCode.Escape)) { Cursor.lockState = CursorLockMode.None; } } void LookAround() { //get the mouse input rotationX += Input.GetAxis("Mouse X"); rotationY += Input.GetAxis("Mouse Y"); //setting the rotation value every update t.localRotation = Quaternion.Euler(-rotationY, rotationX, 0); } }
b. Clamp the rotation of the LookAround()
function
Right now, when we test this function, we find that the player is able to look all the way around and even do a backflip. This is not good, so we need to try to clamp the rotationY
variable.
In the script, we can do this by adding maximum and minimum rotation variables, and by clamping the value of the rotationY
variable in LookAround()
. At the same time, we can also add a variable to adjust the sensitivity of the rotation, and multiply the mouse inputs by it.
PlayerController.cs
using System.Collections; using System.Collections.Generic; using UnityEngine; public class PlayerController : MonoBehaviour { //reference the transform Transform t; [Header("Player Rotation")] public float sensitivity = 1; //clamp variables public float rotationMin; public float rotationMax; //mouse input float rotationX; float rotationY; // Start is called before the first frame update void Start() { t = transform; Cursor.lockState = CursorLockMode.Locked; //Debug.Log(Space.World); } // Update is called once per frame void Update() { LookAround(); //just to debug haha we will remove later if (Input.GetKey(KeyCode.Escape)) { Cursor.lockState = CursorLockMode.None; } } void LookAround() { //get the mouse input rotationX += Input.GetAxis("Mouse X") * sensitivity; rotationY += Input.GetAxis("Mouse Y") * sensitivity; //clamp the values of x and y rotationY = Mathf.Clamp(rotationY, rotationMin, rotationMax); //setting the rotation value every update t.localRotation = Quaternion.Euler(-rotationY, rotationX, 0); } }
Now when we test the game, the player can no longer do a backflip, which is great!
Article continues after the advertisement:
3. Player Movement — Swimming, Walking and Floating Movement
In our game, the player has three different types of movement: Movement on land, movement when underwater, and movement when the player is swimming along the top of the water (which will be referred to as floating). Let’s set up the underwater movement first.
a. Setting up the Input Manager
Now we are going to add the swimming movement. First, we need to edit the input axes in the Input Manager in the Project Settings.
- Change the name of the “Vertical” axis to the “Forward” axis. This will move the player forward or backward, along the Z axis, based on the w and s keys.
- change the name of “fire1” to “Vertical”, and set it to left shift and space. This axis will move our player up and down, along the Y axis.
Now we have three input axes for movement: the X axis for left and right movement, the Y axis for up and down movement, and the Z axis for forward and backward movement. When it comes to swimming movement, we need to remember that the X and Z movement needs to be relative to the local rotation of the player, so we can move diagonally when swimming. However, the Y axis should always be relative to the world space. Thus for movement, we will use the Transform.Translate()
function as follows.
t.Translate(new Vector3(moveX, 0, moveZ) * Time.deltaTime * speed); t.Translate(new Vector3(0, moveY, 0) * Time.deltaTime * speed, Space.World);
By default, the function moves the game object relative to its local rotation, but when we use Space.World
, we can also make it move relative to the world space.
b. Creating the Move Function
The next step is to add a new function to the PlayerController
script. The new function will be called Move()
. When swimming,
- Create Input variables for all three axes
- Get the input values
- Use
t.Translate
to move the character - Call the
Move()
function inFixedUpdate
Remember to call the Move()
function in FixedUpdate
instead of Update
, because the movement should interact with collisions.
PlayerController.cs
using System.Collections; using System.Collections.Generic; using UnityEngine; public class PlayerController : MonoBehaviour { //reference the transform Transform t; [Header("Player Rotation")] public float sensitivity = 1; //clamp variables public float rotationMin; public float rotationMax; //mouse input float rotationX; float rotationY; [Header("Player Movement")] public float speed = 1; float moveX; float moveY; float moveZ; // Start is called before the first frame update void Start() { t = transform; Cursor.lockState = CursorLockMode.Locked; //Debug.Log(Space.World); } // Update is called once per frame void Update() { LookAround(); //just to debug haha we will remove later if (Input.GetKey(KeyCode.Escape)) { Cursor.lockState = CursorLockMode.None; } } private void FixedUpdate() { Move(); } void LookAround() { //get the mouse input rotationX += Input.GetAxis("Mouse X") *sensitivity; rotationY += Input.GetAxis("Mouse Y")*sensitivity; //clamp the values of x and y rotationY = Mathf.Clamp(rotationY, rotationMin, rotationMax); //setting the rotation value every update t.localRotation = Quaternion.Euler(-rotationY, rotationX, 0); } void Move() { //get the movement input moveX = Input.GetAxis("Horizontal"); moveZ = Input.GetAxis("Forward"); moveY = Input.GetAxis("Vertical"); //move the character (swimming ver) t.Translate(new Vector3(moveX, 0, moveZ) * Time.deltaTime * speed); t.Translate(new Vector3(0, moveY, 0) * Time.deltaTime * speed, Space.World); } }
Now, we have set up underwater swimming movement for the player. Next, we want to make the PlayerController
able to know when to walk, when to swim, or when to float.
c. Setting up Colliders in the Scene
In order for the PlayerController
to know what kind of movement to use, we need to do a few things in the Unity scene.
- Create a large box collider at water level and call it “Water Collider”. Set
isTrigger
to true. - Add a new tag called “Ground” and add it to the terrain.
- Set Water Collider to the Water layer.
d. Keeping Track of the State of the Player’s Movement
For this, we need to keep track of two things, whether the player is in water or not, and whether the player has their head above the surface of the water. For whether the player is in water, we can keep track of that via onTriggerEnter and onTriggerExit. However, to keep track of whether the player’s head is above water, we will use unity’s Physics.Raycast()
.
Physics.Raycast(new Vector3(t.position.x, t.position.y + 0.5f, t.position.z), Vector3.down, out hit, Mathf.Infinity, waterMask);
We will take the raycast position to be slightly above the center of the player, and send a raycast down to check if the water collider is there. If the water collider is there, that means it is above the water and we can check the height above the water. If it does not find the water collider that means the player is on land or underwater completely. Now we can write the code.
- Create two static bools,
inWater
andisSwimming
. - Create a function
SwitchMovement()
to toggleinWater
. - Call
SwitchMovement()
when the player enters or exits the Water Collider. - Create a function
SwimingOrFloating()
to check whether the player is swimming underwater or floating along the surface of the water. - Called
SwimmingOrFloating()
before theMove()
function inFixedUpdate()
.
PlayerController.cs
using System.Collections; using System.Collections.Generic; using UnityEngine; public class PlayerController : MonoBehaviour { //reference the transform Transform t; public static bool isSwimming; public static bool inWater; //if inwater and not swimming, then activate float on top of water code. //if not in water, activate walk code //if swimming, activate swimming code public LayerMask waterMask; [Header("Player Rotation")] public float sensitivity = 1; //clamp variables public float rotationMin; public float rotationMax; //mouse input float rotationX; float rotationY; [Header("Player Movement")] public float speed = 1; float moveX; float moveY; float moveZ; // Start is called before the first frame update void Start() { t = transform; Cursor.lockState = CursorLockMode.Locked; inWater = false; } // Update is called once per frame void Update() { LookAround(); //just to debug haha we will remove later if (Input.GetKey(KeyCode.Escape)) { Cursor.lockState = CursorLockMode.None; } } private void FixedUpdate() { SwimmingOrFloating(); Move(); } private void OnTriggerEnter(Collider other) { SwitchMovement(); } private void OnTriggerExit(Collider other) { SwitchMovement(); } void SwitchMovement() { //toggle isSwimming inWater = !inWater; } void SwimmingOrFloating() { bool swimCheck = false; if (inWater) { RaycastHit hit; if (Physics.Raycast(new Vector3(t.position.x, t.position.y + 0.5f, t.position.z), Vector3.down, out hit, Mathf.Infinity, waterMask)) { if (hit.distance < 0.1f) { swimCheck = true; } } else { swimCheck = true; } } isSwimming = swimCheck; Debug.Log(isSwimming); } void LookAround() { //get the mouse input rotationX += Input.GetAxis("Mouse X") *sensitivity; rotationY += Input.GetAxis("Mouse Y")*sensitivity; //clamp the values of x and y rotationY = Mathf.Clamp(rotationY, rotationMin, rotationMax); //setting the rotation value every update t.localRotation = Quaternion.Euler(-rotationY, rotationX, 0); } void Move() { //get the movement input moveX = Input.GetAxis("Horizontal"); moveZ = Input.GetAxis("Forward"); moveY = Input.GetAxis("Vertical"); //move the character (swimming ver) t.Translate(new Vector3(moveX, 0, moveZ) * Time.deltaTime * speed); t.Translate(new Vector3(0, moveY, 0) * Time.deltaTime * speed, Space.World); } }
Don’t forget to set the waterMask
variable to the water layer in the Player GameObject inspector.
Article continues after the advertisement:
e. Adding Walking and Floating Movement
Next, we are going to add the other two movements.
For walking, we want the Rigidbody to use gravity, and we want the player to move based on their Y-rotation but not their X rotation. For the gravity, we can just add and get the Rigidbody component of the player and toggle the gravity in SwitchMovement()
.
Then, the movement. If you use local rotation, the player will be able to move diagonally upwards while on land, which is not good. As such, we will multiply the world space vector with the player’s Y rotation quaternion. This is the equation we get.
t.Translate(new Quaternion(0, t.rotation.y, 0, t.rotation.w) * new Vector3(moveX, 0, moveZ) * Time.deltaTime * speed, Space.World);
Then, floating movement. The floating movement is similar to the swimming movement, except that the player should not be able to increase their Y height. To accomplish this, first we clamp the moveY
input.
moveY = Mathf.Min(moveY, 0);
For clamping the movement along the x-axis, we need a way to allow diagonal movement (movement with reference to the local rotation) without allowing the Y height to increase. This was accomplished by converting the movement vector to a world space vector, then clamping the y increase of that vector.
Vector3 clampedDirection = transform.TransformDirection(new Vector3(moveX, moveY, moveZ)); clampedDirection = new Vector3(clampedDirection.x, Mathf.Min(clampedDirection.y, 0), clampedDirection.z);
Now we can add the code.
- Add if statements to the
Move()
function. - Add and get the Rigidbody component
- Toggle the Rigidbody’s gravity in
SwitchMovement()
.
PlayerController.cs
using System.Collections; using System.Collections.Generic; using UnityEngine; public class PlayerController : MonoBehaviour { //reference the transform Transform t; public static bool isSwimming; public static bool inWater; //if inwater and not swimming, then activate float on top of water code. //if not in water, activate walk code //if swimming, activate swimming code public LayerMask waterMask; Rigidbody rb; [Header("Player Rotation")] public float sensitivity = 1; //clamp variables public float rotationMin; public float rotationMax; //mouse input float rotationX; float rotationY; [Header("Player Movement")] public float speed = 1; float moveX; float moveY; float moveZ; // Start is called before the first frame update void Start() { rb = GetComponent<Rigidbody>(); t = transform; Cursor.lockState = CursorLockMode.Locked; inWater = false; } // Update is called once per frame void Update() { LookAround(); //just to debug haha we will remove later if (Input.GetKey(KeyCode.Escape)) { Cursor.lockState = CursorLockMode.None; } } private void FixedUpdate() { SwimmingOrFloating(); Move(); } private void OnTriggerEnter(Collider other) { SwitchMovement(); } private void OnTriggerExit(Collider other) { SwitchMovement(); } void SwitchMovement() { //toggle isSwimming inWater = !inWater; //change the rigidbody accordingly rb.useGravity = !rb.useGravity; } void SwimmingOrFloating() { bool swimCheck = false; if (inWater) { RaycastHit hit; if (Physics.Raycast(new Vector3(t.position.x, t.position.y + 0.5f, t.position.z), Vector3.down, out hit, Mathf.Infinity, waterMask)) { if (hit.distance < 0.1f) { swimCheck = true; } } else { swimCheck = true; } } isSwimming = swimCheck; Debug.Log(isSwimming); } void LookAround() { //get the mouse input rotationX += Input.GetAxis("Mouse X") *sensitivity; rotationY += Input.GetAxis("Mouse Y")*sensitivity; //clamp the values of x and y rotationY = Mathf.Clamp(rotationY, rotationMin, rotationMax); //setting the rotation value every update t.localRotation = Quaternion.Euler(-rotationY, rotationX, 0); } void Move() { //get the movement input moveX = Input.GetAxis("Horizontal"); moveZ = Input.GetAxis("Forward"); moveY = Input.GetAxis("Vertical"); //check if player is on land if (!inWater) { //move the character (land ver) t.Translate(new Quaternion(0, t.rotation.y, 0, t.rotation.w) * new Vector3(moveX, 0, moveZ) * Time.deltaTime * speed, Space.World); } else { //check here if the player is swimming above water if (!isSwimming) { //if the character is floating on the water, clamp the moveY value, so they cant rise further than that. moveY = Mathf.Min(moveY, 0); //convert the local direction vector into a worldspace vector. Vector3 clampedDirection = transform.TransformDirection(new Vector3(moveX, moveY, moveZ)); //clamp the values clampedDirection = new Vector3(clampedDirection.x, Mathf.Min(clampedDirection.y, 0), clampedDirection.z); t.Translate(clampedDirection * Time.deltaTime * speed, Space.World); } else { //move the character (swimming ver) t.Translate(new Vector3(moveX, 0, moveZ) * Time.deltaTime * speed); t.Translate(new Vector3(0, moveY, 0) * Time.deltaTime * speed, Space.World); } } } }
Article continues after the advertisement:
f. Adjusting the velocity of the player’s Rigidbody
We have completed the three types of movement now. However, you might notice the player still moves when none of the inputs are entered. This is due to the leftover velocity in the Rigidbody component. We could set it to zero all the time, but Unity’s gravity in its physics engine uses the Rigidbody’s velocity to affect the player. So in the code, we just need to clamp it to zero when not on land, and otherwise just clamp the x-value of the velocity.
PlayerController.cs
using System.Collections; using System.Collections.Generic; using UnityEngine; public class PlayerController : MonoBehaviour { //reference the transform Transform t; public static bool isSwimming; public static bool inWater; //if inwater and not swimming, then activate float on top of water code. //if not in water, activate walk code //if swimming, activate swimming code public LayerMask waterMask; Rigidbody rb; [Header("Player Rotation")] public float sensitivity = 1; //clamp variables public float rotationMin; public float rotationMax; //mouse input float rotationX; float rotationY; [Header("Player Movement")] public float speed = 1; float moveX; float moveY; float moveZ; // Start is called before the first frame update void Start() { rb = GetComponent<Rigidbody>(); t = transform; Cursor.lockState = CursorLockMode.Locked; inWater = false; } // Update is called once per frame void Update() { LookAround(); //just to debug haha we will remove later if (Input.GetKey(KeyCode.Escape)) { Cursor.lockState = CursorLockMode.None; } } private void FixedUpdate() { SwimmingOrFloating(); Move(); } private void OnTriggerEnter(Collider other) { SwitchMovement(); } private void OnTriggerExit(Collider other) { SwitchMovement(); } void SwitchMovement() { //toggle isSwimming inWater = !inWater; //change the rigidbody accordingly rb.useGravity = !rb.useGravity; } void SwimmingOrFloating() { bool swimCheck = false; if (inWater) { RaycastHit hit; if (Physics.Raycast(new Vector3(t.position.x, t.position.y + 0.5f, t.position.z), Vector3.down, out hit, Mathf.Infinity, waterMask)) { if (hit.distance < 0.1f) { swimCheck = true; } } else { swimCheck = true; } } isSwimming = swimCheck; Debug.Log(isSwimming); } void LookAround() { //get the mouse input rotationX += Input.GetAxis("Mouse X") *sensitivity; rotationY += Input.GetAxis("Mouse Y")*sensitivity; //clamp the values of x and y rotationY = Mathf.Clamp(rotationY, rotationMin, rotationMax); //setting the rotation value every update t.localRotation = Quaternion.Euler(-rotationY, rotationX, 0); } void Move() { //get the movement input moveX = Input.GetAxis("Horizontal"); moveZ = Input.GetAxis("Forward"); moveY = Input.GetAxis("Vertical"); //check if the player is standing still if (inWater) //If in water, velocity = 0 { rb.velocity = new Vector2(0, 0); } else { if (moveX == 0 && moveZ == 0) { rb.velocity = new Vector2(0, rb.velocity.y); } } //check if player is on land if (!inWater) { //move the character (land ver) t.Translate(new Quaternion(0, t.rotation.y, 0, t.rotation.w) * new Vector3(moveX, 0, moveZ) * Time.deltaTime * speed, Space.World); } else { //check here if the player is swimming above water if (!isSwimming) { //if the character is floating on the water, clamp the moveY value, so they cant rise further than that. moveY = Mathf.Min(moveY, 0); //convert the local direction vector into a worldspace vector. Vector3 clampedDirection = transform.TransformDirection(new Vector3(moveX, moveY, moveZ)); //clamp the values clampedDirection = new Vector3(clampedDirection.x, Mathf.Min(clampedDirection.y, 0), clampedDirection.z); t.Translate(clampedDirection * Time.deltaTime * speed, Space.World); } else { //move the character (swimming ver) t.Translate(new Vector3(moveX, 0, moveZ) * Time.deltaTime * speed); t.Translate(new Vector3(0, moveY, 0) * Time.deltaTime * speed, Space.World); } } } }
With that, we have finished the basic movement of the player.
4. Stats System
Moving on, we will create a simple stats system for our player.
a. Create the player stats UI
- Create a Canvas, set its size to 1920 by 1080, name it “Player Canvas”
- Create 4 sliders to work as the UI, colour and adjust them accordingly
- Position them using a vertical layout group component
- Add TextMeshPro UGUIs as children of the sliders to be text
b. Decreasing Stats at an interval
Next, we will create a new folder under Scripts. Name it UI. Then we will create a new class PlayerStats
[Assets/Scripts/UI]. In this class, we will store the values of the stats, and create functions to change the stat values.
We will first store the values of each stat and its possible maximum. Then, we will create a coroutine that decreases a stat at an interval. This will be used to decrease hunger and thirst over time, and oxygen when underwater (which we will get to in the next section). We will also create a function to change the value of a stat. In the script, create the following.
- Two integer lists,
maxStats
andcurrentStats
ChangeStat()
functionDecreaseStats()
coroutine that decreases a stat at an interval- Three coroutine variables to store the
DecreaseStat()
coroutines - Set the hunger and thirst decrease in start.
PlayerStats.cs
using System.Collections; using System.Collections.Generic; using UnityEngine; public class PlayerStats : MonoBehaviour { public List<int> maxStats; //0 is oxygen, 1 is food, 2 is thirst, 3 is health List<int> currentStats; Coroutine oxygenCo; Coroutine hungerCo; Coroutine thirstCo; // Start is called before the first frame update void Start() { //initialise currentStats to the max amount currentStats = new List<int>(maxStats); //start decreasing the values of hunger and thirst hungerCo = StartCoroutine(DecreaseStats(1, 20, 1)); thirstCo = StartCoroutine(DecreaseStats(2, 20, 1)); } // Update is called once per frame void Update() { } IEnumerator DecreaseStats(int stat, int interval, int amount) { while (true) { yield return new WaitForSeconds(interval); if(currentStats[stat] > 0) { currentStats[stat] = Mathf.Max(currentStats[stat] - amount, 0); } } } public void ChangeStat(int stat, int refreshAmount) { if(refreshAmount > 0) { currentStats[stat] = Mathf.Min(currentStats[stat] + refreshAmount, maxStats[stat]); } else { currentStats[stat] = Mathf.Max(currentStats[stat] + refreshAmount, 0); } } }
Add the PlayerStats script to the player game object and Set the values of the maxStats
. Remember 0 is oxygen, 1 is hunger, 2 is thirst, and 3 is health.
c. Oxygen control
Now, to control the oxygen stat, we will reference the isSwimming
static bool from PlayerController
. This way, we can start the coroutine for decreasing oxygen when isSwimming is true, and refresh the oxygen when isSwimming is false.
However, we run into a problem. this code will run over and over, which will cause problems with the coroutine. However, we can get around this by creating a bool swimCheck. If we change swimCheck whenever the player swims or stops swimming, then we can use it to specify our if statements to only be during the frames where the player is changing between swimming and not swimming, solving our problem. Add to the script the following.
- Add a new bool
swimCheck
- Add the oxygen stat control
PlayerStats.cs
using System.Collections; using System.Collections.Generic; using UnityEngine; public class PlayerStats : MonoBehaviour { public List<int> maxStats; //0 is oxygen, 1 is food, 2 is thirst, 3 is health List<int> currentStats; Coroutine oxygenCo; Coroutine hungerCo; Coroutine thirstCo; bool swimCheck; // Start is called before the first frame update void Start() { //initialise currentStats to the max amount currentStats = new List<int>(maxStats); //start decreasing the values of hunger and thirst hungerCo = StartCoroutine(DecreaseStats(1, 20, 1)); thirstCo = StartCoroutine(DecreaseStats(2, 20, 1)); } // Update is called once per frame void Update() { if (!PlayerController.isSwimming && swimCheck == true) { swimCheck = false; StopCoroutine(oxygenCo); ChangeStat(0, maxStats[0]); } if (PlayerController.isSwimming && swimCheck == false) { swimCheck = true; oxygenCo = StartCoroutine(DecreaseStats(0, 3, 3)); } } IEnumerator DecreaseStats(int stat, int interval, int amount) { while (true) { yield return new WaitForSeconds(interval); if(currentStats[stat] > 0) { currentStats[stat] = Mathf.Max(currentStats[stat] - amount, 0); } } } public void ChangeStat(int stat, int refreshAmount) { if(refreshAmount > 0) { currentStats[stat] = Mathf.Min(currentStats[stat] + refreshAmount, maxStats[stat]); } else { currentStats[stat] = Mathf.Max(currentStats[stat] + refreshAmount, 0); } } }
Article continues after the advertisement:
d. Apply the Stats to the UI
Now we can apply the stats to the UI, so that the slider and text reflect the stat values. Add UnityEngine.UI
and TMPro to the PlayerStats
script, and create a reference for the sliders and text.
- Store the UI sliders and text as variables
- Set the max value of each slider in
Start()
- Update every stat in the
Update()
function
PlayerStats.cs
using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.UI; using TMPro; public class PlayerStats : MonoBehaviour { public List<int> maxStats; //0 is oxygen, 1 is food, 2 is thirst, 3 is health List<int> currentStats; Coroutine oxygenCo; Coroutine hungerCo; Coroutine thirstCo; bool swimCheck; [Header("UI")] public List<Slider> statBars; public List<TextMeshProUGUI> statNums; // Start is called before the first frame update void Start() { //initialise currentStats to the max amount currentStats = new List<int>(maxStats); //start decreasing the values of hunger and thirst hungerCo = StartCoroutine(DecreaseStats(1, 20, 1)); thirstCo = StartCoroutine(DecreaseStats(2, 20, 1)); //initialise the statBars for (int i = 0; i < maxStats.Count; i++) { statBars[i].maxValue = maxStats[i]; } } // Update is called once per frame void Update() { if (!PlayerController.isSwimming && swimCheck == true) { swimCheck = false; StopCoroutine(oxygenCo); ChangeStat(0, maxStats[0]); } if (PlayerController.isSwimming && swimCheck == false) { swimCheck = true; oxygenCo = StartCoroutine(DecreaseStats(0, 3, 3)); } //display currentstats in stat ui for(int i = 0; i < maxStats.Count; i++) { statBars[i].value = currentStats[i]; statNums[i].text = currentStats[i].ToString(); } } IEnumerator DecreaseStats(int stat, int interval, int amount) { while (true) { yield return new WaitForSeconds(interval); if(currentStats[stat] > 0) { currentStats[stat] = Mathf.Max(currentStats[stat] - amount, 0); } } } public void ChangeStat(int stat, int refreshAmount) { if(refreshAmount > 0) { currentStats[stat] = Mathf.Min(currentStats[stat] + refreshAmount, maxStats[stat]); } else { currentStats[stat] = Mathf.Max(currentStats[stat] + refreshAmount, 0); } } }
Conclusion
In this article, we went through how to implement the Dialogue System as well as the NPC Relationship System.
If you would like to download a copy of the codes we have worked on, you can download the project files here. To use the files, you will have to unzip the file (7-Zip can help you do that), and open the folder with Assets and ProjectSettings as a project using Unity. If you have trouble opening the project files, you can check out this article here.
Here are the finished codes for all the scripts we have worked with today:
PlayerController.cs
using System.Collections; using System.Collections.Generic; using UnityEngine; public class PlayerController : MonoBehaviour { //reference the transform Transform t; public static bool isSwimming; public static bool inWater; //if inwater and not swimming, then activate float on top of water code. //if not in water, activate walk code //if swimming, activate swimming code public LayerMask waterMask; Rigidbody rb; [Header("Player Rotation")] public float sensitivity = 1; //clamp variables public float rotationMin; public float rotationMax; //mouse input float rotationX; float rotationY; [Header("Player Movement")] public float speed = 1; float moveX; float moveY; float moveZ; // Start is called before the first frame update void Start() { rb = GetComponent<Rigidbody>(); t = transform; Cursor.lockState = CursorLockMode.Locked; inWater = false; } // Update is called once per frame void Update() { LookAround(); //just to debug haha we will remove later if (Input.GetKey(KeyCode.Escape)) { Cursor.lockState = CursorLockMode.None; } } private void FixedUpdate() { SwimmingOrFloating(); Move(); } private void OnTriggerEnter(Collider other) { SwitchMovement(); } private void OnTriggerExit(Collider other) { SwitchMovement(); } void SwitchMovement() { //toggle isSwimming inWater = !inWater; //change the rigidbody accordingly rb.useGravity = !rb.useGravity; } void SwimmingOrFloating() { bool swimCheck = false; if (inWater) { RaycastHit hit; if (Physics.Raycast(new Vector3(t.position.x, t.position.y + 0.5f, t.position.z), Vector3.down, out hit, Mathf.Infinity, waterMask)) { if (hit.distance < 0.1f) { swimCheck = true; } } else { swimCheck = true; } } isSwimming = swimCheck; Debug.Log(isSwimming); } void LookAround() { //get the mouse input rotationX += Input.GetAxis("Mouse X") *sensitivity; rotationY += Input.GetAxis("Mouse Y")*sensitivity; //clamp the values of x and y rotationY = Mathf.Clamp(rotationY, rotationMin, rotationMax); //setting the rotation value every update t.localRotation = Quaternion.Euler(-rotationY, rotationX, 0); } void Move() { //get the movement input moveX = Input.GetAxis("Horizontal"); moveZ = Input.GetAxis("Forward"); moveY = Input.GetAxis("Vertical"); //check if the player is standing still if (inWater) //If in water, velocity = 0 { rb.velocity = new Vector2(0, 0); } else
PlayerStats.cs
using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.UI; using TMPro; public class PlayerStats : MonoBehaviour { public List<int> maxStats; //0 is oxygen, 1 is food, 2 is thirst, 3 is health List<int> currentStats; Coroutine oxygenCo; Coroutine hungerCo; Coroutine thirstCo; bool swimCheck; [Header("UI")] public List<Slider> statBars; public List<TextMeshProUGUI> statNums; // Start is called before the first frame update void Start() { //initialise currentStats to the max amount currentStats = new List<int>(maxStats); //start decreasing the values of hunger and thirst hungerCo = StartCoroutine(DecreaseStats(1, 20, 1)); thirstCo = StartCoroutine(DecreaseStats(2, 20, 1)); //initialise the statBars for (int i = 0; i < maxStats.Count; i++) { statBars[i].maxValue = maxStats[i]; } } // Update is called once per frame void Update() { if (!PlayerController.isSwimming && swimCheck == true) { swimCheck = false; StopCoroutine(oxygenCo); ChangeStat(0, maxStats[0]); } if (PlayerController.isSwimming && swimCheck == false) { swimCheck = true; oxygenCo = StartCoroutine(DecreaseStats(0, 3, 3)); } //display currentstats in stat ui for(int i = 0; i < maxStats.Count; i++) { statBars[i].value = currentStats[i]; statNums[i].text = currentStats[i].ToString(); } } IEnumerator DecreaseStats(int stat, int interval, int amount) { while (true) { yield return new WaitForSeconds(interval); if(currentStats[stat] > 0) { currentStats[stat] = Mathf.Max(currentStats[stat] - amount, 0); } } } public void ChangeStat(int stat, int refreshAmount) { if(refreshAmount > 0) { currentStats[stat] = Mathf.Min(currentStats[stat] + refreshAmount, maxStats[stat]); } else { currentStats[stat] = Mathf.Max(currentStats[stat] + refreshAmount, 0); } } }
Article continues after the advertisement: