Unity Demo——3D平台跳跃游戏笔记

今天是一个3D平台跳跃游戏的笔记。

我们按照以下分类来对这个项目的代码进行学习:

核心游戏系统 (Core Game Systems)

核心游戏系统是IkunOdyssey项目的基础,负责所有游戏对象(如玩家、敌人、道具等)的通用行为和物理交互。它通过实体基础系统和游戏管理系统,为整个游戏提供了统一的物理、碰撞、状态管理和全局数据控制等功能,保证了游戏世界的有序运行和各模块的高效协作。

实体基础系统

实体基础系统为玩家、敌人等所有游戏对象提供统一的物理运动、碰撞检测和状态管理能力,是实现角色行为和交互的底层支撑。

物理移动系统

public virtual void Accelerate(Vector3 direction, float turningDrag, float acceleration, float topSpeed)
{if (direction.sqrMagnitude > 0){var speed = Vector3.Dot(direction, lateralVelocity);var velocity = direction * speed;var turningVelocity = lateralVelocity - velocity;var turningDelta = turningDrag * turningDragMultiplier * Time.deltaTime;var targetTopSpeed = topSpeed * topSpeedMultiplier;if (lateralVelocity.magnitude < targetTopSpeed || speed < 0){speed += acceleration * accelerationMultiplier * Time.deltaTime;speed = Mathf.Clamp(speed, -targetTopSpeed, targetTopSpeed);}velocity = direction * speed;turningVelocity = Vector3.MoveTowards(turningVelocity, Vector3.zero, turningDelta);lateralVelocity = velocity + turningVelocity;}
}public virtual void Decelerate(float deceleration)
{var delta = deceleration * decelerationMultiplier * Time.deltaTime;lateralVelocity = Vector3.MoveTowards(lateralVelocity, Vector3.zero, delta);
}public virtual void Gravity(float gravity)
{if (!isGrounded){verticalVelocity += Vector3.down * gravity * gravityMultiplier * Time.deltaTime;}
}public virtual void SlopeFactor(float upwardForce, float downwardForce)
{if (!isGrounded || !OnSlopingGround()) return;var factor = Vector3.Dot(Vector3.up, groundNormal);var downwards = Vector3.Dot(localSlopeDirection, lateralVelocity) > 0;var multiplier = downwards ? downwardForce : upwardForce;var delta = factor * multiplier * Time.deltaTime;lateralVelocity += localSlopeDirection * delta;
}

这部分代码实现了实体的物理移动,包括加速、减速、重力和斜坡处理。Accelerate方法让实体根据输入方向平滑加速并支持转向,Decelerate方法让实体速度逐渐归零,Gravity方法在实体不在地面时持续施加重力,SlopeFactor方法则根据实体是否在斜坡上调整其速度,模拟上坡减速和下坡加速的效果。这些方法保证了实体在不同地形和状态下都能有自然的物理表现。 

碰撞检测系统

public virtual bool CapsuleCast(Vector3 direction, float distance, int layer = Physics.DefaultRaycastLayers,QueryTriggerInteraction queryTriggerInteraction = QueryTriggerInteraction.Ignore)
{return CapsuleCast(direction, distance, out _, layer, queryTriggerInteraction);
}public virtual bool CapsuleCast(Vector3 direction, float distance,out RaycastHit hit, int layer = Physics.DefaultRaycastLayers,QueryTriggerInteraction queryTriggerInteraction = QueryTriggerInteraction.Ignore)
{var origin = position - direction * radius + center;var offset = transform.up * (height * 0.5f - radius);var top = origin + offset;var bottom = origin - offset;return Physics.CapsuleCast(top, bottom, radius, direction,out hit, distance + radius, layer, queryTriggerInteraction);
}public virtual bool SphereCast(Vector3 direction, float distance, int layer = Physics.DefaultRaycastLayers,QueryTriggerInteraction queryTriggerInteraction = QueryTriggerInteraction.Ignore)
{return SphereCast(direction, distance, out _, layer, queryTriggerInteraction);
}public virtual bool SphereCast(Vector3 direction, float distance,out RaycastHit hit, int layer = Physics.DefaultRaycastLayers,QueryTriggerInteraction queryTriggerInteraction = QueryTriggerInteraction.Ignore)
{var castDistance = Mathf.Abs(distance - radius);return Physics.SphereCast(position, radius, direction,out hit, castDistance, layer, queryTriggerInteraction);
}public virtual int OverlapEntity(Collider[] result, float skinOffset = 0)
{var contactOffset = skinOffset + controller.skinWidth + Physics.defaultContactOffset;var overlapsRadius = radius + contactOffset;var offset = (height + contactOffset) * 0.5f - overlapsRadius;var top = position + Vector3.up * offset;var bottom = position + Vector3.down * offset;return Physics.OverlapCapsuleNonAlloc(top, bottom, overlapsRadius, result);
}

实现了多种碰撞检测方式,包括胶囊体射线检测(CapsuleCast)、球体射线检测(SphereCast)和实体周围的重叠检测(OverlapEntity)。这些方法可以分别用于角色控制器的前方障碍检测、地面检测以及判断实体是否与其他物体发生接触,从而保证实体在游戏世界中能够正确地移动、落地和响应碰撞。

状态管理系统 

public virtual void Change<TState>() where TState : EntityState<T>
{var type = typeof(TState);if (m_states.ContainsKey(type)){Change(m_states[type]);}
}public virtual void Change(EntityState<T> to)
{if (to != null && Time.timeScale > 0){if (current != null){current.Exit(entity);events.onExit.Invoke(current.GetType());last = current;}current = to;current.Enter(entity);events.onEnter.Invoke(current.GetType());events.onChange?.Invoke();}
}public virtual void Step()
{if (current != null && Time.timeScale > 0){current.Step(entity);}
}

实现了实体的状态管理功能。Change<TState>方法可以通过类型切换到指定的状态,Change(EntityState<T>)方法则切换到具体的状态实例,并自动调用状态的退出和进入方法,同时触发相关事件。Step方法则在每一帧调用当前状态的Step逻辑,实现状态的持续行为处理。通过这种方式,实体可以灵活地在不同状态间切换,实现如移动、跳跃、受伤等复杂行为的管理。 

游戏管理

游戏管理系统负责全局数据的统一管理、关卡进度控制和存档功能,是游戏流程和数据安全的核心保障。

