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()
{
//像判定区间,还有游戏难度,关卡属性,可以放在这里初始化
}
}