使用Unity开发RPG游戏完整指南(下)
发表于2017-05-01
本系列教程教大家制作一款RPG游戏,我们之前已经介绍了游戏的三个场景:主场景、城镇场景和战斗场景的内容。本文是系列教程的最后一篇,将会介绍如何实现添加回合战斗系统、创建攻击单位、选择角色进行攻击和结束战斗。请跟着我们对这款RPG游戏进行一个完美的收尾工作吧!
回合战斗系统
如何为游戏添加回合制战斗系统呢?首先新建游戏对象,并且为其添加TurnSystem的脚本。

TurnSystem脚本将保存所有角色单位(玩家和敌人)的UnitStats脚本列表。然后在每个回合中,它会弹出列表的第一个元素,让该单位操作完再添加到列表中。此外,还需要根据单位的操作轮次来保持列表的顺序。
TurnSystem脚本代码如下所示。在Start方法中创建UnitStats列表,通过标签“PlayerUnit”或“EnemyUnit”迭代所有游戏对象(请记得为对象添加正确的标签)。对于每个单位,TurnSystem脚本获取其UnitStats脚本,计算其下一个操作回合并将其添加到列表中。 添加所有单位后,对列表进行排序。最后禁用菜单,菜单仅在玩家回合内及首个回合开始时使用(调用nextTurn)。
nextTurn方法将从列表中删除第一个UnitStats,并检查角色单位是否死亡。如果该单位是活着的,将计算其下一操作回合,以便再次将其添加到列表中。最后,完成它要做的操作。由于暂未实现角色单位的操作方式,所以现在只在控制台中打印信息来检查逻辑是否正确。另外,如果角色单位死亡,只需调用nextTurn而不必将它添加回列表中。

再次运行游戏,检查控制台是否正确地输出回合信息。
攻击单位
既然实现了基于回合制的战斗系统,那就能让角色单位相互攻击。首先创建攻击(Attack)预制件,供角色单位使用。然后添加玩家和敌人单位的操作脚本,以便他们能够正确攻击。角色单位受到伤害后,将显示带有伤害值的文本预制件。
攻击预制件在场景中不可见,它带有一个AttackTarget脚本。该脚本将描述攻击属性,如攻击和防御系数以及法力消耗。此外,攻击有一个拥有者,即目前发出攻击操作的角色单位。
首先,脚本检查攻击拥有者是否具有足够的魔法值来执行攻击。 如果有,它会基于最小值和最大值随机选择攻击和防御系数。所以,伤害是根据这些系数和单位的攻击和防御来计算的。注意,如果攻击是一次魔法攻击(this.magicAttack是true),那么它将使用该单位的魔法状态动画,否则使用普通攻击状态动画。
最后,脚本播放攻击动画,对目标单位造成伤害,同时降低了攻击拥有者的魔法值。
创建两个攻击预制件:PhysicalAttack和MagicalAttack,每个都有自己的系数。
下面来实现reiceveDamage方法,该方法用于AttackTarget脚本。该方法除了扣除角色单位的生命值以外,还会在角色头顶上显示伤害值文本。
reiceveDamage代码如下图所示。 首先减少角色生命值并播放攻击(Hit)动画。 然后创建伤害文本(使用this.damageTextPrefab)。 请注意,伤害文本必须是HUDCanvas的子对象,并且由于它是UI元素,所以还需正确设置它的localPosition和localScale。 最后,如果单位的生命值小于零,脚本将单位设为死亡状态,更改它的标签并销毁对象。
下面可以实现角色单位的攻击方法了。 敌人单位总是以相同的攻击方式随机攻击某个敌人。这个攻击是EnemyUnitAction中的一个属性。在Awake方法中为该角色单位创建一个副本,并正确设置其拥有者,让每个角色单位保存自己所攻击对象的实例。
然后act方法会随机选择一个目标并发动攻击。 findRandomTarget方法会在轮到该角色时首先列出所有可能的目标(例如“PlayerUnit”标签标记的对象)。 如果列表中至少有一个可能的目标,则生成随机索引来选择一个目标。
玩家单位在各自的回合内,会有两种攻击方式:物理攻击和魔法攻击。需要在Awake方法中正确地实例化这两种攻击,并为这两种攻击设置拥有者。将玩家当前攻击方式默认设置为物理攻击。
然后,act方法会接收一个目标单位作为参数,并对这个目标发出攻击。
现在可以在TurnSystem脚本中调用敌方单位的act方法了。由于还需要正确选择当前单位和攻击方式,现在还不能对玩家单位进行同样操作,下一步就来实现该功能。
选择角色单位进行攻击
每个回合都需要正确选择当前的玩家单位,将下面的SelectUnit脚本添加到PlayerParty对象。这个脚本需要引用战斗菜单,所以在加载战斗场景的时候就要对其进行设置。
此外还要实现三种方法:selectCurrentUnit,selectAttack和attackEnemyTarget。 selectCurrentUnit将某个角色单位设置为当前行动单位,启用操作菜单,以便玩家可以选择操作,并更新HUD以显示当前的单位头像,生命值和魔法值。
当前角色单位会在自己的回合调用selectAttack方法,并禁用操作菜单和启用敌人菜单。PlayerUnitAction脚本中也需要实现selectAttack方法。以便玩家在选定攻击方式后选择目标了。
最后,attackEnemyTarget会禁用两个菜单并调用当前单位的act方法,选择敌人作为攻击目标。
现在需要正确地调用这三个方法。selectCurrentUnit会在玩家单位的回合内通过TurnSystem调用。
第二个方法selectAttack将由HUDCanvas中的PhysicalAttackAction和MagicalAttackAction按钮调用。 由于PlayerParty对象与这些按钮位于不同场景,所以无法在检视面板中为按钮添加OnClick回调函数。将下面的脚本添加到这些按钮对象,在脚本的Start方法中为这些按钮对象添加回调函数。回调函数将会从SelectUnit脚本中调用selectAttack方法。 为这两个按钮添加相同的脚本,只改变脚本的“physical”属性。

