175 lines
8.2 KiB
C#
175 lines
8.2 KiB
C#
using System;
|
||
using System.Collections.Generic;
|
||
using UnityEngine;
|
||
|
||
/// <summary>
|
||
/// 用于描述Note判定情况的委托
|
||
/// </summary>
|
||
public delegate void JudgeEventHandler(JugdeLevel level, Vector3 NotePos, BaseNote baseNote);
|
||
public delegate void ReturnPoolEventHandler(BaseNote note);
|
||
public enum JugdeLevel
|
||
{
|
||
Prefect, Great, Bad, Miss
|
||
};
|
||
/// <summary>
|
||
/// 游戏中所有物件的基类
|
||
/// </summary>
|
||
/// <remarks>
|
||
/// <para>
|
||
/// 在这里,所有基于这个类派生出来的子对象我们都称之为<paramref name="物件"/>
|
||
/// </para>
|
||
/// <para>
|
||
/// 它们都拥有<paramref name="被创建的时间"/>,以及<paramref name="被回收的时间"/>,因此,一个物件至少需要指定一个<paramref name="TargetTime"/>,
|
||
/// 我们可以将装饰物件从这个类派生出来。并且添加<paramref name="IsDecorated"/>去标识那些物件是装饰,使得我们能每一个物件都受到时间组控制。
|
||
/// </para>
|
||
/// <para>
|
||
/// 所以一个每一个物件对象至少都含有一个时间组对象的引用。也许关卡设计者会需要一些装饰物件,装饰物件同样也符合之前物件的特点够更加方便的管理他们。
|
||
/// 我们有时还需要统计这些物件的数量,以便于我们去完成更加复杂的逻辑。
|
||
/// </para>
|
||
/// <para>
|
||
/// <term>因此每一个物件都应该拥有以下属性:</term>
|
||
/// <paramref name="TargetTime"/>,
|
||
/// <paramref name="ItemQuantity"/>,
|
||
/// <paramref name="IsValid"/>,
|
||
/// <paramref name="IsDecorated"/>
|
||
/// </para>
|
||
/// <para>
|
||
/// 对于物件的行为,可以分为几种情况
|
||
/// 一个是物件的运动,物件若是想要确定自己的位置,它就必须拥有歌曲当前的时间,还有当前的速度。
|
||
/// 一个是物件的判定,判定的过程需要判定的区间,判定的范围,判定的所需的手指使用情况,并且传递自身的坐标,判定结果
|
||
/// 最后是物件的回收,当一个物件完成了所有的未尽事宜之后,就会自己返回对象池。
|
||
/// </para>
|
||
/// </remarks>
|
||
public abstract class BaseNote : MonoBehaviour
|
||
{
|
||
/// <summary>
|
||
/// 用于存储自身在数据类的引用,用于检索,该字段仅在谱面编辑器中存在
|
||
/// </summary>
|
||
public RuntimeBaseNoteData SelfRef;
|
||
|
||
/// <summary>
|
||
/// 物件自身的Transform组件引用
|
||
/// </summary>
|
||
protected Transform _transform;
|
||
/// <summary>
|
||
/// 物件渲染层Transform引用
|
||
/// </summary>
|
||
protected Transform _rendererTransform;
|
||
/// <summary>
|
||
/// 物件定位标记的Transform引用
|
||
/// </summary>
|
||
protected Transform _anchorPointTransform;
|
||
|
||
/// <summary>
|
||
/// 目标时间,即一个物件生存周期的终点
|
||
/// </summary>
|
||
[SerializeField]
|
||
public float TargetTime { get; protected set; }
|
||
/// <summary>
|
||
/// 自身物件数,用于分数统计
|
||
/// </summary>
|
||
protected int ItemQuantity { get; set; }
|
||
/// <summary>
|
||
/// 当前Note是否已经失效,当被标记为失效是将移除出管理器的判定序列
|
||
/// </summary>
|
||
public bool IsValid { get; set; }
|
||
/// <summary>
|
||
/// 是否为装饰,勾选之后该物件将不会进入判定流程,它只在被实例化时决定
|
||
/// </summary>
|
||
public bool IsDecorated { get; set; }
|
||
/// <summary>
|
||
/// 歌曲必要信息的容器,提供歌曲时间
|
||
/// </summary>
|
||
public SongInformationContainer _songInformation { get; set; }
|
||
/// <summary>
|
||
/// 物件所属的BPM组
|
||
/// </summary>
|
||
public BPMGroup _BPMGroup { get; set; }
|
||
/// <summary>
|
||
/// 当游玩物件被判定时触发该事件
|
||
/// </summary>
|
||
public event JudgeEventHandler OnNoteJudged;
|
||
/// <summary>
|
||
/// 当物件在最后阶段触发,向对象池传递自身的引用
|
||
/// </summary>
|
||
public event ReturnPoolEventHandler OnNoteUesd;
|
||
/// <summary>
|
||
/// 基于当前时间计算该物件的渲染的位置
|
||
/// </summary>
|
||
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;
|
||
}
|
||
/// <summary>
|
||
/// 检查自身是否超过判定时间(子类可重写)
|
||
/// </summary>
|
||
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);
|
||
}
|
||
/// <summary>
|
||
/// 用于执行Note的判定
|
||
/// </summary>
|
||
public virtual void CheckHit(Dictionary<int, bool> isValidFingerID, Camera MainCamera)
|
||
{
|
||
OnNoteJudged?.Invoke(JugdeLevel.Prefect, _transform.position, this);
|
||
}
|
||
/// <summary>
|
||
/// 用于初始化游戏所需配置等
|
||
/// </summary>
|
||
public void InitConfigFile()
|
||
{
|
||
//像判定区间,还有游戏难度,关卡属性,可以放在这里初始化
|
||
}
|
||
}
|