尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Unity动作游戏开发:构建赛博吸血鬼战斗系统的核心技术架构

Unity动作游戏开发:构建赛博吸血鬼战斗系统的核心技术架构 如果你是一位游戏开发者正在寻找一个能瞬间点燃玩家肾上腺素、兼具视觉冲击与深度策略的动作游戏框架或者你是一位技术爱好者对如何用代码构建一个充满“赛博吸血鬼”美学的战斗世界感到好奇那么你找对地方了。“【TAKUMI³】Cybernetic Vampire [RAVAGE 15] 「ALL JUST」”这个标题初看像是一个神秘的独立游戏或模组。它背后指向的很可能是一个融合了高速战斗、角色成长、技能组合与华丽特效的动作游戏原型或技术演示。这类项目通常不是为了直接发布而是开发者用来验证核心玩法、展示技术实力或探索特定美学风格的“试验场”。本文将为你深度拆解这类高概念动作游戏项目的核心构建逻辑。我们不会停留在复述“它很酷”而是会深入到如何从零开始架构一个类似的“赛博吸血鬼”战斗系统它的核心循环是什么代码层面如何实现高速移动、连击判定与资源管理以及在追求“爽快感”的同时开发者最容易在哪些地方“翻车”无论你是想学习游戏开发实战还是想理解一个复杂游戏系统的设计哲学这篇文章都将提供一条从概念到代码的清晰路径。1. 这篇文章真正要解决的问题如何构建一个“高速、高表现力”的动作游戏核心为什么“赛博吸血鬼”或类似的高概念动作游戏值得单独讨论因为它代表了动作游戏开发中的一个高难度挑战集合。这类游戏的成功绝不只在于美术资源堆砌而在于一套精密耦合的技术与设计系统。核心痛点很多开发者尝试制作酷炫的动作游戏时常陷入两个极端有想法难落地脑子里有华丽的连招和特效但写出来的代码角色动作僵硬判定不准手感稀烂。有技术没灵魂实现了基础的移动和攻击但玩起来枯燥乏味缺乏那个让玩家“上头”的成长循环和正反馈。本文要解决的正是如何跨越从“概念”到“可玩原型”的鸿沟。我们将聚焦于构建一个类似“Cybernetic Vampire”项目的核心技术支柱战斗手感如何实现流畅的移动、精准的输入响应和爽快的打击感技能系统如何设计可扩展、易组合的技能模块支持“吸血鬼”式的生命偷取、能量转化等特色机制状态管理如何优雅地处理角色复杂的动作状态移动、攻击、闪避、受击等避免BUG丛生视觉反馈如何通过代码如屏幕抖动、粒子特效、动画事件而非单纯靠美术来提升战斗的表现力我们将使用一个通用的游戏开发框架如Unity和C#语言来演示但其中设计模式和架构思想适用于任何引擎。2. 核心概念与架构设计在动手写代码前我们必须先建立正确的架构认知。一个混乱的架构会让后期添加新技能或调整手感变得噩梦般困难。2.1 核心循环资源驱动与风险回报“赛博吸血鬼”这类角色其魅力往往在于风险与回报的博弈。一个典型的核心循环可能是攻击命中敌人获取“血液”或“能量”资源。消耗资源强化自身治疗、增伤、获得特殊能力。资源不足时进入虚弱状态需要更激进地进攻来补充。通过高风险高回报的技能如需要蓄力或消耗大量资源来制造爆发点。在代码层面这意味着我们需要一个全局的ResourceManager来管理这些核心资源并让各个系统技能、状态、UI与之交互。2.2 状态机角色行为的指挥官角色在同一时刻只能做一件事要么移动要么攻击要么闪避。用一堆布尔变量isAttacking,isDashing来控制会迅速导致逻辑冲突。有限状态机Finite State Machine, FSM是解决这一问题的标准答案。我们将为角色定义几个核心状态LocomotionState基础移动和待机。AttackState处理攻击连段每个攻击子状态Attack1, Attack2...可以独立配置伤害、硬直和资源获取。DashState处理高速位移闪避/冲刺通常有无敌帧。HitState受击状态播放受击动画并中断当前行为。SkillState释放主动技能。状态机负责状态的切换规则例如可以从LocomotionState切换到AttackState但AttackState中不能直接切换到另一个AttackState除非通过连招逻辑必须先回到LocomotionState或进入一个特殊的ComboWindowState连招窗口状态。2.3 组件化设计高内聚低耦合我们将采用基于组件的架构这是现代游戏引擎如Unity的核心思想。每个功能模块都是一个独立的脚本组件PlayerInputHandler封装所有输入检测向其他组件发送“命令”而不是直接修改状态。CharacterMotor负责物理移动、重力应用、与地面的检测。StateMachine如上所述管理状态流转。SkillSystem管理所有技能的冷却、释放条件和效果应用。HealthSystem管理生命值、护盾以及“吸血鬼”特色的吸血效果。ResourceSystem管理能量、血液等自定义资源。AnimationController作为动画状态机Animator的桥梁根据逻辑状态触发动画。3. 环境准备与项目初始化我们以Unity 2022 LTS版本为例使用C#进行开发。请确保你已安装Unity Hub和对应版本的Unity Editor。创建新项目选择3D核心模板命名为CyberVampirePrototype。设置项目结构在Assets文件夹下创建清晰的目录结构这对中大型项目至关重要。Assets/ ├── _Scripts/ │ ├── Core/ // 核心架构如单例、事件系统 │ ├── Characters/ // 角色相关脚本 │ │ ├── Components/ // 输入、移动、状态机等组件 │ │ └── States/ // 所有状态机状态脚本 │ ├── Skills/ // 技能数据与逻辑 │ ├── UI/ // 用户界面脚本 │ └── Utilities/ // 工具类、扩展方法 ├── _Art/ ├── _Audio/ └── _Prefabs/导入必要资源为了快速验证玩法可以从Unity Asset Store或Mixamo寻找免费的第三人称角色模型和基础动画Idle, Run, Attack, Jump, Hit。4. 核心系统实现从输入到移动4.1 输入处理层创建一个PlayerInputHandler组件它不直接驱动角色而是将原始输入转化为干净的命令事件。// 文件路径Assets/_Scripts/Characters/Components/PlayerInputHandler.cs using UnityEngine; using UnityEngine.InputSystem; // 使用新的Input System public class PlayerInputHandler : MonoBehaviour { // 输入事件供其他组件订阅 public System.ActionVector2 OnMovePerformed; public System.Action OnAttackPressed; public System.Action OnDashPressed; public System.Action OnSkillPressed; private PlayerInputActions _inputActions; private void Awake() { _inputActions new PlayerInputActions(); _inputActions.Enable(); } private void OnEnable() { _inputActions.Player.Move.performed ctx OnMovePerformed?.Invoke(ctx.ReadValueVector2()); _inputActions.Player.Attack.performed ctx OnAttackPressed?.Invoke(); _inputActions.Player.Dash.performed ctx OnDashPressed?.Invoke(); _inputActions.Player.Skill.performed ctx OnSkillPressed?.Invoke(); } private void OnDisable() { // 清理事件订阅防止内存泄漏 _inputActions.Player.Move.performed - ctx OnMovePerformed?.Invoke(ctx.ReadValueVector2()); _inputActions.Player.Attack.performed - ctx OnAttackPressed?.Invoke(); _inputActions.Player.Dash.performed - ctx OnDashPressed?.Invoke(); _inputActions.Player.Skill.performed - ctx OnSkillPressed?.Invoke(); _inputActions.Disable(); } // 获取当前移动输入值用于状态机等需要持续读取的地方 public Vector2 GetMoveInput() { return _inputActions.Player.Move.ReadValueVector2(); } }关键点使用Unity的新Input System它更强大且易于配置。通过事件Action解耦输入与逻辑使StateMachine可以更灵活地响应或忽略输入。4.2 角色移动与物理创建一个CharacterMotor组件负责将输入转化为实际位移。// 文件路径Assets/_Scripts/Characters/Components/CharacterMotor.cs using UnityEngine; [RequireComponent(typeof(CharacterController))] public class CharacterMotor : MonoBehaviour { [Header(Movement Settings)] [SerializeField] private float _walkSpeed 5f; [SerializeField] private float _runSpeed 8f; [SerializeField] private float _rotationSpeed 15f; [SerializeField] private float _gravity -9.81f; private CharacterController _controller; private Vector3 _velocity; private bool _isGrounded; private float _currentSpeed; private void Awake() { _controller GetComponentCharacterController(); _currentSpeed _walkSpeed; } public void Move(Vector2 inputDirection, bool isRunning) { _isGrounded _controller.isGrounded; if (_isGrounded _velocity.y 0) { _velocity.y -2f; // 轻微向下的力确保贴地 } // 计算移动 Vector3 moveDirection new Vector3(inputDirection.x, 0, inputDirection.y); moveDirection Camera.main.transform.TransformDirection(moveDirection); // 相对于相机方向 moveDirection.y 0; moveDirection.Normalize(); _currentSpeed isRunning ? _runSpeed : _walkSpeed; _controller.Move(moveDirection * _currentSpeed * Time.deltaTime); // 角色朝向移动方向可选取决于游戏类型 if (moveDirection.magnitude 0.1f) { Quaternion targetRotation Quaternion.LookRotation(moveDirection); transform.rotation Quaternion.Slerp(transform.rotation, targetRotation, _rotationSpeed * Time.deltaTime); } // 应用重力 _velocity.y _gravity * Time.deltaTime; _controller.Move(_velocity * Time.deltaTime); } // 用于DashState的瞬间位移 public void Dash(Vector3 direction, float dashPower) { _controller.Move(direction * dashPower * Time.deltaTime); } public bool IsGrounded() _isGrounded; }5. 心脏系统有限状态机FSM的实现这是整个角色控制的核心。我们将实现一个轻量但功能完整的FSM。// 文件路径Assets/_Scripts/Characters/Components/StateMachine.cs using System.Collections.Generic; using UnityEngine; public class StateMachine : MonoBehaviour { private IState _currentState; private DictionarySystem.Type, IState _stateTable new DictionarySystem.Type, IState(); private void Update() { _currentState?.OnUpdate(); } private void FixedUpdate() { _currentState?.OnFixedUpdate(); } public void AddState(IState state) { state.SetOwner(this); _stateTable.Add(state.GetType(), state); } public void SwitchStateT() where T : IState { if (_stateTable.TryGetValue(typeof(T), out IState newState)) { _currentState?.OnExit(); _currentState newState; _currentState.OnEnter(); } else { Debug.LogError($State {typeof(T)} not found in state table!); } } public T GetStateT() where T : class, IState { if (_stateTable.TryGetValue(typeof(T), out IState state)) { return state as T; } return null; } } // 状态接口 public interface IState { void SetOwner(StateMachine owner); void OnEnter(); void OnUpdate(); void OnFixedUpdate(); void OnExit(); }现在实现第一个具体状态LocomotionState。// 文件路径Assets/_Scripts/Characters/States/LocomotionState.cs using UnityEngine; public class LocomotionState : IState { private StateMachine _stateMachine; private PlayerInputHandler _input; private CharacterMotor _motor; private Animator _animator; public void SetOwner(StateMachine owner) { _stateMachine owner; _input owner.GetComponentPlayerInputHandler(); _motor owner.GetComponentCharacterMotor(); _animator owner.GetComponentInChildrenAnimator(); } public void OnEnter() { // 进入移动状态重置一些动画参数 if (_animator ! null) { _animator.SetBool(IsMoving, false); _animator.ResetTrigger(Attack); } Debug.Log(Enter Locomotion State); } public void OnUpdate() { Vector2 moveInput _input.GetMoveInput(); bool isRunning Input.GetKey(KeyCode.LeftShift); // 简单示例实际应放入InputHandler // 移动角色 _motor.Move(moveInput, isRunning); // 更新动画 if (_animator ! null) { _animator.SetBool(IsMoving, moveInput.magnitude 0.1f); _animator.SetFloat(MoveSpeed, moveInput.magnitude * (isRunning ? 2f : 1f)); } // 状态转换条件检测 if (_input ! null) { // 示例按下攻击键切换到攻击状态 // 注意这里应该通过事件触发这里为简化使用轮询 if (Keyboard.current ! null Keyboard.current.leftCtrlKey.wasPressedThisFrame) // 假设攻击键 { _stateMachine.SwitchStateAttackState(); return; } if (Keyboard.current ! null Keyboard.current.spaceKey.wasPressedThisFrame) // 假设闪避键 { _stateMachine.SwitchStateDashState(); return; } } } public void OnFixedUpdate() { } public void OnExit() { } }6. “赛博吸血鬼”特色系统实现技能与资源6.1 可扩展技能系统我们设计一个基于ScriptableObject的技能数据容器和一个通用的技能执行器。// 文件路径Assets/_Scripts/Skills/SkillData.cs using UnityEngine; [CreateAssetMenu(fileName NewSkill, menuName CyberVampire/Skill)] public class SkillData : ScriptableObject { public string skillName; public float cooldown 1f; public float resourceCost 10f; // 消耗的“血液”或能量 public float resourceGainOnHit 5f; // 命中后获取的资源 public GameObject castVFX; // 施法特效 public AudioClip castSFX; // 更多可配置参数伤害、范围、持续时间等 }// 文件路径Assets/_Scripts/Characters/Components/SkillSystem.cs using System.Collections.Generic; using UnityEngine; public class SkillSystem : MonoBehaviour { [SerializeField] private ListSkillData _equippedSkills new ListSkillData(); private DictionarySkillData, float _cooldownTimers new DictionarySkillData, float(); private ResourceSystem _resourceSystem; private void Awake() { _resourceSystem GetComponentResourceSystem(); foreach (var skill in _equippedSkills) { _cooldownTimers[skill] 0f; } } private void Update() { // 更新所有技能的冷却 var keys new ListSkillData(_cooldownTimers.Keys); foreach (var skill in keys) { if (_cooldownTimers[skill] 0) { _cooldownTimers[skill] - Time.deltaTime; } } } public bool TryCastSkill(int skillIndex) { if (skillIndex 0 || skillIndex _equippedSkills.Count) return false; SkillData skill _equippedSkills[skillIndex]; // 检查冷却和资源 if (_cooldownTimers[skill] 0) { Debug.Log(${skill.skillName} is on cooldown!); return false; } if (_resourceSystem ! null !_resourceSystem.TrySpendResource(skill.resourceCost)) { Debug.Log(Not enough resource!); return false; } // 执行技能逻辑这里应触发动画、状态切换等 Debug.Log($Casting {skill.skillName}); if (skill.castVFX ! null) { Instantiate(skill.castVFX, transform.position Vector3.up, Quaternion.identity); } // 触发技能状态 GetComponentStateMachine()?.SwitchStateSkillCastState(); // 假设有一个技能释放状态 // 进入冷却 _cooldownTimers[skill] skill.cooldown; return true; } // 当技能命中敌人时调用用于“吸血”等效果 public void OnSkillHitEnemy(SkillData skillUsed) { if (_resourceSystem ! null skillUsed ! null) { _resourceSystem.GainResource(skillUsed.resourceGainOnHit); } } }6.2 资源与生命系统实现“吸血鬼”特色的资源联动。// 文件路径Assets/_Scripts/Characters/Components/ResourceSystem.cs using UnityEngine; using UnityEngine.Events; public class ResourceSystem : MonoBehaviour { [SerializeField] private float _maxResource 100f; [SerializeField] private float _currentResource 50f; [SerializeField] private float _resourceRegenRate 2f; // 每秒回复 public UnityEventfloat OnResourceChanged; // 用于更新UI private void Update() { if (_currentResource _maxResource) { _currentResource _resourceRegenRate * Time.deltaTime; _currentResource Mathf.Min(_currentResource, _maxResource); OnResourceChanged?.Invoke(_currentResource / _maxResource); // 发送百分比 } } public bool TrySpendResource(float amount) { if (_currentResource amount) { _currentResource - amount; OnResourceChanged?.Invoke(_currentResource / _maxResource); return true; } return false; } public void GainResource(float amount) { _currentResource amount; _currentResource Mathf.Min(_currentResource, _maxResource); OnResourceChanged?.Invoke(_currentResource / _maxResource); } public float GetResourcePercentage() _currentResource / _maxResource; }// 文件路径Assets/_Scripts/Characters/Components/HealthSystem.cs using UnityEngine; using UnityEngine.Events; public class HealthSystem : MonoBehaviour { [SerializeField] private float _maxHealth 100f; [SerializeField] private float _currentHealth; public bool isInvincible false; public UnityEventfloat OnHealthChanged; // 血量变化事件 public UnityEvent OnDeath; // 死亡事件 private void Start() { _currentHealth _maxHealth; } public void TakeDamage(float damage) { if (isInvincible) return; _currentHealth - damage; _currentHealth Mathf.Max(_currentHealth, 0); OnHealthChanged?.Invoke(_currentHealth / _maxHealth); // 触发受击状态 var stateMachine GetComponentStateMachine(); if (stateMachine ! null _currentHealth 0) { stateMachine.SwitchStateHitState(); } if (_currentHealth 0) { OnDeath?.Invoke(); // 切换到死亡状态或销毁对象 Debug.Log(gameObject.name has been defeated.); } } // “吸血鬼”核心吸血方法 public void StealHealth(float amount, HealthSystem targetHealthSystem) { if (targetHealthSystem ! null) { float stolen Mathf.Min(amount, targetHealthSystem._currentHealth); targetHealthSystem.TakeDamage(stolen); Heal(stolen * 0.5f); // 假设吸血效率为50% } } public void Heal(float amount) { _currentHealth amount; _currentHealth Mathf.Min(_currentHealth, _maxHealth); OnHealthChanged?.Invoke(_currentHealth / _maxHealth); } }7. 整合与运行创建你的第一个赛博吸血鬼组装角色预制体在场景中放入一个角色模型带Animator。为其添加CharacterController组件。依次添加我们编写的脚本PlayerInputHandler,CharacterMotor,StateMachine,SkillSystem,ResourceSystem,HealthSystem。为StateMachine组件添加状态。在Inspector中我们可以通过一个初始化脚本来完成这里为了清晰我们创建一个PlayerStateInitializer脚本。// 文件路径Assets/_Scripts/Characters/PlayerStateInitializer.cs using UnityEngine; public class PlayerStateInitializer : MonoBehaviour { private void Start() { var stateMachine GetComponentStateMachine(); if (stateMachine ! null) { stateMachine.AddState(new LocomotionState()); stateMachine.AddState(new AttackState()); // 需要实现 stateMachine.AddState(new DashState()); // 需要实现 stateMachine.AddState(new HitState()); // 需要实现 stateMachine.AddState(new SkillCastState()); // 需要实现 // 初始状态 stateMachine.SwitchStateLocomotionState(); } } }配置Input System在项目设置中启用Input System Package。创建一个PlayerInputActionsAsset右键 Create - Input Actions。定义Action Maps和Actions如Player/Move(Vector2),Player/Attack(Button),Player/Dash,Player/Skill。将PlayerInputHandler脚本中的PlayerInputActions实例关联到这个Asset。创建技能资产右键 Create - CyberVampire - Skill。命名如BloodSlash配置冷却时间、资源消耗和获取。将这个Asset拖拽到角色的SkillSystem组件的Equipped Skills列表中。运行测试按下Play键。使用WASD移动角色Shift奔跑。按下预设的攻击键如LeftCtrl观察角色是否切换到攻击状态需要在AttackState中实现动画和伤害逻辑。在攻击命中敌人时需要实现攻击检测SkillSystem的OnSkillHitEnemy应被调用从而为ResourceSystem增加资源。在UI上绑定HealthSystem和ResourceSystem的OnXXXChanged事件实时更新血条和能量条。8. 常见问题与排查思路问题现象可能原因排查方式解决方案角色无响应无法移动1. Input System未正确配置或启用。2.CharacterController组件未添加或被禁用。3.StateMachine未正确初始化或初始状态未设置。1. 检查Console是否有Input System相关错误。2. 在运行时检查角色GameObject的组件是否激活。3. 在PlayerStateInitializer的Start方法开始处打日志看是否执行。1. 确保Input Actions Asset已创建并绑定。2. 确保CharacterController存在且启用。3. 确保PlayerStateInitializer脚本在角色上且StateMachine组件已添加。动画不播放或状态切换异常1. Animator Controller未赋值或状态机参数名拼写错误。2. 状态切换条件逻辑有误导致状态锁死。3. 动画状态机与代码逻辑状态机不同步。1. 检查角色模型子对象的Animator组件。2. 在状态类的OnEnter和OnExit中打日志观察切换流程。3. 使用Unity的Animation窗口观察动画状态机实际状态。1. 核对Animator中Bool、Trigger等参数名与代码中SetBool/SetTrigger使用的字符串是否完全一致。2. 检查状态转换条件确保每个状态都有明确的出口。攻击无法命中敌人1. 攻击检测逻辑未实现如射线检测、碰撞体检测。2. 攻击检测的时机不对动画事件未触发。3. 敌人Layer未被检测到。1. 在攻击动画的关键帧添加事件调用检测方法。2. 在检测方法中Debug.DrawRay或打印日志确认检测是否触发和命中。3. 检查Physics设置中的Layer碰撞矩阵。1. 在AttackState中实现一个PerformAttackDetection方法在动画事件中调用它。2. 使用Physics.OverlapSphere或Raycast在武器位置进行检测并过滤“Enemy”层。技能释放无效果1.SkillSystem中的TryCastSkill未被调用。2. 资源不足或技能在冷却中。3. 技能释放未触发对应的角色状态如SkillCastState。1. 在TryCastSkill开始处打日志确认是否被调用及传入的index。2. 检查ResourceSystem的当前资源值和_cooldownTimers字典。3. 检查SkillCastState是否已添加到状态机并正确实现。1. 确保输入绑定正确并在PlayerInputHandler中触发对应事件或在某个状态如LocomotionState中监听按键调用TryCastSkill。2. 在UI上直观显示资源量和冷却时间。角色移动“飘”或穿墙1.CharacterController的碰撞检测问题。2. 移动速度过快单帧位移过大。3. 角色或环境的碰撞体设置不当。1. 调整CharacterController的Slope Limit、Step Offset和Skin Width。2. 检查_currentSpeed值是否异常大。3. 确保环境物体有Collider且角色CharacterController的Collider尺寸合理。1. 在Update中移动使用Time.deltaTime在FixedUpdate中进行物理相关操作。2. 对于高速冲刺Dash可以考虑使用带缓存的射线检测来预判碰撞。9. 最佳实践与进阶方向当你成功跑通基础原型后以下实践能让你的“赛博吸血鬼”项目更专业、更易维护使用事件系统解耦不要让SkillSystem直接调用HealthSystem的StealHealth。改为发布一个OnSkillHit事件由HealthSystem或其他系统来订阅并处理。这大大降低了脚本间的直接依赖。数据驱动设计将角色的基础属性血量、速度、伤害、技能的所有参数、甚至状态机的部分转换条件都抽离到ScriptableObject或配置表中。这样策划或你自己调整平衡性时无需修改代码只需调整Asset。动画状态机与代码状态机分离但同步Unity的Animator是一个强大的视觉状态机但逻辑复杂时难以维护。我们的代码FSM是“逻辑状态机”它应该驱动Animator这个“表现状态机”。在每个逻辑状态的OnEnter中设置Animator的参数触发动画切换。实现一个伤害传递系统创建一个DamageInfo结构体包含伤害值、伤害类型、攻击者、是否暴击等信息。在攻击检测到命中时生成一个DamageInfo并发送给目标的HealthSystem。这样未来添加元素伤害、伤害减免等系统会非常方便。对象池管理特效和弹幕高速战斗会产生大量粒子特效和飞行物。务必使用对象池来管理它们的生成与回收这是避免性能骤降的关键。为状态机加入子状态机当角色状态复杂后例如在“攻击状态”中又有“轻攻击”、“重攻击”、“蓄力”等子状态可以考虑实现嵌套子状态机让逻辑更清晰。关注帧数独立性所有涉及移动、计时如冷却、Buff持续时间的地方务必使用Time.deltaTimeUpdate中或Time.fixedDeltaTimeFixedUpdate中确保在不同帧率下游戏体验一致。构建一个像“【TAKUMI³】Cybernetic Vampire”这样气质鲜明的动作游戏是一个将严谨的软件工程与感性的游戏设计相结合的过程。本文为你搭建了核心的技术骨架从输入处理、物理移动、状态管理到技能和资源系统。这个骨架是通用的你可以为其注入不同的“灵魂”——通过调整数值、设计独特的技能效果、创造富有张力的敌人AI以及打磨极致的视听反馈最终创造出属于你自己的、让玩家血脉贲张的赛博世界。记住第一个可玩的、手感扎实的原型远比一个停留在概念阶段的华丽想法更有价值。现在就基于这个框架开始你的创作吧。
返回列表