本教程教大家如何使用Unity创建一个RPG游戏。该游戏将包含三个场景:主场景、城镇场景和战斗场景。我们已经在《使用Unity开发RPG游戏完整指南(上)》中介绍了主场景的实现,下面就让我们看看最能渲染游戏氛围的城镇场景和战斗场景是如何构建的吧!

请下载本教程的示例项目(使用版本:Unity 5.4.0f3),下载地址:http://pan.baidu.com/s/1pLv0uwz

城镇场景
首先,我们需要新建一个场景,命名为“Town”。然后按照下列的步骤,就可以开始创建城镇场景了。

1.将瓦片地图集成到Unity
城镇场景包含一个瓦片地图,我们需要首先创建这个只有瓦片层的瓦片地图,将它添加到Unity中。本教程将用到下面的地图,您可以在源代码文件中找到它。当然,您也可以按自己的想法使用Unity 2D新功能创建瓦片地图。
 


该地图中有一层名为“buildings”,它必须能与玩家发生碰撞,所以我们需要在Unity中创建这些瓦片的碰撞器,并为每个可碰撞的瓦片设置碰撞区域。打开瓦片碰撞器编辑器(Tiled Collision Editor),为瓦片增加一个矩形来代表碰撞区域。每个可碰撞的瓦片都必须执行这一步。
 


下面就可以实现地图导入工作了,借助第三方工具Tiled2Unity,将瓦片地图导入Unity。这个程序会加载瓦片地图并在Unity中创建对应的游戏对象,下载Tiled2Unity.unitypackage,并导入Unity。
 


这一步完成之后,Tiled2Unity会在城镇场景中创建一个游戏对象,作为城镇地图。它会自动为可碰撞瓦片创建碰撞器。下图显示了场景中的城镇对象及其在检视面板中的属性。
 

场景中的城镇对象


 

 
城镇对象在检视面板中的属性


现在我们可以试着开始游戏,检查一下地图是否正确加载。

2.玩家预制件
我们已经成功将城镇场景中的瓦片地图添加到Unity中,下面来创建玩家预制件,让玩家能够在城镇中四处移动,并与可碰撞瓦片发生碰撞。

首先,新建游戏对象命名为Player,并按照下图为其添加Sprite Renderer,Box Collider 2D以及Rigidbody 2D组件。请注意,要将Rigidbody2D的重力因子(Gravity Scale)属性设置为0,以免玩家受到重力影响
 


然后,创建玩家动画。玩家有四个行走动画和四个空闲动画,每个动画对应于一个方向。创建所有动画,分别命名为IdleLeft、IdleRight、IdleUp、IldeDown、WalkingLeft、WalkingRight、WalkingUp 和WalkingDown。

接下来,添加玩家Animator。新建Animator,命名为PlayerAnimator,并且将创建好的动画添加到其中。将Animator添加到玩家对象的检视面板之后,就可以使用Unity的动画窗口和玩家的精灵图集来创建动画了。下图为WalkingUp动画示例。
 


现在需要在玩家Animator中配置动画的过渡。Animator有两个参数:DirectionX和DirectionY,它们描述了当前玩家移动的方向。例如,如果一个玩家要向左移动,那么DirectionX是-1而DirectionY是0,后面的移动脚本中会正确设置这些参数。

每个空闲动画都会有到各个行走动画的过渡。根据动画的方向来设置对应的方向参数值。例如,如果DrectionX等于-1,那么Idle Left将会改变会Walking Left。同样,每个行走动画都要能够过渡到对应的空闲动画。最后,如果玩家改变了行走方向而没有停止,那么就需要更新动画。这样,我们同时需要在各个行走动画之间添加过渡。

玩家Animator的最终效果如下图所示。下图展示了动画之间过渡示例(IdleLeft至WalkingLeft,和WalkingLeft至dleLeft)。
 


 

 
IdleLeft至WalkingLeft的过渡

 

 
WalkingLeft至dleLeft的过渡


新建PlayerMovement脚本,在FixedUpdate方法中完成所有的移动。使用水平轴和垂直轴的输入来检查玩家应该朝哪个方向移动。在没有输入不同移动方向之前,玩家可以朝指定方向持续移动,例如玩家在没有被指定向右移动前可以一直向左行走。水平和垂直方向都可以使用这样的逻辑。当朝指定方向移动时,需要设置Animator参数。最后,为Player的Rigidbody2D组件加入速度。
 