第三个方法attackEnemyTarget将会在敌方单位菜单项中调用。现在来实现CreateEnemyMenuItems脚本的selectEnemyTarget方法,该方法是按钮的回调函数,用于寻找PlayerParty对象并调用其attackEnemyTarget方法。
最后更新HUD并显示当前单位的头像、生命值和魔法值。
使用下面的脚本来显示单位的生命值和魔法值。该脚本在Start方法中初始化文本最初的localScale。然后在Update方法中根据单位的当前状态值更新localScale。 此外,changeUnit方法用于改变当前正在显示的角色单位,抽象方法newStatValue用于获取当前的状态值。
该脚本不是直接使用,而是另外创建两个特殊脚本:ShowUnitHealth和ShowUnitHealth,分别实现各自的抽象方法。这两个脚本的唯一方法newStatValue会返回当前角色单位的状态(生命值或魔法值)。
现在可以将这两个脚本添加到生命条和法力条对象中。然后将其X坐标设为零,这样缩放操作只会影响显示条的右边部分。

最后,当前角色单位更改时,需要在这些脚本中调用changeUnit方法。首先是SelectUnit脚本的selectCurrentUnit方法。 将actionsMenu设为激活状态后,再调用当前单位的updateHUD方法。
updateHUD方法在各回合开始时将PlayerUnitFace对象的精灵设置为当前单位头像,头像属性保存在PlayerUnitAction中。然后,将自己设置为ShowUnitHealth和ShowUnitMana中的当前单位。
现在可以运行游戏,看看是否能选择不同的操作,并检查角色单位的状态能否正常更新。菜单中最后需要实现的操作是撤离(Run)操作。

结束战斗
下面添加结束战斗的三种方式:
如果玩家胜利,将从enemy encounter中获得奖励。 为了实现该功能,需要为enemy encounter对象添加以下脚本。在Start方法中设置TurnSystem对象的enemy encounter属性。然后,collectReward方法(将从TurnSystem调用)将为所有存活的玩家单位平分该敌方据点的经验。

下面实现collectReward方法中使用的receiveExperience方法。 该方法用于供UnitStats保存接收到的经验值。
最后,在TurnSystem脚本中调用collectReward方法。更改nextTurn方法,使用“EnemyUnit”标签查找对象来检查是否仍有存活的敌方单位。请注意,当角色单位死亡后要将其标签更改为“DeadUnit”,从而让该方法在查找过程中忽略该单位。 如果没有存活的敌方单位,它会调用enemy encounter 的collectReward方法,然后回到城镇场景。
另一方面,如果没有存活的玩家单位,那就表示玩家失败,此时游戏会回到主场景。
最后一种结束战斗的方式是逃离战斗。这可以通过在操作面板中选择逃离操作来完成。所以,需要为run按钮添加下面的脚本,并添加按钮的OnClick回调函数。
RunFromBattle脚本中包含tryRunning方法, 该方法会生成0到1之间的随机数,并将其与逃跑机会(runningChance)属性进行比较。 如果生成的随机数小于逃跑机会,玩家就可以成功逃离战斗并返回到城镇场景。否则,玩家继续进入下一回合。

