Ever wanted to create a game like Harvest Moon in Unity? Check out Part 32 of our guide here, where we added touchups to our in-game scenes and a new weather system. You can also find Part 31 of our guide here, where we went through the food system, Inn characters and lighting.
1. Touching up scenes and adding the town square
Before we start on our basic weather system, we first need to touch up all the overworld scenes in our project.
a. Adding new furniture from imported asset packs
This whole touching-up process can be summarised into these simple steps:
- Add suitable furniture/props to decorate each scene
- Rebake the navmesh surface
- Rebake the lighting settings
- Adjust the location entry points to fit each expanded/relocated scene
- Adjust the NPC locations during their schedules
Repeat the same five steps for each scene to give more character to each of them. Here is the list of new imported asset packs that we used in this part:
- Medieval Interior Asset Pack by SOI – Used for the Inn Scene
- Village Asset Pack by Styloo – Used for both Town and Town Square
- 700+ Pixel Art Texture – 256×256 by FlakDeu – Textures for general walls and floors.
- Furniture Kit by Kenney – Large interior furniture pack for homes
And we can see there is a significant improvement in the visuals of our game.

b. Creating the town square
The town square is mainly a separate part of the town that will hold special events.
Thus, we will leave the plot in the middle of the land that will have exclusive features like outdoor benches, a stage and pop-up booths which will only appear when an event is happening.
And just like the rest of our scenes, add the location entry points, baked lighting and navmesh, and the preview of this scene in the main town scene.

2. Building our Weather System
Now let’s start with our weather system, it will consist of 5 different weather states: Sunny, Rainy, Snow, Heavy Snow and Typhoon
a. Creating our Rain and Snow particles
We can create simple rain using Unity’s inbuilt particle system. To set it up, here are the values in the particle system that need to be modified:
- Duration – 5 seconds
- Looping box is checked
- Start Lifetime – 2
- 3D Start Size is set to Random Between Two Constants:
- Ranges between 0.1, 0.5, 0.1 to 0.1, 1, 0.1
- Max Particles – 3000
- Emission rate over time – 3000
- Shape – Box
- Scale is 50, 1, 50
- Velocity over Lifetime is set to Random Between Two Constants
- Ranges between 1, -45, 1 to 0, -55, 0
- Collision is
- Type – World
- Dampen – 1
- Renderer material is Default – Particle
For our snow particles, we will clone the rain particles and make a few adjustments:
- Duration – 4 seconds
- Prewarm is checked
- Start Lifetime – 4
- 3D Start Size – 0.5, 0.5, 0.5
- Emission rate over time – 500
And we should have 2 particle systems that look like this:


To create heavy snow particles, we will clone the snow particle system and change the emission rate over time from 500 to 3000. Put all three particle systems under our Essentials parent game object.
b. Programming the Weather System
Let’s start by making our scriptable object, WeatherData. It will contain the enum of our different weather types and the probabilities of them in each season.
WeatherData.cs
using UnityEngine;
[CreateAssetMenu(fileName = "WeatherData", menuName = "Weather/WeatherData")]
//Script for handling the Weather Data
public class WeatherData : ScriptableObject
{
public enum WeatherType {
Sunny, Rain, Snow, Typhoon, HeavySnow
}
public WeatherProbability[] springWeather;
public WeatherProbability[] summerWeather;
public WeatherProbability[] fallWeather;
public WeatherProbability[] winterWeather;
}
We will also need a WeatherProbability struct, that will pair each WeatherData with a value.
WeatherProbability.cs
using UnityEngine;
[System.Serializable]
public struct WeatherProbability
{
public WeatherData.WeatherType weatherType;
[Range(0f, 1f)]
public float probability;
}
Lastly, we will need to create the WeatherManager class, which will track, randomise and update the weather in the game. We added a debug log at the bottom to track in the debug panel what is the current weather state in the game.
WeatherManager.cs
using UnityEngine;
public class WeatherManager : MonoBehaviour, ITimeTracker
{
public static WeatherManager Instance { get; private set; }
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;
}
}
public WeatherData.WeatherType WeatherToday { get; private set; }
public WeatherData.WeatherType WeatherTomorrow { get; private set; }
//Check if the weather has been set before
bool weatherSet = false;
//The weather data
[SerializeField] WeatherData weatherData;
void Start()
{
//Add this to TimeManager's Listener list
TimeManager.Instance.RegisterTracker(this);
}
public WeatherData.WeatherType ComputeWeather(GameTimestamp.Season season)
{
if(weatherData == null)
{
throw new System.Exception("No weather data loaded");
}
//What are the possible weathers to compute
WeatherProbability[] weatherSet = null;
switch (season)
{
case GameTimestamp.Season.Spring:
weatherSet = weatherData.springWeather;
break;
case GameTimestamp.Season.Summer:
weatherSet = weatherData.summerWeather;
break;
case GameTimestamp.Season.Fall:
weatherSet = weatherData.fallWeather;
break;
case GameTimestamp.Season.Winter:
weatherSet = weatherData.winterWeather;
break;
}
//Roll a random value
float randomValue = Random.Range(0, 1f);
//Initialise probability
float culmProbability = 0;
foreach(WeatherProbability weatherProbability in weatherSet)
{
culmProbability += weatherProbability.probability;
//If in the probability
if (randomValue <= culmProbability) {
return weatherProbability.weatherType;
}
}
//Sunny by default
return WeatherData.WeatherType.Sunny;
}
public void ClockUpdate(GameTimestamp timestamp)
{
//Check if it is 6am
if(timestamp.hour == 6 && timestamp.minute == 0)
{
//Set the current weather
if (!weatherSet)
{
WeatherToday = ComputeWeather(timestamp.season);
}
else
{
WeatherToday = WeatherTomorrow;
}
//Set the forecast
WeatherTomorrow = ComputeWeather(timestamp.season);
weatherSet = true;
Debug.Log("The weather is " + WeatherToday.ToString());
}
}
}
Next, we will create a WeatherData scriptable object and set the frequency of each weather type for each season

c. Implementing the Weather Effects
Now we will make the weather effects appear in the game. Create a new script WeatherEffectController, which will manage what particles will follow the player depending on the weather state
WeatherEffectController.cs
using System.Collections;
using UnityEngine;
public class WeatherEffectController : MonoBehaviour
{
[SerializeField]
GameObject rain,snow,heavySnow;
Transform player;
private void Start()
{
SceneTransitionManager.Instance.onLocationLoad.AddListener(LoadParticle);
player = FindFirstObjectByType<PlayerController>().transform;
}
private void FixedUpdate()
{
//Move the particle to the player
if (player != null)
{
Vector3 pos = new Vector3(player.position.x, transform.position.y, player.position.z);
transform.position = Vector3.Lerp(transform.position, pos, Time.fixedDeltaTime);
}
}
void LoadParticle()
{
//Disable everything
rain.SetActive(false);
snow.SetActive(false);
heavySnow.SetActive(false);
//Move the particle to the player
if (player != null)
{
Vector3 pos = new Vector3(player.position.x, transform.position.y, player.position.z);
transform.position = pos;
}
//Check if indoor
if (SceneTransitionManager.Instance.CurrentlyIndoor())
{
return;
}
if(WeatherManager.Instance.WeatherToday == WeatherData.WeatherType.Rain)
{
//Rain
rain.SetActive(true);
}
if (WeatherManager.Instance.WeatherToday == WeatherData.WeatherType.Snow)
{
snow.SetActive(true);
}
if (WeatherManager.Instance.WeatherToday == WeatherData.WeatherType.HeavySnow)
{
heavySnow.SetActive(true);
}
}
}
d. Creating a Weather Forecast System
Let’s create a simple weather forecast system, which will tell the player what tomorrow’s weather will be through their home television.
We will first create an interactive object, Television, which will display the weather the next day.
Television.cs
using UnityEngine;
public class Television : InteractableObject
{
public override void Pickup()
{
//For now, just do a weather forecast
WeatherData.WeatherType weather = WeatherManager.Instance.WeatherTomorrow;
DialogueManager.Instance.StartDialogue(DialogueManager.CreateSimpleMessage(
"The weather tomorrow is " + weather.ToString()
));;
}
}
Afterwards, the script will be attached to a TV game object in the player’s home as shown below:

This feature should be executed like this once the TV game object is interacted with:

