时间管理
一、设置时间信息UI
1、UI-Image,命名为StatusBar,调整大小、位置等。颜色F4DDB7
2、复制一个HandSlot,作为StatusBar的子物体,变更StatusBar在Hierarchy上的位置。如上图
使打开背包按钮时被物品栏面板遮挡
3、变更复制的HandSlot的位置和大小
4、以StatusBar为父物体,Create Empty,命名为TimeInfo。调整位置(锚定right middle)、大小。
5、复制步骤3中的HandSlot,作为TimeInfo的子物体,命名为Weather。更改位置和颜色FF9500
6、以TimeInfo为父物体UI-Text,重命名为Time,设置大小位置和文字
7、以TimeInfo为父物体UI-Text,重命名为Date,设置大小位置和文字
二、设置时间系统,更新游戏时间
1、设置日期
(1) Scripts文件夹下创建Time 文件夹,其下创建GameTimestamp.cs,设置日期和星期
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
[System.Serializable]
public class GameTimestamp
{
public enum Season { Spring, Summer, Fall, Winter }
public enum DayOfTheWeek { Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday }
public Season season;
public int year,day,hour,minute;
//创建一个GameTimestemp类的构造函数
public GameTimestamp(int year, Season season, int day, int hour, int minute)
{
this.year = year;
this.season = season;
this.day = day;
this.hour = hour;
this.minute = minute;
}
//更新时钟
public void UpdateClock()
{
minute++;
if (minute >= 60) { minute = 0; hour++; }
if (hour >= 24) { hour = 0; day++; }
if (day > 30)
{
day = 1;
if (season == Season.Winter) { season = Season.Spring; year++; }
else { season++; }
}
}
//创建一个新的构造函数
public GameTimestamp(GameTimestamp timestamp)
{
this.year = timestamp.year;
this.season = timestamp.season;
this.day = timestamp.day;
this.hour = timestamp.hour;
this.minute = timestamp.minute;
}
//获取给定日期的星期几
public DayOfTheWeek GetDayOfTheWeek()
{
// 计算从起始日期到指定日期所经过的天数
int daysPassed = YearsToDays(year) + SeasonsToDays(season) + day;
// 计算星期几的索引
int dayIndex = daysPassed % 7;
// 将星期几的索引转换为具体的星期几枚举值,并返回
return (DayOfTheWeek)dayIndex;
}
//在需要的时候单独将小时转换为分钟
public static int HoursToMinute(int hour) { return hour * 60; }
public static int DaysToHours(int days) { return days * 24; }
public static int SeasonsToDays(Season season)
{
//将季节枚举转换为索引
int seasonIndex = (int)season;
return seasonIndex * 30;
}
public static int YearsToDays(int year) { return year * 4 * 30; }
}
(2) AI的建议
设置星期几:创建DateCalculator.cs,
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System;
using UnityEngine.UI;
public class DateCalculator : MonoBehaviour
{
// 假设的起始日期(可根据需要更改这个值)
public DateTime startDate = new DateTime(2023, 1, 1); // 2023年1月1日
// 要增加的天数
public int daysToAdd = 10;
// 用于显示结果的UI元素(例如Text组件)
public Text resultText;
void Start()
{
// 计算新日期
DateTime endDate = startDate.AddDays(daysToAdd);
// 获取星期几(注意:DayOfWeek枚举从Sunday=0开始)
DayOfWeek dayOfWeek = endDate.DayOfWeek;
// 将DayOfWeek转换为更易读的字符串
string dayOfWeekString = GetDayOfWeekString(dayOfWeek);
// 显示结果
if (resultText != null)
{
resultText.text = $"{endDate.ToString("yyyy-MM-dd")} 是 {dayOfWeekString}";
}
else
{
Debug.Log($"{endDate.ToString("yyyy-MM-dd")} 是 {dayOfWeekString}");
}
}
// 辅助函数:将DayOfWeek枚举转换为字符串
string GetDayOfWeekString(DayOfWeek day)
{
switch (day)
{
case DayOfWeek.Sunday:return "星期日";
case DayOfWeek.Monday:return "星期一";
case DayOfWeek.Tuesday:return "星期二";
case DayOfWeek.Wednesday:return "星期三";
case DayOfWeek.Thursday:return "星期四";
case DayOfWeek.Friday:return "星期五";
case DayOfWeek.Saturday:return "星期六";
default:return "未知";
}
}
}
2、设置时间流逝逻辑
(1) 给Manager添加TimeManager.cs组件,创建单例
public class TimeManager : MonoBehaviour
{
public static TimeManager Instance { get; private set; }
private void Awake()
{
if (Instance != null && Instance != this) { Destroy(this); }
else { Instance = this; }
}
}
(2) 编辑TimeManager.cs,设置时间系统
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class TimeManager : MonoBehaviour
{
public static TimeManager Instance { get; private set; }
private void Awake()
{
if (Instance != null && Instance != this){Destroy(this);}
else{ Instance = this;}
}
// 通过SerializeField属性,允许在Unity编辑器中查看和修改这个字段,但不暴露给Inspector以外的代码
[SerializeField]
// 存储游戏时间的时间戳
public GameTimestamp timestamp;
// 时间缩放比例,默认为1.0f
public float timeScale = 1.0f;
void Start()
{
// 初始化时间戳为指定的日期和时间
timestamp = new GameTimestamp(0,GameTimestamp.Season.Spring,1,6,0);
// 开始协程,定时更新游戏时间
StartCoroutine(TimeUpdate());
}
// 协程,定时更新游戏时间
IEnumerator TimeUpdate()
{
while (true)
{
// 更新游戏时间
Tick();
// 按照时间缩放比例等待一段时间
yield return new WaitForSeconds(1/timeScale);
}
}
//调用GameTimestamp.cs中更新时钟的方法
public void Tick()
{
timestamp.UpdateClock();
}
}
解析
IEnumerator TimeUpdate()
{
while (true)
{
// 更新游戏时间
Tick();
// 按照时间缩放比例等待一段时间
yield return new WaitForSeconds(1/timeScale);
}
}
协程 是一种异步执行的机制,可以在一段时间后返回结果或者暂停执行
它可以在执行过程中暂停(通过yield
语句),并在未来的某个时间点自动恢复执行,而不需要显式地调用它。协程非常适合处理需要等待(如网络请求、动画完成等)但又不想阻塞主线程的情况
yield return new WaitForSeconds(1 / timeScale);
**yield
**:在协程中,yield
关键字用于暂停协程的执行,直到某个条件满足或某个操作完成。
在Unity的协程中,yield
后面通常会跟一个表达式,该表达式决定了协程何时恢复执行
**new WaitForSeconds(1 / timeScale)
**:创建WaitForSeconds
对象的表达式。
WaitForSeconds :
是一个用于协程(Coroutine)的类,用于在协程暂停执行一段指定的时间。
通常在创建需要等待一段时间才能继续执行的任务(如动画、延迟响应、定时检查等)时使用
**1 / timeScale
**:等待的时间长度。
timeScale :
一个float
类型的变量,用于控制游戏时间的流速。
根据timeScale
的值来调整等待的时间:例如,如果timeScale
是2
,那么1 / timeScale
的结果是0.5
,意味着协程将等待0.5秒而不是1秒。这允许你通过调整timeScale
来加速或减慢游戏时间的流速,而不需要修改协程中的等待时间。
timeScale的值越大,1 / timeScale的计算结果越小,即暂停的时间越短。这意味着游戏时间会更快流逝,协程在暂停后会更快地继续执行下一步。相反,如果timeScale的值越小,暂停的时间越长,游戏时间会更慢流逝。
如果将这两句的位置互换,即先等待一段时间,再更新游戏时间,会导致以下不同:
-
先等待一段时间:在这种情况下,首先会等待一段时间(由timeScale决定),然后才会执行Tick()方法来更新游戏时间。这意味着游戏时间只会在等待时间之后才会更新,延迟了游戏时间的更新。
-
先更新游戏时间:在这种情况下,首先会执行Tick()方法来更新游戏时间,然后再等待一段时间(由timeScale决定)。这意味着游戏时间会立即更新,然后再等待一段时间,以便按照时间缩放比例等待。
总而言之,这两句的位置互换会影响到游戏时间的更新和等待时间的触发顺序(更新时间延迟)
三、光照随时间变换——更改Directional Light的Rotation.x
1、原理
(1) 太阳一昼夜移动360度,每小时移动360/24=15度,每分钟移动15/60=0.25度
(2) 正午太阳在正南方(角色的头顶),与地平线之间的夹角是90度,午夜(0点)为-90度
(3) 太阳的角度=0.25*时间(分钟) -90度
2、添加日照逻辑
(1) 编辑TimeManager.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class TimeManager : MonoBehaviour
{
public static TimeManager Instance { get; private set; }
private void Awake()
{
if (Instance != null && Instance != this) { Destroy(this);}
else {Instance = this;}
}
[SerializeField]
GameTimestamp timestamp;
public float timeScale = 1.0f;
[Header("Day and Night cycle")]
//太阳的位置
public Transform sunTransform;
void Start()
{
timestamp = new GameTimestamp(0,GameTimestamp.Season.Spring,1,6,0);
StartCoroutine(TimeUpdate());
}
IEnumerator TimeUpdate()
{
while (true)
{
Tick();
yield return new WaitForSeconds(1 / timeScale);
}
}
public void Tick()
{
timestamp.UpdateClock();
//当前时间转换为分钟数(距0点的分钟数)
int timeInMinutes = GameTimestamp.HoursToMinutes(timestamp.hour) + timestamp.minute;
//太阳与地平线之间的夹角(左水平线夹角为0,零点时夹角为-90)
float sunAngle = .25f * timeInMinutes - 90;
//太阳的X轴旋转角度设置为sunAngle
sunTransform.eulerAngles = new Vector3(sunAngle,0,0);
}
}
(2) 赋值
3、测试方法
(1) 运行后加快运行时间,观察光照和影子
(2) 编辑PlayerController.cs测试:按下右方括号,时间加速
void Update()
{
Move();
Interact();
//测试光线逻辑
if(Input.GetKey(KeyCode.RightBracket))
{
TimeManager.Instance.Tick();
}
}
(3) 在后续会删除这个方法
四、时间影响场景
1、创建一个接口
(1) 创建ITimeTracker.cs作为接口,在每一帧都可以采用接口中的方法
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public interface ITimeTracker
{
void ClockUpdate(GameTimestamp timestamp);
}
(2) 作用:
允许不同的游戏对象或组件监听和响应游戏时钟的更新
用于同步游戏内的各种事件或行为,确保它们按照游戏的时间线正确执行
2、实时更新时间,并将时间信息传递给已注册的时间跟踪器进行处理
编辑TimeManager.cs,以实时更新时间,并将时间信息传递给已注册的时间跟踪器进行处理
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class TimeManager : MonoBehaviour
{
public static TimeManager Instance { get; private set; }
private void Awake()
{
if (Instance != null && Instance != this) { Destroy(this); }
else { Instance = this; }
}
[Header("Internal Clock")]
[SerializeField]
GameTimestamp timestamp;
public float timeScale = 1.0f;
[Header("Day and Night cycle")]
public Transform sunTransform;
//定义一个名为listeners的列表变量,以便在游戏中实时更新时间信息
//存储所有已注册的时间跟踪器。这些时间跟踪器可以通过注册和注销来管理
List<ITimeTracker> listeners = new List<ITimeTracker>();
void Start()
{
timestamp = new GameTimestamp(0, GameTimestamp.Season.Spring, 1, 6, 0);
StartCoroutine(TimeUpdate());
}
IEnumerator TimeUpdate()
{
while (true)
{
Tick();
yield return new WaitForSeconds(1 / timeScale);
}
}
public void Tick()
{
//更新时间戳
timestamp.UpdateClock();
//遍历所有已注册的时间跟踪器
foreach (ITimeTracker listener in listeners)
{
//调用ClockUpdate方法,将当前时间戳作为参数
listener.ClockUpdate(timestamp);
}
//调用新增的方法(太阳移动)
UpdateSunMovement();
}
//创建单独的太阳移动的方法,控制太阳在游戏中的位置和旋转
void UpdateSunMovement()
{
int timeInMinutes = GameTimestamp.HoursToMinutes(timestamp.hour) + timestamp.minute;
float sunAngle = .25f * timeInMinutes - 90;
sunTransform.eulerAngles = new Vector3(sunAngle, 0, 0);
}
//注册时间跟踪器,将其添加到listeners列表中
public void RegisterTracker(ITimeTracker listener)
{
listeners.Add(listener);
}
//注销时间跟踪器,将其从listeners列表中移除
public void UnregisterTracker(ITimeTracker listener)
{
listeners.Remove(listener);
}
}
五、显示时间
1、编辑UIManager.cs,更新Unity的Inspector面板上UIManager.cs组件下的Startas Bar中的时间
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
//添加接口,管理时间
public class UIManager : MonoBehaviour, ITimeTracker
{
public static UIManager Instance { get; private set; }
private void Awake()
{
if (Instance != null && Instance != this) { Destroy(this); }
else { Instance = this; }
}
[Header("Startas Bar")]
public Image toolEquipSlotImage;
//手持工具栏中的时间文本
public Text timeText;
public Text dateText;
[Header("Inventory System")]
public GameObject inventoryPanel;
public HandInventorySlot toolHandSlot;
public InventorySlot[] toolSlots;
public HandInventorySlot itemHandSlot;
public InventorySlot[] itemSlots;
public Text itemNameText;
public Text itemDescriptionText;
private void Start()
{
RenderInventory();
AssignSlotIndex();
//注册时间跟踪器(将UIManager添加到时间跟踪器的列表中)
TimeManager.Instance.RegisterTracker(this);
}
public void AssignSlotIndex()
{
for (int i = 0; i < toolSlots.Length; i++)
{
toolSlots[i].AssignIndex(i);
itemSlots[i].AssignIndex(i);
}
}
public void RenderInventory()
{
ItemData[] inventoryToolSlot = InventoryManager.Instance.tools;
RenderInventoryPanel(inventoryToolSlot, toolSlots);
ItemData[] inventoryItemSlots = InventoryManager.Instance.items;
RenderInventoryPanel(inventoryItemSlots, itemSlots);
toolHandSlot.Display(InventoryManager.Instance.equippedTool);
itemHandSlot.Display(InventoryManager.Instance.equippedItem);
ItemData equippedTool = InventoryManager.Instance.equippedTool;
if (equippedTool != null)
{
toolEquipSlotImage.sprite = equippedTool.thumbnail;
toolEquipSlotImage.gameObject.SetActive(true);
return;
}
toolEquipSlotImage.gameObject.SetActive(false);
}
void RenderInventoryPanel(ItemData[] slots, InventorySlot[] uiSlots)
{
for (int i = 0; i < uiSlots.Length; i++)
{
uiSlots[i].Display(slots[i]);
}
}
public void ToggleInventoryPanel()
{
inventoryPanel.SetActive(!inventoryPanel.activeSelf);
RenderInventory();
}
public void DisplayItemInfo(ItemData data)
{
if (data == null)
{
itemNameText.text = "";
itemDescriptionText.text = "";
return;
}
itemNameText.text = data.name;
itemDescriptionText.text = data.description;
}
//处理UI时间回调(回调:在特定的时刻执行自己定义的代码,实现特定的功能)
//更新游戏中的时钟
public void ClockUpdate(GameTimestamp timestamp)
{
//获取小时和分钟的值
int hours = timestamp.hour;
int minutes = timestamp.minute;
string prefix = "AM ";
//转换为下午的时间
if (hours > 12)
{
prefix = "PM ";
hours -= 12;
}
timeText.text = prefix + hours + ":" + minutes.ToString("00");
//获取日期\星期
int day = timestamp.day;
string season = timestamp.season.ToString();
string dayOfTheWeek = timestamp.GetDayOfTheWeek().ToString();
dateText.text = season + " " + day + " (" + dayOfTheWeek +")";
}
}
2、赋值(日期和时间)
六、把时间系统运用到耕地上
1、耕地在灌溉24小时后,变为耕种土壤
(1) 编辑GameTimestamp.cs,创建一个构造函数
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
[System.Serializable]
public class GameTimestamp
{
public int year;
public enum Season { Spring, Summer, Fall, Winter }
public enum DayOfTheWeek { Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday }
public Season season;
public int day;
public int hour;
public int minute;
public GameTimestamp(int year, Season season, int day, int hour, int minute)
{
this.year = year;
this.season = season;
this.day = day;
this.hour = hour;
this.minute = minute;
}
//创建一个新的构造函数
public GameTimestamp(GameTimestamp timestamp)
{
this.year=timestamp.year;
this.season = timestamp.season;
this.day = timestamp.day;
this.hour = timestamp.hour;
this.minute = timestamp.minute;
}
public void UpdateClock()
{
minute++;
if (minute >= 60) { minute = 0; hour++; }
if (hour >= 24) { hour = 0; day++; }
if (day > 30)
{
day = 1;
if (season == Season.Winter) { season = Season.Spring; year++; }
else { season++; }
}
}
public DayOfTheWeek GetDayOfTheWeek()
{
int daysPassed = YearsToDays(year) + SeasonsToDays(season) + day;
int dayIndex = daysPassed % 7;
return (DayOfTheWeek)dayIndex;
}
public static int HoursToMinutes(int hour) { return hour * 60; }
public static int DaysToHours(int days) { return days * 24; }
public static int SeasonsToDays(Season season)
{
int seasonIndex = (int)season;
return seasonIndex * 30;
}
public static int YearsToDays(int year) { return year * 4 * 30; }
}
(2) 编辑TimeManager.cs,获取新的时间戳用于计算灌溉时间
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class TimeManager : MonoBehaviour
{
public static TimeManager Instance { get; private set; }
private void Awake()
{
if (Instance != null && Instance != this) { Destroy(this); }
else { Instance = this; }
}
[SerializeField]
GameTimestamp timestamp;
public float timeScale = 1.0f;
[Header("Day and Night cycle")]
public Transform sunTransform;
List<ITimeTracker> listeners = new List<ITimeTracker>();
void Start()
{
timestamp = new GameTimestamp(0, GameTimestamp.Season.Spring, 1, 6, 0);
StartCoroutine(TimeUpdate());
}
IEnumerator TimeUpdate()
{
while (true)
{
Tick();
yield return new WaitForSeconds(1 / timeScale);
}
}
public void Tick()
{
timestamp.UpdateClock();
foreach (ITimeTracker listener in listeners)
{
listener.ClockUpdate(timestamp);
}
UpdateSunMovement();
}
void UpdateSunMovement()
{
int timeInMinutes = GameTimestamp.HoursToMinutes(timestamp.hour) + timestamp.minute;
float sunAngle = .25f * timeInMinutes - 90;
sunTransform.eulerAngles = new Vector3(sunAngle, 0, 0);
}
//获取新的时间戳
public GameTimestamp GetGameTimestamp()
{
return new GameTimestamp(timestamp);
}
public void RegisterTracker(ITimeTracker listener)
{
listeners.Add(listener);
}
public void UnregisterTracker(ITimeTracker listener)
{
listeners.Remove(listener);
}
}
(3) 编辑GameTimestamp.cs,创建一个新的方法,比较两个时间戳
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
[System.Serializable]
public class GameTimestamp
{
public int year;
public enum Season { Spring, Summer, Fall, Winter }
public enum DayOfTheWeek { Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday }
public Season season;
public int day;
public int hour;
public int minute;
public GameTimestamp(int year, Season season, int day, int hour, int minute)
{
this.year = year;
this.season = season;
this.day = day;
this.hour = hour;
this.minute = minute;
}
//创建一个新的构造函数
public GameTimestamp(GameTimestamp timestamp)
{
this.year = timestamp.year;
this.season = timestamp.season;
this.day = timestamp.day;
this.hour = timestamp.hour;
this.minute = timestamp.minute;
}
public void UpdateClock()
{
minute++;
if (minute >= 60) { minute = 0; hour++; }
if (hour >= 24) { hour = 0; day++; }
if (day > 30)
{
day = 1;
if (season == Season.Winter) { season = Season.Spring; year++; }
else { season++; }
}
}
public DayOfTheWeek GetDayOfTheWeek()
{
int daysPassed = YearsToDays(year) + SeasonsToDays(season) + day;
int dayIndex = daysPassed % 7;
return (DayOfTheWeek)dayIndex;
}
public static int HoursToMinutes(int hour) { return hour * 60; }
public static int DaysToHours(int days) { return days * 24; }
public static int SeasonsToDays(Season season)
{
int seasonIndex = (int)season;
return seasonIndex * 30;
}
public static int YearsToDays(int year) { return year * 4 * 30; }
// 比较两个不同的时间戳
public static int CompareTimestamp(GameTimestamp timestamp1, GameTimestamp timestamp2)
{
int timestamp1Hours = DaysToHours(YearsToDays(timestamp1.year)) + DaysToHours(SeasonsToDays(timestamp1.season)) + DaysToHours(timestamp1.day) + timestamp1.hour;
int timestamp2Hours = DaysToHours(YearsToDays(timestamp2.year)) + DaysToHours(SeasonsToDays(timestamp2.season)) + DaysToHours(timestamp2.day) + timestamp2.hour;
int difference = timestamp2Hours - timestamp1Hours;
return Mathf.Abs(difference);
}
}
(4) 编辑Land.cs,设置灌溉后土壤变化
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
//添加接口
public class Land : MonoBehaviour, ITimeTracker
{
public enum LandStatus { Soil, Farmland, Watered }
public LandStatus landStatus;
public Material soilMat, farmlandMat, wateredMat;
new Renderer renderer;
public GameObject select;
//灌溉时间
GameTimestamp timeWatered;
void Start()
{
renderer = GetComponent<Renderer>();
SwitchLandStatus(LandStatus.Soil);
//select.SetActive(false);
Select(false);
//注册时间戳
TimeManager.Instance.RegisterTracker(this);
}
public void SwitchLandStatus(LandStatus statusToSwitch)
{
landStatus = statusToSwitch;
Material materialToSwitch = soilMat;
switch (statusToSwitch)
{
case LandStatus.Soil: materialToSwitch = soilMat; break;
case LandStatus.Farmland: materialToSwitch = farmlandMat; break;
//灌溉后土壤状态
case LandStatus.Watered:
materialToSwitch = wateredMat;
//灌溉后流逝的时间
timeWatered = TimeManager.Instance.GetGameTimestamp();
break;
}
renderer.material = materialToSwitch;
}
public void Select(bool toggle)
{
select.SetActive(toggle);
}
public void Interact()
{
ItemData toolSlot = InventoryManager.Instance.equippedTool;
EquipmentData equipmentTool = toolSlot as EquipmentData;
if (equipmentTool != null)
{
EquipmentData.ToolType toolType = equipmentTool.toolType;
switch (toolType)
{
case EquipmentData.ToolType.Hoe:
SwitchLandStatus(LandStatus.Farmland); break;
case EquipmentData.ToolType.WateringCan:
SwitchLandStatus(LandStatus.Watered); break;
}
}
}
//设置灌溉后土壤的变化
public void ClockUpdate(GameTimestamp timestamp)
{
if (landStatus == LandStatus.Watered)
{
int hoursElapsed = GameTimestamp.CompareTimestamp(timeWatered, timestamp);
Debug.Log(hoursElapsed + "上次灌溉时间");
if (hoursElapsed > 24)
{
SwitchLandStatus(LandStatus.Farmland);
}
}
}
}