using System; using System.Collections.Generic; using UnityEngine; /// /// 用于描述Note判定情况的委托 /// public delegate void JudgeEventHandler(JugdeLevel level, Vector3 NotePos, BaseNote baseNote); public delegate void ReturnPoolEventHandler(BaseNote note); public enum JugdeLevel { Prefect, Great, Bad, Miss }; /// /// 游戏中所有物件的基类 /// /// /// /// 在这里,所有基于这个类派生出来的子对象我们都称之为 /// /// /// 它们都拥有,以及,因此,一个物件至少需要指定一个, /// 我们可以将装饰物件从这个类派生出来。并且添加去标识那些物件是装饰,使得我们能每一个物件都受到时间组控制。 /// /// /// 所以一个每一个物件对象至少都含有一个时间组对象的引用。也许关卡设计者会需要一些装饰物件,装饰物件同样也符合之前物件的特点够更加方便的管理他们。 /// 我们有时还需要统计这些物件的数量,以便于我们去完成更加复杂的逻辑。 /// /// /// 因此每一个物件都应该拥有以下属性: /// , /// , /// , /// /// /// /// 对于物件的行为,可以分为几种情况 /// 一个是物件的运动,物件若是想要确定自己的位置,它就必须拥有歌曲当前的时间,还有当前的速度。 /// 一个是物件的判定,判定的过程需要判定的区间,判定的范围,判定的所需的手指使用情况,并且传递自身的坐标,判定结果 /// 最后是物件的回收,当一个物件完成了所有的未尽事宜之后,就会自己返回对象池。 /// /// public abstract class BaseNote : MonoBehaviour { /// /// 用于存储自身在数据类的引用,用于检索,该字段仅在谱面编辑器中存在 /// public RuntimeBaseNoteData SelfRef; /// /// 物件自身的Transform组件引用 /// protected Transform _transform; /// /// 物件渲染层Transform引用 /// protected Transform _rendererTransform; /// /// 物件定位标记的Transform引用 /// protected Transform _anchorPointTransform; /// /// 目标时间,即一个物件生存周期的终点 /// [SerializeField] public float TargetTime { get; protected set; } /// /// 自身物件数,用于分数统计 /// protected int ItemQuantity { get; set; } /// /// 当前Note是否已经失效,当被标记为失效是将移除出管理器的判定序列 /// public bool IsValid { get; set; } /// /// 是否为装饰,勾选之后该物件将不会进入判定流程,它只在被实例化时决定 /// public bool IsDecorated { get; set; } /// /// 歌曲必要信息的容器,提供歌曲时间 /// public SongInformationContainer _songInformation { get; set; } /// /// 物件所属的BPM组 /// public BPMGroup _BPMGroup { get; set; } /// /// 当游玩物件被判定时触发该事件 /// public event JudgeEventHandler OnNoteJudged; /// /// 当物件在最后阶段触发,向对象池传递自身的引用 /// public event ReturnPoolEventHandler OnNoteUesd; /// /// 基于当前时间计算该物件的渲染的位置 /// protected virtual void UpdateRenderer(float currentTime, float noteSpeed) { /* 物件渲染机制: * 一个物件的生命周期是从被游戏控制器生成开始,到被游戏控制器判定标记失活之后,被对象池回收结束 * 其特点是,一旦生成,就会存在至自己的目标时间 * 至少生存至 目标时间-最大判定区间 ,至多生存至 目标时间+最大判定区间。 * * 在一个物件的生命周期内,其位置是关于时间的函数 * 它由音符的速度,目标时间与当前时间的差值(在未抵达判定区域前时当前时间通常小于目标时间) * 因此,它相对于0平面的位移是:f(currentTime)=NoteSpeed*(targetTime-currentTime) * 一旦一个音符被实例化,那么在它被回收失活之前,它的位置将如以上运动学方程所示 * * 渲染时间范围(instantiateOffset)和物件速度(NoteSpeed)在一局游戏中将不会是固定的,甚至在某些情况下,物件速度有可能会是负数 * 依据这样的方式计算出的物件将会从0平面的反向生成,向着背离玩家的方向运动直到被失活 * (targetTime-currentTime)这一项的值会从正数,逐渐变成0,最后变成负数 * 这意味着,即使物件速度发生了更改,物件的位置也会被重新计算,无论变换如何的复杂 * 物件一定会在目标时间到达判定平面,视存活时间穿过判定平面 * * 由于需要平衡游戏性能消耗的缘故,在实际的游戏中,一首曲子中的所有物件并不会全部生成 * 一个物件只有在进入渲染时间范围之后才会被实例化 * 渲染时间范围是在控制器依据当前游戏的物件速度,以及游戏场景中音符固定的下落距离,反向计算出的一个数值 * 已经被实例化的物件,即使在渲染时间范围变动后,到达了渲染时间范围之外,它也不会立刻消失 * 它依然会计算自己的位置,并且等待生命周期的结束 * 由于这里使用的时间是毫秒,所以在计算时需要更换会国际单位 */ float z = (TargetTime - currentTime) * noteSpeed; //使用世界坐标进行赋值 _transform.position = new Vector3(_transform.position.x, _transform.position.y, z); } protected virtual void UpdateAnchorPoint(float currentTime, float noteSpeed, float beatTime) { if (_anchorPointTransform.gameObject.activeSelf == true) return; if (_rendererTransform.position.z > 800 * noteSpeed * 0.001) return; _anchorPointTransform.gameObject.SetActive(true); } protected virtual void ThisObjectIsValid() { /* 由于物体其实并不存储于对象池中,对象池只负责提供新的对象 * 因此,物件在一般情况下是由其他数据结构管理的 * 为了方便区分那些超过了有效时间的物件 * 设置了一个用于检查的标志 */ IsValid = true; } protected virtual void ReturnNoteToPool() { OnNoteUesd?.Invoke(this); //因为在对象池回收时,存储在这些时间里面的函数并不会清空,需要手动清理 OnNoteJudged = null; } /// /// 检查自身是否超过判定时间(子类可重写) /// protected virtual void CheckMiss(float CurrentTime, float missInterval) { if (CurrentTime < TargetTime + missInterval) return; IsValid = true; OnNoteJudged?.Invoke(JugdeLevel.Miss, _transform.position, this); OnNoteUesd?.Invoke(this); } /// /// 用于执行Note的判定 /// public virtual void CheckHit(Dictionary isValidFingerID, Camera MainCamera) { OnNoteJudged?.Invoke(JugdeLevel.Prefect, _transform.position, this); } /// /// 用于初始化游戏所需配置等 /// public void InitConfigFile() { //像判定区间,还有游戏难度,关卡属性,可以放在这里初始化 } }