游戏主控制器

public class Game : Singleton<Game>
{public UnityEvent<int> OnRetriesSet;public UnityEvent OnSavingRequested;public int initialRetries = 3;public List<GameLevel> levels;protected int m_retries;protected int m_dataIndex;protected DateTime m_createdAt;protected DateTime m_updatedAt;public int retries{get { return m_retries; }set{m_retries = value;OnRetriesSet?.Invoke(m_retries);}}public static void LockCursor(bool value = true){
#if UNITY_STANDALONE || UNITY_WEBGLCursor.visible = value;Cursor.lockState = value ? CursorLockMode.Locked : CursorLockMode.None;
#endif}public virtual void LoadState(int index, GameData data){m_dataIndex = index;m_retries = data.retries;m_createdAt = DateTime.Parse(data.createdAt);m_updatedAt = DateTime.Parse(data.updatedAt);for (int i = 0; i < data.levels.Length; i++){levels[i].LoadState(data.levels[i]);}}public virtual LevelData[] LevelsData(){return levels.Select(level => level.ToData()).ToArray();}public virtual GameLevel GetCurrentLevel(){var scene = GameLoader.instance.currentScene;return levels.Find((level) => level.scene == scene);}public virtual int GetCurrentLevelIndex(){var scene = GameLoader.instance.currentScene;return levels.FindIndex((level) => level.scene == scene);}public virtual void RequestSaving(){GameSaver.instance.Save(ToData(), m_dataIndex);OnSavingRequested?.Invoke();}public virtual void UnlockLevelBySceneName(string sceneName){var level = levels.Find((level) => level.scene == sceneName);if (level != null){level.locked = false;}}public virtual void UnlockNextLevel(){var index = GetCurrentLevelIndex() + 1;if (index >= 0 && index < levels.Count){levels[index].locked = false;}}public virtual GameData ToData(){return new GameData(){retries = m_retries,levels = LevelsData(),createdAt = m_createdAt.ToString(),updatedAt = DateTime.UtcNow.ToString()};}protected override void Awake(){base.Awake();retries = initialRetries;DontDestroyOnLoad(gameObject);}
}

实现了游戏的全局管理功能。Game类采用单例模式,负责管理重试次数、关卡列表、数据加载与保存、关卡解锁等全局逻辑。它还通过事件机制与UI等其他系统解耦,保证了游戏流程的统一和数据的持久化。Awake方法确保Game对象在场景切换时不会被销毁,保证了全局数据的持续有效。

游戏数据管理 

[Serializable]
public class GameData
{public int retries;public LevelData[] levels;public string createdAt;public string updatedAt;public static GameData Create(){return new GameData(){retries = Game.instance.initialRetries,createdAt = DateTime.UtcNow.ToString(),updatedAt = DateTime.UtcNow.ToString(),levels = Game.instance.levels.Select((level) =>{return new LevelData(){locked = level.locked};}).ToArray()};}public virtual int TotalStars(){return levels.Aggregate(0, (acc, level) =>{var total = level.CollectedStars();return acc + total;});}public virtual int TotalCoins(){return levels.Aggregate(0, (acc, level) => acc + level.coins);}public virtual string ToJson(){return JsonUtility.ToJson(this);}public static GameData FromJson(string json){return JsonUtility.FromJson<GameData>(json);}
}

定义了游戏的数据结构和序列化方法。GameData类用于存储玩家的重试次数、关卡进度、创建和更新时间等信息,并提供了JSON序列化和反序列化的方法,方便数据的保存和读取。它还支持统计所有关卡的星星和金币总数,便于全局进度统计和成就系统的实现。

存档系统 

public class GameSaver : Singleton<GameSaver>
{public enum Mode { Binary, JSON, PlayerPrefs }public Mode mode = Mode.Binary;public string fileName = "save";public string binaryFileExtension = "data";protected static readonly int TotalSlots = 5;public virtual void Save(GameData data, int index){switch (mode){default:case Mode.Binary:SaveBinary(data, index);break;case Mode.JSON:SaveJSON(data, index);break;case Mode.PlayerPrefs:SavePlayerPrefs(data, index);break;}}public virtual GameData Load(int index){switch (mode){default:case Mode.Binary:return LoadBinary(index);case Mode.JSON:return LoadJSON(index);case Mode.PlayerPrefs:return LoadPlayerPrefs(index);}}public virtual void Delete(int index){switch (mode){default:case Mode.Binary:case Mode.JSON:DeleteFile(index);break;case Mode.PlayerPrefs:DeletePlayerPrefs(index);break;}}public virtual GameData[] LoadList(){var list = new GameData[TotalSlots];for (int i = 0; i < TotalSlots; i++){var data = Load(i);if (data != null){list[i] = data;}}return list;}// ... 省略具体的文件操作方法
}

实现了多格式的存档系统,支持二进制、JSON和PlayerPrefs三种存档方式,并支持多存档槽位。Save、Load、Delete等方法根据当前模式选择不同的存储方式,LoadList方法可以一次性加载所有存档槽的数据,方便实现多存档和存档管理功能。这样可以保证玩家的游戏进度能够安全、灵活地保存和恢复。

玩家系统 (Player System)

玩家系统是游戏中最核心的交互模块,负责玩家角色的输入响应、属性管理、动画音效、状态切换等一系列行为控制,确保玩家在游戏世界中的一切操作都能被准确、流畅地反馈和表现出来。

玩家核心控制

玩家核心控制模块负责玩家角色的整体行为调度,包括输入管理、属性管理、生命值、状态切换等,是所有玩家行为的总入口。

玩家主控制器

public class Player : Entity<Player>
{public PlayerEvents playerEvents;public Transform pickableSlot;public Transform skin;public PlayerInputManager inputs { get; protected set; }public PlayerStatsManager stats { get; protected set; }public Health health { get; protected set; }public bool onWater { get; protected set; }public bool holding { get; protected set; }public int jumpCounter { get; protected set; }public int airSpinCounter { get; protected set; }public int airDashCounter { get; protected set; }public float lastDashTime { get; protected set; }public Vector3 lastWallNormal { get; protected set; }public Pole pole { get; protected set; }public Collider water { get; protected set; }public Pickable pickable { get; protected set; }public virtual bool isAlive => !health.isEmpty;public virtual bool canStandUp => !SphereCast(Vector3.up, originalHeight);protected virtual void InitializeInputs() => inputs = GetComponent<PlayerInputManager>();protected virtual void InitializeStats() => stats = GetComponent<PlayerStatsManager>();protected virtual void InitializeHealth() => health = GetComponent<Health>();protected virtual void InitializeTag() => tag = GameTags.Player;protected override void Awake(){base.Awake();InitializeInputs();InitializeStats();InitializeHealth();InitializeTag();}
}

这段代码定义了玩家的主控制器Player类,继承自实体基础系统。它聚合了输入管理、属性管理、生命值、事件等多个组件,并在Awake阶段完成初始化。通过这些成员变量和初始化方法,玩家对象能够统一管理自身的输入、属性、生命状态和标签,成为所有玩家行为的核心调度中心。 

玩家输入管理 

public class PlayerInputManager : MonoBehaviour
{public Vector2 moveInput { get; private set; }public bool jumpPressed { get; private set; }public bool dashPressed { get; private set; }// ... 其他输入状态void Update(){moveInput = new Vector2(Input.GetAxis("Horizontal"), Input.GetAxis("Vertical"));jumpPressed = Input.GetButtonDown("Jump");dashPressed = Input.GetButtonDown("Dash");// ... 其他输入检测}
}

实现了玩家输入的采集和管理。PlayerInputManager会在每一帧读取键盘或手柄的输入状态,并将结果存储在对应的属性中,供玩家主控制器和状态机随时读取。这样可以实现输入与行为的解耦,方便后续扩展和多平台适配。

玩家属性管理

[CreateAssetMenu(fileName = "NewPlayerStats", menuName = "PLAYER TWO/Platformer Project/Player/New Player Stats")]
public class PlayerStats : EntityStats<PlayerStats>
{public float acceleration = 13f;public float deceleration = 28f;public float topSpeed = 6f;public int multiJumps = 1;public float maxJumpHeight = 17f;public float minJumpHeight = 10f;public float dashForce = 25f;public float dashDuration = 0.3f;// ... 其他属性
}