[C#] 纯文本查看 复制代码
public class PlayerMovement : MonoBehaviour {
[SerializeField]
private float speed;
  
[SerializeField]
private Animator animator;
  
void FixedUpdate () {
float moveHorizontal = Input.GetAxis ("Horizontal");
float moveVertical = Input.GetAxis ("Vertical");
  
Vector2 currentVelocity = gameObject.GetComponent<Rigidbody2D> ().velocity;
  
float newVelocityX = 0f;
if (moveHorizontal < 0 && currentVelocity.x <= 0) {
newVelocityX = -speed;
animator.SetInteger ("DirectionX", -1);
} else if (moveHorizontal > 0 && currentVelocity.x >= 0) {
newVelocityX = speed;
animator.SetInteger ("DirectionX", 1);
} else {
animator.SetInteger ("DirectionX", 0);
}
  
float newVelocityY = 0f;
if (moveVertical < 0 && currentVelocity.y <= 0) {
newVelocityY = -speed;
animator.SetInteger ("DirectionY", -1);
} else if (moveVertical > 0 && currentVelocity.y >= 0) {
newVelocityY = speed;
animator.SetInteger ("DirectionY", 1);
} else {
animator.SetInteger ("DirectionY", 0);
}
  
gameObject.GetComponent<Rigidbody2D> ().velocity = new Vector2 (newVelocityX, newVelocityY);
}
}

 
Player预制件如下图所示。



现在可以运行游戏并让玩家在地图上四处移动了。请记得检查瓦片碰撞器是否正常。
 


3.开始战斗
玩家可以通过与敌人生成器进行交互来开始战斗。 敌人生成器是一个不可移动的对象,当被玩家触碰该对象时,将切换到另一个场景,即战斗场景(Battle Scene)。

此外,敌人生成器将负责在战斗场景中创建敌方单位对象。 这一步可通过创建EnemyEncounter预制件来实现,敌方单位将作为其子对象。 像主场景中的玩家单位一样,现在仅为敌人单位添加UnitStats脚本和Sprite Renderer组件。您可以通过新建预制件并添加所需的敌人单位作为子对象来创建EnemyEncounter。 您还需要正确设置其坐标,以便在战斗场景中的正确位置创建敌人。下图为敌人单位示例。



然后新建EnemySpawner预制件,它带有一个碰撞器组件和一个Rigidbody2D,以便与Player预制件发生碰撞。
 


同样,还需要一个SpawnEnemy脚本,其中实现了OnCollisionEnter2D方法,用于检查是否与Player预制件发生碰撞。通过检查发生碰撞的另一对象标签是否为“Player”来判断该对象是否为玩家预制件(要记得正确设置Player预制件的标签)。如果发生碰撞,就会进入战斗场景并将Spawning属性设置为true。

在战斗场景中创建一个敌人,脚本需要一个Enemy Encounter Prefab属性,并且敌人生成器必须不能在切换场景时被销毁(这一步已在Start方法中完成了)。当加载场景时(使用OnSceneLoaded方法),如果被加载场景是战斗场景,那么敌人生成器就会销毁自身,并且当Spawning属性为True时,实例化一个Enemy Encounter对象。通过这种方式可以确保只有一个敌人生成器会实例化Enemy Encounter预制件,而所有的敌人生成器都会销毁。
 

[C#] 纯文本查看 复制代码

 

public class SpawnEnemy : MonoBehaviour {
  
[SerializeField]
private GameObject enemyEncounterPrefab;
  
private bool spawning = false;
  
void Start() {
DontDestroyOnLoad (this.gameObject);
  
SceneManager.sceneLoaded += OnSceneLoaded;
}
  
private void OnSceneLoaded(Scene scene, LoadSceneMode mode) {
if (scene.name == "Battle") {
if (this.spawning) {
Instantiate (enemyEncounterPrefab);
}
SceneManager.sceneLoaded -= OnSceneLoaded;
Destroy (this.gameObject);
}
}
  
void OnTriggerEnter2D(Collider2D other) {
if (other.gameObject.tag == "Player") {
this.spawning = true;
SceneManager.LoadScene ("Battle");
}
}
}

 

现在可以运行游戏并与敌人生成器交互。请尝试创建一个空的战斗场景来测试是否能正确切换到战斗场景。
 



战斗场景
1. 背景和HUD画布
首先创建战斗场景要用到的画布。与主场景相似,需要两个画布,一个用于显示背景,另一个用于显示HUD元素。

背景画布与主场景相同,这里不再赘述。而HUD画布会需要大量的元素,以支撑与玩家的正常交互。

首先添加一个操作菜单,它用于显示玩家可能会出现的操作。新建一个空对象作为所有菜单项的父对象。 每个操作菜单项分别是一个按钮,作为ActionsMenu的子对象。

接下来添加三个可能的操作:物理攻击(PhysicalAttackAction),魔法攻击(MagicAttackAction)和战斗撤退(RunAction)。每个操作都有OnClick事件,但暂时不进行设置。下图仅显示了PhysicalAttackAction,其它操作仅图片不同,其他参数都是相同的。这些菜单项的源图片是从许多图标图集中切割的的精灵图片。
 


要添加到HUD画布的第二个菜单是EnemyUnitsMenu。 该菜单将用于显示敌人单位,以便玩家可以选择一个敌人进行攻击。与ActionsMenu类似,它是一个空对象,用于对其菜单项进行分组。 敌人的菜单项将会在战斗开始时由敌人单位创建。

为了让敌人单位来创建菜单项,需要先制作菜单项预制件。这个预制件叫做TargetEnemy,是一个按钮。该按钮的OnClick回调函数将实现选择敌人为目标。
 


为EnemyUnit预制件添加两个脚本,来处理它的菜单项事件:杀敌(KillEnemy)和创建敌人菜单项(CreateEnemyMenuItem)。
 


KillEnemy脚本非常简单。它有一个对应自身单位的菜单项属性,当单位被销毁时(调用OnDestroy方法),这个菜单项也会被销毁。
 

[C#] 纯文本查看 复制代码

 

public class KillEnemy : MonoBehaviour {
  
public GameObject menuItem;
  
void OnDestroy() {
Destroy (this.menuItem);
}
}

 

接下来创建CreateEnemyMenuItem脚本。这个脚本将负责创建其菜单项并设置OnClick回调函数。这些步骤都在Awake方法中完成。首先,根据现有菜单项的数量来计算菜单项坐标。然后将其实例化为EnemyUnitsMenu的子对象,随后通过脚本设置其localPosition和localScale。 最后,将OnClick回调函数设置为selectEnemyTarget方法,并将菜单项设置为该单位在KillEnemy脚本中对应的菜单项。selectEnemyTarget方法用于让玩家能够攻击敌人单位。但现在还不需要这样的代码。所以暂时留空。
 

[C#] 纯文本查看 复制代码

 

public class CreateEnemyMenuItems : MonoBehaviour {
  
[SerializeField]
private GameObject targetEnemyUnitPrefab;
  
[SerializeField]
private Sprite menuItemSprite;
  
[SerializeField]
private Vector2 initialPosition, itemDimensions;
  
[SerializeField]
private KillEnemy killEnemyScript;
  
// Use this for initialization
void Awake () {
GameObject enemyUnitsMenu = GameObject.Find ("EnemyUnitsMenu");
  
GameObject[] existingItems = GameObject.FindGameObjectsWithTag ("TargetEnemyUnit");
Vector2 nextPosition = new Vector2 (this.initialPosition.x + (existingItems.Length * this.itemDimensions.x), this.initialPosition.y);
  
GameObject targetEnemyUnit = Instantiate (this.targetEnemyUnitPrefab, enemyUnitsMenu.transform) as GameObject;
targetEnemyUnit.name = "Target" + this.gameObject.name;
targetEnemyUnit.transform.localPosition = nextPosition;
targetEnemyUnit.transform.localScale = new Vector2 (0.7f, 0.7f);
targetEnemyUnit.GetComponent<Button> ().onClick.AddListener (() => 
selectEnemyTarget());
targetEnemyUnit.GetComponent<Image> ().sprite = this.menuItemSprite;
  
killEnemyScript.menuItem = targetEnemyUnit;
}
  
public void selectEnemyTarget() {
}
  
}

 

最后要添加的HUD元素是显示玩家单位信息的,如生命值和法力值等。首先新建游戏对象命名为PlayerUnitInformation来放置所有HUD元素。然后,为该对象添加Image子对象命名为PlayerUnitFace。该图片将会显示当前面对的单位。暂时任选一个单位头像作为其目标图片。
 


下面添加是命条和对应的文本。生命条是一个图片,显示生命值的精灵,而文本则用于显示HP信息。最后按同样方式添加法力条,只需要改变显示的精灵和文本信息即可。因为这两个条非常相似,下面仅展示生命条的检视面板。
 


目前战斗场景如下图所示。这是场景视图的截图,因为还要添加非常多内容才能正常运行战斗场景。下面的图片展示了场景中对象的层次结构。
 

 



2.单位动画
接下来我们要创建单位动画。 每个单位将有四个动画:空闲(Idle),物理攻击(PhysicalAttack),魔法攻击(MagicalAttack)和攻击(Hit)。 首先为其中一个玩家单位(例如MageUnit法师单位)创建动画器(Animator),然后将它添加到对应的预制件中。
 


选择该预制件并打开Animator视图,按照下图那样配置动画状态机。为每个动画创建一个状态,其中默认状态是空闲,所有其他动画在结束播放时都会过渡为空闲状态。


 


现在需要创建四个动画来并将它们添加到对应的状态。下图显示了法师单位的魔法攻击动画。您可以在Animator视图中按照相同的过程创建所有动画,这里不再赘述。另外,您必须对所有角色单位(包括敌方)都进行这样的设置。
 


还需要定义何时播放这些动画,这将在为角色单位添加更多功能时完成。暂时让所有角色单位仅默认地播放空闲动画。

现在运行游戏,可以看到所有角色单位都在播放空闲动画。但请注意,需要从主场景切换到战斗场景才能看到这些角色单位。
 



总结
本篇教程主要为您介绍了该RPG游戏城镇场景和部分战斗场景的实现过程,未完待续,敬请期待下篇。

锐亚教育