HartoukChartEditor/Assets/Script/PlayObject/BaseNote.cs

175 lines
8.2 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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