 定义了玩家的属性配置,采用ScriptableObject方式,便于在Unity编辑器中可视化调整。属性包括加速度、最大速度、跳跃高度、冲刺参数等,所有与玩家能力相关的数值都集中在这里统一管理,方便平衡和调优。

玩家状态管理

public class PlayerStateManager : EntityStateManager<Player>
{protected override List<EntityState<Player>> GetStateList(){return new List<EntityState<Player>>(){new IdlePlayerState(),new MovePlayerState(),new JumpPlayerState(),new SwimPlayerState(),// ... 其他状态};}
}

实现了玩家的状态管理器。通过重写GetStateList方法,将所有玩家可能的状态(如Idle、Move、Jump、Swim等)注册到状态机中。这样,玩家可以根据输入和环境在不同状态间切换,实现复杂的行为逻辑和动画切换。 

玩家功能模块

玩家功能模块包括动画、音效、粒子、相机等,负责将玩家的行为以视觉和听觉的方式反馈给玩家,提升游戏体验。

玩家动画控制

public class PlayerAnimator : MonoBehaviour
{public Animator animator;public void SetMoveSpeed(float speed){animator.SetFloat("MoveSpeed", speed);}public void SetJump(bool isJumping){animator.SetBool("IsJumping", isJumping);}// ... 其他动画参数设置
}

通过Animator组件控制玩家的动画状态。根据玩家的移动速度、跳跃等行为,动态设置动画参数,实现角色动作与实际操作的同步。

玩家音效与粒子 

public class PlayerAudio : MonoBehaviour
{public AudioSource audioSource;public AudioClip jumpClip;public AudioClip dashClip;public void PlayJump(){audioSource.PlayOneShot(jumpClip);}public void PlayDash(){audioSource.PlayOneShot(dashClip);}
}

负责播放玩家的音效反馈。每当玩家执行跳跃、冲刺等动作时,调用对应方法即可播放相应音效,增强操作的打击感和沉浸感。 

敌人系统 (Enemy System)

敌人系统负责游戏中所有敌方单位的行为逻辑、属性管理、AI感知、攻击判定、动画音效等。它不仅让敌人具备基础的移动、攻击、受伤、死亡等能力,还支持复杂的AI状态切换、路径巡逻、与玩家互动等高级功能,是游戏挑战性和趣味性的关键来源。

敌人核心控制

敌人核心控制模块负责敌人对象的整体行为调度,包括属性管理、状态切换、AI感知、攻击判定等,是所有敌人行为的总入口。

public class Enemy : Entity<Enemy>
{public EnemyEvents enemyEvents;protected Player m_player;public EnemyStatsManager stats { get; protected set; }public WaypointManager waypoints { get; protected set; }public Health health { get; protected set; }public Player player { get; protected set; }protected virtual void InitializeStatsManager() => stats = GetComponent<EnemyStatsManager>();protected virtual void InitializeWaypointsManager() => waypoints = GetComponent<WaypointManager>();protected virtual void InitializeHealth() => health = GetComponent<Health>();protected virtual void InitializeTag() => tag = GameTags.Enemy;protected override void Awake(){base.Awake();InitializeTag();InitializeStatsManager();InitializeWaypointsManager();InitializeHealth();}public override void ApplyDamage(int amount, Vector3 origin){if (!health.isEmpty && !health.recovering){health.Damage(amount);enemyEvents.OnDamage?.Invoke();if (health.isEmpty){controller.enabled = false;enemyEvents.OnDie?.Invoke();}}}public virtual void Revive(){if (!health.isEmpty) return;health.Reset();controller.enabled = true;enemyEvents.OnRevive.Invoke();}protected override void OnUpdate(){HandleSight();ContactAttack();}
}

定义了敌人的主控制器Enemy类,继承自实体基础系统。它聚合了属性管理、路径点管理、生命值、事件等多个组件,并在Awake阶段完成初始化。ApplyDamage方法处理敌人受伤和死亡,Revive方法实现复活,OnUpdate方法则每帧处理AI感知和攻击判定。通过这些成员变量和方法,敌人对象能够统一管理自身的属性、生命、AI和与玩家的互动。

敌人属性与状态管理

敌人属性配置

[CreateAssetMenu(fileName = "NewEnemyStats", menuName = "PLAYER TWO/Platformer Project/Enemy/New Enemy Stats")]
public class EnemyStats : EntityStats<EnemyStats>
{public float acceleration = 10f;public float deceleration = 20f;public float topSpeed = 4f;public float gravity = 30f;public float rotationSpeed = 600f;public bool canAttackOnContact = true;public int contactDamage = 1;public float contactPushBackForce = 5f;public float spotRange = 10f;public float viewRange = 15f;// ... 其他属性
}

这段代码定义了敌人的属性配置,采用ScriptableObject方式,便于在Unity编辑器中可视化调整。属性包括加速度、最大速度、重力、攻击参数、感知范围等,所有与敌人能力相关的数值都集中在这里统一管理,方便平衡和调优。

敌人属性管理器 

public class EnemyStatsManager : EntityStatsManager<EnemyStats> {}

通过泛型继承,直接复用实体属性管理器的功能,实现对EnemyStats的统一管理。

敌人状态管理 

public class EnemyStateManager : EntityStateManager<Enemy>
{protected override List<EntityState<Enemy>> GetStateList(){return new List<EntityState<Enemy>>(){new IdleEnemyState(),new PatrolEnemyState(),new ChaseEnemyState(),new AttackEnemyState(),new HurtEnemyState(),new DieEnemyState(),// ... 其他状态};}
}

通过重写GetStateList方法,将所有敌人可能的状态(如Idle、Patrol、Chase、Attack、Hurt、Die等)注册到状态机中。这样,敌人可以根据AI逻辑和环境在不同状态间切换,实现复杂的行为逻辑和动画切换。 

敌人AI与感知系统

视野与感知

protected virtual void HandleSight()
{if (!player){var overlaps = Physics.OverlapSphereNonAlloc(position, stats.current.spotRange, m_sightOverlaps);for (int i = 0; i < overlaps; i++){if (m_sightOverlaps[i].CompareTag(GameTags.Player)){if (m_sightOverlaps[i].TryGetComponent<Player>(out var player)){this.player = player;enemyEvents.OnPlayerSpotted?.Invoke();return;}}}}else{var distance = Vector3.Distance(position, player.position);if ((player.health.current == 0) || (distance > stats.current.viewRange)){player = null;enemyEvents.OnPlayerScaped?.Invoke();}}
}

实现了敌人的视野感知功能。HandleSight方法会在每一帧检测周围是否有玩家进入感知范围,如果发现玩家则触发“发现玩家”事件;如果玩家离开视野或死亡,则触发“玩家逃脱”事件。这样可以实现敌人对玩家的动态感知和追踪。

路径点与巡逻 

public class WaypointManager : MonoBehaviour
{public Transform[] waypoints;public int currentIndex = 0;public Transform GetCurrentWaypoint(){if (waypoints.Length == 0) return null;return waypoints[currentIndex];}public void MoveToNextWaypoint(){if (waypoints.Length == 0) return;currentIndex = (currentIndex + 1) % waypoints.Length;}
}

实现了敌人的路径点巡逻功能。WaypointManager管理一组巡逻点,敌人可以依次移动到每个点,实现循环巡逻。通过GetCurrentWaypoint和MoveToNextWaypoint方法,敌人AI可以灵活地控制巡逻路线。 

敌人攻击与交互

public virtual void ContactAttack()
{if (stats.current.canAttackOnContact){var overlaps = OverlapEntity(m_contactAttackOverlaps, stats.current.contactOffset);for (int i = 0; i < overlaps; i++){if (m_contactAttackOverlaps[i].CompareTag(GameTags.Player) &&m_contactAttackOverlaps[i].TryGetComponent<Player>(out var player)){var stepping = controller.bounds.max + Vector3.down * stats.current.contactSteppingTolerance;if (!player.IsPointUnderStep(stepping)){if (stats.current.contactPushback){lateralVelocity = -transform.forward * stats.current.contactPushBackForce;}player.ApplyDamage(stats.current.contactDamage, transform.position);enemyEvents.OnPlayerContact?.Invoke();}}}}
}

实现了敌人的接触攻击判定,ContactAttack方法会检测敌人周围是否有玩家,如果有则对玩家造成伤害,并根据配置决定是否将敌人自身击退。这样可以实现敌人与玩家的近战碰撞和伤害判定。 

敌人动画与音效

动画控制

public class EnemyAnimator : MonoBehaviour
{public Animator animator;public void SetMoveSpeed(float speed){animator.SetFloat("MoveSpeed", speed);}public void SetAttack(bool isAttacking){animator.SetBool("IsAttacking", isAttacking);}// ... 其他动画参数设置
}

通过Animator组件控制敌人的动画状态。根据敌人的移动速度、攻击等行为,动态设置动画参数,实现角色动作与实际AI行为的同步。

音效反馈 

public class EnemyAudio : MonoBehaviour
{public AudioSource audioSource;public AudioClip attackClip;public AudioClip hurtClip;public void PlayAttack(){audioSource.PlayOneShot(attackClip);}public void PlayHurt(){audioSource.PlayOneShot(hurtClip);}
}

播放敌人的音效反馈,每当敌人执行攻击、受伤等动作时,调用对应方法即可播放相应音效,增强敌人的存在感和打击感。 

敌人事件系统

[Serializable]
public class EnemyEvents
{public UnityEvent OnDamage;public UnityEvent OnDie;public UnityEvent OnRevive;public UnityEvent OnPlayerSpotted;public UnityEvent OnPlayerScaped;public UnityEvent OnPlayerContact;
}

定义了敌人的事件系统,通过UnityEvent,敌人可以在受伤、死亡、复活、发现玩家、玩家逃脱、接触玩家等关键时刻触发事件,方便与UI、音效、特效等其他系统解耦联动。 

关卡系统 (Level System)

关卡系统负责游戏中每一关的流程控制、数据管理、评分统计、重生与完成判定等,是游戏内容组织、进度推进和挑战反馈的核心模块。它不仅管理关卡的启动、暂停、重生、完成等流程,还负责关卡内的分数、星星、金币等收集与统计,确保玩家的游戏体验连贯且富有成就感。

关卡核心管理

关卡核心管理模块负责关卡的整体生命周期,包括关卡的初始化、暂停、重生、完成等,是所有关卡行为的总入口。

关卡主控制器

public class GameLevel : MonoBehaviour
{public string scene;public bool locked = true;public float time;public int coins;public bool[] stars = new bool[3];public void LoadState(LevelData data){locked = data.locked;time = data.time;coins = data.coins;stars = (bool[])data.stars.Clone();}public LevelData ToData(){return new LevelData{locked = this.locked,time = this.time,coins = this.coins,stars = (bool[])this.stars.Clone()};}
}

定义了关卡的主控制器GameLevel类,负责记录关卡的场景名、解锁状态、最佳时间、金币数和星星收集情况。LoadState和ToData方法用于关卡数据的加载和保存,方便与全局存档系统对接,实现关卡进度的持久化。

关卡数据结构 

[Serializable]
public class LevelData
{public bool locked;public float time;public int coins;public bool[] stars = new bool[3];public int CollectedStars(){int count = 0;foreach (var s in stars)if (s) count++;return count;}
}

定义了关卡的数据结构LevelData,包含解锁状态、最佳时间、金币数和星星收集情况,并提供了统计已收集星星数量的方法。该结构体用于关卡数据的存档、加载和统计。 

关卡流程控制

关卡启动与暂停

public class LevelStarter : MonoBehaviour
{void Start(){// 初始化关卡相关内容// 比如重置分数、计时、玩家位置等}
}public class LevelPauser : MonoBehaviour
{public void Pause(){Time.timeScale = 0;}public void Resume(){Time.timeScale = 1;}
}

实现了关卡的启动和暂停功能,LevelStarter在关卡开始时初始化相关内容,LevelPauser则通过修改Time.timeScale来实现暂停和恢复,保证玩家可以随时中断和继续游戏。 

关卡重生与完成

public class LevelRespawner : MonoBehaviour
{public Transform respawnPoint;public Player player;public void Respawn(){player.transform.position = respawnPoint.position;player.Respawn();}
}public class LevelFinisher : MonoBehaviour
{public void FinishLevel(){// 统计分数、保存进度、切换场景等LevelScore.instance.Consolidate();Game.instance.UnlockNextLevel();// ... 其他完成关卡的逻辑}
}

实现了关卡的重生和完成判定,LevelRespawner负责将玩家重置到重生点并恢复状态,LevelFinisher则在关卡完成时统计分数、保存进度并解锁下一关,确保关卡流程的完整性和连贯性。 

关卡评分与统计

public class LevelScore : Singleton<LevelScore>
{public UnityEvent<int> OnCoinsSet;public UnityEvent<bool[]> OnStarsSet;public UnityEvent OnScoreLoaded;public int coins { get; set; }public bool[] stars { get; private set; }public float time { get; private set; }public bool stopTime { get; set; } = true;public virtual void Reset(){time = 0;coins = 0;// stars初始化}public virtual void CollectStar(int index){stars[index] = true;OnStarsSet?.Invoke(stars);}public virtual void Consolidate(){// 更新关卡最佳成绩、保存数据// ...Game.instance.RequestSaving();}void Update(){if (!stopTime){time += Time.deltaTime;}}
}

这段代码实现了关卡的评分系统。LevelScore负责记录和统计关卡内的金币、星星、用时等数据,并通过事件通知UI等系统更新显示。CollectStar方法用于收集星星,Consolidate方法用于关卡完成时保存成绩和进度,Update方法则负责计时。

关卡UI与反馈

public class HUD : MonoBehaviour
{public Text retries;public Text coins;public Text health;public Text timer;public Image[] starsImages;protected Game m_game;protected LevelScore m_score;protected Player m_player;public virtual void Refresh(){// 刷新UI显示coins.text = m_score.coins.ToString("000");retries.text = m_game.retries.ToString("00");health.text = m_player.health.current.ToString("0");// 星星和计时等}void Awake(){m_game = Game.instance;m_score = LevelScore.instance;m_player = FindObjectOfType<Player>();// 监听事件,自动刷新}void Update(){// 实时更新计时器}
}

实现了关卡内的HUD显示,HUD负责将金币、重试次数、生命值、计时器、星星等信息实时显示在屏幕上,并通过事件机制与评分系统、游戏管理系统联动,保证UI信息的实时性和准确性。

关卡事件与数据交互 

关卡系统通过GameLevel、LevelData、LevelScore等类与全局的GameData、Game等系统进行数据交互,实现关卡进度的保存、加载、统计和解锁。每当关卡完成、分数更新、星星收集等事件发生时,都会通过Consolidate、ToData等方法将数据同步到全局存档,保证玩家的游戏进度不会丢失。

Consolidate(合并/整合)方法用于在关卡完成时,将本次游戏过程中获得的分数、星星、用时等数据,合并到当前关卡的GameLevel对象中,并触发全局存档。它确保如果玩家本次表现比历史最好成绩更好,就会更新关卡的最佳记录。

public virtual void Consolidate()
{if (m_level != null){if (m_level.time == 0 || time < m_level.time){m_level.time = time;}if (coins > m_level.coins){m_level.coins = coins;}m_level.stars = (bool[])stars.Clone();m_game.RequestSaving();}
}
  • 如果本次用时比历史最佳更短,则更新最佳用时。
  • 如果本次金币数比历史更多,则更新金币数。
  • 星星收集情况也会被更新。
  • 最后调用m_game.RequestSaving(),将最新数据保存到全局存档。

ToData方法的作用是将当前对象(如关卡、全局游戏)的运行时状态,转换为可序列化的数据结构(如LevelData、GameData),以便进行存档、网络传输或数据统计。

public LevelData ToData()
{return new LevelData{locked = this.locked,time = this.time,coins = this.coins,stars = (bool[])this.stars.Clone()};
}

 将GameLevel对象的状态(解锁、时间、金币、星星)转换为LevelData结构,便于存档和数据传递。

用户界面系统 (UI System)

UI系统负责游戏中所有用户界面元素的显示与交互,包括游戏内HUD、关卡选择、存档管理、动画反馈等。它不仅为玩家提供实时的游戏信息(如分数、生命、星星、计时等),还承担着菜单导航、存档操作、关卡切换等重要功能,是玩家与游戏世界沟通的桥梁。

HUD(游戏内信息显示)

HUD(Head-Up Display)是游戏过程中最常见的UI,负责实时显示玩家的分数、生命、金币、星星、计时等关键信息。

public class HUD : MonoBehaviour
{public string retriesFormat = "00";public string coinsFormat = "000";public string healthFormat = "0";public Text retries;public Text coins;public Text health;public Text timer;public Image[] starsImages;protected Game m_game;protected LevelScore m_score;protected Player m_player;protected float timerStep;protected static float timerRefreshRate = .1f;protected virtual void UpdateCoins(int value){coins.text = value.ToString(coinsFormat);}protected virtual void UpdateRetries(int value){retries.text = value.ToString(retriesFormat);}protected virtual void UpdateHealth(){health.text = m_player.health.current.ToString(healthFormat);}protected virtual void UpdateStars(bool[] value){for (int i = 0; i < starsImages.Length; i++){starsImages[i].enabled = value[i];}}protected virtual void UpdateTimer(){timerStep += Time.deltaTime;if (timerStep >= timerRefreshRate){timer.text = GameLevel.FormattedTime(m_score.time);timerStep = 0;}}public virtual void Refresh(){UpdateCoins(m_score.coins);UpdateRetries(m_game.retries);UpdateHealth();UpdateStars(m_score.stars);}protected virtual void Awake(){m_game = Game.instance;m_score = LevelScore.instance;m_player = FindObjectOfType<Player>();m_score.OnScoreLoaded.AddListener(() =>{m_score.OnCoinsSet.AddListener(UpdateCoins);m_score.OnStarsSet.AddListener(UpdateStars);m_game.OnRetriesSet.AddListener(UpdateRetries);m_player.health.onChange.AddListener(UpdateHealth);Refresh();});}protected virtual void Update() => UpdateTimer();
}

实现了HUD的全部核心功能。它通过监听分数、星星、重试次数、生命值等事件,实时刷新UI显示,并在Update中定时刷新计时器。Awake方法中完成了所有依赖对象的获取和事件绑定,Refresh方法则可手动强制刷新所有UI元素,保证信息的准确性和实时性。 

关卡选择与存档管理UI

关卡卡片与关卡列表

public class UILevelCard : MonoBehaviour
{public Text levelName;public Image[] stars;public Button playButton;public void SetData(LevelData data){levelName.text = "Level " + (data.levelIndex + 1);for (int i = 0; i < stars.Length; i++){stars[i].enabled = data.stars != null && i < data.stars.Length && data.stars[i];}playButton.interactable = !data.locked;}
}

实现了关卡选择界面中每个关卡卡片的显示。SetData方法根据LevelData设置关卡名称、星星收集情况和按钮可用状态,方便玩家直观了解每一关的进度和成就。 

存档卡片与存档列表

public class UISaveCard : MonoBehaviour
{public Text saveName;public Text progress;public Button loadButton;public Button deleteButton;public void SetData(GameData data, int slotIndex){saveName.text = $"存档{slotIndex + 1}";progress.text = $"星星:{data.TotalStars()} 金币:{data.TotalCoins()}";loadButton.interactable = true;deleteButton.interactable = true;}
}

实现了存档管理界面中每个存档卡片的显示。SetData方法根据GameData设置存档名称和进度信息,并控制按钮的可用性,方便玩家管理多个存档。 

UI动画与交互辅助

UI动画控制

public class UIAnimator : MonoBehaviour
{public Animator animator;public void PlayShow(){animator.SetTrigger("Show");}public void PlayHide(){animator.SetTrigger("Hide");}
}

通过Animator组件控制UI的显示和隐藏动画。PlayShow和PlayHide方法分别触发不同的动画状态,提升UI的动态表现力和用户体验。

UI焦点与自动滚动 

public class UIFocusKeeper : MonoBehaviour
{public Selectable defaultSelectable;void OnEnable(){if (defaultSelectable != null)defaultSelectable.Select();}
}

保证UI界面激活时自动聚焦到指定的按钮或输入框,提升键盘/手柄操作的友好性。 

事件系统 (Event System)

事件系统是游戏各个模块之间解耦通信的桥梁。它通过事件发布与监听机制,让玩家、敌人、关卡、UI等系统能够在关键时刻(如受伤、死亡、分数变化、关卡完成等)互相通知和响应,极大提升了代码的灵活性、可维护性和扩展性。

事件定义与基础结构

[Serializable]
public class PlayerEvent : UnityEvent<Player> {}

定义了一个带有Player参数的UnityEvent,方便在玩家相关的事件(如受伤、死亡、复活等)中传递玩家对象本身,实现更灵活的事件响应。

[Serializable]
public class EnemyEvents
{public UnityEvent OnDamage;public UnityEvent OnDie;public UnityEvent OnRevive;public UnityEvent OnPlayerSpotted;public UnityEvent OnPlayerScaped;public UnityEvent OnPlayerContact;
}

敌人相关的所有事件,包括受伤、死亡、复活、发现玩家、玩家逃脱、接触玩家等。每个事件都可以在Inspector中绑定多个响应函数,实现灵活的事件驱动。

[Serializable]
public class EntityEvents : UnityEvent {}

 定义了实体通用事件,适用于所有继承自Entity的对象,便于统一管理和扩展。

事件触发与监听

事件触发

// 以玩家受伤为例(Player.cs)
public override void ApplyDamage(int amount, Vector3 origin)
{if (!health.isEmpty && !health.recovering){health.Damage(amount);playerEvents.OnHurt?.Invoke();if (health.isEmpty){playerEvents.OnDie?.Invoke();}}
}

在玩家受到伤害时,先减少生命值,然后触发OnHurt事件。如果生命值归零,则触发OnDie事件。这样可以让UI、音效、动画等系统及时响应玩家受伤和死亡。

// 以敌人受伤为例(Enemy.cs)
public override void ApplyDamage(int amount, Vector3 origin)
{if (!health.isEmpty && !health.recovering){health.Damage(amount);enemyEvents.OnDamage?.Invoke();if (health.isEmpty){controller.enabled = false;enemyEvents.OnDie?.Invoke();}}
}

 在敌人受到伤害时,先减少生命值,然后触发OnDamage事件。如果生命值归零,则触发OnDie事件。这样可以让特效、音效、分数统计等系统及时响应敌人受伤和死亡。

事件监听

// 以UI监听玩家生命变化为例(HUD.cs)
protected virtual void Awake()
{m_game = Game.instance;m_score = LevelScore.instance;m_player = FindObjectOfType<Player>();m_player.health.onChange.AddListener(UpdateHealth);// 其他事件监听...
}

在HUD初始化时,监听玩家生命值变化事件,每当生命值变化时自动刷新UI显示,保证信息的实时性。

// 以关卡分数监听金币变化为例(LevelScore.cs)
public virtual int coins
{get { return m_coins; }set{m_coins = value;OnCoinsSet?.Invoke(m_coins);}
}

 在金币数量变化时,自动触发OnCoinsSet事件,通知所有监听者(如HUD)及时更新显示。

工具和辅助系统 (Tools & Utilities)

工具与辅助系统为项目开发和运行提供了各种便捷功能,包括类型名称工具、编辑器扩展、路径点管理、接口定义等。这些工具类和辅助脚本虽然不直接参与游戏核心玩法,但极大提升了开发效率、代码复用性和项目的可维护性。

类型名称工具

public static class ClassTypeName
{public static string Get<T>(){return typeof(T).Name;}
}

实现了一个静态工具类,用于获取任意类型的类名字符串。通过泛型方法Get<T>(),可以方便地在日志、调试、反射等场景下获取类型名称,提升代码的可读性和调试效率。 

编辑器扩展

#if UNITY_EDITOR
using UnityEditor;
using UnityEngine;public class ExampleEditorTool
{[MenuItem("Tools/Example/Print Hello")]public static void PrintHello(){Debug.Log("Hello from Editor Tool!");}
}
#endif

这段代码演示了如何通过Unity编辑器扩展自定义菜单项。开发者可以在Unity菜单栏中添加自定义工具,提升开发效率,比如批量操作、自动化处理等。 

路径点与导航辅助

public class WaypointManager : MonoBehaviour
{public Transform[] waypoints;public int currentIndex = 0;public Transform GetCurrentWaypoint(){if (waypoints.Length == 0) return null;return waypoints[currentIndex];}public void MoveToNextWaypoint(){if (waypoints.Length == 0) return;currentIndex = (currentIndex + 1) % waypoints.Length;}
}

实现了路径点管理功能,常用于敌人巡逻、NPC移动等场景。WaypointManager维护一组路径点,支持获取当前路径点和切换到下一个路径点,方便AI和导航系统的实现。 

接口与事件辅助

public interface IEntityContact
{void OnEntityContact(Entity entity);
}

定义了一个实体接触接口。实现该接口的组件可以在与其他实体发生接触时被自动调用,实现如机关触发、拾取、特殊交互等功能,提升系统的扩展性和灵活性。 

游戏标签和配置 (Game Configuration)

游戏标签和配置系统为项目提供了统一的标签定义、全局常量、Layer/Tag管理以及项目级的参数配置。它不仅方便代码中对不同类型对象的快速识别和分组,还为后续的物理碰撞、事件判定、关卡管理等提供了基础支撑,是保证项目规范性和可维护性的关键部分。

游戏标签定义

public static class GameTags
{public const string Player = "Player";public const string Enemy = "Enemy";public const string Platform = "Platform";public const string InteractiveRail = "InteractiveRail";// ... 其他标签
}

这段代码定义了一个静态类GameTags,集中管理了项目中所有用到的Tag字符串常量。这样做可以避免在代码中硬编码字符串,减少拼写错误,提高代码的可读性和可维护性。比如在碰撞检测、查找对象、事件判定等场景下,直接用GameTags.Player等常量即可。 

Layer/Tag 配置与使用

// 检查碰撞体是否为玩家
if (other.CompareTag(GameTags.Player))
{// 玩家相关逻辑
}

这段代码展示了如何在实际开发中使用标签常量进行对象类型判断。通过CompareTag方法结合GameTags常量,可以高效且安全地判断对象类型,便于实现不同对象的专属逻辑。 

项目全局配置

在Unity项目中,ProjectSettings/目录下包含了诸如TagManager.asset、InputManager.asset、Physics2DSettings.asset等全局配置文件。这些文件通过Unity编辑器进行管理,决定了项目的输入映射、物理参数、标签和Layer等全局行为。

示例说明:

  • TagManager.asset:管理所有Tag和Layer的定义,保证代码和编辑器中的标签一致。
  • InputManager.asset:配置所有输入轴和按键映射,便于多平台适配。
  • Physics2DSettings.asset/PhysicsSettings.asset:配置物理引擎参数,如重力、碰撞层、物理步长等。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/news/914165.shtml
繁体地址,请注明出处:http://hk.pswp.cn/news/914165.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【C语言】回调函数、转移表、qsort 使用与基于qsort改造冒泡排序

文章目录数组指针/指针数组函数指针函数指针数组函数指针数组用途(转移表)回调函数qsort函数基于qsort改造冒泡排序源码数组指针/指针数组 int arr1[5] { 1,2,3,4,5 };int (*p1)[5] &arr1; //p1是数组指针变量int* arr2[5] { 0 }; //arr2是指针数组指针数组是存放指…

vue3 uniapp 使用ref更新值后子组件没有更新 ref reactive的区别?使用from from -item执行表单验证一直提示没有值

遇到这样一个问题&#xff0c;我有个1个页面A&#xff0c;一个from表单组件&#xff0c;一个form-item组件&#xff0c; 使用是这样的&#xff0c;我在父组件A中使用 &#xff0c;执行表单验证一直提示没有值咱们先来讲一讲ref 和reactive的区别 ref 用来创建一个基本类型或单…

PyQt5布局管理(QBoxLayout(框布局))

QBoxLayout&#xff08;框布局&#xff09; 采用QBoxLayout类可以在水平和垂直方向上排列控件&#xff0c;QHBoxLayout和 QVBoxLayout类继承自QBoxLayout类。 QHBoxLayout&#xff08;水平布局&#xff09; 采用QHBoxLayout类&#xff0c;按照从左到右的顺序来添加控件。QHBoxL…

Grok 4作战图刷爆全网,80%华人横扫硅谷!清华上交校友领衔,95后站C位

来源 | 新智元短短两年&#xff0c;马斯克Grok 4的横空出世&#xff0c;让xAI团队一举站上AI之巅。昨日一小时发布会&#xff0c;Grok 4让所有人大开眼界&#xff0c;直接刷爆了AIME 2025、人类最后的考试&#xff08;HLE&#xff09;两大基准。这是狂堆20万GPU才换来的惊人成果…

AI大模型(七)Langchain核心模块与实战(二)

Langchain核心模块与实战&#xff08;二&#xff09;Langchian向量数据库检索Langchian构建向量数据库和检索器批量搜索返回与之相似度最高的第一个检索器和模型结合得到非笼统的答案LangChain构建代理通过代理去调用Langchain构建RAG的对话应用包含历史记录的对话生成Langchia…

Flutter基础(前端教程①-容器和控件位置)

一个红色背景的 Container垂直排列的 Column 布局中央的 ElevatedButton按钮下方的白色文本import package:flutter/material.dart;void main() {runApp(const MyApp()); }class MyApp extends StatelessWidget {const MyApp({Key? key}) : super(key: key);overrideWidget bu…

CSS flex

目录 flex-box和flex-item 主轴和副轴 ​编辑 flex-box的属性 flex-direction flex-wrap flex-flow justify-content ​编辑​align-items align-content flex-item的属性 flex-basis flex-grow flex-shrink flex flex-box和flex-item 当把一个块级元素的displ…

【JMeter】执行系统命令

步骤如下&#xff1a; 添加JSP233 Sampler&#xff1a;右击线程组>添加>取样器>JSR223 Sampler2.填写脚本&#xff0c;执行后查看日志。res "ipconfig".execute().text log.info(res)res "python -c \"print(11)\"".execute().text l…

AI Agent开发学习系列 - langchain之memory(1):内存中的短时记忆

内存中的短时记忆&#xff0c;在 LangChain 中通常指 ConversationBufferMemory 这类“对话缓冲记忆”工具。它的作用是&#xff1a;在内存中保存最近的对话历史&#xff0c;让大模型能理解上下文&#xff0c;实现连续对话。 对话缓冲记忆”工具 主要特点 只保留最近的对话内容…

uniapp实现微信小程序端图片保存到相册

效果图展示 安装插件海报画板导入到项目里面&#xff0c;在页面直接使用 <template><view><button click"saveToAlbum" class"save-button">保存到相册</button><image :src"path" mode"widthFix" v-if&qu…

Java生产带文字、带边框的二维码

Java 生成带文字、带边框的二维码1、Java 生成带文字的二维码1.1、导入jar包1.2、普通单一的二维码1.2.1、代码示例1.2.2、效果1.3、带文字的二维码1.&#xff13;.&#xff11;、代码示例1.3.2、效果2、带边框的二维码2.1、代码示例2.2、带边框的二维码效果 1、Java 生成带文字…

ARM单片机启动流程(三)(栈空间综合理解及相关实际应用)

文章目录1、引出栈空间问题2、解决问题2.1、RAM空间2.2、RAM空间具体分布2.3、关于栈空间的使用2.4、栈溢出2.5、变量的消亡2.6、回到关键字static2.7、合法性的判断1、引出栈空间问题 从static关键字引出该部分内容。 为什么能从static引出来&#xff1f; 在使用该关键字的…

【RK3568+PG2L50H开发板实验例程】FPGA部分 | 键控LED实验

本原创文章由深圳市小眼睛科技有限公司创作&#xff0c;版权归本公司所有&#xff0c;如需转载&#xff0c;需授权并注明出处&#xff08;www.meyesemi.com) 1.实验简介 实验目的&#xff1a; 从创建工程到编写代码&#xff0c;完成引脚约束&#xff0c;最后生成 bit 流下载到…

【Python练习】039. 编写一个函数,反转一个单链表

039. 编写一个函数,反转一个单链表 039. 编写一个函数,反转一个单链表方法 1:迭代实现运行结果代码解释方法 2:递归实现运行结果代码解释选择方法迭代法与递归法的区别039. 编写一个函数,反转一个单链表 在 Python 中,可以通过迭代或递归的方式反转一个单链表。 方法 1…

BERT代码简单笔记

参考视频&#xff1a;BERT代码(源码)从零解读【Pytorch-手把手教你从零实现一个BERT源码模型】_哔哩哔哩_bilibili 一、BertTokenizer BertTokenizer 是基于 WordPiece 算法的 BERT 分词器&#xff0c;继承自 PreTrainedTokenizer。 继承的PretrainedTokenizer&#xff0c;具…

PID控制算法理论学习基础——单级PID控制

这是一篇我在学习PID控制算法的过程中的学习记录。在一开始学习PID的时候&#xff0c;我也看了市面上许多的资料&#xff0c;好的资料固然有&#xff0c;但是更多的是不知所云。&#xff08;有的是写的太过深奥&#xff0c;有的则是照搬挪用&#xff0c;对原理则一问三不知&…

【Elasticsearch】function_score与rescore

它们俩都是用来“**干涉评分**”的&#xff0c;但**工作阶段不同、性能开销不同、能做的事也不同**。一句话总结&#xff1a;> **function_score** 在 **第一次算分** 时就动手脚&#xff1b; > **rescore** 在 **拿到 Top-N 结果后** 再“重新打分”。下面把“能干嘛”…

无广告纯净体验 WPS2016 精简版:移除联网模块 + 非核心组件,古董电脑也能跑

各位办公小能手们&#xff01;今天给你们介绍一款超神的办公软件——WPS2016精简版&#xff01;它有多小呢&#xff1f;才33MB&#xff0c;简直就是软件界的小不点儿&#xff01;别看它个头小&#xff0c;功能可一点儿都不含糊&#xff0c;文字、表格、演示这三大功能它全都有。…

《PyWin32:Python与Windows的桥梁,解锁系统自动化新姿势》

什么是 PyWin32在 Windows 平台的 Python 开发领域中&#xff0c;PyWin32 是一个举足轻重的库&#xff0c;它为 Python 开发者打开了一扇直接通往 Windows 操作系统底层功能的大门。简单来说&#xff0c;PyWin32 是用于 Python 访问 Windows API&#xff08;Application Progra…

vite如何生成gzip,并在服务器上如何设置开启

1. 安装插件npm install vite-plugin-compression -D2. 在 vite.config.ts 中配置TypeScriptimport { defineConfig } from vite import compression from vite-plugin-compressionexport default defineConfig({plugins: [compression({algorithm: gzip,ext: .gz,threshold: 1…