现在可以让游戏完全运行了。从头到尾进行完整的游戏战斗,检查一切是否正常。还可以尝试添加不同的enemy encounter并调整一些游戏参数,如单位状态和攻击系数。
同样还可以尝试添加一些这篇教程没有涉及到的元素,例如更加智能的敌人和关卡系统等。
总结
本系列教程就为大家介绍到这里,希望您喜欢!如果您有推荐的Unity相关教程,请在下方留言,可能下一个教程就来自于您!
如何为游戏添加回合制战斗系统呢?首先新建游戏对象,并且为其添加TurnSystem的脚本。

TurnSystem脚本将保存所有角色单位(玩家和敌人)的UnitStats脚本列表。然后在每个回合中,它会弹出列表的第一个元素,让该单位操作完再添加到列表中。此外,还需要根据单位的操作轮次来保持列表的顺序。
TurnSystem脚本代码如下所示。在Start方法中创建UnitStats列表,通过标签“PlayerUnit”或“EnemyUnit”迭代所有游戏对象(请记得为对象添加正确的标签)。对于每个单位,TurnSystem脚本获取其UnitStats脚本,计算其下一个操作回合并将其添加到列表中。 添加所有单位后,对列表进行排序。最后禁用菜单,菜单仅在玩家回合内及首个回合开始时使用(调用nextTurn)。
nextTurn方法将从列表中删除第一个UnitStats,并检查角色单位是否死亡。如果该单位是活着的,将计算其下一操作回合,以便再次将其添加到列表中。最后,完成它要做的操作。由于暂未实现角色单位的操作方式,所以现在只在控制台中打印信息来检查逻辑是否正确。另外,如果角色单位死亡,只需调用nextTurn而不必将它添加回列表中。

