Ever wanted to create a game like Subnautica in Unity? Check out Part 2 of our guide here, where we go through how to set up a the terrain for the game and the day-night cycle.
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.
Author’s Note: If you’ve watched and followed Part 1 of the video tutorial, you’ll realise there is a small error in the code. Make sure you change moveY()
to moveZ()
to make sure your code is working. You can watch the 1st minute of the video tutorial below for clarification on this.
1. Day-Night Cycle
First, you’re going to want to create a folder in Assets > Scripts, and name it “Time”. All scripts pertaining to the management of time for the game will be put into this folder. To create the illusion of day and night, we will have to rotate the directional light of the scene. If you take a look at the directional light, rotating it makes it look like the sun in the game is rising and setting. We will have to manipulate this in the script itself to create the day-night cycle.
a. Setting up the GameTimeStamp
class
The time system that we want to create will be rather complex. Let’s start off by creating a class called GameTimeStamp
. This class will store the number of minutes, hours and days that have passed in the game. Take note that this will not be a MonoBehaviour
class, so you can remove that from your script – and because of this, we can also remove the default Start()
and Update()
functions. We’ll also need to make the script serializable so that we can see it clearly in the inspector.
Then, we’ll create the integer variables that will store the day, hours and minutes of the game.
b. Creating the class constructor for GameTimeStamp
We’ll need to create two constructors for GameTimeStamp
. One to set up the class, and the other that helps create a new timestamp with reference to an existing one.
GameTimeStamp.cs
using System.Collections; using System.Collections.Generic; using UnityEngine; [System.Serializable] public class GameTimeStamp: MonoBehaviour{ public int day; public int hour; public int minute; //class constructor - sets up the class public GameTimeStamp(int day, int hour, int minute) { this.day = day; this.hour = hour; this.minute = minute; } //create a new timestamp from an existing one public GameTimeStamp(GameTimeStamp timeStamp) { this.day = timeStamp.day; this.hour = timeStamp.hour; this.minute = timeStamp.minute; } }
c. UpdateClock()
function
The next step is to create a function that will increase the time. Every time it’s called, it should increment each minute by one, and ensure that it doesn’t exceed 60. This function will also convert minutes to hours, and hours to the number of days.
For this game, we’ll change the rate of time a little bit such that one second in real-time would translate into an hour in-game. In the actual game, a day in-game translates to 20 minutes in real-time (i.e 20h to a day in-game), so let’s go ahead and model after that.
d. HoursToMinutes()
function
Following up, we’ll have to create a function that will help to convert the number of hours to minutes. Since an hour is equivalent to 60 minutes, we’ll just multiply the number of hours by 60 and return that for this function.
e. TimestampInMinutes()
function
This function helps to return the total number of minutes based on the GameTimeStamp function. Since a day in-game is equivalent to 20 in-game hours, we’ll add up the total number of minutes in the current timestamp. We’ll utilize the hour-to-minute function that we’ve created and multiply the number of days by 20 since there are 20h in a day in-game. Similarly, we’ll call the current timestamp’s number of hours, and also use the HoursToMinutes()
function we’ve created. Next, we’ll just call up the total number of minutes in the current timestamp as well, where no conversion is necessary. Finally, we’ll just add everything together now that they’re in minutes, and return that in this function.
Here’s what the code should look like after all the functions are added:
GameTimeStamp.cs
using System.Collections; using System.Collections.Generic; using UnityEngine; [System.Serializable] public class GameTimeStamp { public int day; public int hour; public int minute; //class constructor - sets up the class public GameTimeStamp(int day, int hour, int minute) { this.day = day; this.hour = hour; this.minute = minute; } //create a new timestamp from an existing one public GameTimeStamp(GameTimeStamp timeStamp) { this.day = timeStamp.day; this.hour = timeStamp.hour; this.minute = timeStamp.minute; } //this function increases the time incrementally public void UpdateClock() { minute++; //60 minutes in 1 hour if (minute >= 60) { //reset minutes minute = 0; hour++; } //20 hours in 1 day if (hour >= 20) { //Reset hours hour = 0; day++; } } //Convert hours to minutes public static int HoursToMinutes(int hour) { //60 minutes = 1 hour return hour * 60; } //returns the current time stamp in minutes. public static int TimestampInMinutes(GameTimeStamp timestamp) { return (HoursToMinutes(timestamp.day * 20) + HoursToMinutes(timestamp.hour) + timestamp.minute); } }
Our GameTimeStamp
code is now complete! We can go back into unity now and move on to the next step, which is creating a new script for our game’s TimeManager
.
f. Setting up the TimeManager
class
We’ll need to create another class that will help us manage the game timestamps. So we’ll head into the Time folder we’ve created and create a new script called TimeManager
. This script is a MonoBehavior
script and also a singleton class.
Since there is only 1 copy of the TimeManager
class at any given time, we will create a static Instance
variable in the class, so that whenever we want to access our TimeManager
instance, we can simply use TimeManager.Instance
.
For a more detailed explanation of singleton classes, as well as the TimeManager.Instance
setup, check out this article about singletons in Unity.
We also make the Instance
variable have a public getter, but a private setter. This will ensure that other scripts can only access the instance variable, but cannot modify it. The TimeManager
class itself will also ensure that there is only 1 copy of the TimeManager
in the scene at any time (see the Awake()
function below).
We want this class to handle the game time stamp and also update the timestamp. So in order to do this, we’ll create a header called Internal Clock
, serialize the script, and also create a GameTimeStamp
called timestamp
. We’ll also create a timeScale
variable, which we will set to a default value of 1.0. This will allow us to multiply the speed of the game time in the event we want to fast forward time within the game. Sort of like in Sims!
g. Awake()
function
The next thing we must do is ensure that there is only a single instance of the class at one time, so we’ll create a function called Awake()
to do so. The logic is that if there is more than one instance, we will delete the extra instance (so that there is only 1 instance of the class at any time).
h. Start()
& Update()
function
First, we’ll need to initialise the timestamp
. Then, in order to update the game timestamp, we’ll use a co-routine with a while
loop within it. Since a minute in-game is equivalent to one second in real-time, we will update the game timestamp every second. Let’s create the co-routine and call it TimeUpdate()
.
With that, our TimeManager
should be able to initialize a game timestamp
, and continually update it every second. We can now return to Unity and create an empty GameObject called “Game Manager”, which will be used to store the UI as well as the other management classes. Make the PlayerCanvas a child of Game Manager simply by dragging it to re-assign it. Remember to reset the position of Game Manager as well!
You can now drag the TimeManager
script into the Game Manager object to make it a component of Game Manager. You will see that you can now change the time scale and also set the day, hour and minute.
TimeManager.cs
using System.Collections; using System.Collections.Generic; using UnityEngine; public class TimeManager : MonoBehaviour { public static TimeManager Instance { get; private set; } [Header("Internal Clock")] [SerializeField] GameTimeStamp timestamp; public float timeScale = 1.0f; private void Awake() { //If there is more than one instance, destroy the extra if (Instance != null && Instance != this) { Destroy(this); } else { //Set the static instance to this instance Instance = this; } } // Start is called before the first frame update void Start() { //Initialise the time stamp timestamp = new GameTimeStamp(1, 6, 0); StartCoroutine(TimeUpdate()); } IEnumerator TimeUpdate() { while (true) { timestamp.UpdateClock(); yield return new WaitForSeconds(1 / timeScale); } } }
i. Setting up the Sun’s rotation
For this portion, we’ll want the sun (i.e. Directional Light in the Scene) to rotate every second and reflect the current time. In Subnautica, day and night are split up such that 15 hours in-game (15 minutes in real-time) are considered ‘Day’ and 5 hours in-game (5 minutes in real-time) are considered ‘Night’. So taking the horizon as the axis, we’ll want the sun to move from east to west (180 degrees) in 15 in-game hours, and for it to move back to its original position (next 180 degrees) afterwards in 5 in-game hours. Don’t worry – we did the math for you this time because we’re awesome:
- During the day – rotate 12 degrees an hour; 0.2 degrees every minute (in-game)
- During the night – rotate 36 degrees an hour; 0.6 degrees every minute (in-game)
Reminder: 1 minute of in-game time is equivalent to 1 second in real-time.
Here’s what you need to do in the TimeManager
script:
- Create a header called
Day and Night Cycle
- Create a
Transform
variable calledsunTransform
- Create new function to update sun movement –
UpdateSunMovement()
In order to move the sun according to what we calculated earlier, we can simply use if
statements.
Set the variable sunAngle
to 0 to initialise it. Then, we’ll set the condition such that if the time in minutes is less than 15 * 60 (total number of minutes in-game per day), we’ll multiply the angle of the sun during the day to be 0.2 degrees multiplied by the number of minutes. Otherwise, if the time in minutes is more than so, we’ll calculate where the sun should be during the night. However, for the angle of the sun during the night, we must remember that its position has passed the first 180 degrees (which is the horizon), so ensure that you include this in your calculations for the sun angle:
- Take the total time in minutes and deduct 1 in-game day (15*60) from it
- Multiply the above by the night angle calculation above, which is 0.6 degrees per minute
- Finally, add 180 degrees to it since the sun has set below the axis (horizon)
The last thing we have to do is apply the angle to the directional light. We can then add the newly created UpdateSunMovement()
function into the timeUpdate()
function. To make the script less cluttered, we’ll create another function called Tick()
that has the UpdateSunMovement()
function, then put the Tick()
function within the TimeUpdate()
function.
The code should look like this:
TimeManager.cs
using System.Collections; using System.Collections.Generic; using UnityEngine; public class TimeManager : MonoBehaviour { public static TimeManager Instance { get; private set; } [Header("Internal Clock")] [SerializeField] GameTimeStamp timestamp; public float timeScale = 1.0f; [Header("Day and Night cycle")] //The transform of the directional light (sun) public Transform sunTransform; Vector3 sunAngle; private void Awake() { //If there is more than one instance, destroy the extra if (Instance != null && Instance != this) { Destroy(this); } else { //Set the static instance to this instance Instance = this; } } // Start is called before the first frame update void Start() { //Initialise the time stamp timestamp = new GameTimeStamp(1, 6, 0); StartCoroutine(TimeUpdate()); } private void Update() { sunTransform.rotation = Quaternion.Slerp(sunTransform.rotation, Quaternion.Euler(sunAngle), 1f * Time.deltaTime); } IEnumerator TimeUpdate() { while (true) { timestamp.UpdateClock(); Tick(); yield return new WaitForSeconds(1 / timeScale); } } void Tick() { timestamp.UpdateClock(); UpdateSunMovement(); } //Day and night cycle void UpdateSunMovement() { //Convert the current time to minutes int timeInMinutes = GameTimeStamp.HoursToMinutes(timestamp.hour) + timestamp.minute; //during daytime //Sun moves .2 degrees in a minute //during nighttime //Sun moves .6 degrees in a minute //At midnight (0:00), the angle of the sun should be -90 float sunAngle = 0; if(timeInMinutes <= 15 * 60) { sunAngle = .2f * timeInMinutes; } else if(timeInMinutes > 15 * 60) { sunAngle = 180f + 0.6f * (timeInMinutes - (15 * 60)); } //Apply the angle to the directional light this.sunAngle = new Vector3(sunAngle, 0, 0); } }
Now we can go back to Unity and go into the Game Manager object and add the Directional Light (Transform) into the TimeManager
(Script) – you should now be able to see the movement of the directional light once you hit play! To smooth out the movement of the sun, we’ve added another line of code in the Update()
function by using Quaternion.Slerp()
.
Lerp
stands for Linear Interpolation. It takes 2 values and finds a point between the 2 values, and is often used to simulate movement between 2 points (see this part of our article for more information). Slerp
stands for Spherical Linear Interpolation, and works similarly, except that it is applied to rotations as opposed to movement in a straight line.
j. Setting up the Game Manager GameObject
We’ve covered a little bit about how to set up the Game Manager game object earlier, but here’s a summarised version of what you should have:
- Create an empty GameObject called “Game Manager”, which will be used to store the UI as well as the other management classes
- Make the PlayerCanvas a child of Game Manager simply by dragging it to re-assign it
- Reset the position of Game Manager
- Drag the
TimeManager
script into the Game Manager object to make it a component of Game Manager - Move the player canvas onto the Game Manager object you’ve created
- Put the sun on the Game Manager object
k. Setting up Debug functions (to skip time)
To begin debugging the program, let’s start with creating a function to skip time and a function to retrieve the current time stamp.
In the SkipTime()
function, it should ensure that as time is being skipped, it simultaneously updates everything in the Tick()
function correctly. Essentially, it will take in a timestamp that the you want to skip to, compute the difference between the specified timestamp and the current time stamp in minutes, then repeatedly call the Tick()
function until it reaches the specified timestamp.
The code should look like this:
TimeManager.cs
using System.Collections; using System.Collections.Generic; using UnityEngine; public class TimeManager : MonoBehaviour { public static TimeManager Instance { get; private set; } [Header("Internal Clock")] [SerializeField] GameTimeStamp timestamp; public float timeScale = 1.0f; [Header("Day and Night cycle")] //The transform of the directional light (sun) public Transform sunTransform; Vector3 sunAngle; private void Awake() { //If there is more than one instance, destroy the extra if (Instance != null && Instance != this) { Destroy(this); } else { //Set the static instance to this instance Instance = this; } } // Start is called before the first frame update void Start() { //Initialise the time stamp timestamp = new GameTimeStamp(1, 6, 0); StartCoroutine(TimeUpdate()); } private void Update() { sunTransform.rotation = Quaternion.Slerp(sunTransform.rotation, Quaternion.Euler(sunAngle), 1f * Time.deltaTime); } //Load the time from a save public void LoadTime(GameTimeStamp timestamp) { IEnumerator TimeUpdate() { while (true) { Tick(); yield return new WaitForSeconds(1 / timeScale); } } void Tick() { timestamp.UpdateClock(); UpdateSunMovement(); } //Day and night cycle void UpdateSunMovement() { //Convert the current time to minutes int timeInMinutes = GameTimeStamp.HoursToMinutes(timestamp.hour) + timestamp.minute; //during daytime //Sun moves .2 degrees in a minute //during nighttime //Sun moves .6 degrees in a minute //At midnight (0:00), the angle of the sun should be -90 float sunAngle = 0; if(timeInMinutes <= 15 * 60) { sunAngle = .2f * timeInMinutes; } else if(timeInMinutes > 15 * 60) { sunAngle = 180f + 0.6f * (timeInMinutes - (15 * 60)); } //Apply the angle to the directional light //sunTransform.eulerAngles = new Vector3(sunAngle, 0, 0); this.sunAngle = new Vector3(sunAngle, 0, 0); } // function to skip time public void SkipTime(GameTimeStamp timeToSkipTo) { //Convert to minutes int timeToSkipInMinutes = GameTimeStamp.TimestampInMinutes(timeToSkipTo); Debug.Log("Time to skip to:" + timeToSkipInMinutes); int timeNowInMinutes = GameTimeStamp.TimestampInMinutes(timestamp); Debug.Log("Time now: " + timeNowInMinutes); int differenceInMinutes = timeToSkipInMinutes - timeNowInMinutes; Debug.Log(differenceInMinutes + " minutes will be advanced"); //Check if the timestamp to skip to has already been reached if (differenceInMinutes <= 0) return; for (int i = 0; i < differenceInMinutes; i++) { Tick(); } } //Get the timestamp public GameTimeStamp GetGameTimestamp() { //Return a cloned instance return new GameTimeStamp(timestamp); } }
l. Setting up the ITimeTracker
interface
The last part of the time system that we need to create would be a time tracker interface. The proper definition of what an interface does is summarised below:
“An interface is a collection of method signatures and properties. You can think of interfaces like a contract that classes can implement: when a class implements an interface, a class instance can be treated as an instance of that interface. This functionality means that you can group together multiple disparate types and treat them in the same way.”
Explanation from Unity Learn
In essence, we want to be able to inform a class when a certain time has been met so that the class can execute an action. For instance, if we want to trigger an event during the gameplay during a specified time, the interface helps to keep track of what time it currently is before the event can happen, as opposed to constantly referring to the TimeManager
script. Here are the steps to begin coding this portion:
- Create
ITimeTracker
in the Time folder - Remove
Monobehavior
andStart
&Update
scripts - Create a function called
ClockUpdate()
ITimeTracker.cs
using System.Collections; using System.Collections.Generic; using UnityEngine; public interface ITimeTracker { void ClockUpdate(GameTimeStamp timestamp); }
- Go back into the
TimeManager
script and create a list of time tracker interfaces below the variables you’ve declared - Call
ClockUpdate()
with the current timestamp underTick()
function: This means that every time the clock is updated, it sends a message to every time tracker. We will further explore this in the later parts of the tutorial. Feel free to modify this how you like depending on what you want to do with it! - Create functions to manage listeners (adding and removing) –
RegisterTracker()
andUnregisterTracker()
Here’s what it will look like:
using System.Collections; using System.Collections.Generic; using UnityEngine; public class TimeManager : MonoBehaviour { public static TimeManager Instance { get; private set; } [Header("Internal Clock")] [SerializeField] GameTimeStamp timestamp; public float timeScale = 1.0f; [Header("Day and Night cycle")] //The transform of the directional light (sun) public Transform sunTransform; Vector3 sunAngle; //List of Objects to inform of changes to the time List<ITimeTracker> listeners = new List<ITimeTracker>(); private void Awake() { //If there is more than one instance, destroy the extra if (Instance != null && Instance != this) { Destroy(this); } else { //Set the static instance to this instance Instance = this; } } // Start is called before the first frame update void Start() { //Initialise the time stamp timestamp = new GameTimeStamp(1, 6, 0); StartCoroutine(TimeUpdate()); } private void Update() { sunTransform.rotation = Quaternion.Slerp(sunTransform.rotation, Quaternion.Euler(sunAngle), 1f * Time.deltaTime); } //Load the time from a save public void LoadTime(GameTimeStamp timestamp) { IEnumerator TimeUpdate() { while (true) { Tick(); yield return new WaitForSeconds(1 / timeScale); } } void Tick() { timestamp.UpdateClock(); //Inform each of the listeners of the new time state foreach (ITimeTracker listener in listeners) { listener.ClockUpdate(timestamp); } UpdateSunMovement(); } //Day and night cycle void UpdateSunMovement() { //Convert the current time to minutes int timeInMinutes = GameTimeStamp.HoursToMinutes(timestamp.hour) + timestamp.minute; //during daytime //Sun moves .2 degrees in a minute //during nighttime //Sun moves .6 degrees in a minute //At midnight (0:00), the angle of the sun should be -90 float sunAngle = 0; if(timeInMinutes <= 15 * 60) { sunAngle = .2f * timeInMinutes; } else if(timeInMinutes > 15 * 60) { sunAngle = 180f + 0.6f * (timeInMinutes - (15 * 60)); } //Apply the angle to the directional light //sunTransform.eulerAngles = new Vector3(sunAngle, 0, 0); this.sunAngle = new Vector3(sunAngle, 0, 0); } // function to skip time public void SkipTime(GameTimeStamp timeToSkipTo) { //Convert to minutes int timeToSkipInMinutes = GameTimeStamp.TimestampInMinutes(timeToSkipTo); Debug.Log("Time to skip to:" + timeToSkipInMinutes); int timeNowInMinutes = GameTimeStamp.TimestampInMinutes(timestamp); Debug.Log("Time now: " + timeNowInMinutes); int differenceInMinutes = timeToSkipInMinutes - timeNowInMinutes; Debug.Log(differenceInMinutes + " minutes will be advanced"); //Check if the timestamp to skip to has already been reached if (differenceInMinutes <= 0) return; for (int i = 0; i < differenceInMinutes; i++) { Tick(); } } //Get the timestamp public GameTimeStamp GetGameTimestamp() { //Return a cloned instance return new GameTimeStamp(timestamp); } //Handling Listeners //Add the object to the list of listeners public void RegisterTracker(ITimeTracker listener) { listeners.Add(listener); } //Remove the object from the list of listeners public void UnregisterTracker(ITimeTracker listener) { listeners.Remove(listener); } }
2. About Terrain
a. Installing Terrain Package & Previewing Packages
Unity has some built-in terrain tools, as well as a terrain package.
i. To access the Unity terrain package, go to Window > Package Manager.
ii. Make sure Unit Registry is selected, then go into Advanced Project Settings.
iii. Select Enable Preview Packages
iv. Type into the search bar “Terrain” and the Terrain Tools package should show up. Then click Install to add it to your project.
Note: You’ll see after you’ve downloaded it that there is an option to Download Asset Samples from Asset Store. The asset store has some samples of terrain that you can select from, so you can actually browse through these assets and use any ones that you might like.
v. Go to Window > Terrain > Terrain Toolbox, and drag it next to the Inspector.
The terrain toolbox enables you to create new terrains, edit its utilities, visualisation modes, and more. These tools help with creating terrains easily with functionalities like adjusting its settings and creating terrain presets!
b. Building the Terrain
Let’s begin by creating a new terrain. For this step, we’ll set the total terrain width and total terrain height to 500m, and the terrain height to 1000m. Then, click “Create” at the very bottom. This will create a terrain group in which it has the terrain.
If we go into the Terrain Group, you’ll notice that the inspector shows that it has a script and a group ID. This ID is used for all terrains created within the group if you have more than one.
i. Terrain Brushes
The terrain tools are quite powerful in that it allows you to manipulate the terrains in multitudes of methods, depending on what you want. This includes creating neighbor terrains, painting terrains, and more.
Here is a rundown of the main functionalities of terrain brushes:
Raising or Lowering the Terrain | Enables you to adjust the terrain’s height in the brush shape that you have selected |
Set Height | Enables you to sample height level |
Sculpt | Create terraces, bridges, clone terrains, add noise to create the illusion of randomness of the terrain |
Effects | Enables the addition of contrast between different portions of terrain, ability to sharped peaks of raised terrains, or flatten slopes |
Erosion | Simulates different types of erosion on terrain |
Paint Holes | Enables you to create holes in terrain |
Stamp Terrain | Allows you to sample the terrain and use it as a brush |
Transform | Enables you to pinch, smudge, and twist your terrain |
c. Expanding the Terrain
You can expand the terrain by creating neighbour terrains. This automatically matches the height to your selected terrain, and you can have them mirror or clamp your terrain!
d. Sample of completed Terrain
Conclusion
Now, with your terrain, you can go ahead at put your player on it and let them enjoy their new little playground. Thank you for tuning in to the second part of this Subnautica game tutorial. We hope this was useful! Let us know what you think in the comments section below, or on the video itself!
Below are the final codes that we have at the end of this part. If you like to, you can also download the project files.
PlayerController.cs
using System.Collections; using System.Collections.Generic; using UnityEngine; public class PlayerController : MonoBehaviour { //reference the transform Transform t; public static bool inWater; public static bool isSwimming; //if not in water, walk //if in water and not swimming, float //if in water and swimming, swim public LayerMask waterMask; [Header("Player Rotation")] public float sensitivity = 1; //clamp variables public float rotationMin; public float rotationMax; //mouse input variables float rotationX; float rotationY; [Header("Player Movement")] public float speed = 1; float moveX; float moveY; float moveZ; Rigidbody rb; // Start is called before the first frame update void Start() { rb = GetComponent<Rigidbody>(); t = this.transform; Cursor.lockState = CursorLockMode.Locked; inWater = false; } private void FixedUpdate() { SwimmingOrFloating(); Move(); } private void OnTriggerEnter(Collider other) { SwitchMovement(); } private void OnTriggerExit(Collider other) { SwitchMovement(); } void SwitchMovement() { //toggle inWater 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("isSwiming = " + isSwimming); } // Update is called once per frame void Update() { LookAround(); //debug function to unlock cursor if (Input.GetKey(KeyCode.Escape)) { Cursor.lockState = CursorLockMode.None; } } void LookAround() { //get the mous input rotationX += Input.GetAxis("Mouse X")*sensitivity; rotationY += Input.GetAxis("Mouse Y")*sensitivity; //clamp the y rotation 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"); moveY = Input.GetAxis("Vertical"); moveZ = Input.GetAxis("Forward"); //check if the player is in water if (inWater) { rb.velocity = new Vector2(0,0); } else { //check if the player is standing still if(moveX == 0 && moveZ == 0) { rb.velocity = new Vector2(0, rb.velocity.y); } } 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 if the player is swimming under water or floating along the top if (!isSwimming) { //move the player (floating ver) //clamp the moveY value, so they cannot use space or shift to move up moveY = Mathf.Min(moveY, 0); //conver the local direction vector into a worldspace vector/ Vector3 clampedDirection = t.TransformDirection(new Vector3(moveX, moveY, moveZ)); //clamp the values of this worldspace vector 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); } } } }
GameTimeStamp.cs
using System.Collections; using System.Collections.Generic; using UnityEngine; [System.Serializable] public class GameTimeStamp { public int day; public int hour; public int minute; //class constructor - sets up the class public GameTimeStamp(int day, int hour, int minute) { this.day = day; this.hour = hour; this.minute = minute; } //create a new timestamp from an existing one public GameTimeStamp(GameTimeStamp timeStamp) { this.day = timeStamp.day; this.hour = timeStamp.hour; this.minute = timeStamp.minute; } //function to increase the time incrementally public void UpdateClock() { minute++; //60 minutes in an hour if(minute >= 60) { //reset minutes minute = 0; hour++; } //20 hours in 1 day if(hour >= 20) { //reset hours hour = 0; day++; } } //convert hours to minutes public static int HoursToMinutes(int hour) { //60 minutes = 1 hour return hour * 60; } //returns the current time stamp in minutes public static int TimestampInMinutes(GameTimeStamp timestamp) { return (HoursToMinutes(timestamp.day * 20) + HoursToMinutes(timestamp.hour) + timestamp.minute); } }
TimeManager.cs
using System.Collections; using System.Collections.Generic; using UnityEngine; public class TimeManager : MonoBehaviour { public static TimeManager Instance { get; private set; } [Header("Internal Clock")] [SerializeField] GameTimeStamp timestamp; public float timeScale = 1.0f; [Header("Day and Night Cycle")] //the transform of the directional light (sun) public Transform sunTransform; Vector3 sunAngle; //list of objects to inpform of changes to the time List<ITimeTracker> listeners = new List<ITimeTracker>(); private void Awake() { //If there is more than one instance, destroy the extra if (Instance != null && Instance != this) { Destroy(this); } else { //set the static instance to this instance Instance = this; } } // Start is called before the first frame update void Start() { //initialise the time stamp timestamp = new GameTimeStamp(1, 6, 0); StartCoroutine(TimeUpdate()); } IEnumerator TimeUpdate() { while (true) { Tick(); yield return new WaitForSeconds(1 / timeScale); } } void Tick() { timestamp.UpdateClock(); UpdateSunMovement(); //inform each listener of the new timestate foreach(ITimeTracker listener in listeners) { listener.ClockUpdate(timestamp); } } //Day and Night cycle void UpdateSunMovement() { //convert the current time to minutes int timeInMinutes = GameTimeStamp.HoursToMinutes(timestamp.hour) + timestamp.minute; //during daytime //sun moves 0.2 degrees in a minute //during nighttime //sun moves 0.6 degrees in a minute float sunAngle = 0; if(timeInMinutes <= 15 * 60) { sunAngle = 0.2f * timeInMinutes; } else if(timeInMinutes > 15* 60) { sunAngle = 180f + 0.6f * (timeInMinutes - (15 * 60)); } //Apply angle to the directional light //sunTransform.eulerAngles = new Vector3(sunAngle, 0, 0); this.sunAngle = new Vector3(sunAngle, 0, 0); } // Update is called once per frame void Update() { sunTransform.rotation = Quaternion.Slerp(sunTransform.rotation, Quaternion.Euler(sunAngle), 1f * Time.deltaTime); } //function to skip time public void SkipTime(GameTimeStamp timeToSkipTo) { //convert to minutes int timeToSkipInMinutes = GameTimeStamp.TimestampInMinutes(timeToSkipTo); Debug.Log("Time Skip to:" + timeToSkipInMinutes); int timeNowInMinutes = GameTimeStamp.TimestampInMinutes(timestamp); Debug.Log("Time now: " + timeNowInMinutes); int differenceInMinutes = timeToSkipInMinutes - timeNowInMinutes; Debug.Log("Skip " + differenceInMinutes + " minutes"); //Check if the timestamp to skip to has already been reached if (differenceInMinutes <= 0) return; for (int i=0; i < differenceInMinutes; i++) { Tick(); } } public GameTimeStamp GetGameTimestamp() { //return a cloned instance return new GameTimeStamp(timestamp); } //handle listeners list //add an object to the list public void RegisterTracker(ITimeTracker listener) { listeners.Add(listener); } //Remove an object from the list public void UnregisterTracker(ITimeTracker listener) { listeners.Remove(listener); } }
ITimeTracker.cs
using System.Collections; using System.Collections.Generic; using UnityEngine; public interface ITimeTracker { void ClockUpdate(GameTimeStamp timestamp); }
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)); } //displaye 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); } } }