e. Adding Gameplay Mechanics to the Weather
Now that we added the weather visual effects into the game, we also want them to affect the gameplay.
Firstly, we will make it such that when it rains, the farming plots outside will be automatically watered. In GameStateManager, we will create a function called RainOnLand that starts at 6:01 am to check if it’s raining and change the land status to watered.
GameStateManager.cs
using System;
using System.Collections;
using System.Collections.Generic;
using System.Runtime.InteropServices.WindowsRuntime;
using UnityEngine;
using UnityEngine.Events;
public class GameStateManager : MonoBehaviour, ITimeTracker
{
//lights that turn on at night
public GameObject[] nightLights;
public static GameStateManager Instance { get; private set; }
//Check if the screen has finished fading out
bool screenFadedOut;
//To track interval updates
private int minutesElapsed = 0;
//Event triggered every 15 minutes
public UnityEvent onIntervalUpdate;
[SerializeField]
GameBlackboard blackboard = new GameBlackboard();
//This blackboard will not be saved
GameBlackboard sceneItemsBoard = new GameBlackboard();
const string TIMESTAMP = "Timestamp";
public GameBlackboard GetBlackboard()
{
return blackboard;
}
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()
{
//Add this to TimeManager's Listener list
TimeManager.Instance.RegisterTracker(this);
SceneTransitionManager.Instance.onLocationLoad.AddListener(RenderPersistentObjects);
}
public void ClockUpdate(GameTimestamp timestamp)
{
UpdateShippingState(timestamp);
UpdateFarmState(timestamp);
IncubationManager.UpdateEggs();
blackboard.SetValue(TIMESTAMP, timestamp);
if (timestamp.hour == 0 && timestamp.minute == 0)
{
OnDayReset();
}
//6:01 am, do rain on land logic
if(timestamp.hour == 6 && timestamp.minute == 1) {
RainOnLand();
}
if(minutesElapsed >= 15)
{
minutesElapsed = 0;
onIntervalUpdate?.Invoke();
}
else
{
minutesElapsed++;
}
// the lights will turn on at 6pm
nightLights = GameObject.FindGameObjectsWithTag("Lights");
foreach (GameObject i in nightLights)
{
if (timestamp.hour >= 18)
{
i.GetComponent<Light>().enabled = true;
}
else
{
i.GetComponent<Light>().enabled = false;
}
}
}
//Called when the day has been reset
void OnDayReset()
{
Debug.Log("Day has been reset");
foreach(NPCRelationshipState npc in RelationshipStats.relationships)
{
npc.hasTalkedToday = false;
npc.giftGivenToday = false;
}
AnimalFeedManager.ResetFeedboxes();
AnimalStats.OnDayReset();
//
}
void UpdateShippingState(GameTimestamp timestamp)
{
//Check if the hour is here (Exactly 1800 hours)
if(timestamp.hour == ShippingBin.hourToShip && timestamp.minute == 0)
{
ShippingBin.ShipItems();
}
}
void RainOnLand()
{
//Check if raining
if (WeatherManager.Instance.WeatherToday != WeatherData.WeatherType.Rain) {
return;
}
//Retrieve the Land and Farm data from the static variable
List<LandSaveState> landData = LandManager.farmData.Item1;
for(int i=0; i< landData.Count; i++)
{
//Set the last watered to now
if (landData[i].landStatus != Land.LandStatus.Soil)
{
landData[i] = new LandSaveState(Land.LandStatus.Watered, TimeManager.Instance.GetGameTimestamp(), landData[i].obstacleStatus);
}
}
}
void UpdateFarmState(GameTimestamp timestamp)
{
//Update the Land and Crop Save states as long as the player is outside of the Farm scene
if (SceneTransitionManager.Instance.currentLocation != SceneTransitionManager.Location.Farm)
{
//If there is nothing to update to begin with, stop
if (LandManager.farmData == null) return;
//Retrieve the Land and Farm data from the static variable
List<LandSaveState> landData = LandManager.farmData.Item1;
List<CropSaveState> cropData = LandManager.farmData.Item2;
//If there are no crops planted, we don't need to worry about updating anything
if (cropData.Count == 0) return;
for (int i = 0; i < cropData.Count; i++)
{
//Get the crop and corresponding land data
CropSaveState crop = cropData[i];
LandSaveState land = landData[crop.landID];
//Check if the crop is already wilted
if (crop.cropState == CropBehaviour.CropState.Wilted) continue;
//Update the Land's state
land.ClockUpdate(timestamp);
//Update the crop's state based on the land state
if (land.landStatus == Land.LandStatus.Watered)
{
crop.Grow();
}
else if (crop.cropState != CropBehaviour.CropState.Seed)
{
crop.Wither();
}
//Update the element in the array
cropData[i] = crop;
landData[crop.landID] = land;
}
/*LandManager.farmData.Item2.ForEach((CropSaveState crop) => {
Debug.Log(crop.seedToGrow + "\n Health: " + crop.health + "\n Growth: " + crop.growth + "\n State: " + crop.cropState.ToString());
});*/
}
}
//Instantiate the GameObject such that it stays in the scene even after the player leaves
public GameObject PersistentInstantiate(GameObject gameObject, Vector3 position, Quaternion rotation)
{
GameObject item = Instantiate(gameObject, position, rotation);
//Save it to the blackboard
(GameObject, Vector3) itemInformation = (gameObject, position);
List<(GameObject, Vector3)> items = sceneItemsBoard.GetOrInitList<(GameObject, Vector3)>(SceneTransitionManager.Instance.currentLocation.ToString());
items.Add(itemInformation);
sceneItemsBoard.Debug();
return item;
}
public void PersistentDestroy(GameObject item)
{
if (sceneItemsBoard.TryGetValue(SceneTransitionManager.Instance.currentLocation.ToString(), out List<(GameObject, Vector3)> items))
{
int index = items.FindIndex(i => i.Item2 == item.transform.position);
items.RemoveAt(index);
}
Destroy(item);
}
void RenderPersistentObjects()
{
Debug.Log("Rendering Persistent Objects");
if (sceneItemsBoard.TryGetValue(SceneTransitionManager.Instance.currentLocation.ToString(), out List<(GameObject, Vector3)> items)) {
foreach((GameObject, Vector3) item in items)
{
Instantiate(item.Item1, item.Item2, Quaternion.identity);
}
}
}
public void Sleep()
{
//Call a fadeout
UIManager.Instance.FadeOutScreen();
screenFadedOut = false;
StartCoroutine(TransitionTime());
//Restore Stamina fully when sleeping
PlayerStats.RestoreStamina();
}
IEnumerator TransitionTime()
{
//Calculate how many ticks we need to advance the time to 6am
//Get the time stamp of 6am the next day
GameTimestamp timestampOfNextDay = TimeManager.Instance.GetGameTimestamp();
timestampOfNextDay.day += 1;
timestampOfNextDay.hour = 6;
timestampOfNextDay.minute = 0;
Debug.Log(timestampOfNextDay.day + " " + timestampOfNextDay.hour + ":" + timestampOfNextDay.minute);
//Wait for the scene to finish fading out before loading the next scene
while (!screenFadedOut)
{
yield return new WaitForSeconds(1f);
}
TimeManager.Instance.SkipTime(timestampOfNextDay);
//Save
SaveManager.Save(ExportSaveState());
//Reset the boolean
screenFadedOut = false;
UIManager.Instance.ResetFadeDefaults();
}
//Called when the screen has faded out
public void OnFadeOutComplete()
{
screenFadedOut = true;
}
public GameSaveState ExportSaveState()
{
//Retrieve Farm Data
FarmSaveState farmSaveState = FarmSaveState.Export();
//Retrieve inventory data
InventorySaveState inventorySaveState = InventorySaveState.Export();
PlayerSaveState playerSaveState = PlayerSaveState.Export();
//Time
GameTimestamp timestamp = TimeManager.Instance.GetGameTimestamp();
//Relationships
//RelationshipSaveState relationshipSaveState = RelationshipSaveState.Export();
WeatherSaveState weather = WeatherSaveState.Export();
return new GameSaveState(blackboard, farmSaveState, inventorySaveState, timestamp, playerSaveState, weather);
}
public void LoadSave()
{
//Retrieve the loaded save
GameSaveState save = SaveManager.Load();
//Load up the parts
blackboard = save.blackboard;
blackboard.Debug();
//Time
TimeManager.Instance.LoadTime(save.timestamp);
//Inventory
save.inventorySaveState.LoadData();
//Farming data
save.farmSaveState.LoadData();
//Player Stats
save.playerSaveState.LoadData();
//Weather
save.weatherSaveState.LoadData();
//Relationship stats
AnimalStats.LoadStats();
RelationshipStats.LoadStats();
//save.relationshipSaveState.LoadData();
}
}
Now in our Land script, we will add logic to check for rain
Land.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Land : MonoBehaviour, ITimeTracker
{
public int id;
public enum LandStatus
{
Soil, Farmland, Watered
}
public LandStatus landStatus;
public Material soilMat, farmlandMat, wateredMat;
new Renderer renderer;
//The selection gameobject to enable when the player is selecting the land
public GameObject select;
//Cache the time the land was watered
GameTimestamp timeWatered;
[Header("Crops")]
//The crop prefab to instantiate
public GameObject cropPrefab;
//The crop currently planted on the land
CropBehaviour cropPlanted = null;
//Obstacles
public enum FarmObstacleStatus { None, Rock, Wood, Weeds}
[Header("Obstacles")]
public FarmObstacleStatus obstacleStatus;
public GameObject rockPrefab, woodPrefab, weedsPrefab;
//Store the instantiated obstacle as a variable so we can access it
GameObject obstacleObject;
// Start is called before the first frame update
void Start()
{
//Get the renderer component
renderer = GetComponent<Renderer>();
//Set the land to soil by default
SwitchLandStatus(LandStatus.Soil);
//Deselect the land by default
Select(false);
//Add this to TimeManager's Listener list
TimeManager.Instance.RegisterTracker(this);
}
public void LoadLandData(LandStatus landStatusToSwitch, GameTimestamp lastWatered, FarmObstacleStatus obstacleStatusToSwitch)
{
//Set land status accordingly
landStatus = landStatusToSwitch;
timeWatered = lastWatered;
Material materialToSwitch = soilMat;
//Decide what material to switch to
switch (landStatusToSwitch)
{
case LandStatus.Soil:
//Switch to the soil material
materialToSwitch = soilMat;
break;
case LandStatus.Farmland:
//Switch to farmland material
materialToSwitch = farmlandMat;
break;
case LandStatus.Watered:
//Switch to watered material
materialToSwitch = wateredMat;
break;
}
//Get the renderer to apply the changes
renderer.material = materialToSwitch;
switch (obstacleStatusToSwitch)
{
case FarmObstacleStatus.None:
//Destroy the Obstacle object, if any
if (obstacleObject != null) Destroy(obstacleObject);
break;
case FarmObstacleStatus.Rock:
//Instantiate the obstacle prefab on the land and assign it to obstacleObject
obstacleObject = Instantiate(rockPrefab, transform);
break;
case FarmObstacleStatus.Wood:
//Instantiate the obstacle prefab on the land and assign it to obstacleObject
obstacleObject = Instantiate(woodPrefab, transform);
break;
case FarmObstacleStatus.Weeds:
//Instantiate the obstacle prefab on the land and assign it to obstacleObject
obstacleObject = Instantiate(weedsPrefab, transform);
break;
}
//Move the obstacle object to the top of the land gameobject
if (obstacleObject != null) obstacleObject.transform.position = new Vector3(transform.position.x, 0.1f, transform.position.z);
//Set the status accordingly
obstacleStatus = obstacleStatusToSwitch;
}
public void SwitchLandStatus(LandStatus statusToSwitch)
{
//Set land status accordingly
landStatus = statusToSwitch;
Material materialToSwitch = soilMat;
//Decide what material to switch to
switch (statusToSwitch)
{
case LandStatus.Soil:
//Switch to the soil material
materialToSwitch = soilMat;
break;
case LandStatus.Farmland:
//Switch to farmland material
materialToSwitch = farmlandMat;
break;
case LandStatus.Watered:
//Switch to watered material
materialToSwitch = wateredMat;
//Cache the time it was watered
timeWatered = TimeManager.Instance.GetGameTimestamp();
break;
}
//Get the renderer to apply the changes
renderer.material = materialToSwitch;
LandManager.Instance.OnLandStateChange(id, landStatus, timeWatered, obstacleStatus);
}
public void SetObstacleStatus(FarmObstacleStatus statusToSwitch)
{
switch (statusToSwitch)
{
case FarmObstacleStatus.None:
//Destroy the Obstacle object, if any
if (obstacleObject != null) Destroy(obstacleObject);
break;
case FarmObstacleStatus.Rock:
//Instantiate the obstacle prefab on the land and assign it to obstacleObject
obstacleObject = Instantiate(rockPrefab, transform);
break;
case FarmObstacleStatus.Wood:
//Instantiate the obstacle prefab on the land and assign it to obstacleObject
obstacleObject = Instantiate(woodPrefab, transform);
break;
case FarmObstacleStatus.Weeds:
//Instantiate the obstacle prefab on the land and assign it to obstacleObject
obstacleObject = Instantiate(weedsPrefab, transform);
break;
}
//Move the obstacle object to the top of the land gameobject
if(obstacleObject != null) obstacleObject.transform.position = new Vector3(transform.position.x, 0.1f, transform.position.z);
//Set the status accordingly
obstacleStatus = statusToSwitch;
LandManager.Instance.OnLandStateChange(id, landStatus, timeWatered, obstacleStatus);
}
public void Select(bool toggle)
{
select.SetActive(toggle);
}
//When the player presses the interact button while selecting this land
public void Interact()
{
//Check the player's tool slot
ItemData toolSlot = InventoryManager.Instance.GetEquippedSlotItem(InventorySlot.InventoryType.Tool);
//If there's nothing equipped, return
if (!InventoryManager.Instance.SlotEquipped(InventorySlot.InventoryType.Tool))
{
return;
}
//Try casting the itemdata in the toolslot as EquipmentData
EquipmentData equipmentTool = toolSlot as EquipmentData;
//Check if it is of type EquipmentData
if(equipmentTool != null)
{
//Get the tool type
EquipmentData.ToolType toolType = equipmentTool.toolType;
switch (toolType)
{
case EquipmentData.ToolType.Hoe:
SwitchLandStatus(LandStatus.Farmland);
break;
case EquipmentData.ToolType.WateringCan:
//The land must be tilled first
if (landStatus != LandStatus.Soil)
{
SwitchLandStatus(LandStatus.Watered);
}
break;
case EquipmentData.ToolType.Shovel:
//Remove the crop from the land
if(cropPlanted != null)
{
cropPlanted.RemoveCrop();
}
//Remove weed obstacle
if (obstacleStatus == FarmObstacleStatus.Weeds) SetObstacleStatus(FarmObstacleStatus.None);
break;
case EquipmentData.ToolType.Axe:
//Remove wood obstacle
if (obstacleStatus == FarmObstacleStatus.Wood) SetObstacleStatus(FarmObstacleStatus.None);
break;
case EquipmentData.ToolType.Pickaxe:
//Remove rock obstacle
if (obstacleStatus == FarmObstacleStatus.Rock) SetObstacleStatus(FarmObstacleStatus.None);
break;
}
//We don't need to check for seeds if we have already confirmed the tool to be an equipment
return;
}
//Try casting the itemdata in the toolslot as SeedData
SeedData seedTool = toolSlot as SeedData;
///Conditions for the player to be able to plant a seed
///1: He is holding a tool of type SeedData
///2: The Land State must be either watered or farmland
///3. There isn't already a crop that has been planted
///4. There are no obstacles
if(seedTool != null && landStatus != LandStatus.Soil && cropPlanted == null && obstacleStatus == FarmObstacleStatus.None)
{
SpawnCrop();
//Plant it with the seed's information
cropPlanted.Plant(id, seedTool);
//Consume the item
InventoryManager.Instance.ConsumeItem(InventoryManager.Instance.GetEquippedSlot(InventorySlot.InventoryType.Tool));
}
}
public CropBehaviour SpawnCrop()
{
//Instantiate the crop object parented to the land
GameObject cropObject = Instantiate(cropPrefab, transform);
//Move the crop object to the top of the land gameobject
cropObject.transform.position = new Vector3(transform.position.x, 0.1f, transform.position.z);
//Access the CropBehaviour of the crop we're going to plant
cropPlanted = cropObject.GetComponent<CropBehaviour>();
return cropPlanted;
}
public void ClockUpdate(GameTimestamp timestamp)
{
//When raining, have it watered if it is farmland
if (WeatherManager.Instance.WeatherToday == WeatherData.WeatherType.Rain && landStatus == LandStatus.Farmland)
{
SwitchLandStatus(LandStatus.Watered);
}
//Checked if 24 hours has passed since last watered
if (landStatus == LandStatus.Watered)
{
//Hours since the land was watered
int hoursElapsed = GameTimestamp.CompareTimestamps(timeWatered, timestamp);
Debug.Log(hoursElapsed + " hours since this was watered");
//Grow the planted crop, if any
if(cropPlanted != null)
{
cropPlanted.Grow();
}
if(hoursElapsed > 24)
{
//Dry up (Switch back to farmland)
SwitchLandStatus(LandStatus.Farmland);
}
}
//Handle the wilting of the plant when the land is not watered
if(landStatus != LandStatus.Watered && cropPlanted != null)
{
//If the crop has already germinated, start the withering
if (cropPlanted.cropState != CropBehaviour.CropState.Seed)
{
cropPlanted.Wither();
}
}
}
private void OnDestroy()
{
//Unsubscribe from the list on destroy
TimeManager.Instance.UnregisterTracker(this);
}
}
Now during the case of a typhoon, we don’t want the player to leave the house as it is unsafe. Thus we created an if statement to restrict the player’s movement during a typhoon.
LocationEntryPoint.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class LocationEntryPoint : MonoBehaviour
{
[SerializeField]
SceneTransitionManager.Location locationToSwitch;
[SerializeField]
//For locking doors
bool locked = false;
private void OnTriggerEnter(Collider other)
{
//Check if the collider belongs to the player
if(other.tag == "Player")
{
if(WeatherManager.Instance.WeatherToday == WeatherData.WeatherType.Typhoon && SceneTransitionManager.Instance.currentLocation == SceneTransitionManager.Location.PlayerHome || locked)
{
//You cant go out in a typhoon
DialogueManager.Instance.StartDialogue(DialogueManager.CreateSimpleMessage("It's not safe to go out."));
return;
}
//Switch scenes to the location of the entry point
SceneTransitionManager.Instance.SwitchLocation(locationToSwitch);
}
//Characters walking through here and items thrown will be despawned
if(other.tag == "Item")
{
Destroy(other.gameObject);
}
}
}
f. Load Weather and Weather UI
Before adding the final touches to our weather system, we will create a debug feature to skip entire days by holding shift and pressing the right square bracket key
PlayerController.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class PlayerController : MonoBehaviour
{
//Movement Components
private CharacterController controller;
private Animator animator;
private float moveSpeed = 4f;
[Header("Movement System")]
public float walkSpeed = 4f;
public float runSpeed = 8f;
private float gravity = 9.81f;
//Interaction components
PlayerInteraction playerInteraction;
// Start is called before the first frame update
void Start()
{
//Get movement components
controller = GetComponent<CharacterController>();
animator = GetComponent<Animator>();
//Get interaction component
playerInteraction = GetComponentInChildren<PlayerInteraction>();
}
// Update is called once per frame
void Update()
{
//Runs the function that handles all movement
Move();
//Runs the function that handles all interaction
Interact();
//Debugging purposes only
//Skip the time when the right square bracket is pressed
if (Input.GetKey(KeyCode.RightBracket))
{
if (Input.GetKey(KeyCode.LeftShift)) {
//Advance the entire day
for(int i =0; i< 60*24; i++)
{
TimeManager.Instance.Tick();
}
} else
{
TimeManager.Instance.Tick();
}
}
//Toggle relationship panel
if (Input.GetKeyDown(KeyCode.R))
{
UIManager.Instance.ToggleRelationshipPanel();
}
if (Input.GetKeyDown(KeyCode.N))
{
SceneTransitionManager.Location location = LocationManager.GetNextLocation(SceneTransitionManager.Location.PlayerHome, SceneTransitionManager.Location.Farm);
Debug.Log(location);
}
}
public void Interact()
{
//Tool interaction
if (Input.GetButtonDown("Fire1"))
{
//Interact
playerInteraction.Interact();
}
//Item interaction
if (Input.GetButtonDown("Fire2"))
{
playerInteraction.ItemInteract();
}
//Keep items
if (Input.GetButtonDown("Fire3"))
{
playerInteraction.ItemKeep();
}
}
public void Move()
{
//Get the horizontal and vertical inputs as a number
float horizontal = Input.GetAxisRaw("Horizontal");
float vertical = Input.GetAxisRaw("Vertical");
//Direction in a normalised vector
Vector3 dir = new Vector3(horizontal, 0f, vertical).normalized;
Vector3 velocity = moveSpeed * Time.deltaTime * dir;
if (controller.isGrounded)
{
velocity.y = 0;
}
velocity.y -= Time.deltaTime * gravity;
//Is the sprint key pressed down?
if (Input.GetButton("Sprint"))
{
//Set the animation to run and increase our movespeed
moveSpeed = runSpeed;
animator.SetBool("Running", true);
} else
{
//Set the animation to walk and decrease our movespeed
moveSpeed = walkSpeed;
animator.SetBool("Running", false);
}
//Check if there is movement
if (dir.magnitude >= 0.1f)
{
//Look towards that direction
transform.rotation = Quaternion.LookRotation(dir);
//Move if allowed
if (controller.enabled)
{
controller.Move(velocity);
}
}
//Animation speed parameter
animator.SetFloat("Speed", dir.magnitude);
}
}
Now it’s time to add the weather into our save system to make it persist across game states.
We will create a new struct WeatherSaveState.
WeatherSaveState.cs
using UnityEngine;
[System.Serializable]
public struct WeatherSaveState
{
public WeatherData.WeatherType weather;
public WeatherSaveState(WeatherData.WeatherType weather)
{
this.weather = weather;
}
public void LoadData()
{
WeatherManager.Instance.LoadWeather(this);
}
public static WeatherSaveState Export()
{
return new WeatherSaveState(WeatherManager.Instance.WeatherTomorrow);
}
}
Next, we will update our GameSaveState to include the weather.
GameSaveState.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
[System.Serializable]
public class GameSaveState
{
//Blackboard
public GameBlackboard blackboard;
//Farm Data
public FarmSaveState farmSaveState;
//Inventory
public InventorySaveState inventorySaveState;
//Time
public GameTimestamp timestamp;
//PlayerStats
public PlayerSaveState playerSaveState;
//Weather State
public WeatherSaveState weatherSaveState;
//Relationships
//public RelationshipSaveState relationshipSaveState;
public GameSaveState(
GameBlackboard blackboard,
FarmSaveState farmSaveState,
InventorySaveState inventorySaveState,
GameTimestamp timestamp,
PlayerSaveState playerSaveState,
WeatherSaveState weatherSaveState
//RelationshipSaveState relationshipSaveState
)
{
this.blackboard = blackboard;
this.farmSaveState = farmSaveState;
this.inventorySaveState = inventorySaveState;
this.timestamp = timestamp;
this.playerSaveState = playerSaveState;
this.weatherSaveState = weatherSaveState;
//this.relationshipSaveState = relationshipSaveState;
}
}
Then we naturally have to add a LoadWeather function to the WeatherManager script
WeatherManager.cs
using UnityEngine;
public class WeatherManager : MonoBehaviour, ITimeTracker
{
public static WeatherManager Instance { get; private set; }
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;
}
}
public WeatherData.WeatherType WeatherToday { get; private set; }
public WeatherData.WeatherType WeatherTomorrow { get; private set; }
//Check if the weather has been set before
bool weatherSet = false;
//The weather data
[SerializeField] WeatherData weatherData;
void Start()
{
//Add this to TimeManager's Listener list
TimeManager.Instance.RegisterTracker(this);
}
public WeatherData.WeatherType ComputeWeather(GameTimestamp.Season season)
{
if(weatherData == null)
{
throw new System.Exception("No weather data loaded");
}
//What are the possible weathers to compute
WeatherProbability[] weatherSet = null;
switch (season)
{
case GameTimestamp.Season.Spring:
weatherSet = weatherData.springWeather;
break;
case GameTimestamp.Season.Summer:
weatherSet = weatherData.summerWeather;
break;
case GameTimestamp.Season.Fall:
weatherSet = weatherData.fallWeather;
break;
case GameTimestamp.Season.Winter:
weatherSet = weatherData.winterWeather;
break;
}
//Roll a random value
float randomValue = Random.Range(0, 1f);
//Initialise probability
float culmProbability = 0;
foreach(WeatherProbability weatherProbability in weatherSet)
{
culmProbability += weatherProbability.probability;
//If in the probability
if (randomValue <= culmProbability) {
return weatherProbability.weatherType;
}
}
//Sunny by default
return WeatherData.WeatherType.Sunny;
}
public void LoadWeather(WeatherSaveState saveState)
{
weatherSet = true;
WeatherToday = saveState.weather;
//Set the forecast
WeatherTomorrow = ComputeWeather(TimeManager.Instance.GetGameTimestamp().season);
}
public void ClockUpdate(GameTimestamp timestamp)
{
//Check if it is 6am
if(timestamp.hour == 6 && timestamp.minute == 0)
{
//Set the current weather
if (!weatherSet)
{
WeatherToday = ComputeWeather(timestamp.season);
}
else
{
WeatherToday = WeatherTomorrow;
}
//Set the forecast
WeatherTomorrow = ComputeWeather(timestamp.season);
weatherSet = true;
Debug.Log("The weather is " + WeatherToday.ToString());
}
}
}
Lastly, we will integrate the weather UI into the game, updating UIManager to include a ChangeStaminaUI function and calling that function in WeatherManager.
UIManager.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using TMPro;
public class UIManager : MonoBehaviour, ITimeTracker
{
public static UIManager Instance { get; private set; }
[Header("Screen Management")]
public GameObject menuScreen;
public enum Tab
{
Inventory, Relationships, Animals
}
//The current selected tab
public Tab selectedTab;
[Header("Status Bar")]
//Tool equip slot on the status bar
public Image toolEquipSlot;
//Tool Quantity text on the status bar
public TextMeshProUGUI toolQuantityText;
//Time UI
public TextMeshProUGUI timeText;
public TextMeshProUGUI dateText;
[Header("Inventory System")]
//The inventory panel
public GameObject inventoryPanel;
//The tool equip slot UI on the Inventory panel
public HandInventorySlot toolHandSlot;
//The tool slot UIs
public InventorySlot[] toolSlots;
//The item equip slot UI on the Inventory panel
public HandInventorySlot itemHandSlot;
//The item slot UIs
public InventorySlot[] itemSlots;
[Header("Item info box")]
public GameObject itemInfoBox;
public TextMeshProUGUI itemNameText;
public TextMeshProUGUI itemDescriptionText;
[Header("Screen Transitions")]
public GameObject fadeIn;
public GameObject fadeOut;
[Header("Prompts")]
public YesNoPrompt yesNoPrompt;
public NamingPrompt namingPrompt;
[SerializeField] InteractBubble interactBubble;
[Header("Player Stats")]
public TextMeshProUGUI moneyText;
[Header("Shop")]
public ShopListingManager shopListingManager;
[Header("Relationships")]
public RelationshipListingManager relationshipListingManager;
public AnimalListingManager animalRelationshipListingManager;
[Header("Calendar")]
public CalendarUIListing calendar;
[Header("Stamina")]
public Sprite[] staminaUI;
public Image StaminaUIImage;
public int staminaCount;
[Header("Weather")]
public Sprite[] weatherUI;
public Image WeatherUIImage;
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;
}
}
private void Start()
{
PlayerStats.RestoreStamina();
RenderInventory();
AssignSlotIndexes();
RenderPlayerStats();
DisplayItemInfo(null);
ChangeWeatherUI();
//Add UIManager to the list of objects TimeManager will notify when the time updates
TimeManager.Instance.RegisterTracker(this);
}
#region Prompts
public void TriggerNamingPrompt(string message, System.Action<string> onConfirmCallback)
{
//Check if another prompt is already in progress
if (namingPrompt.gameObject.activeSelf)
{
//Queue the prompt
namingPrompt.QueuePromptAction(() => TriggerNamingPrompt(message, onConfirmCallback));
return;
}
//Open the panel
namingPrompt.gameObject.SetActive(true);
namingPrompt.CreatePrompt(message, onConfirmCallback);
}
public void TriggerYesNoPrompt(string message, System.Action onYesCallback)
{
//Set active the gameobject of the Yes No Prompt
yesNoPrompt.gameObject.SetActive(true);
yesNoPrompt.CreatePrompt(message, onYesCallback);
}
#endregion
#region Tab Management
public void ToggleMenuPanel()
{
menuScreen.SetActive(!menuScreen.activeSelf);
OpenWindow(selectedTab);
TabBehaviour.onTabStateChange?.Invoke();
}
//Manage the opening of windows associated with the tab
public void OpenWindow(Tab windowToOpen)
{
//Disable all windows
relationshipListingManager.gameObject.SetActive(false);
inventoryPanel.SetActive(false);
animalRelationshipListingManager.gameObject.SetActive(false);
//Open the corresponding window and render it
switch (windowToOpen)
{
case Tab.Inventory:
inventoryPanel.SetActive(true);
RenderInventory();
break;
case Tab.Relationships:
relationshipListingManager.gameObject.SetActive(true);
relationshipListingManager.Render(RelationshipStats.relationships);
break;
case Tab.Animals:
animalRelationshipListingManager.gameObject.SetActive(true);
animalRelationshipListingManager.Render(AnimalStats.animalRelationships);
break;
}
//Set the selected tab
selectedTab = windowToOpen;
}
#endregion
#region Fadein Fadeout Transitions
public void FadeOutScreen()
{
fadeOut.SetActive(true);
}
public void FadeInScreen()
{
fadeIn.SetActive(true);
}
public void OnFadeInComplete()
{
//Disable Fade in Screen when animation is completed
fadeIn.SetActive(false);
}
//Reset the fadein fadeout screens to their default positions
public void ResetFadeDefaults()
{
fadeOut.SetActive(false);
fadeIn.SetActive(true);
}
#endregion
#region Inventory
//Iterate through the slot UI elements and assign it its reference slot index
public void AssignSlotIndexes()
{
for (int i = 0; i < toolSlots.Length; i++)
{
toolSlots[i].AssignIndex(i);
itemSlots[i].AssignIndex(i);
}
}
//Render the inventory screen to reflect the Player's Inventory.
public void RenderInventory()
{
//Get the respective slots to process
ItemSlotData[] inventoryToolSlots = InventoryManager.Instance.GetInventorySlots(InventorySlot.InventoryType.Tool);
ItemSlotData[] inventoryItemSlots = InventoryManager.Instance.GetInventorySlots(InventorySlot.InventoryType.Item);
//Render the Tool section
RenderInventoryPanel(inventoryToolSlots, toolSlots);
//Render the Item section
RenderInventoryPanel(inventoryItemSlots, itemSlots);
//Render the equipped slots
toolHandSlot.Display(InventoryManager.Instance.GetEquippedSlot(InventorySlot.InventoryType.Tool));
itemHandSlot.Display(InventoryManager.Instance.GetEquippedSlot(InventorySlot.InventoryType.Item));
//Get Tool Equip from InventoryManager
ItemData equippedTool = InventoryManager.Instance.GetEquippedSlotItem(InventorySlot.InventoryType.Tool);
//Text should be empty by default
toolQuantityText.text = "";
//Check if there is an item to display
if (equippedTool != null)
{
//Switch the thumbnail over
toolEquipSlot.sprite = equippedTool.thumbnail;
toolEquipSlot.gameObject.SetActive(true);
//Get quantity
int quantity = InventoryManager.Instance.GetEquippedSlot(InventorySlot.InventoryType.Tool).quantity;
if (quantity > 1)
{
toolQuantityText.text = quantity.ToString();
}
return;
}
toolEquipSlot.gameObject.SetActive(false);
}
//Iterate through a slot in a section and display them in the UI
void RenderInventoryPanel(ItemSlotData[] slots, InventorySlot[] uiSlots)
{
for (int i = 0; i < uiSlots.Length; i++)
{
//Display them accordingly
uiSlots[i].Display(slots[i]);
}
}
public void ToggleInventoryPanel()
{
//If the panel is hidden, show it and vice versa
inventoryPanel.SetActive(!inventoryPanel.activeSelf);
RenderInventory();
}
//Display Item info on the Item infobox
public void DisplayItemInfo(ItemData data)
{
//If data is null, reset
if (data == null)
{
itemNameText.text = "";
itemDescriptionText.text = "";
itemInfoBox.SetActive(false);
return;
}
itemInfoBox.SetActive(true);
itemNameText.text = data.name;
itemDescriptionText.text = data.description;
}
#endregion
#region Time
//Callback to handle the UI for time
public void ClockUpdate(GameTimestamp timestamp)
{
//Handle the time
//Get the hours and minutes
int hours = timestamp.hour;
int minutes = timestamp.minute;
//AM or PM
string prefix = "AM ";
//Convert hours to 12 hour clock
if (hours >= 12)
{
//Time becomes PM
prefix = "PM ";
//12 PM and later
hours = hours - 12;
Debug.Log(hours);
}
//Special case for 12am/pm to display it as 12 instead of 0
hours = hours == 0 ? 12 : hours;
//Format it for the time text display
timeText.text = prefix + hours + ":" + minutes.ToString("00");
//Handle the Date
int day = timestamp.day;
string season = timestamp.season.ToString();
string dayOfTheWeek = timestamp.GetDayOfTheWeek().ToString();
//Format it for the date text display
dateText.text = season + " " + day + " (" + dayOfTheWeek + ")";
}
#endregion
//Render the UI of the player stats in the HUD
public void RenderPlayerStats()
{
moneyText.text = PlayerStats.Money + PlayerStats.CURRENCY;
staminaCount = PlayerStats.Stamina;
ChangeStaminaUI();
}
//Open the shop window with the shop items listed
public void OpenShop(List<ItemData> shopItems)
{
//Set active the shop window
shopListingManager.gameObject.SetActive(true);
shopListingManager.Render(shopItems);
}
public void ToggleRelationshipPanel()
{
GameObject panel = relationshipListingManager.gameObject;
panel.SetActive(!panel.activeSelf);
//If open, render the screen
if (panel.activeSelf)
{
relationshipListingManager.Render(RelationshipStats.relationships);
}
}
public void InteractPrompt(Transform item, string message, float offset)
{
interactBubble.gameObject.SetActive(true);
interactBubble.transform.position = item.transform.position + new Vector3(0, offset, 0);
interactBubble.Display(message);
}
public void DeactivateInteractPrompt()
{
interactBubble.gameObject.SetActive(false);
}
public void ChangeStaminaUI()
{
if (staminaCount <= 45) StaminaUIImage.sprite = staminaUI[3]; // exhausted
else if (staminaCount <= 80) StaminaUIImage.sprite = staminaUI[2]; // tired
else if (staminaCount <= 115) StaminaUIImage.sprite = staminaUI[1]; // active
else if (staminaCount <= 150) StaminaUIImage.sprite = staminaUI[0]; // energised
}
public void ChangeWeatherUI()
{
var WeatherToday = WeatherManager.Instance.WeatherToday;
switch (WeatherToday)
{
case WeatherData.WeatherType.Sunny:
WeatherUIImage.sprite = weatherUI[0];
break;
case WeatherData.WeatherType.Rain:
WeatherUIImage.sprite = weatherUI[1];
break;
case WeatherData.WeatherType.Snow:
WeatherUIImage.sprite = weatherUI[2];
break;
case WeatherData.WeatherType.HeavySnow:
WeatherUIImage.sprite = weatherUI[3];
break;
}
}
}
3. Stacking Inventory Items and dialogue improvements
a. Freezing the player during ongoing dialogue
Firstly, we will add an if statement to DialogueManager so the player will not move during dialogue.
DialogueManager.cs
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using TMPro;
using System.Text.RegularExpressions;
public class DialogueManager : MonoBehaviour
{
public static DialogueManager Instance { get; private set; }
[Header("Dialogue Components")]
public GameObject dialoguePanel;
public TextMeshProUGUI speakerText;
public TextMeshProUGUI dialogueText;
//The lines to queue during the dialogue sequence
Queue<DialogueLine> dialogueQueue;
Action onDialogueEnd = null;
bool isTyping = false;
//TODO: Implement a proper player control stop mechanism
PlayerController playerController;
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;
}
}
private void Start()
{
playerController = FindFirstObjectByType<PlayerController>();
}
//Initialises the dialogue
public void StartDialogue(List<DialogueLine> dialogueLinesToQueue)
{
//Convert the list to a queue
dialogueQueue = new Queue<DialogueLine>(dialogueLinesToQueue);
if (playerController != null)
{
playerController.enabled = false;
}
UpdateDialogue();
}
//Initialises the dialogue, but with an Action to execute once it finishes
public void StartDialogue(List<DialogueLine> dialogueLinesToQueue, Action onDialogueEnd)
{
StartDialogue(dialogueLinesToQueue);
this.onDialogueEnd = onDialogueEnd;
}
//Cycle through the dialogue lines
public void UpdateDialogue()
{
if (isTyping)
{
isTyping = false;
return;
}
//Reset our dialogue text
dialogueText.text = string.Empty;
//Check if there are any more lines in the queue
if(dialogueQueue.Count == 0)
{
//If not, end the dialogue
EndDialogue();
return;
}
//The current dialogue line to put in
DialogueLine line = dialogueQueue.Dequeue();
Talk(line.speaker, ParseVariables(line.message));
}
//Closes the dialogue
public void EndDialogue()
{
//Close the dialogue panel
dialoguePanel.SetActive(false);
if (playerController != null)
{
playerController.enabled = true;
}
//Invoke whatever Action queued on dialogue end (if any)
onDialogueEnd?.Invoke();
//Reset the Action
onDialogueEnd = null;
}
public void Talk(string speaker, string message)
{
//Set the dialogue panel active
dialoguePanel.SetActive(true);
//Set the speaker text to the speaker
speakerText.text = speaker;
//If there is no speaker, do not show the speaker text panel
speakerText.transform.parent.gameObject.SetActive(speaker != "");
//Set the dialogue text to the message
//dialogueText.text = message;
StartCoroutine(TypeText(message));
}
IEnumerator TypeText(string textToType)
{
isTyping = true;
//Convert the string to an array of chars
char[] charsToType = textToType.ToCharArray();
for(int i =0; i < charsToType.Length; i++)
{
dialogueText.text += charsToType[i];
yield return new WaitForEndOfFrame();
//Skip the typing sequence and just show the full text
if (!isTyping)
{
dialogueText.text = textToType;
//Break out from the loop
break;
}
}
//Typing sequence complete
isTyping = false;
}
//Converts a simple string into a List of Dialogue lines to put into DialogueManager
public static List<DialogueLine> CreateSimpleMessage(string message)
{
//The Dialogue line we want to output
DialogueLine messageDialogueLine = new DialogueLine("",message);
List<DialogueLine> listToReturn = new List<DialogueLine>();
listToReturn.Add(messageDialogueLine);
return listToReturn;
}
//Filter to see if there is any dialogue lines we can overwrite with
public static List<DialogueLine> SelectDialogue(List<DialogueLine> dialogueToExecute, DialogueCondition[] conditions)
{
//Replace the dialogue set with the highest condition score
int highestConditionScore = -1;
foreach(DialogueCondition condition in conditions)
{
//Check if conditions met first
if(condition.CheckConditions(out int score))
{
if(score > highestConditionScore)
{
highestConditionScore = score;
dialogueToExecute = condition.dialogueLine;
Debug.Log("Will play " + condition.id);
}
}
}
return dialogueToExecute;
}
/// <summary>
/// Any {} in the message will be retrieved from the blackboard
/// </summary>
/// <param name="message">The string to pass in </param>
/// <returns></returns>
string ParseVariables(string message)
{
if(GameStateManager.Instance != null)
{
//Get the blackboard
GameBlackboard blackboard = GameStateManager.Instance.GetBlackboard();
if(blackboard != null)
{
//Look for strings enclosed with {}
string pattern = @"\{([^}]+?)\}";
//Regex replacement step
message = Regex.Replace(message, pattern, match => {
//The variable name enclosed in the "{}"
string variableName = match.Groups[1].Value;
//If there is a string value, return it
if (blackboard.TryGetValueAsString(variableName, out string strValue))
{
return strValue;
}
//Nothing found, so nothing is returned
return "";
});
}
}
return message;
}
}
WeatherManager.cs
using UnityEngine;
public class WeatherManager : MonoBehaviour, ITimeTracker
{
public static WeatherManager Instance { get; private set; }
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;
}
}
public WeatherData.WeatherType WeatherToday { get; private set; }
public WeatherData.WeatherType WeatherTomorrow { get; private set; }
//Check if the weather has been set before
bool weatherSet = false;
//The weather data
[SerializeField] WeatherData weatherData;
void Start()
{
//Add this to TimeManager's Listener list
TimeManager.Instance.RegisterTracker(this);
}
public WeatherData.WeatherType ComputeWeather(GameTimestamp.Season season)
{
if(weatherData == null)
{
throw new System.Exception("No weather data loaded");
}
//What are the possible weathers to compute
WeatherProbability[] weatherSet = null;
switch (season)
{
case GameTimestamp.Season.Spring:
weatherSet = weatherData.springWeather;
break;
case GameTimestamp.Season.Summer:
weatherSet = weatherData.summerWeather;
break;
case GameTimestamp.Season.Fall:
weatherSet = weatherData.fallWeather;
break;
case GameTimestamp.Season.Winter:
weatherSet = weatherData.winterWeather;
break;
}
//Roll a random value
float randomValue = Random.Range(0, 1f);
//Initialise probability
float culmProbability = 0;
foreach(WeatherProbability weatherProbability in weatherSet)
{
culmProbability += weatherProbability.probability;
//If in the probability
if (randomValue <= culmProbability) {
return weatherProbability.weatherType;
}
}
//Sunny by default
return WeatherData.WeatherType.Sunny;
}
public void LoadWeather(WeatherSaveState saveState)
{
weatherSet = true;
WeatherToday = saveState.weather;
//Set the forecast
WeatherTomorrow = ComputeWeather(TimeManager.Instance.GetGameTimestamp().season);
}
public void ClockUpdate(GameTimestamp timestamp)
{
//Check if it is 6am
if(timestamp.hour == 6 && timestamp.minute == 0)
{
//Set the current weather
if (!weatherSet)
{
WeatherToday = ComputeWeather(timestamp.season);
}
else
{
WeatherToday = WeatherTomorrow;
}
UIManager.Instance.ChangeWeatherUI();
//Set the forecast
WeatherTomorrow = ComputeWeather(timestamp.season);
weatherSet = true;
Debug.Log("The weather is " + WeatherToday.ToString());
}
}
}
b. Stacking harvest in the inventory
Initially, when we harvest crops from our farm, they will not stack in the inventory and instead override what the player already has in hand. Thus we will fix that in InteractableObject.
InteractableObject.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;
public class InteractableObject : MonoBehaviour
{
//The item information the GameObject is supposed to represent
public ItemData item;
public UnityEvent onInteract = new UnityEvent();
[SerializeField]
protected string interactText = "Interact";
[SerializeField]
protected float offset = 1.5f;
public virtual void Pickup()
{
//Call the OnInteract Callback
onInteract?.Invoke();
//Check if the player is holding on to an item
if (InventoryManager.Instance.SlotEquipped(InventorySlot.InventoryType.Item))
{
//Send the item to inventory before equipping
InventoryManager.Instance.HandToInventory(InventorySlot.InventoryType.Item);
}
//Set the player's inventory to the item
InventoryManager.Instance.EquipHandSlot(item);
//Update the changes in the scene
InventoryManager.Instance.RenderHand();
//Disable the prompt
OnMoveAway();
//Destroy this instance so as to not have multiple copies
GameStateManager.Instance.PersistentDestroy(gameObject);
}
//When the player is hovering around the item
public virtual void OnHover()
{
UIManager.Instance.InteractPrompt(transform, interactText, offset);
}
//What happens when the player is in front of the item
public virtual void OnMoveAway()
{
UIManager.Instance.DeactivateInteractPrompt();
}
}
4. Conclusion
In this article, we added touchups to our in-game scenes and a new weather system.
If you are a Patreon supporter, you can download the project files for what we have done so far. To use the files, you will have to unzip the file (7-Zip can help you do that).
Here is the final code for all the scripts we have worked on today:
WeatherData.cs
using UnityEngine;
[CreateAssetMenu(fileName = "WeatherData", menuName = "Weather/WeatherData")]
//Script for handling the Weather Data
public class WeatherData : ScriptableObject
{
public enum WeatherType {
Sunny, Rain, Snow, Typhoon, HeavySnow
}
public WeatherProbability[] springWeather;
public WeatherProbability[] summerWeather;
public WeatherProbability[] fallWeather;
public WeatherProbability[] winterWeather;
}
WeatherProbability.cs
using UnityEngine;
[System.Serializable]
public struct WeatherProbability
{
public WeatherData.WeatherType weatherType;
[Range(0f, 1f)]
public float probability;
}
WeatherManager.cs
using UnityEngine;
public class WeatherManager : MonoBehaviour, ITimeTracker
{
public static WeatherManager Instance { get; private set; }
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;
}
}
public WeatherData.WeatherType WeatherToday { get; private set; }
public WeatherData.WeatherType WeatherTomorrow { get; private set; }
//Check if the weather has been set before
bool weatherSet = false;
//The weather data
[SerializeField] WeatherData weatherData;
void Start()
{
//Add this to TimeManager's Listener list
TimeManager.Instance.RegisterTracker(this);
}
public WeatherData.WeatherType ComputeWeather(GameTimestamp.Season season)
{
if(weatherData == null)
{
throw new System.Exception("No weather data loaded");
}
//What are the possible weathers to compute
WeatherProbability[] weatherSet = null;
switch (season)
{
case GameTimestamp.Season.Spring:
weatherSet = weatherData.springWeather;
break;
case GameTimestamp.Season.Summer:
weatherSet = weatherData.summerWeather;
break;
case GameTimestamp.Season.Fall:
weatherSet = weatherData.fallWeather;
break;
case GameTimestamp.Season.Winter:
weatherSet = weatherData.winterWeather;
break;
}
//Roll a random value
float randomValue = Random.Range(0, 1f);
//Initialise probability
float culmProbability = 0;
foreach(WeatherProbability weatherProbability in weatherSet)
{
culmProbability += weatherProbability.probability;
//If in the probability
if (randomValue <= culmProbability) {
return weatherProbability.weatherType;
}
}
//Sunny by default
return WeatherData.WeatherType.Sunny;
}
public void LoadWeather(WeatherSaveState saveState)
{
weatherSet = true;
WeatherToday = saveState.weather;
//Set the forecast
WeatherTomorrow = ComputeWeather(TimeManager.Instance.GetGameTimestamp().season);
}
public void ClockUpdate(GameTimestamp timestamp)
{
//Check if it is 6am
if(timestamp.hour == 6 && timestamp.minute == 0)
{
//Set the current weather
if (!weatherSet)
{
WeatherToday = ComputeWeather(timestamp.season);
}
else
{
WeatherToday = WeatherTomorrow;
}
UIManager.Instance.ChangeWeatherUI();
//Set the forecast
WeatherTomorrow = ComputeWeather(timestamp.season);
weatherSet = true;
Debug.Log("The weather is " + WeatherToday.ToString());
}
}
}
WeatherEffectController.cs
using System.Collections;
using UnityEngine;
public class WeatherEffectController : MonoBehaviour
{
[SerializeField]
GameObject rain,snow,heavySnow;
Transform player;
private void Start()
{
SceneTransitionManager.Instance.onLocationLoad.AddListener(LoadParticle);
player = FindFirstObjectByType<PlayerController>().transform;
}
private void FixedUpdate()
{
//Move the particle to the player
if (player != null)
{
Vector3 pos = new Vector3(player.position.x, transform.position.y, player.position.z);
transform.position = Vector3.Lerp(transform.position, pos, Time.fixedDeltaTime);
}
}
void LoadParticle()
{
//Disable everything
rain.SetActive(false);
snow.SetActive(false);
heavySnow.SetActive(false);
//Move the particle to the player
if (player != null)
{
Vector3 pos = new Vector3(player.position.x, transform.position.y, player.position.z);
transform.position = pos;
}
//Check if indoor
if (SceneTransitionManager.Instance.CurrentlyIndoor())
{
return;
}
if(WeatherManager.Instance.WeatherToday == WeatherData.WeatherType.Rain)
{
//Rain
rain.SetActive(true);
}
if (WeatherManager.Instance.WeatherToday == WeatherData.WeatherType.Snow)
{
snow.SetActive(true);
}
if (WeatherManager.Instance.WeatherToday == WeatherData.WeatherType.HeavySnow)
{
heavySnow.SetActive(true);
}
}
}
Television.cs
using UnityEngine;
public class Television : InteractableObject
{
public override void Pickup()
{
//For now, just do a weather forecast
WeatherData.WeatherType weather = WeatherManager.Instance.WeatherTomorrow;
DialogueManager.Instance.StartDialogue(DialogueManager.CreateSimpleMessage(
"The weather tomorrow is " + weather.ToString()
));;
}
}
GameStateManager.cs
using System;
using System.Collections;
using System.Collections.Generic;
using System.Runtime.InteropServices.WindowsRuntime;
using UnityEngine;
using UnityEngine.Events;
public class GameStateManager : MonoBehaviour, ITimeTracker
{
//lights that turn on at night
public GameObject[] nightLights;
public static GameStateManager Instance { get; private set; }
//Check if the screen has finished fading out
bool screenFadedOut;
//To track interval updates
private int minutesElapsed = 0;
//Event triggered every 15 minutes
public UnityEvent onIntervalUpdate;
[SerializeField]
GameBlackboard blackboard = new GameBlackboard();
//This blackboard will not be saved
GameBlackboard sceneItemsBoard = new GameBlackboard();
const string TIMESTAMP = "Timestamp";
public GameBlackboard GetBlackboard()
{
return blackboard;
}
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()
{
//Add this to TimeManager's Listener list
TimeManager.Instance.RegisterTracker(this);
SceneTransitionManager.Instance.onLocationLoad.AddListener(RenderPersistentObjects);
}
public void ClockUpdate(GameTimestamp timestamp)
{
UpdateShippingState(timestamp);
UpdateFarmState(timestamp);
IncubationManager.UpdateEggs();
blackboard.SetValue(TIMESTAMP, timestamp);
if (timestamp.hour == 0 && timestamp.minute == 0)
{
OnDayReset();
}
//6:01 am, do rain on land logic
if(timestamp.hour == 6 && timestamp.minute == 1) {
RainOnLand();
}
if(minutesElapsed >= 15)
{
minutesElapsed = 0;
onIntervalUpdate?.Invoke();
}
else
{
minutesElapsed++;
}
// the lights will turn on at 6pm
nightLights = GameObject.FindGameObjectsWithTag("Lights");
foreach (GameObject i in nightLights)
{
if (timestamp.hour >= 18)
{
i.GetComponent<Light>().enabled = true;
}
else
{
i.GetComponent<Light>().enabled = false;
}
}
}
//Called when the day has been reset
void OnDayReset()
{
Debug.Log("Day has been reset");
foreach(NPCRelationshipState npc in RelationshipStats.relationships)
{
npc.hasTalkedToday = false;
npc.giftGivenToday = false;
}
AnimalFeedManager.ResetFeedboxes();
AnimalStats.OnDayReset();
//
}
void UpdateShippingState(GameTimestamp timestamp)
{
//Check if the hour is here (Exactly 1800 hours)
if(timestamp.hour == ShippingBin.hourToShip && timestamp.minute == 0)
{
ShippingBin.ShipItems();
}
}
void RainOnLand()
{
//Check if raining
if (WeatherManager.Instance.WeatherToday != WeatherData.WeatherType.Rain) {
return;
}
//Retrieve the Land and Farm data from the static variable
List<LandSaveState> landData = LandManager.farmData.Item1;
for(int i=0; i< landData.Count; i++)
{
//Set the last watered to now
if (landData[i].landStatus != Land.LandStatus.Soil)
{
landData[i] = new LandSaveState(Land.LandStatus.Watered, TimeManager.Instance.GetGameTimestamp(), landData[i].obstacleStatus);
}
}
}
void UpdateFarmState(GameTimestamp timestamp)
{
//Update the Land and Crop Save states as long as the player is outside of the Farm scene
if (SceneTransitionManager.Instance.currentLocation != SceneTransitionManager.Location.Farm)
{
//If there is nothing to update to begin with, stop
if (LandManager.farmData == null) return;
//Retrieve the Land and Farm data from the static variable
List<LandSaveState> landData = LandManager.farmData.Item1;
List<CropSaveState> cropData = LandManager.farmData.Item2;
//If there are no crops planted, we don't need to worry about updating anything
if (cropData.Count == 0) return;
for (int i = 0; i < cropData.Count; i++)
{
//Get the crop and corresponding land data
CropSaveState crop = cropData[i];
LandSaveState land = landData[crop.landID];
//Check if the crop is already wilted
if (crop.cropState == CropBehaviour.CropState.Wilted) continue;
//Update the Land's state
land.ClockUpdate(timestamp);
//Update the crop's state based on the land state
if (land.landStatus == Land.LandStatus.Watered)
{
crop.Grow();
}
else if (crop.cropState != CropBehaviour.CropState.Seed)
{
crop.Wither();
}
//Update the element in the array
cropData[i] = crop;
landData[crop.landID] = land;
}
/*LandManager.farmData.Item2.ForEach((CropSaveState crop) => {
Debug.Log(crop.seedToGrow + "\n Health: " + crop.health + "\n Growth: " + crop.growth + "\n State: " + crop.cropState.ToString());
});*/
}
}
//Instantiate the GameObject such that it stays in the scene even after the player leaves
public GameObject PersistentInstantiate(GameObject gameObject, Vector3 position, Quaternion rotation)
{
GameObject item = Instantiate(gameObject, position, rotation);
//Save it to the blackboard
(GameObject, Vector3) itemInformation = (gameObject, position);
List<(GameObject, Vector3)> items = sceneItemsBoard.GetOrInitList<(GameObject, Vector3)>(SceneTransitionManager.Instance.currentLocation.ToString());
items.Add(itemInformation);
sceneItemsBoard.Debug();
return item;
}
public void PersistentDestroy(GameObject item)
{
if (sceneItemsBoard.TryGetValue(SceneTransitionManager.Instance.currentLocation.ToString(), out List<(GameObject, Vector3)> items))
{
int index = items.FindIndex(i => i.Item2 == item.transform.position);
items.RemoveAt(index);
}
Destroy(item);
}
void RenderPersistentObjects()
{
Debug.Log("Rendering Persistent Objects");
if (sceneItemsBoard.TryGetValue(SceneTransitionManager.Instance.currentLocation.ToString(), out List<(GameObject, Vector3)> items)) {
foreach((GameObject, Vector3) item in items)
{
Instantiate(item.Item1, item.Item2, Quaternion.identity);
}
}
}
public void Sleep()
{
//Call a fadeout
UIManager.Instance.FadeOutScreen();
screenFadedOut = false;
StartCoroutine(TransitionTime());
//Restore Stamina fully when sleeping
PlayerStats.RestoreStamina();
}
IEnumerator TransitionTime()
{
//Calculate how many ticks we need to advance the time to 6am
//Get the time stamp of 6am the next day
GameTimestamp timestampOfNextDay = TimeManager.Instance.GetGameTimestamp();
timestampOfNextDay.day += 1;
timestampOfNextDay.hour = 6;
timestampOfNextDay.minute = 0;
Debug.Log(timestampOfNextDay.day + " " + timestampOfNextDay.hour + ":" + timestampOfNextDay.minute);
//Wait for the scene to finish fading out before loading the next scene
while (!screenFadedOut)
{
yield return new WaitForSeconds(1f);
}
TimeManager.Instance.SkipTime(timestampOfNextDay);
//Save
SaveManager.Save(ExportSaveState());
//Reset the boolean
screenFadedOut = false;
UIManager.Instance.ResetFadeDefaults();
}
//Called when the screen has faded out
public void OnFadeOutComplete()
{
screenFadedOut = true;
}
public GameSaveState ExportSaveState()
{
//Retrieve Farm Data
FarmSaveState farmSaveState = FarmSaveState.Export();
//Retrieve inventory data
InventorySaveState inventorySaveState = InventorySaveState.Export();
PlayerSaveState playerSaveState = PlayerSaveState.Export();
//Time
GameTimestamp timestamp = TimeManager.Instance.GetGameTimestamp();
//Relationships
//RelationshipSaveState relationshipSaveState = RelationshipSaveState.Export();
WeatherSaveState weather = WeatherSaveState.Export();
return new GameSaveState(blackboard, farmSaveState, inventorySaveState, timestamp, playerSaveState, weather);
}
public void LoadSave()
{
//Retrieve the loaded save
GameSaveState save = SaveManager.Load();
//Load up the parts
blackboard = save.blackboard;
blackboard.Debug();
//Time
TimeManager.Instance.LoadTime(save.timestamp);
//Inventory
save.inventorySaveState.LoadData();
//Farming data
save.farmSaveState.LoadData();
//Player Stats
save.playerSaveState.LoadData();
//Weather
save.weatherSaveState.LoadData();
//Relationship stats
AnimalStats.LoadStats();
RelationshipStats.LoadStats();
//save.relationshipSaveState.LoadData();
}
}
Land.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Land : MonoBehaviour, ITimeTracker
{
public int id;
public enum LandStatus
{
Soil, Farmland, Watered
}
public LandStatus landStatus;
public Material soilMat, farmlandMat, wateredMat;
new Renderer renderer;
//The selection gameobject to enable when the player is selecting the land
public GameObject select;
//Cache the time the land was watered
GameTimestamp timeWatered;
[Header("Crops")]
//The crop prefab to instantiate
public GameObject cropPrefab;
//The crop currently planted on the land
CropBehaviour cropPlanted = null;
//Obstacles
public enum FarmObstacleStatus { None, Rock, Wood, Weeds}
[Header("Obstacles")]
public FarmObstacleStatus obstacleStatus;
public GameObject rockPrefab, woodPrefab, weedsPrefab;
//Store the instantiated obstacle as a variable so we can access it
GameObject obstacleObject;
// Start is called before the first frame update
void Start()
{
//Get the renderer component
renderer = GetComponent<Renderer>();
//Set the land to soil by default
SwitchLandStatus(LandStatus.Soil);
//Deselect the land by default
Select(false);
//Add this to TimeManager's Listener list
TimeManager.Instance.RegisterTracker(this);
}
public void LoadLandData(LandStatus landStatusToSwitch, GameTimestamp lastWatered, FarmObstacleStatus obstacleStatusToSwitch)
{
//Set land status accordingly
landStatus = landStatusToSwitch;
timeWatered = lastWatered;
Material materialToSwitch = soilMat;
//Decide what material to switch to
switch (landStatusToSwitch)
{
case LandStatus.Soil:
//Switch to the soil material
materialToSwitch = soilMat;
break;
case LandStatus.Farmland:
//Switch to farmland material
materialToSwitch = farmlandMat;
break;
case LandStatus.Watered:
//Switch to watered material
materialToSwitch = wateredMat;
break;
}
//Get the renderer to apply the changes
renderer.material = materialToSwitch;
switch (obstacleStatusToSwitch)
{
case FarmObstacleStatus.None:
//Destroy the Obstacle object, if any
if (obstacleObject != null) Destroy(obstacleObject);
break;
case FarmObstacleStatus.Rock:
//Instantiate the obstacle prefab on the land and assign it to obstacleObject
obstacleObject = Instantiate(rockPrefab, transform);
break;
case FarmObstacleStatus.Wood:
//Instantiate the obstacle prefab on the land and assign it to obstacleObject
obstacleObject = Instantiate(woodPrefab, transform);
break;
case FarmObstacleStatus.Weeds:
//Instantiate the obstacle prefab on the land and assign it to obstacleObject
obstacleObject = Instantiate(weedsPrefab, transform);
break;
}
//Move the obstacle object to the top of the land gameobject
if (obstacleObject != null) obstacleObject.transform.position = new Vector3(transform.position.x, 0.1f, transform.position.z);
//Set the status accordingly
obstacleStatus = obstacleStatusToSwitch;
}
public void SwitchLandStatus(LandStatus statusToSwitch)
{
//Set land status accordingly
landStatus = statusToSwitch;
Material materialToSwitch = soilMat;
//Decide what material to switch to
switch (statusToSwitch)
{
case LandStatus.Soil:
//Switch to the soil material
materialToSwitch = soilMat;
break;
case LandStatus.Farmland:
//Switch to farmland material
materialToSwitch = farmlandMat;
break;
case LandStatus.Watered:
//Switch to watered material
materialToSwitch = wateredMat;
//Cache the time it was watered
timeWatered = TimeManager.Instance.GetGameTimestamp();
break;
}
//Get the renderer to apply the changes
renderer.material = materialToSwitch;
LandManager.Instance.OnLandStateChange(id, landStatus, timeWatered, obstacleStatus);
}
public void SetObstacleStatus(FarmObstacleStatus statusToSwitch)
{
switch (statusToSwitch)
{
case FarmObstacleStatus.None:
//Destroy the Obstacle object, if any
if (obstacleObject != null) Destroy(obstacleObject);
break;
case FarmObstacleStatus.Rock:
//Instantiate the obstacle prefab on the land and assign it to obstacleObject
obstacleObject = Instantiate(rockPrefab, transform);
break;
case FarmObstacleStatus.Wood:
//Instantiate the obstacle prefab on the land and assign it to obstacleObject
obstacleObject = Instantiate(woodPrefab, transform);
break;
case FarmObstacleStatus.Weeds:
//Instantiate the obstacle prefab on the land and assign it to obstacleObject
obstacleObject = Instantiate(weedsPrefab, transform);
break;
}
//Move the obstacle object to the top of the land gameobject
if(obstacleObject != null) obstacleObject.transform.position = new Vector3(transform.position.x, 0.1f, transform.position.z);
//Set the status accordingly
obstacleStatus = statusToSwitch;
LandManager.Instance.OnLandStateChange(id, landStatus, timeWatered, obstacleStatus);
}
public void Select(bool toggle)
{
select.SetActive(toggle);
}
//When the player presses the interact button while selecting this land
public void Interact()
{
//Check the player's tool slot
ItemData toolSlot = InventoryManager.Instance.GetEquippedSlotItem(InventorySlot.InventoryType.Tool);
//If there's nothing equipped, return
if (!InventoryManager.Instance.SlotEquipped(InventorySlot.InventoryType.Tool))
{
return;
}
//Try casting the itemdata in the toolslot as EquipmentData
EquipmentData equipmentTool = toolSlot as EquipmentData;
//Check if it is of type EquipmentData
if(equipmentTool != null)
{
//Get the tool type
EquipmentData.ToolType toolType = equipmentTool.toolType;
switch (toolType)
{
case EquipmentData.ToolType.Hoe:
SwitchLandStatus(LandStatus.Farmland);
break;
case EquipmentData.ToolType.WateringCan:
//The land must be tilled first
if (landStatus != LandStatus.Soil)
{
SwitchLandStatus(LandStatus.Watered);
}
break;
case EquipmentData.ToolType.Shovel:
//Remove the crop from the land
if(cropPlanted != null)
{
cropPlanted.RemoveCrop();
}
//Remove weed obstacle
if (obstacleStatus == FarmObstacleStatus.Weeds) SetObstacleStatus(FarmObstacleStatus.None);
break;
case EquipmentData.ToolType.Axe:
//Remove wood obstacle
if (obstacleStatus == FarmObstacleStatus.Wood) SetObstacleStatus(FarmObstacleStatus.None);
break;
case EquipmentData.ToolType.Pickaxe:
//Remove rock obstacle
if (obstacleStatus == FarmObstacleStatus.Rock) SetObstacleStatus(FarmObstacleStatus.None);
break;
}
//We don't need to check for seeds if we have already confirmed the tool to be an equipment
return;
}
//Try casting the itemdata in the toolslot as SeedData
SeedData seedTool = toolSlot as SeedData;
///Conditions for the player to be able to plant a seed
///1: He is holding a tool of type SeedData
///2: The Land State must be either watered or farmland
///3. There isn't already a crop that has been planted
///4. There are no obstacles
if(seedTool != null && landStatus != LandStatus.Soil && cropPlanted == null && obstacleStatus == FarmObstacleStatus.None)
{
SpawnCrop();
//Plant it with the seed's information
cropPlanted.Plant(id, seedTool);
//Consume the item
InventoryManager.Instance.ConsumeItem(InventoryManager.Instance.GetEquippedSlot(InventorySlot.InventoryType.Tool));
}
}
public CropBehaviour SpawnCrop()
{
//Instantiate the crop object parented to the land
GameObject cropObject = Instantiate(cropPrefab, transform);
//Move the crop object to the top of the land gameobject
cropObject.transform.position = new Vector3(transform.position.x, 0.1f, transform.position.z);
//Access the CropBehaviour of the crop we're going to plant
cropPlanted = cropObject.GetComponent<CropBehaviour>();
return cropPlanted;
}
public void ClockUpdate(GameTimestamp timestamp)
{
//When raining, have it watered if it is farmland
if (WeatherManager.Instance.WeatherToday == WeatherData.WeatherType.Rain && landStatus == LandStatus.Farmland)
{
SwitchLandStatus(LandStatus.Watered);
}
//Checked if 24 hours has passed since last watered
if (landStatus == LandStatus.Watered)
{
//Hours since the land was watered
int hoursElapsed = GameTimestamp.CompareTimestamps(timeWatered, timestamp);
Debug.Log(hoursElapsed + " hours since this was watered");
//Grow the planted crop, if any
if(cropPlanted != null)
{
cropPlanted.Grow();
}
if(hoursElapsed > 24)
{
//Dry up (Switch back to farmland)
SwitchLandStatus(LandStatus.Farmland);
}
}
//Handle the wilting of the plant when the land is not watered
if(landStatus != LandStatus.Watered && cropPlanted != null)
{
//If the crop has already germinated, start the withering
if (cropPlanted.cropState != CropBehaviour.CropState.Seed)
{
cropPlanted.Wither();
}
}
}
private void OnDestroy()
{
//Unsubscribe from the list on destroy
TimeManager.Instance.UnregisterTracker(this);
}
}
LocationEntryPoint.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class LocationEntryPoint : MonoBehaviour
{
[SerializeField]
SceneTransitionManager.Location locationToSwitch;
[SerializeField]
//For locking doors
bool locked = false;
private void OnTriggerEnter(Collider other)
{
//Check if the collider belongs to the player
if(other.tag == "Player")
{
if(WeatherManager.Instance.WeatherToday == WeatherData.WeatherType.Typhoon && SceneTransitionManager.Instance.currentLocation == SceneTransitionManager.Location.PlayerHome || locked)
{
//You cant go out in a typhoon
DialogueManager.Instance.StartDialogue(DialogueManager.CreateSimpleMessage("It's not safe to go out."));
return;
}
//Switch scenes to the location of the entry point
SceneTransitionManager.Instance.SwitchLocation(locationToSwitch);
}
//Characters walking through here and items thrown will be despawned
if(other.tag == "Item")
{
Destroy(other.gameObject);
}
}
}
PlayerController.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class PlayerController : MonoBehaviour
{
//Movement Components
private CharacterController controller;
private Animator animator;
private float moveSpeed = 4f;
[Header("Movement System")]
public float walkSpeed = 4f;
public float runSpeed = 8f;
private float gravity = 9.81f;
//Interaction components
PlayerInteraction playerInteraction;
// Start is called before the first frame update
void Start()
{
//Get movement components
controller = GetComponent<CharacterController>();
animator = GetComponent<Animator>();
//Get interaction component
playerInteraction = GetComponentInChildren<PlayerInteraction>();
}
// Update is called once per frame
void Update()
{
//Runs the function that handles all movement
Move();
//Runs the function that handles all interaction
Interact();
//Debugging purposes only
//Skip the time when the right square bracket is pressed
if (Input.GetKey(KeyCode.RightBracket))
{
if (Input.GetKey(KeyCode.LeftShift)) {
//Advance the entire day
for(int i =0; i< 60*24; i++)
{
TimeManager.Instance.Tick();
}
} else
{
TimeManager.Instance.Tick();
}
}
//Toggle relationship panel
if (Input.GetKeyDown(KeyCode.R))
{
UIManager.Instance.ToggleRelationshipPanel();
}
if (Input.GetKeyDown(KeyCode.N))
{
SceneTransitionManager.Location location = LocationManager.GetNextLocation(SceneTransitionManager.Location.PlayerHome, SceneTransitionManager.Location.Farm);
Debug.Log(location);
}
}
public void Interact()
{
//Tool interaction
if (Input.GetButtonDown("Fire1"))
{
//Interact
playerInteraction.Interact();
}
//Item interaction
if (Input.GetButtonDown("Fire2"))
{
playerInteraction.ItemInteract();
}
//Keep items
if (Input.GetButtonDown("Fire3"))
{
playerInteraction.ItemKeep();
}
}
public void Move()
{
//Get the horizontal and vertical inputs as a number
float horizontal = Input.GetAxisRaw("Horizontal");
float vertical = Input.GetAxisRaw("Vertical");
//Direction in a normalised vector
Vector3 dir = new Vector3(horizontal, 0f, vertical).normalized;
Vector3 velocity = moveSpeed * Time.deltaTime * dir;
if (controller.isGrounded)
{
velocity.y = 0;
}
velocity.y -= Time.deltaTime * gravity;
//Is the sprint key pressed down?
if (Input.GetButton("Sprint"))
{
//Set the animation to run and increase our movespeed
moveSpeed = runSpeed;
animator.SetBool("Running", true);
} else
{
//Set the animation to walk and decrease our movespeed
moveSpeed = walkSpeed;
animator.SetBool("Running", false);
}
//Check if there is movement
if (dir.magnitude >= 0.1f)
{
//Look towards that direction
transform.rotation = Quaternion.LookRotation(dir);
//Move if allowed
if (controller.enabled)
{
controller.Move(velocity);
}
}
//Animation speed parameter
animator.SetFloat("Speed", dir.magnitude);
}
}
WeatherSaveState.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class PlayerController : MonoBehaviour
{
//Movement Components
private CharacterController controller;
private Animator animator;
private float moveSpeed = 4f;
[Header("Movement System")]
public float walkSpeed = 4f;
public float runSpeed = 8f;
private float gravity = 9.81f;
//Interaction components
PlayerInteraction playerInteraction;
// Start is called before the first frame update
void Start()
{
//Get movement components
controller = GetComponent<CharacterController>();
animator = GetComponent<Animator>();
//Get interaction component
playerInteraction = GetComponentInChildren<PlayerInteraction>();
}
// Update is called once per frame
void Update()
{
//Runs the function that handles all movement
Move();
//Runs the function that handles all interaction
Interact();
//Debugging purposes only
//Skip the time when the right square bracket is pressed
if (Input.GetKey(KeyCode.RightBracket))
{
if (Input.GetKey(KeyCode.LeftShift)) {
//Advance the entire day
for(int i =0; i< 60*24; i++)
{
TimeManager.Instance.Tick();
}
} else
{
TimeManager.Instance.Tick();
}
}
//Toggle relationship panel
if (Input.GetKeyDown(KeyCode.R))
{
UIManager.Instance.ToggleRelationshipPanel();
}
if (Input.GetKeyDown(KeyCode.N))
{
SceneTransitionManager.Location location = LocationManager.GetNextLocation(SceneTransitionManager.Location.PlayerHome, SceneTransitionManager.Location.Farm);
Debug.Log(location);
}
}
public void Interact()
{
//Tool interaction
if (Input.GetButtonDown("Fire1"))
{
//Interact
playerInteraction.Interact();
}
//Item interaction
if (Input.GetButtonDown("Fire2"))
{
playerInteraction.ItemInteract();
}
//Keep items
if (Input.GetButtonDown("Fire3"))
{
playerInteraction.ItemKeep();
}
}
public void Move()
{
//Get the horizontal and vertical inputs as a number
float horizontal = Input.GetAxisRaw("Horizontal");
float vertical = Input.GetAxisRaw("Vertical");
//Direction in a normalised vector
Vector3 dir = new Vector3(horizontal, 0f, vertical).normalized;
Vector3 velocity = moveSpeed * Time.deltaTime * dir;
if (controller.isGrounded)
{
velocity.y = 0;
}
velocity.y -= Time.deltaTime * gravity;
//Is the sprint key pressed down?
if (Input.GetButton("Sprint"))
{
//Set the animation to run and increase our movespeed
moveSpeed = runSpeed;
animator.SetBool("Running", true);
} else
{
//Set the animation to walk and decrease our movespeed
moveSpeed = walkSpeed;
animator.SetBool("Running", false);
}
//Check if there is movement
if (dir.magnitude >= 0.1f)
{
//Look towards that direction
transform.rotation = Quaternion.LookRotation(dir);
//Move if allowed
if (controller.enabled)
{
controller.Move(velocity);
}
}
//Animation speed parameter
animator.SetFloat("Speed", dir.magnitude);
}
}
GameSaveState.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
[System.Serializable]
public class GameSaveState
{
//Blackboard
public GameBlackboard blackboard;
//Farm Data
public FarmSaveState farmSaveState;
//Inventory
public InventorySaveState inventorySaveState;
//Time
public GameTimestamp timestamp;
//PlayerStats
public PlayerSaveState playerSaveState;
//Weather State
public WeatherSaveState weatherSaveState;
//Relationships
//public RelationshipSaveState relationshipSaveState;
public GameSaveState(
GameBlackboard blackboard,
FarmSaveState farmSaveState,
InventorySaveState inventorySaveState,
GameTimestamp timestamp,
PlayerSaveState playerSaveState,
WeatherSaveState weatherSaveState
//RelationshipSaveState relationshipSaveState
)
{
this.blackboard = blackboard;
this.farmSaveState = farmSaveState;
this.inventorySaveState = inventorySaveState;
this.timestamp = timestamp;
this.playerSaveState = playerSaveState;
this.weatherSaveState = weatherSaveState;
//this.relationshipSaveState = relationshipSaveState;
}
}
UIManager.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using TMPro;
public class UIManager : MonoBehaviour, ITimeTracker
{
public static UIManager Instance { get; private set; }
[Header("Screen Management")]
public GameObject menuScreen;
public enum Tab
{
Inventory, Relationships, Animals
}
//The current selected tab
public Tab selectedTab;
[Header("Status Bar")]
//Tool equip slot on the status bar
public Image toolEquipSlot;
//Tool Quantity text on the status bar
public TextMeshProUGUI toolQuantityText;
//Time UI
public TextMeshProUGUI timeText;
public TextMeshProUGUI dateText;
[Header("Inventory System")]
//The inventory panel
public GameObject inventoryPanel;
//The tool equip slot UI on the Inventory panel
public HandInventorySlot toolHandSlot;
//The tool slot UIs
public InventorySlot[] toolSlots;
//The item equip slot UI on the Inventory panel
public HandInventorySlot itemHandSlot;
//The item slot UIs
public InventorySlot[] itemSlots;
[Header("Item info box")]
public GameObject itemInfoBox;
public TextMeshProUGUI itemNameText;
public TextMeshProUGUI itemDescriptionText;
[Header("Screen Transitions")]
public GameObject fadeIn;
public GameObject fadeOut;
[Header("Prompts")]
public YesNoPrompt yesNoPrompt;
public NamingPrompt namingPrompt;
[SerializeField] InteractBubble interactBubble;
[Header("Player Stats")]
public TextMeshProUGUI moneyText;
[Header("Shop")]
public ShopListingManager shopListingManager;
[Header("Relationships")]
public RelationshipListingManager relationshipListingManager;
public AnimalListingManager animalRelationshipListingManager;
[Header("Calendar")]
public CalendarUIListing calendar;
[Header("Stamina")]
public Sprite[] staminaUI;
public Image StaminaUIImage;
public int staminaCount;
[Header("Weather")]
public Sprite[] weatherUI;
public Image WeatherUIImage;
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;
}
}
private void Start()
{
PlayerStats.RestoreStamina();
RenderInventory();
AssignSlotIndexes();
RenderPlayerStats();
DisplayItemInfo(null);
ChangeWeatherUI();
//Add UIManager to the list of objects TimeManager will notify when the time updates
TimeManager.Instance.RegisterTracker(this);
}
#region Prompts
public void TriggerNamingPrompt(string message, System.Action<string> onConfirmCallback)
{
//Check if another prompt is already in progress
if (namingPrompt.gameObject.activeSelf)
{
//Queue the prompt
namingPrompt.QueuePromptAction(() => TriggerNamingPrompt(message, onConfirmCallback));
return;
}
//Open the panel
namingPrompt.gameObject.SetActive(true);
namingPrompt.CreatePrompt(message, onConfirmCallback);
}
public void TriggerYesNoPrompt(string message, System.Action onYesCallback)
{
//Set active the gameobject of the Yes No Prompt
yesNoPrompt.gameObject.SetActive(true);
yesNoPrompt.CreatePrompt(message, onYesCallback);
}
#endregion
#region Tab Management
public void ToggleMenuPanel()
{
menuScreen.SetActive(!menuScreen.activeSelf);
OpenWindow(selectedTab);
TabBehaviour.onTabStateChange?.Invoke();
}
//Manage the opening of windows associated with the tab
public void OpenWindow(Tab windowToOpen)
{
//Disable all windows
relationshipListingManager.gameObject.SetActive(false);
inventoryPanel.SetActive(false);
animalRelationshipListingManager.gameObject.SetActive(false);
//Open the corresponding window and render it
switch (windowToOpen)
{
case Tab.Inventory:
inventoryPanel.SetActive(true);
RenderInventory();
break;
case Tab.Relationships:
relationshipListingManager.gameObject.SetActive(true);
relationshipListingManager.Render(RelationshipStats.relationships);
break;
case Tab.Animals:
animalRelationshipListingManager.gameObject.SetActive(true);
animalRelationshipListingManager.Render(AnimalStats.animalRelationships);
break;
}
//Set the selected tab
selectedTab = windowToOpen;
}
#endregion
#region Fadein Fadeout Transitions
public void FadeOutScreen()
{
fadeOut.SetActive(true);
}
public void FadeInScreen()
{
fadeIn.SetActive(true);
}
public void OnFadeInComplete()
{
//Disable Fade in Screen when animation is completed
fadeIn.SetActive(false);
}
//Reset the fadein fadeout screens to their default positions
public void ResetFadeDefaults()
{
fadeOut.SetActive(false);
fadeIn.SetActive(true);
}
#endregion
#region Inventory
//Iterate through the slot UI elements and assign it its reference slot index
public void AssignSlotIndexes()
{
for (int i = 0; i < toolSlots.Length; i++)
{
toolSlots[i].AssignIndex(i);
itemSlots[i].AssignIndex(i);
}
}
//Render the inventory screen to reflect the Player's Inventory.
public void RenderInventory()
{
//Get the respective slots to process
ItemSlotData[] inventoryToolSlots = InventoryManager.Instance.GetInventorySlots(InventorySlot.InventoryType.Tool);
ItemSlotData[] inventoryItemSlots = InventoryManager.Instance.GetInventorySlots(InventorySlot.InventoryType.Item);
//Render the Tool section
RenderInventoryPanel(inventoryToolSlots, toolSlots);
//Render the Item section
RenderInventoryPanel(inventoryItemSlots, itemSlots);
//Render the equipped slots
toolHandSlot.Display(InventoryManager.Instance.GetEquippedSlot(InventorySlot.InventoryType.Tool));
itemHandSlot.Display(InventoryManager.Instance.GetEquippedSlot(InventorySlot.InventoryType.Item));
//Get Tool Equip from InventoryManager
ItemData equippedTool = InventoryManager.Instance.GetEquippedSlotItem(InventorySlot.InventoryType.Tool);
//Text should be empty by default
toolQuantityText.text = "";
//Check if there is an item to display
if (equippedTool != null)
{
//Switch the thumbnail over
toolEquipSlot.sprite = equippedTool.thumbnail;
toolEquipSlot.gameObject.SetActive(true);
//Get quantity
int quantity = InventoryManager.Instance.GetEquippedSlot(InventorySlot.InventoryType.Tool).quantity;
if (quantity > 1)
{
toolQuantityText.text = quantity.ToString();
}
return;
}
toolEquipSlot.gameObject.SetActive(false);
}
//Iterate through a slot in a section and display them in the UI
void RenderInventoryPanel(ItemSlotData[] slots, InventorySlot[] uiSlots)
{
for (int i = 0; i < uiSlots.Length; i++)
{
//Display them accordingly
uiSlots[i].Display(slots[i]);
}
}
public void ToggleInventoryPanel()
{
//If the panel is hidden, show it and vice versa
inventoryPanel.SetActive(!inventoryPanel.activeSelf);
RenderInventory();
}
//Display Item info on the Item infobox
public void DisplayItemInfo(ItemData data)
{
//If data is null, reset
if (data == null)
{
itemNameText.text = "";
itemDescriptionText.text = "";
itemInfoBox.SetActive(false);
return;
}
itemInfoBox.SetActive(true);
itemNameText.text = data.name;
itemDescriptionText.text = data.description;
}
#endregion
#region Time
//Callback to handle the UI for time
public void ClockUpdate(GameTimestamp timestamp)
{
//Handle the time
//Get the hours and minutes
int hours = timestamp.hour;
int minutes = timestamp.minute;
//AM or PM
string prefix = "AM ";
//Convert hours to 12 hour clock
if (hours >= 12)
{
//Time becomes PM
prefix = "PM ";
//12 PM and later
hours = hours - 12;
Debug.Log(hours);
}
//Special case for 12am/pm to display it as 12 instead of 0
hours = hours == 0 ? 12 : hours;
//Format it for the time text display
timeText.text = prefix + hours + ":" + minutes.ToString("00");
//Handle the Date
int day = timestamp.day;
string season = timestamp.season.ToString();
string dayOfTheWeek = timestamp.GetDayOfTheWeek().ToString();
//Format it for the date text display
dateText.text = season + " " + day + " (" + dayOfTheWeek + ")";
}
#endregion
//Render the UI of the player stats in the HUD
public void RenderPlayerStats()
{
moneyText.text = PlayerStats.Money + PlayerStats.CURRENCY;
staminaCount = PlayerStats.Stamina;
ChangeStaminaUI();
}
//Open the shop window with the shop items listed
public void OpenShop(List<ItemData> shopItems)
{
//Set active the shop window
shopListingManager.gameObject.SetActive(true);
shopListingManager.Render(shopItems);
}
public void ToggleRelationshipPanel()
{
GameObject panel = relationshipListingManager.gameObject;
panel.SetActive(!panel.activeSelf);
//If open, render the screen
if (panel.activeSelf)
{
relationshipListingManager.Render(RelationshipStats.relationships);
}
}
public void InteractPrompt(Transform item, string message, float offset)
{
interactBubble.gameObject.SetActive(true);
interactBubble.transform.position = item.transform.position + new Vector3(0, offset, 0);
interactBubble.Display(message);
}
public void DeactivateInteractPrompt()
{
interactBubble.gameObject.SetActive(false);
}
public void ChangeStaminaUI()
{
if (staminaCount <= 45) StaminaUIImage.sprite = staminaUI[3]; // exhausted
else if (staminaCount <= 80) StaminaUIImage.sprite = staminaUI[2]; // tired
else if (staminaCount <= 115) StaminaUIImage.sprite = staminaUI[1]; // active
else if (staminaCount <= 150) StaminaUIImage.sprite = staminaUI[0]; // energised
}
public void ChangeWeatherUI()
{
var WeatherToday = WeatherManager.Instance.WeatherToday;
switch (WeatherToday)
{
case WeatherData.WeatherType.Sunny:
WeatherUIImage.sprite = weatherUI[0];
break;
case WeatherData.WeatherType.Rain:
WeatherUIImage.sprite = weatherUI[1];
break;
case WeatherData.WeatherType.Snow:
WeatherUIImage.sprite = weatherUI[2];
break;
case WeatherData.WeatherType.HeavySnow:
WeatherUIImage.sprite = weatherUI[3];
break;
}
}
}
DialogueManager.cs
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using TMPro;
using System.Text.RegularExpressions;
public class DialogueManager : MonoBehaviour
{
public static DialogueManager Instance { get; private set; }
[Header("Dialogue Components")]
public GameObject dialoguePanel;
public TextMeshProUGUI speakerText;
public TextMeshProUGUI dialogueText;
//The lines to queue during the dialogue sequence
Queue<DialogueLine> dialogueQueue;
Action onDialogueEnd = null;
bool isTyping = false;
//TODO: Implement a proper player control stop mechanism
PlayerController playerController;
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;
}
}
private void Start()
{
playerController = FindFirstObjectByType<PlayerController>();
}
//Initialises the dialogue
public void StartDialogue(List<DialogueLine> dialogueLinesToQueue)
{
//Convert the list to a queue
dialogueQueue = new Queue<DialogueLine>(dialogueLinesToQueue);
if (playerController != null)
{
playerController.enabled = false;
}
UpdateDialogue();
}
//Initialises the dialogue, but with an Action to execute once it finishes
public void StartDialogue(List<DialogueLine> dialogueLinesToQueue, Action onDialogueEnd)
{
StartDialogue(dialogueLinesToQueue);
this.onDialogueEnd = onDialogueEnd;
}
//Cycle through the dialogue lines
public void UpdateDialogue()
{
if (isTyping)
{
isTyping = false;
return;
}
//Reset our dialogue text
dialogueText.text = string.Empty;
//Check if there are any more lines in the queue
if(dialogueQueue.Count == 0)
{
//If not, end the dialogue
EndDialogue();
return;
}
//The current dialogue line to put in
DialogueLine line = dialogueQueue.Dequeue();
Talk(line.speaker, ParseVariables(line.message));
}
//Closes the dialogue
public void EndDialogue()
{
//Close the dialogue panel
dialoguePanel.SetActive(false);
if (playerController != null)
{
playerController.enabled = true;
}
//Invoke whatever Action queued on dialogue end (if any)
onDialogueEnd?.Invoke();
//Reset the Action
onDialogueEnd = null;
}
public void Talk(string speaker, string message)
{
//Set the dialogue panel active
dialoguePanel.SetActive(true);
//Set the speaker text to the speaker
speakerText.text = speaker;
//If there is no speaker, do not show the speaker text panel
speakerText.transform.parent.gameObject.SetActive(speaker != "");
//Set the dialogue text to the message
//dialogueText.text = message;
StartCoroutine(TypeText(message));
}
IEnumerator TypeText(string textToType)
{
isTyping = true;
//Convert the string to an array of chars
char[] charsToType = textToType.ToCharArray();
for(int i =0; i < charsToType.Length; i++)
{
dialogueText.text += charsToType[i];
yield return new WaitForEndOfFrame();
//Skip the typing sequence and just show the full text
if (!isTyping)
{
dialogueText.text = textToType;
//Break out from the loop
break;
}
}
//Typing sequence complete
isTyping = false;
}
//Converts a simple string into a List of Dialogue lines to put into DialogueManager
public static List<DialogueLine> CreateSimpleMessage(string message)
{
//The Dialogue line we want to output
DialogueLine messageDialogueLine = new DialogueLine("",message);
List<DialogueLine> listToReturn = new List<DialogueLine>();
listToReturn.Add(messageDialogueLine);
return listToReturn;
}
//Filter to see if there is any dialogue lines we can overwrite with
public static List<DialogueLine> SelectDialogue(List<DialogueLine> dialogueToExecute, DialogueCondition[] conditions)
{
//Replace the dialogue set with the highest condition score
int highestConditionScore = -1;
foreach(DialogueCondition condition in conditions)
{
//Check if conditions met first
if(condition.CheckConditions(out int score))
{
if(score > highestConditionScore)
{
highestConditionScore = score;
dialogueToExecute = condition.dialogueLine;
Debug.Log("Will play " + condition.id);
}
}
}
return dialogueToExecute;
}
/// <summary>
/// Any {} in the message will be retrieved from the blackboard
/// </summary>
/// <param name="message">The string to pass in </param>
/// <returns></returns>
string ParseVariables(string message)
{
if(GameStateManager.Instance != null)
{
//Get the blackboard
GameBlackboard blackboard = GameStateManager.Instance.GetBlackboard();
if(blackboard != null)
{
//Look for strings enclosed with {}
string pattern = @"\{([^}]+?)\}";
//Regex replacement step
message = Regex.Replace(message, pattern, match => {
//The variable name enclosed in the "{}"
string variableName = match.Groups[1].Value;
//If there is a string value, return it
if (blackboard.TryGetValueAsString(variableName, out string strValue))
{
return strValue;
}
//Nothing found, so nothing is returned
return "";
});
}
}
return message;
}
}
InteractableObject.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;
public class InteractableObject : MonoBehaviour
{
//The item information the GameObject is supposed to represent
public ItemData item;
public UnityEvent onInteract = new UnityEvent();
[SerializeField]
protected string interactText = "Interact";
[SerializeField]
protected float offset = 1.5f;
public virtual void Pickup()
{
//Call the OnInteract Callback
onInteract?.Invoke();
//Check if the player is holding on to an item
if (InventoryManager.Instance.SlotEquipped(InventorySlot.InventoryType.Item))
{
//Send the item to inventory before equipping
InventoryManager.Instance.HandToInventory(InventorySlot.InventoryType.Item);
}
//Set the player's inventory to the item
InventoryManager.Instance.EquipHandSlot(item);
//Update the changes in the scene
InventoryManager.Instance.RenderHand();
//Disable the prompt
OnMoveAway();
//Destroy this instance so as to not have multiple copies
GameStateManager.Instance.PersistentDestroy(gameObject);
}
//When the player is hovering around the item
public virtual void OnHover()
{
UIManager.Instance.InteractPrompt(transform, interactText, offset);
}
//What happens when the player is in front of the item
public virtual void OnMoveAway()
{
UIManager.Instance.DeactivateInteractPrompt();
}
}