[C#] 纯文本查看 复制代码
public class TurnSystem : MonoBehaviour { private List [SerializeField] private GameObject actionsMenu, enemyUnitsMenu; void Start() { unitsStats = new List GameObject[] playerUnits = GameObject.FindGameObjectsWithTag( "PlayerUnit" ); foreach (GameObject playerUnit in playerUnits) { UnitStats currentUnitStats = playerUnit.GetComponent currentUnitStats.calculateNextActTurn (0); unitsStats.Add (currentUnitStats); } GameObject[] enemyUnits = GameObject.FindGameObjectsWithTag( "EnemyUnit" ); foreach (GameObject enemyUnit in enemyUnits) { UnitStats currentUnitStats = enemyUnit.GetComponent currentUnitStats.calculateNextActTurn (0); unitsStats.Add (currentUnitStats); } unitsStats.Sort (); this .actionsMenu.SetActive ( false ); this .enemyUnitsMenu.SetActive ( false ); this .nextTurn (); } public void nextTurn() { UnitStats currentUnitStats = unitsStats [0]; unitsStats.Remove (currentUnitStats); if (!currentUnitStats.isDead ()) { GameObject currentUnit = currentUnitStats.gameObject; currentUnitStats.calculateNextActTurn (currentUnitStats.nextActTurn); unitsStats.Add (currentUnitStats); unitsStats.Sort (); if (currentUnit.tag == "PlayerUnit" ) { Debug.Log ( "Player unit acting" ); } else { Debug.Log ( "Enemy unit acting" ); } } else { this .nextTurn (); } } } |
再次运行游戏,检查控制台是否正确地输出回合信息。
攻击单位
既然实现了基于回合制的战斗系统,那就能让角色单位相互攻击。首先创建攻击(Attack)预制件,供角色单位使用。然后添加玩家和敌人单位的操作脚本,以便他们能够正确攻击。角色单位受到伤害后,将显示带有伤害值的文本预制件。
攻击预制件在场景中不可见,它带有一个AttackTarget脚本。该脚本将描述攻击属性,如攻击和防御系数以及法力消耗。此外,攻击有一个拥有者,即目前发出攻击操作的角色单位。
首先,脚本检查攻击拥有者是否具有足够的魔法值来执行攻击。 如果有,它会基于最小值和最大值随机选择攻击和防御系数。所以,伤害是根据这些系数和单位的攻击和防御来计算的。注意,如果攻击是一次魔法攻击(this.magicAttack是true),那么它将使用该单位的魔法状态动画,否则使用普通攻击状态动画。
最后,脚本播放攻击动画,对目标单位造成伤害,同时降低了攻击拥有者的魔法值。
[C#] 纯文本查看 复制代码
public class AttackTarget : MonoBehaviour { public GameObject owner; [SerializeField] private string attackAnimation; [SerializeField] private bool magicAttack; [SerializeField] private float manaCost; [SerializeField] private float minAttackMultiplier; [SerializeField] private float maxAttackMultiplier; [SerializeField] private float minDefenseMultiplier; [SerializeField] private float maxDefenseMultiplier; public void hit(GameObject target) { UnitStats ownerStats = this .owner.GetComponent UnitStats targetStats = target.GetComponent if (ownerStats.mana >= this .manaCost) { float attackMultiplier = (Random.value * ( this .maxAttackMultiplier - this .minAttackMultiplier)) + this .minAttackMultiplier; float damage = ( this .magicAttack) ? attackMultiplier * ownerStats.magic : attackMultiplier * ownerStats.attack; float defenseMultiplier = (Random.value * ( this .maxDefenseMultiplier - this .minDefenseMultiplier)) + this .minDefenseMultiplier; damage = Mathf.Max(0, damage - (defenseMultiplier * targetStats.defense)); this .owner.GetComponent this .attackAnimation); targetStats.receiveDamage (damage); ownerStats.mana -= this .manaCost; } } } |
创建两个攻击预制件:PhysicalAttack和MagicalAttack,每个都有自己的系数。
下面来实现reiceveDamage方法,该方法用于AttackTarget脚本。该方法除了扣除角色单位的生命值以外,还会在角色头顶上显示伤害值文本。
reiceveDamage代码如下图所示。 首先减少角色生命值并播放攻击(Hit)动画。 然后创建伤害文本(使用this.damageTextPrefab)。 请注意,伤害文本必须是HUDCanvas的子对象,并且由于它是UI元素,所以还需正确设置它的localPosition和localScale。 最后,如果单位的生命值小于零,脚本将单位设为死亡状态,更改它的标签并销毁对象。
[C#] 纯文本查看 复制代码
public void receiveDamage( float damage) { this .health -= damage; animator.Play ( "Hit" ); GameObject HUDCanvas = GameObject.Find ( "HUDCanvas" ); GameObject damageText = Instantiate ( this .damageTextPrefab, HUDCanvas.transform) as GameObject; damageText.GetComponent "" + damage; damageText.transform.localPosition = this .damageTextPosition; damageText.transform.localScale = new Vector2 (1.0f, 1.0f); if ( this .health <= 0) { this .dead = true ; this .gameObject.tag = "DeadUnit" ; Destroy ( this .gameObject); } } |
下面可以实现角色单位的攻击方法了。 敌人单位总是以相同的攻击方式随机攻击某个敌人。这个攻击是EnemyUnitAction中的一个属性。在Awake方法中为该角色单位创建一个副本,并正确设置其拥有者,让每个角色单位保存自己所攻击对象的实例。
然后act方法会随机选择一个目标并发动攻击。 findRandomTarget方法会在轮到该角色时首先列出所有可能的目标(例如“PlayerUnit”标签标记的对象)。 如果列表中至少有一个可能的目标,则生成随机索引来选择一个目标。
[C#] 纯文本查看 复制代码
public class EnemyUnitAction : MonoBehaviour { [SerializeField] private GameObject attack; [SerializeField] private string targetsTag; void Awake () { this .attack = Instantiate ( this .attack); this .attack.GetComponent this .gameObject; } GameObject findRandomTarget() { GameObject[] possibleTargets = GameObject.FindGameObjectsWithTag (targetsTag); if (possibleTargets.Length > 0) { int targetIndex = Random.Range (0, possibleTargets.Length); GameObject target = possibleTargets [targetIndex]; return target; } return null ; } public void act() { GameObject target = findRandomTarget (); this .attack.GetComponent } } |
玩家单位在各自的回合内,会有两种攻击方式:物理攻击和魔法攻击。需要在Awake方法中正确地实例化这两种攻击,并为这两种攻击设置拥有者。将玩家当前攻击方式默认设置为物理攻击。
然后,act方法会接收一个目标单位作为参数,并对这个目标发出攻击。
[C#] 纯文本查看 复制代码
public class PlayerUnitAction : MonoBehaviour { [SerializeField] private GameObject physicalAttack; [SerializeField] private GameObject magicalAttack; private GameObject currentAttack; void Awake () { this .physicalAttack = Instantiate ( this .physicalAttack, this .transform) as GameObject; this .magicalAttack = Instantiate ( this .magicalAttack, this .transform) as GameObject; this .physicalAttack.GetComponent this .gameObject; this .magicalAttack.GetComponent this .gameObject; this .currentAttack = this .physicalAttack; } public void act(GameObject target) { this .currentAttack.GetComponent } } |
现在可以在TurnSystem脚本中调用敌方单位的act方法了。由于还需要正确选择当前单位和攻击方式,现在还不能对玩家单位进行同样操作,下一步就来实现该功能。
[C#] 纯文本查看 复制代码
public void nextTurn() { UnitStats currentUnitStats = unitsStats [0]; unitsStats.Remove (currentUnitStats); if (!currentUnitStats.isDead ()) { GameObject currentUnit = currentUnitStats.gameObject; currentUnitStats.calculateNextActTurn (currentUnitStats.nextActTurn); unitsStats.Add (currentUnitStats); unitsStats.Sort (); if (currentUnit.tag == "PlayerUnit" ) { Debug.Log( "Player unit acting" ); } else { currentUnit.GetComponent } } else { this .nextTurn (); } } |
选择角色单位进行攻击
每个回合都需要正确选择当前的玩家单位,将下面的SelectUnit脚本添加到PlayerParty对象。这个脚本需要引用战斗菜单,所以在加载战斗场景的时候就要对其进行设置。
此外还要实现三种方法:selectCurrentUnit,selectAttack和attackEnemyTarget。 selectCurrentUnit将某个角色单位设置为当前行动单位,启用操作菜单,以便玩家可以选择操作,并更新HUD以显示当前的单位头像,生命值和魔法值。
当前角色单位会在自己的回合调用selectAttack方法,并禁用操作菜单和启用敌人菜单。PlayerUnitAction脚本中也需要实现selectAttack方法。以便玩家在选定攻击方式后选择目标了。
最后,attackEnemyTarget会禁用两个菜单并调用当前单位的act方法,选择敌人作为攻击目标。
[C#] 纯文本查看 复制代码
public class SelectUnit : MonoBehaviour { private GameObject currentUnit; private GameObject actionsMenu, enemyUnitsMenu; void Awake() { SceneManager.sceneLoaded += OnSceneLoaded; } private void OnSceneLoaded(Scene scene, LoadSceneMode mode) { if (scene.name == "Battle" ) { this .actionsMenu = GameObject.Find ( "ActionsMenu" ); this .enemyUnitsMenu = GameObject.Find ( "EnemyUnitsMenu" ); } } public void selectCurrentUnit(GameObject unit) { this .currentUnit = unit; this .actionsMenu.SetActive ( true ); } public void selectAttack( bool physical) { this .currentUnit.GetComponent this .actionsMenu.SetActive ( false ); this .enemyUnitsMenu.SetActive ( true ); } public void attackEnemyTarget(GameObject target) { this .actionsMenu.SetActive ( false ); this .enemyUnitsMenu.SetActive ( false ); this .currentUnit.GetComponent } } public class PlayerUnitAction : MonoBehaviour { [SerializeField] private GameObject physicalAttack; [SerializeField] private GameObject magicalAttack; private GameObject currentAttack; public void selectAttack( bool physical) { this .currentAttack = (physical) ? this .physicalAttack : this .magicalAttack; } |
现在需要正确地调用这三个方法。selectCurrentUnit会在玩家单位的回合内通过TurnSystem调用。
[C#] 纯文本查看 复制代码
public void nextTurn() { UnitStats currentUnitStats = unitsStats [0]; unitsStats.Remove (currentUnitStats); if (!currentUnitStats.isDead ()) { GameObject currentUnit = currentUnitStats.gameObject; currentUnitStats.calculateNextActTurn (currentUnitStats.nextActTurn); unitsStats.Add (currentUnitStats); unitsStats.Sort (); if (currentUnit.tag == "PlayerUnit" ) { this .playerParty.GetComponent } else { currentUnit.GetComponent } } else { this .nextTurn (); } } |
第二个方法selectAttack将由HUDCanvas中的PhysicalAttackAction和MagicalAttackAction按钮调用。 由于PlayerParty对象与这些按钮位于不同场景,所以无法在检视面板中为按钮添加OnClick回调函数。将下面的脚本添加到这些按钮对象,在脚本的Start方法中为这些按钮对象添加回调函数。回调函数将会从SelectUnit脚本中调用selectAttack方法。 为这两个按钮添加相同的脚本,只改变脚本的“physical”属性。
[C#] 纯文本查看 复制代码
public class AddButtonCallback : MonoBehaviour { [SerializeField] private bool physical; void Start () { this .gameObject.GetComponent } private void addCallback() { GameObject playerParty = GameObject.Find ( "PlayerParty" ); playerParty.GetComponent this .physical); } } |


第三个方法attackEnemyTarget将会在敌方单位菜单项中调用。现在来实现CreateEnemyMenuItems脚本的selectEnemyTarget方法,该方法是按钮的回调函数,用于寻找PlayerParty对象并调用其attackEnemyTarget方法。
[C#] 纯文本查看 复制代码
public void selectEnemyTarget() { GameObject partyData = GameObject.Find ( "PlayerParty" ); partyData.GetComponent this .gameObject); } |
最后更新HUD并显示当前单位的头像、生命值和魔法值。
使用下面的脚本来显示单位的生命值和魔法值。该脚本在Start方法中初始化文本最初的localScale。然后在Update方法中根据单位的当前状态值更新localScale。 此外,changeUnit方法用于改变当前正在显示的角色单位,抽象方法newStatValue用于获取当前的状态值。
[C#] 纯文本查看 复制代码
public abstract class ShowUnitStat : MonoBehaviour { [SerializeField] protected GameObject unit; [SerializeField] private float maxValue; private Vector2 initialScale; void Start() { this .initialScale = this .gameObject.transform.localScale; } void Update() { if ( this .unit) { float newValue = this .newStatValue (); float newScale = ( this .initialScale.x * newValue) / this .maxValue; this .gameObject.transform.localScale = new Vector2(newScale, this .initialScale.y); } } public void changeUnit(GameObject newUnit) { this .unit = newUnit; } abstract protected float newStatValue(); } |
该脚本不是直接使用,而是另外创建两个特殊脚本:ShowUnitHealth和ShowUnitHealth,分别实现各自的抽象方法。这两个脚本的唯一方法newStatValue会返回当前角色单位的状态(生命值或魔法值)。
[C#] 纯文本查看 复制代码
public class ShowUnitHealth : ShowUnitStat { override protected float newStatValue() { return unit.GetComponent } } public class ShowUnitMana : ShowUnitStat { override protected float newStatValue() { return unit.GetComponent } } |
现在可以将这两个脚本添加到生命条和法力条对象中。然后将其X坐标设为零,这样缩放操作只会影响显示条的右边部分。

最后,当前角色单位更改时,需要在这些脚本中调用changeUnit方法。首先是SelectUnit脚本的selectCurrentUnit方法。 将actionsMenu设为激活状态后,再调用当前单位的updateHUD方法。
[C#] 纯文本查看 复制代码
public void selectCurrentUnit(GameObject unit) { this .currentUnit = unit; this .actionsMenu.SetActive ( true ); this .currentUnit.GetComponent } |
updateHUD方法在各回合开始时将PlayerUnitFace对象的精灵设置为当前单位头像,头像属性保存在PlayerUnitAction中。然后,将自己设置为ShowUnitHealth和ShowUnitMana中的当前单位。
[C#] 纯文本查看 复制代码
[SerializeField] private Sprite faceSprite; public void updateHUD() { GameObject playerUnitFace = GameObject.Find ( "PlayerUnitFace" ) as GameObject; playerUnitFace.GetComponent this .faceSprite; GameObject playerUnitHealthBar = GameObject.Find ( "PlayerUnitHealthBar" ) as GameObject; playerUnitHealthBar.GetComponent this .gameObject); GameObject playerUnitManaBar = GameObject.Find ( "PlayerUnitManaBar" ) as GameObject; playerUnitManaBar.GetComponent this .gameObject); } |
现在可以运行游戏,看看是否能选择不同的操作,并检查角色单位的状态能否正常更新。菜单中最后需要实现的操作是撤离(Run)操作。

结束战斗
下面添加结束战斗的三种方式:
- 所有敌人单位都死亡了,玩家胜利。
- 所有玩家单位都死亡了,玩家失败。
- 玩家逃离战斗。
如果玩家胜利,将从enemy encounter中获得奖励。 为了实现该功能,需要为enemy encounter对象添加以下脚本。在Start方法中设置TurnSystem对象的enemy encounter属性。然后,collectReward方法(将从TurnSystem调用)将为所有存活的玩家单位平分该敌方据点的经验。
[C#] 纯文本查看 复制代码
public class CollectReward : MonoBehaviour { [SerializeField] private float experience; public void Start() { GameObject turnSystem = GameObject.Find ( "TurnSystem" ); turnSystem.GetComponent this .gameObject; } public void collectReward() { GameObject[] livingPlayerUnits = GameObject.FindGameObjectsWithTag ( "PlayerUnit" ); float experiencePerUnit = this .experience / ( float )livingPlayerUnits.Length; foreach (GameObject playerUnit in livingPlayerUnits) { playerUnit.GetComponent } Destroy ( this .gameObject); } } |

下面实现collectReward方法中使用的receiveExperience方法。 该方法用于供UnitStats保存接收到的经验值。
[C#] 纯文本查看 复制代码
public void receiveExperience( float experience) { this .currentExperience += experience; } |
最后,在TurnSystem脚本中调用collectReward方法。更改nextTurn方法,使用“EnemyUnit”标签查找对象来检查是否仍有存活的敌方单位。请注意,当角色单位死亡后要将其标签更改为“DeadUnit”,从而让该方法在查找过程中忽略该单位。 如果没有存活的敌方单位,它会调用enemy encounter 的collectReward方法,然后回到城镇场景。
另一方面,如果没有存活的玩家单位,那就表示玩家失败,此时游戏会回到主场景。
[C#] 纯文本查看 复制代码
public void nextTurn() { GameObject[] remainingEnemyUnits = GameObject.FindGameObjectsWithTag ( "EnemyUnit" ); if (remainingEnemyUnits.Length == 0) { this .enemyEncounter.GetComponent SceneManager.LoadScene ( "Town" ); } GameObject[] remainingPlayerUnits = GameObject.FindGameObjectsWithTag ( "PlayerUnit" ); if (remainingPlayerUnits.Length == 0) { SceneManager.LoadScene( "Title" ); } UnitStats currentUnitStats = unitsStats [0]; unitsStats.Remove (currentUnitStats); if (!currentUnitStats.isDead ()) { GameObject currentUnit = currentUnitStats.gameObject; currentUnitStats.calculateNextActTurn (currentUnitStats.nextActTurn); unitsStats.Add (currentUnitStats); unitsStats.Sort (); if (currentUnit.tag == "PlayerUnit" ) { this .playerParty.GetComponent } else { currentUnit.GetComponent } } else { this .nextTurn (); } } |
最后一种结束战斗的方式是逃离战斗。这可以通过在操作面板中选择逃离操作来完成。所以,需要为run按钮添加下面的脚本,并添加按钮的OnClick回调函数。
RunFromBattle脚本中包含tryRunning方法, 该方法会生成0到1之间的随机数,并将其与逃跑机会(runningChance)属性进行比较。 如果生成的随机数小于逃跑机会,玩家就可以成功逃离战斗并返回到城镇场景。否则,玩家继续进入下一回合。
[C#] 纯文本查看 复制代码
public class RunFromBattle : MonoBehaviour { [SerializeField] private float runnningChance; public void tryRunning() { float randomNumber = Random.value; if (randomNumber < this .runnningChance) { SceneManager.LoadScene ( "Town" ); } else { GameObject.Find( "TurnSystem" ).GetComponent } } |

现在可以让游戏完全运行了。从头到尾进行完整的游戏战斗,检查一切是否正常。还可以尝试添加不同的enemy encounter并调整一些游戏参数,如单位状态和攻击系数。
同样还可以尝试添加一些这篇教程没有涉及到的元素,例如更加智能的敌人和关卡系统等。
总结
本系列教程就为大家介绍到这里,希望您喜欢!如果您有推荐的Unity相关教程,请在下方留言,可能下一个教程就来自于您!