您的位置:首页 > 移动开发 > Unity3D

Unity学习笔记1 简易2D横版RPG游戏制作(一)

2014-04-27 23:44 946 查看
这个教程是参考一个YouTube上面的教程做的,原作者的教程做得比较简单,我先参考着做一遍,毕竟我也只是个初学者,还没办法完全自制哈哈。不过我之前也看过一个2D平台游戏的系列教程了,以后会整合起来,做出一个类似冒险岛那样的游戏。

原视频链接:点击打开链接   这是个YouTube视频链接,如果可以“友情访问外网”的朋友可以自己看看。视频全英无字幕,如果看起来有压力的话那其实也可以不看。反正我已经将里面的代码都整理在这里了。

原本我是打算把这篇东西整理到自己的qq空间的,不过发空间很麻烦,就算了,新浪博客也不方便,那就用CSDN吧,以后我的Unity学习笔记都会整理到这个博客里面。谈不上是教程,只能说是自己研究过程的一些整理。欢迎大家一起交流,一起进步,如果这篇笔记可以帮到更多的朋友学习Unity,那就最好不过了。

好了,废话不多说,马上开始。这次是我尝试着制作一个类似魂斗罗和超级玛丽那样的横版游戏,以后学得比较多了会整理更复杂的教程。

一、角色移动

打开Unity,我用的是4.3的版本,选择创建2D游戏或者3D游戏其实都可以,我喜欢创建3D的,这样方便一些。

修改摄像机的视图,改成正交视图。(3D才需要改,2D不用改,3D可以在两种视图模式下进行切换,比2D好些。)



(projection里面,orthographic即是正交视图,在3D情况下默认是Perspective,即透视视图,2D则默认为orthographic)

改完orthographic之后,可以修改size,在正交视图里面size决定画面的大小,size越大视野的范围越大,每件物体相应的可见面积就会缩小。我把size设为5。如果觉得画面视野还不够大的话可以把size改得更小,比如4、3这样。

刚开始的时候,画面会比较暗,有两种方法可以处理,第一种方法是在游戏场景中放置光源。



如上图,有好几种light可以选择。

另一种方法是在Edit的RenderSettings里面进行设置



如图,修改Ambient Light(环境光)的颜色,默认是深灰色,可以调成浅灰色或者白色(白色太亮,不推荐,浅灰色和浅黄色都可以选择,环境光相当于全局光线,如果选择其他颜色,比如紫色或者绿色的话,则会让整个画面变得相当诡异。)

设置完成之后我们就可以开始制作啦:

首先,需要创建一个基本的地形和角色,由于是2D的平面游戏,所以都使用GameObject→Create Other里面的Quad即可。(原视频教程里面是用quad的,不过我想以后可能会换成其他的)

我们先创建三个,第一个改名为Player,第二个就叫做Enemy,第三个叫做Floor,然后将Floor拉长,将其Scale的X变为100,想调得更大也可以。



(这里还多了一个GameManager,因为是事后截的图,所以多了一个,后面会用到,这个和前三个物体不同,这个是个空物体。)

接着我们新建几个文件夹,分别是Prefabs(预设)、Materials(材质)、Scripts(脚本)、Scenes(场景)、Textures(纹理)。

在菜单栏File→Save Scene里保存当前场景,我命名为Scene1。



然后在Materials文件夹里面创建三个material,分别命名为Player、Enemy、Floor,将它们调整成自己喜欢的颜色,我将Player弄成天蓝色,将Enemy弄成红色,将Floor弄成土黄色。这个只是个人爱好啦,用于区分这些物体的,可以随便弄。(Floor和Enemy都去掉Mesh
Collider,加上Box Collider,另外我们还要在Enemy身上添加一个Rigidbody组件。)将Player上面的Mesh Collider去掉,加上这两个:



然后创建第一个脚本。这次我全部使用c#脚本,所以后面有提到的脚本,除非有特别提到是js,否则都是c#。脚本是学习笔记中的重点内容,其他的倒不是特别重要。

(这次用CharacterController组件来控制角色的移动,所以在角色身上去掉Mesh Collider的组件,然后添加Character Controller和Box Collider组件。)

这个脚本命名为Controller2D。下面贴出内容:

    (脚本里面的中文都是我自己加进去的注释,可以全部删掉)

using UnityEngine;
  using System.Collections;
  
  public class Controller2D : MonoBehaviour {
   //引用CharacterController
   CharacterController characterController;
   //重力
   public float gravity = 10;
   //水平移动的速度
   public float walkSpeed = 5;
   //弹跳高度
   public float jumpHeight = 5;
  
   //显示角色当前正受到攻击
   float takenDamage = 0.2f;
  
   // 控制角色的移动方向
   Vector3 moveDirection = Vector3.zero;
   float horizontal = 0;
   // Use this for initialization
   void Start () {
   characterController = GetComponent<CharacterController>();
  
   }
  
   // Update is called once per frame
   void Update () {
   //控制角色的移动
   characterController.Move (moveDirection * Time.deltaTime);
   horizontal = Input.GetAxis("Horizontal");
   //控制角色的重力
   moveDirection.y -= gravity * Time.deltaTime;
   //控制角色右移(按d键和右键时) 在这里不直接使用0而是用0.01f是因为使用0之后会持续移动,无法静止
   if (horizontal > 0.01f) {
   moveDirection.x = horizontal * walkSpeed;
   }
   //控制角色左移(按a键和左键时)
   if (horizontal < 0.01f) {
   moveDirection.x = horizontal * walkSpeed;
   }
   // 弹跳控制
   if (characterController.isGrounded) {
   if(Input.GetKeyDown(KeyCode.Space)){
   moveDirection.y = jumpHeight;
   }
   }
   }
  
  
   public IEnumerator TakenDamage(){
   renderer.enabled = false;
   yield return new WaitForSeconds(takenDamage);
   renderer.enabled = true;
   yield return new WaitForSeconds(takenDamage);
   renderer.enabled = false;
   yield return new WaitForSeconds(takenDamage);
   renderer.enabled = true;
   yield return new WaitForSeconds(takenDamage);
   renderer.enabled = false;
   yield return new WaitForSeconds(takenDamage);
   renderer.enabled = true;
   yield return new WaitForSeconds(takenDamage);
   }
  }
这个脚本比较简单,而且都已经做了注释,就不解释了。做完这个脚本之后保存,然后拖拽到Player身上,角色就可以活动了。后面的这个IEnumerator TakenDamage是用来控制角色被攻击时进行闪烁的,这个本来是后面的内容,脚本先放这里了,就先提及一下。

二、相机跟随

接下来解决的是角色跳跃和相机跟随的问题。这个问题比较容易。而且由于笔记是我跟着一些视频教程做了几节之后再整理的,所以稍微有点“先进”。上面的脚本里面已经解决了角色的跳跃问题。

// 弹跳控制
   if (characterController.isGrounded) {
   if(Input.GetKeyDown(KeyCode.Space)){
   moveDirection.y = jumpHeight;
   }
   }
很简单,就是上面的这一小段代码。用空格键来进行反重力,当然,KeyCode是可以随便设置的。为了方便,我又改成了这样:

if (characterController.isGrounded) {
   if(Input.GetKeyDown(KeyCode.Space)||Input.GetKeyDown(KeyCode.K)){
   moveDirection.y = jumpHeight;
   }
   }
这样就可以用空格键或者K键进行跳跃了,平时我自己的习惯是利用WSAD键位进行上下左右的移动,用JK键进行攻击和跳跃。这种按键布局和FC游戏机的手柄最为相似。

Unity也可以用手柄的,不过这个我还不太会,现在也暂时不用研究这个。

接下来新建一个Camera2D的脚本。(脚本名字是可以随你喜欢的,不过有时候脚本的名字会影响到调用,所以必须尽量规范。)

using UnityEngine;
  using System.Collections;
  
  public class Camera2D : MonoBehaviour {
  
   public Transform player;
  
   public float smoothRate = 0.5f;
  
   private Transform thisTransform;
   private Vector2 velocity;
  
   // Use this for initialization
   void Start () {
   thisTransform = transform;
   velocity = new Vector2 (0.5f, 0.5f);
   }
  
   // Update is called once per frame
   void Update () {
   Vector2 newPos2D = Vector2.zero;
   //Mathf.SmoothDamp平滑阻尼,这个函数用于描述随着时间的推移逐渐改变一个值到期望值,这里用于随着时间的推移(0.5秒)让摄像机跟着角色的移动而移动
   newPos2D.x = Mathf.SmoothDamp (thisTransform.position.x, player.position.x, ref velocity.x, smoothRate);
   newPos2D.y = Mathf.SmoothDamp (thisTransform.position.y, player.position.y, ref velocity.y, smoothRate);
  
   Vector3 newPos = new Vector3 (newPos2D.x, newPos2D.y, transform.position.z);
   //Vector3.Slerp 球形插值,通过t数值在from和to之间插值。返回的向量的长度将被插值到from到to的长度之间。time.time此帧开始的时间(只读)。这是以秒计算到游戏开始的时间。也就是说,从游戏开始到到现在所用的时间。
   transform.position = Vector3.Slerp (transform.position, newPos, Time.time);
   }
  }
好了,这个就是摄像机的脚本,这个脚本不采用死死咬着角色不放的方法,而是采取缓慢跟随的效果。摄像机跟随的脚本可以千变万化,有空再研究其他的,这个不要太纠结。
三、游戏控制器和纹理
现在角色会动了,摄像机也可以跟随了。(不要问我为什么场景这么丑,脚本才是关键,场景只需要更换贴图什么的,这个其实在现在来说并不是重点。)接着就是制作一个简单的游戏控制器,也就是前面的GameManager。这个东西这次是用来显示一些纹理内容的,比如在画面上显示角色的生命值,以及当角色死掉之后重新开始等等……好了,现在创建一个空物体,然后命名为GameManager,创建一个同名脚本扔上去,然后打开这个脚本,我们要进行编辑:
  using UnityEngine;
  using System.Collections;
  
  public class GameManager : MonoBehaviour {
   //Controller2D脚本的参量
   public Controller2D controller2D;
   //角色生命值
   public Texture playersHealthTexture;
   //控制上面那个Teture的屏幕所在位置
   public float screenPositionX;
   public float screenPositionY;
   //控制桌面图标的大小
   public int iconSizeX = 25;
   public int iconSizeY = 25;
   //初始生命值
   public int playersHealth = 3;
   GameObject player;
   //这个地方定义了私有变量player作为一个GameObject,然后用下面的FindGameObjectWithTag获取它,这样的话,在下面的伤害判断时,就可以用player.renderer了。
   void Start(){
   player = GameObject.FindGameObjectWithTag("Player");
   }
  
   //OnGUI函数最好不要出现多次,容易造成混乱,所以我把要展示的东西都整合在这个里面
   void OnGUI(){
  
   //控制角色生命值的心的显示
   for (int h =0; h < playersHealth; h++) {
   GUI.DrawTexture(new Rect(screenPositionX + (h*iconSizeX),screenPositionY,iconSizeX,iconSizeY),playersHealthTexture,ScaleMode.ScaleToFit,true,0);
   }
   }
  
   void PlayerDamaged(int damage){ //此处使用player.renderer.enabled来进行判断,如果角色没有在闪烁,也就是存在的状态为真,那么才会受到伤害,这样可以避免角色连续受伤,还有另外一种方法是采用计时,这里没有采用那种方法。
   if (player.renderer.enabled) {
   if (playersHealth > 0) {
   playersHealth -= damage;
   }
  
   if (playersHealth <= 0) {
   RestartScene ();
   }
   }
   }
  
   void RestartScene(){
   Application.LoadLevel (Application.loadedLevel);
   }
  }
好了,内容已经粘贴出来。有三个地方我认为有必要稍微解释一下,作为备忘的。第一个地方是在OnGUI函数里面,for循环用来画出playersHealthTexture的个数,这个东西在这里就是我们想要展示出来的角色的生命值(我用爱心表示)

接着用Photoshop或者GraphicsGale画一个透明的爱心。不用很大,我画的是32乘以32像素的,当然,要画的很大也可以,不过长宽比必须等于一,而且最好保持2的N次方的大小尺寸,比如32乘以32,256乘以256这样的尺寸。记得是弄成透明的png,不要保存成白色png。PS的很简单就不说了,在GraphicsGale里面保存成透明背景的有点特殊。所以我顺便截一下图:(首先,我们要画完一个爱心……)



然后再点击左下角


的左上角的那个


然后就可以打开当前这幅画的属性:



在透明度那里打勾(默认是没有打勾的),然后用下边的那只滴管笔

点击你自己画面中的背景颜色。我这里是白色,所以点击之后就是白色的

,然后你的作品的背景颜色是其他颜色的,那么在这里出现的就是其他颜色的。

然后就可以确定了。然后在菜单栏选择“文件”→“另存为”就可以保存透明背景的PNG图片出来了。这个功能还是蛮有意思的,PS没办法这样直接保存成PNG,需要对背景稍微处理一下。个人觉得,如果只是画像素画的话,GG确实比PS有一定的便利性。当然,我看的视频教程里面,那位兄台用的是GIMP,其实和PS差不多,也是可以的,直接创建一张透明背景的图片来画画。


画完我们的爱心之后就导入到Unity的项目里面去,这个很简单,就不多说了。直接拖拽放到Textures文件夹就可以了。重命名为Heart(名字可以随便,只要尽量不使用中文名就行)

在Inspector面板修改如下,然后点击Apply。



然后拖拽到GameManager的Inspector面板的脚本里面的Players Health Texture位置:



运行测试:



成功!现在我们的角色有生命值了哈哈!

(在Game视窗中,我将画面大小调成了16:9是因为我用的是笔记本电脑,对于大部分的笔记本电脑来说,都是这个长宽比的尺寸,如果不是的话可以在那里点击之后选择其他的,比如4:3等等,甚至可以进行自定义,这个就不多说了。)

现在如果我们修改

的值,就可以改变运行时在画面上出现的爱心(生命值)的个数,当然,也可以在脚本里面对playersHealth变量进行直接修改。

由于OnGUI函数是每一帧都进行实时渲染的,所以一旦被敌人攻击造成HP的伤害,那么OnGUI函数就会自动减少当前的HP数量。同理,如果角色因为某些原因增加了HP,也是一样的。

四、敌人与伤害计算

好了,HP设置好了,自然就要设置简单的敌人以及伤害计算了。

此处用到的是这个函数:

  void PlayerDamaged(int damage){ //此处使用player.renderer.enabled来进行判断,如果角色没有在闪烁,也就是存在的状态为真,那么才会受到伤害,这样可以避免角色连续受伤,还有另外一种方法是采用计时,这里没有采用那种方法。
   if (player.renderer.enabled) {
   if (playersHealth > 0) {
   playersHealth -= damage;
   }
  
   if (playersHealth <= 0) {
   RestartScene ();
   }
   }
   }

这个在上面已经出现过了。

我参考的一个视频教程中并没有if (player.renderer.enabled)的判断条件,我增加了这个是因为这样可以控制Player在闪烁的过程中不会再被重复扣血,否则的话,如果我们的Player撞到了敌人,如果敌人和我们一直在做着相向运动的话,那么我们会不明不白地被扣光HP然后挂掉的~~~~

(这里补充一个小常识:HP是hit point的缩写,很多人可能误以为HP是health point吧,毕竟这个游戏术语就是用来表示生命的,似乎表示健康点也没什么不对。我一开始也是这么想的,后来看了一些教程和资料之后才发现,其实HP指的是“可以承受的打击点数或次数”,这样的话,用health point来解释当然是解释不通的。而mp这个是mana point,就没什么问题,就不解释了。)

接下来我们要来对敌人进行编辑啦。上面已经做好了敌人的物体,我的这个敌人去掉了自带的Mesh Collider组件,增添了Box Collider和Rigidbody(刚体)组件。在Rigidbody里去掉Use Gravity的勾选,在Constraints选项里进行这样的设置:


也就是冻结了敌人物体可能造成的不必要的三向旋转(XYZ轴)和Z轴的位移。


接着在Box Collider里面,我们将Is Trigger打勾,这样的话就变成了一个触发器,可以用于进行碰触判断。然后我们将Size的X值扩大到1.3,如图:



我们这样做的目的是让这个物体的实际碰触区域比它的实际体积大。为什么要这样做呢?其实这个是对Player物体身上的Character Controller组件的不足的一个补充。



由于Character Controller组件的碰触范围不是一个方形,而是一个圆形,为了避免物体插入Floor,就必须缩小圆圈,这样的话,如果不放大敌人的碰触区间,就会使得Player物体在于敌人进行碰撞时,需要和敌人的身体相嵌入近一半才会产生碰触的效果,那样就实在是太核突了……

接着新建一个Enemy2D脚本,然后弄进以下内容:

using UnityEngine;
  using System.Collections;
  
  public class Enemy2D : MonoBehaviour {
   //GameManager脚本的参量
   public GameManager gameManager;
   //敌人移动的初始和停止位置,用于控制敌人在一定范围内移动
   float startPos;
   float endPos;
   //控制敌人向右移动的一个增量
   public int unitsToMove = 5;
   //敌人移动的速度
   public int moveSpeed = 2;
   //左右移动的布尔值
   bool moveRight = true;
  
   void Awake(){
   startPos = transform.position.x;
   endPos = startPos + unitsToMove;
   }
   //此处这个Update函数用于控制敌人的左右移动,当向右移动到一定距离后就会反向移动,同理,左移一定距离之后也是。
   void Update(){
   if (moveRight) {
   rigidbody.position += Vector3.right * moveSpeed * Time.deltaTime;
   }
   if (rigidbody.position.x >= endPos) {
   moveRight = false;
   }
   if (moveRight==false) {
   rigidbody.position -= Vector3.right * moveSpeed * Time.deltaTime;
   }
   if (rigidbody.position.x <= startPos) {
   moveRight = true;
   }
   }
  
  
   int damageValue = 1;
   //这里利用sendmessage函数使得角色与敌人自己碰撞时,发送一个扣血的message到gamemanager函数之中,然后就会在每次碰撞时减掉一滴血。
   void OnTriggerEnter(Collider col){
   if (col.gameObject.tag == "Player") {
   gameManager.SendMessage("PlayerDamaged",damageValue,SendMessageOptions.DontRequireReceiver);
   gameManager.controller2D.SendMessage("TakenDamage",SendMessageOptions.DontRequireReceiver);
   }
   }
  }
保存,弄到我们的Enemy身上。

现在,利用SendMessage函数,可以向GameManager物体(上面的脚本)发送消息了,第一个发送的消息是“PlayerDamaged”,这个可以用来使Player的HP造成伤害,角色的HP其实并没有在角色自己身上,而是在GameManager物体上。第二个发送的消息是给Controller2D函数的,这个虽然是在Player身上,但是上面的GameManager已经捆绑了这个,所以同样发送给GameManager即可。这个是用来使角色受伤时进行闪烁的,使用的是IEnumerator接口。我们同样用里面的renderer.enabled判断角色是否处于闪烁状态。

五、AI移动

上面那个比较复杂的Enemy2D脚本其实已经包含了这一节的内容了。我就只需要把那部分内容简单地复制一下:

void Update(){
   if (moveRight) {
   rigidbody.position += Vector3.right * moveSpeed * Time.deltaTime;
   }
   if (rigidbody.position.x >= endPos) {
   moveRight = false;
   }
   if (moveRight==false) {
   rigidbody.position -= Vector3.right * moveSpeed * Time.deltaTime;
   }
   if (rigidbody.position.x <= startPos) {
   moveRight = true;
   }
   }
因为内容比较简单,就不多做解释了。这个教程里面提到的AI移动其实是非常简单的,以后还会继续深入设计一些比较复杂的AI。

六、角色攻击

接下来这一讲是关于player attack的。我们创建一个叫做PlayerAttack的脚本,扔到我们的player身上。然后输入以下内容:

using UnityEngine;
  using System.Collections;
  
  public class PlayerAttack : MonoBehaviour {
  
   public Rigidbody bulletPrefab;
  
   // Update is called once per frame
   void Update () {
   if (Input.GetKeyDown (KeyCode.J)) {
   BulletAttack();
   }
   }
   //按下攻击按键时创建子弹的prefab,也就是bulletPrefab。
   void BulletAttack(){
   //下面的这句话非常经典,利用as Rigidbody将Instantiate的GameObject强制转换为Rigidbody类型。
   Rigidbody bPrefab = Instantiate (bulletPrefab, transform.position, Quaternion.identity)as Rigidbody;
   bPrefab.rigidbody.AddForce (Vector3.right * 500);
   }
  
  }
保存,现在我们的Player就可以利用J键发射子弹了。不过这个子弹是一个prefab,我们需要自己设计一个出来才行。

首先我们新建一个quad,命名为Bullet。然后将XYZ的scale设置为0.3(XY肯定要0.3,至于Z,对于一个平面物体来说,没所谓。)然后添加Rigidbody组件和Box Collider组件,在Rigidbody里面去掉Use Gravity,在Constraints的Freeze Rotation里面三项全打勾,在Freeze Position里面的Z打勾。在Box Collider里面的Is Trigger打勾,其他的不用设置。然后创建一个叫做Bullet的脚本,扔到Bullet物体上。

然后我们把这个Bullet物体拖拽到Prefabs文件夹位置,在文件夹中就会自动生成一个叫做Bullet的预设。以后如果需要对预设进行修改的话,只需要重新造一个克隆物体出来修改,然后修改完点击Apply,就会影响到所有的预设体。

最后,我们把bullet的prefab拉到Player的PlayerAttack上。然后将Player和Enemy的tag改为和他们的名字一样的tag。现在我们的主人公会发射子弹了。

在Bullet脚本中进行以下添加:

using UnityEngine;
  using System.Collections;
  
  public class Bullet : MonoBehaviour {
   //用于碰撞时摧毁两个物体
   void OnTriggerEnter(Collider other){
   if (other.gameObject.tag == "Enemy") {
   Destroy(gameObject);
   Destroy(other.gameObject);
   }
   }
  }
保存,现在我们的子弹可以破坏敌人了。不过敌人会被一击消灭,而且子弹飞出画面不会自行摧毁。这些问题就留着下一节解决。

七、角色攻击2

接下来到第七讲。现在我们需要让我们的子弹变得会自动消失。也就是给发射过程设置一个自动摧毁的时间。避免出现子弹越来越多,而且永不消失的情况。首先我们需要在Bullet脚本里面增加一个FixedUpdate函数,这个函数和Update函数有什么不同呢?这里顺便引用一段网上的话:

从字面上理解,它们都是在更新时会被调用,并且会循环的调用。

但是Update会在每次渲染新的一帧时,被调用。

而FixedUpdate会在每个固定的时间间隔被调用,那么要是Update 和FixedUpdate的时间间隔一样,是不是就一样呢?答案是不一定,因为Update受当前渲染的物体,更确切的说是三角形的数量影响,有时快有时慢,帧率会变化,update被调用的时间间隔就发生变化。

但是FixedUpdate则不受帧率的变化,它是以固定的时间间隔来被调用,那么这个时间间隔怎么设置呢?

Edit->Project Setting->time下面的Fixed timestep。

(原链接:http://blog.sina.com.cn/s/blog_6bf81fa60106cmnq.html )

现在这样我们就理解这两个的不同了,下面是添加的内容:

void FixedUpdate(){
   Destroy (gameObject, 1.25f);
   }
利用Destroy函数给出的时间,让物体在1.25秒之后自行摧毁。

接下来我们修改一下PlayerAttack脚本如下:

using UnityEngine;
  using System.Collections;
  
  public class PlayerAttack : MonoBehaviour {
  
   public Rigidbody bulletPrefab;
  
   float attackRate = 0.5f;
   float coolDown;
  
   // Update is called once per frame
   void Update () {
   if (Time.time >= coolDown) {
   if (Input.GetKeyDown (KeyCode.J)){
   BulletAttack ();
   }
   }
   }
   //按下攻击按键时创建子弹的prefab,也就是bulletPrefab。
   void BulletAttack(){
   //下面的这句话非常经典,利用as Rigidbody将Instantiate的GameObject强制转换为Rigidbody类型。
   Rigidbody bPrefab = Instantiate (bulletPrefab, transform.position, Quaternion.identity)as Rigidbody;
   bPrefab.rigidbody.AddForce (Vector3.right * 500);
   coolDown = Time.time + attackRate;
   }
  }
现在增加了两个变量,第一个是attackRate,第二个是coolDown,有了这两个变量之后,在每次使用BulletAttack函数时,coolDown会自动等于当前时间加上attackRate(0.5秒),而每一帧里面,都需要进行判断Time.time是否大于coolDown,如果小于的话就无法射击,这样就实现了冷却的效果。

接下来需要考虑一个问题,那就是子弹的发射方向的问题,因为现在我们的子弹只能向右边发射,很不科学。所以为了解决这个问题,我参考的原视频教程里面将PlayerAttack脚本整个整合到Controller2D里面了。然后在外面的Controller2D位置需要重新连接一次bullet的prefab。



接下来对Controller2D脚本进行一些加工,使我们的子弹可以向左边发射:

using UnityEngine;
  using System.Collections;
  
  public class Controller2D : MonoBehaviour {
   //引用CharacterController
   CharacterController characterController;
   //重力
   public float gravity = 10;
   //水平移动的速度
   public float walkSpeed = 5;
   //弹跳高度
   public float jumpHeight = 5;
  
   //显示角色当前正受到攻击
   float takenDamage = 0.2f;
  
   // 控制角色的移动方向
   Vector3 moveDirection = Vector3.zero;
   float horizontal = 0;
   //原PlayerAttack脚本里面的变量,把那个脚本和当前脚本合并,PlayerAttack脚本已经删除。
   public Rigidbody bulletPrefab;
   float attackRate = 0.5f;
   float coolDown;
   bool lookRight = true;
   // Use this for initialization
   void Start () {
   characterController = GetComponent<CharacterController>();
  
   }
  
   // Update is called once per frame
   void Update () {
   //控制角色的移动
   characterController.Move (moveDirection * Time.deltaTime);
   horizontal = Input.GetAxis("Horizontal");
   //控制角色的重力
   moveDirection.y -= gravity * Time.deltaTime;
  
   if (horizontal == 0) {
   moveDirection.x = horizontal;
   }
  
  
   //控制角色右移(按d键和右键时) 在这里不直接使用0而是用0.01f是因为使用0之后会持续移动,无法静止
   if (horizontal > 0.01f) {
   lookRight = true;
   moveDirection.x = horizontal * walkSpeed;
   }
   //控制角色左移(按a键和左键时)
   if (horizontal < -0.01f) {
   lookRight = false;
   moveDirection.x = horizontal * walkSpeed;
   }
   // 弹跳控制
   if (characterController.isGrounded) {
   if(Input.GetKeyDown(KeyCode.Space)||Input.GetKeyDown(KeyCode.K)){
   moveDirection.y = jumpHeight;
   }
   }
   //原PlayerAttack的函数
   if (Time.time >= coolDown) {
   if (Input.GetKeyDown (KeyCode.J)){
   BulletAttack ();
   }
   }
   }
   //原PlayerAttack的函数,现在和当前脚本合并了。
   //按下攻击按键时创建子弹的prefab,也就是bulletPrefab。
   void BulletAttack(){
   if (lookRight) {
   //下面的这句话非常经典,利用as Rigidbody将Instantiate的GameObject强制转换为Rigidbody类型。
   Rigidbody bPrefab = Instantiate (bulletPrefab, transform.position, Quaternion.identity)as Rigidbody;
   bPrefab.rigidbody.AddForce (Vector3.right * 500);
   coolDown = Time.time + attackRate;
   }
   else {
   //下面的这句话非常经典,利用as Rigidbody将Instantiate的GameObject强制转换为Rigidbody类型。
   Rigidbody bPrefab = Instantiate (bulletPrefab, transform.position, Quaternion.identity)as Rigidbody;
   bPrefab.rigidbody.AddForce (-Vector3.right * 500);
   coolDown = Time.time + attackRate;
   }
   }
  
   public IEnumerator TakenDamage(){
   renderer.enabled = false;
   yield return new WaitForSeconds(takenDamage);
   renderer.enabled = true;
   yield return new WaitForSeconds(takenDamage);
   renderer.enabled = false;
   yield return new WaitForSeconds(takenDamage);
   renderer.enabled = true;
   yield return new WaitForSeconds(takenDamage);
   renderer.enabled = false;
   yield return new WaitForSeconds(takenDamage);
   renderer.enabled = true;
   yield return new WaitForSeconds(takenDamage);
   }
  }
这个脚本里面主要增加了lookRight的布尔值以及在发射子弹时的一些判定。另外用

if (horizontal == 0) {

moveDirection.x = horizontal;

}

这个if函数使角色在不移动的情况下保持静止。

八、AI血值

接下来处理另一个问题,现在我们的敌人都是打一下就死掉的,但是很多横版游戏的敌人是非常彪悍的,并不是打一下就马上挂掉。所以我们开始来给AI设置简单的血值。

首先打开Enemy2D脚本,修改成下面这个样纸:

using UnityEngine;
  using System.Collections;
  
  public class Enemy2D : MonoBehaviour {
   //GameManager脚本的参量
   public GameManager gameManager;
   //敌人移动的初始和停止位置,用于控制敌人在一定范围内移动
   float startPos;
   float endPos;
   //控制敌人向右移动的一个增量
   public int unitsToMove = 5;
   //敌人移动的速度
   public int moveSpeed = 2;
   //左右移动的布尔值
   bool moveRight = true;
  
   //敌人的HP
   int enemyHealth = 1;
   //敌人的种类
   public bool basicEnemy;
   public bool advancedEnemy;
  
   void Awake(){
   startPos = transform.position.x;
   endPos = startPos + unitsToMove;
  
   if (basicEnemy) {
   enemyHealth = 3;
   }
  
   if (advancedEnemy) {
   enemyHealth = 6;
   }
   }
   //此处这个Update函数用于控制敌人的左右移动,当向右移动到一定距离后就会反向移动,同理,左移一定距离之后也是。
   void Update(){
   if (moveRight) {
   rigidbody.position += Vector3.right * moveSpeed * Time.deltaTime;
   }
   if (rigidbody.position.x >= endPos) {
   moveRight = false;
   }
   if (moveRight==false) {
   rigidbody.position -= Vector3.right * moveSpeed * Time.deltaTime;
   }
   if (rigidbody.position.x <= startPos) {
   moveRight = true;
   }
   }
  
  
   int damageValue = 1;
   //这里利用sendmessage函数使得角色与敌人自己碰撞时,发送一个扣血的message到gamemanager函数之中,然后就会在每次碰撞时减掉一滴血。
   void OnTriggerEnter(Collider col){
   if (col.gameObject.tag == "Player") {
   gameManager.SendMessage("PlayerDamaged",damageValue,SendMessageOptions.DontRequireReceiver);
   gameManager.controller2D.SendMessage("TakenDamage",SendMessageOptions.DontRequireReceiver);
   }
   }
  
   //下面这个函数用于判定敌人自己被攻击时的扣血。
   void EnemyDamaged(int damage){
   if (enemyHealth > 0) {
   enemyHealth -= damage;
   }
  
   if (enemyHealth <= 0) {
   enemyHealth = 0;
   Destroy(gameObject);
   }
   }
  }
然后我们同样要修改Bullet的脚本,让子弹与敌人发送碰撞时不是瞬间摧毁敌人,而是对敌人身上的Enemy2D脚本发送一个扣血的数量,这个数量一旦为零,Enemy2D脚本就会将敌人自行摧毁。
  using UnityEngine;
  using System.Collections;
  
  public class Bullet : MonoBehaviour {
  
   int damageValue = 1;
  
   //用于碰撞时摧毁两个物体
   void OnTriggerEnter(Collider other){
   if (other.gameObject.tag == "Enemy") {
   Destroy(gameObject);
   other.gameObject.SendMessage("EnemyDamaged",damageValue,SendMessageOptions.DontRequireReceiver);
   }
   }
  
   void FixedUpdate(){
   Destroy (gameObject, 1.25f);
   }
  }
另外,在刚刚的Enemy2D脚本里面,我们还设置了basicEnemy和advancedEnemy两个布尔值,分别设定我们当前敌人的HP为3和6,这样我们就可以通过这些布尔值来创建不同HP的敌人。(暂时只设定两种,当然可以随你喜欢,要多少种就做多少种。)

然后我们将Enemy物体做成一个prefab,在场景中创建两个Enemy,一个打勾为basic,一个打勾为advanced,这样我们的场景中就出现了一个3滴血和一个6滴血的敌人啦。


(这个是basic的)


(这个是advanced的)

    为了区分敌人,我做了另外一个紫色的material,命名为advancedEnemy,然后拖拽到我们的那个设定为advancedEnemy的Enemy预设体身上。

运行,我们看到的情况就是这样:



左边的敌人三下攻击就挂掉,右边的则需要攻击六下。

九、经验系统

敌人的设置暂时可以了。接下来我们来设置经验值系统。在传统的RPG中,打怪升级是永恒不变的主题。所以我们也想要设置一个类似的系统,哈哈。

将GameManager脚本修改成这个样纸:

using UnityEngine;
  using System.Collections;
  
  public class GameManager : MonoBehaviour {
   //Controller2D脚本的参量
   public Controller2D controller2D;
   //角色生命值
   public Texture playersHealthTexture;
   //控制上面那个Teture的屏幕所在位置
   public float screenPositionX;
   public float screenPositionY;
   //控制桌面图标的大小
   public int iconSizeX = 25;
   public int iconSizeY = 25;
   //初始生命值
   public int playersHealth = 3;
   GameObject player;
  
   int curEXP = 0;
   int maxEXP = 50;
   int level = 1;
   //这个布尔值用于判定是否显示角色的状态
   bool playerStats;
   //下面这个用于显示角色的状态
   public GUIText statsDisplay;
  
   //这个地方定义了私有变量player作为一个GameObject,然后用下面的FindGameObjectWithTag获取它,这样的话,在下面的伤害判断时,就可以用player.renderer了。
   void Start(){
   player = GameObject.FindGameObjectWithTag("Player");
   }
  
   void Update(){
   if (curEXP >= maxEXP) {
   LevelUp();
   }
  
   if (Input.GetKeyDown (KeyCode.C)) {
   playerStats = !playerStats;
   }
  
   if (playerStats) {
   statsDisplay.text = "等级:" + level + "\n经验:" + curEXP + "/" + maxEXP;
   }
   else {
   statsDisplay.text = "";
   }
   }
   //这个函数用于角色升级
   void LevelUp(){
   curEXP = 0;
   maxEXP = maxEXP + 50;
   level++;
  
   //升级的功效
   playersHealth++;
   }
  
  
   //OnGUI函数最好不要出现多次,容易造成混乱,所以我把要展示的东西都整合在这个里面
   void OnGUI(){
  
   //控制角色生命值的心的显示
   for (int h =0; h < playersHealth; h++) {
   GUI.DrawTexture(new Rect(screenPositionX + (h*iconSizeX),screenPositionY,iconSizeX,iconSizeY),playersHealthTexture,ScaleMode.ScaleToFit,true,0);
   }
   }
  
   void PlayerDamaged(int damage){ //此处使用player.renderer.enabled来进行判断,如果角色没有在闪烁,也就是存在的状态为真,那么才会受到伤害,这样可以避免角色连续受伤,还有另外一种方法是采用计时,这里没有采用那种方法。
   if (player.renderer.enabled) {
   if (playersHealth > 0) {
   playersHealth -= damage;
   }
  
   if (playersHealth <= 0) {
   RestartScene ();
   }
   }
   }
  
   void RestartScene(){
   Application.LoadLevel (Application.loadedLevel);
   }
  }
然后保存。然后我们在场景中创建一个GUI Text物体。命名为GUIStats。接下来我们需要导入一个字体文件。(当然,你想用默认字体也是可以的,做游戏的时候一般会用到NGUI或者其他类似的GUI插件来做字体或者其他的一些效果。因为我现在还没怎么接触那些插件,所以暂时不涉及插件方面的内容,直接制作就好了……虽然在画面上可能简陋和丑了一些,不过这些要慢慢来的,没办法一口吃成胖子。)

我在字体大宝库网http://font.knowsky.com/下载了一个字体,

http://font.knowsky.com/down/6707.html 迷你简卡通,当然你也可以随便找自己喜欢的字体。不过记住要是ttf格式的。因为我们的Unity目前只支持这种格式的字体,常见的其他格式还有otf,还有一些其他类型的格式,需要转换的话,可以用一款叫做fontcreator的工具,这款工具除了可以用来将otf转成我们喜欢的ttf之外,还可以编辑、自己设计自己喜欢的字体,不过这个不是现在的内容,以后再进行整理吧。

在我们的Unity里面创建一个叫做Fonts的文件夹,然后把我们的迷你简卡通字体扔进去。注意这个字体的名字尽量改成英文,不然容易出问题。在Unity里面,所有文件夹和文件的名称尽量不要有中文,另外,保存的游戏、项目的路径里面也最好不要有中文。

完成之后我们看GUIStats的Inspector栏,在Font里面进行选择,默认是Arial字体,我们现在可以改成我们导入的minikatongjianti了(我是这样命名的……)



将GUIStats的transform位置修改一下,这个会影响到我们运行游戏时字体出现的位置,当然你还可以修改字体颜色、大小等等,我就不想改这些了。



然后我们运行游戏,并且按C键,也就是我们刚刚设置的那个用于显示的键位。(你要是设置成其他按键也是一样的。)



这个就是运行之后的效果,哈哈,虽然很简陋,不过已经大功告成了!

现在我们需要对两个脚本进行一些修改:

首先是将GameManager脚本里面的curEXP变量改成public的,这样的话才可以被外边调用到。然后我们打开Enemy2D脚本,修改下面部分的内容:

void EnemyDamaged(int damage){
   if (enemyHealth > 0) {
   enemyHealth -= damage;
   }
  
   if (enemyHealth <= 0) {
   enemyHealth = 0;
   Destroy(gameObject);
   gameManager.curEXP += 10;
   }
   }
然后保存。现在在摧毁敌人的同时,我们的GameManager就会接收一个curEXP加十的信号,这样就可以为我们的角色增加EXP了。不过有个地方需要特别注意一下,不然的话会报错:由于我们的敌人是预设体而不是物体,所以每个敌人预设体的Enemy2D脚本上的GameManager脚本都要自己进行关联,不然的话就会在消灭敌人的时候因为找不到GameManager而无法发送增加经验值的信号,导致报错。

报错的内容为:NullReferenceException: Object reference not set to an instance of an object(需要注意一下)



现在刷刷刷干掉两个敌人,看,我们有20点经验值啦嘻嘻~~



接下来我们来做点对HP的测试,因为要打五只怪才能升一级,比较麻烦,我们直接增加一个按键加经验的功能好了(节操何在……)。非常简单,我们只需要在GameManager脚本的Update函数里面增加这么一个if判断:

if (Input.GetKeyDown (KeyCode.E)) {

curEXP += 10;

}

然后我们每次按E就会增加10点经验值。当然,这个是测试的,真的设计游戏的时候不要这么玩,否则游戏的可玩性和平衡性就全完蛋了。



我们可以看到,当我们的经验增加到超过50时,爱心的个数就会变成4,而且经验清零,之后如果想从第二级升到第三级,就需要100点经验值了,以后每个等级需要多50点经验值,以此类推。

十、主菜单和等级设置

开始这一讲之前需要做一些补充。(我看的视频教程里面有提到说,如果对GUI这块还不太清楚的话需要看一下他前面的几个教程,所以我稍微花了点时间把他之前的几个教程给看完。)

里面是教你怎么设置背景图片以及做一些简单的按钮的。这个教程看完之后觉得比较简单,就不再赘述了,直接贴出里面用到的代码:

using UnityEngine;
  using System.Collections;
  
  public class MainMenu : MonoBehaviour {
  
   public Texture backgroundTexture;
   public GUIStyle random1;
  
   public float guiPlacementX1;
   public float guiPlacementX2;
   public float guiPlacementY1;
   public float guiPlacementY2;
  
   public bool showGUIOutline = true;
  
   void OnGUI(){
  
  //显示背景
   GUI.DrawTexture (new Rect (0, 0, Screen.width, Screen.height), backgroundTexture);
  
  //显示按钮
   if (showGUIOutline) {
   if (GUI.Button (new Rect (Screen.width * guiPlacementX1, Screen.height * guiPlacementY1, Screen.width * .5f, Screen.height * .1f), "开始游戏")) {
   print ("Clicked Play Game");
   }
  
   if (GUI.Button (new Rect (Screen.width * guiPlacementX2, Screen.height * guiPlacementY2, Screen.width * .5f, Screen.height * .1f), "设置")) {
   print ("Clicked Options");
   }
   } else {
   if (GUI.Button (new Rect (Screen.width * guiPlacementX1, Screen.height * guiPlacementY1, Screen.width * .5f, Screen.height * .1f), "", random1)) {
   print ("Clicked Play Game");
   }
  
   if (GUI.Button (new Rect (Screen.width * guiPlacementX2, Screen.height * guiPlacementY2, Screen.width * .5f, Screen.height * .1f), "", random1)) {
   print ("Clicked Options");
   }
   }
   }
  
  }
另外呢,还有一个教程是设置场景的plane的。里面用到了一个链接,如下:

http://wiki.unity3d.com/index.php?title=CreatePlane

使用这个链接里面的方法创建的plane的面数会比较少,如果是开发iOS游戏的话,那这个还是挺有必要了解了解的,不过我现在暂时不需要在这个东西上面研究和花费时间。

稍微研究完我们的GUI之后,接下来继续我们的RPG研究。

接下来我们可以重新创建一个场景,命名为Mainmenu的,然后我们把上面那个脚本保存之后放到新创建的场景的摄像机物体上面去。接着我们需要进行一点点简单的修改。使我们能够在Mainmenu场景点击“开始游戏”时进入我们前面设计的第一关。

这个很简单。

首先我们在上面的脚本里面添加一个公共的字符串变量,名字随便,我命名为Level,即:public string Level; 然后我们在按键的功能的那个地方添加Application.LoadLevel(Level);

if (showGUIOutline) {
if (GUI.Button (new Rect (Screen.width * guiPlacementX1, Screen.height * guiPlacementY1, Screen.width * .5f, Screen.height * .1f), "开始游戏")) {
print ("Clicked Play Game");
Application.LoadLevel(Level);
}
其实使用Application.LoadLevel()可以在括号里面直接添加场景的序号,比如我们的场景1序号就是1,(因为Mainmenu场景的序号为0),或者是直接打双引号调用场景名称。比如Application.LoadLevel(“Scene1”); 但是这样做有个问题,那就是如果你的场景改名字了,你就需要修改脚本里面这个地方的名字,或者是如果这一部分的脚本内容复制到其他地方去用的话,那就需要修改同样的内容。为了避免这种情况,我采用了public string,这样的话可以在物体上面直接打出我们的场景的名字,就可以加载了,是不是方便了很多呢?


Level里的Scene1是我自己打进去的,可以随时修改成别的内容。)
当然,不要忘了在菜单栏的File→Build Settings里面将我们目前的两个场景拖拽进去。



以后如果添加了新的场景的话,也要进行同样的处理,否则的话是没办法加载和切换其他场景的。

在我们刚刚的MainMenu脚本里面,我们还需要为之添加一张用来做背景的图片,我随便弄了一张,就是我现在的电脑壁纸。我的壁纸是我自己做的,分辨率是1366*768,所以需要进行以下的设置:



然后我们来试运行一下:



很好,嘻嘻,我们现在有了一个比较简单的开始菜单了~~至于换图片,美工处理和优化什么的,暂时先不用急,等编码部分解决了就马上给我们的整个场景和角色进行打扮和处理。

接下来需要考虑的就是防止角色冲出游戏画面的问题了,我们的角色如果飞出画面的话是会直接坠落身亡的……(不对,应该是会无限坠落,并且坠落速度大于相机的跟随速度,所以相机永远跟不上,画面里也就永远不会显示角色……汗……)所以呢,我们来稍微做点处理,很简单,类似地板那样,弄两个墙壁,然后呢,现在的场景很单调,我们再弄几块石头放到我们的场景上面去。

注意由于我们现在所有的东西都是平面物体,去掉Mesh Collider之后换成Box Collider,而Box Collider对于平面物体来说,如果要使其发生碰撞的话,Z轴上的值必须相等,所以暂时需要把所有物体的Z轴上的值设置为零。(如果你的物体不是平面,而是个立方体的话就另当别论了,不过这个以后再说,现在画面丑陋就丑陋吧……)

还有一个问题需要修复一下,那就是我们的子弹是不可以穿透墙壁的,怎么解决这个问题呢?我们可以给Bullet脚本增加一个内容,使其在碰到带有某类tag的物体是自行摧毁,即可解决这个问题。

现在我们在场景中创建一个空物体,命名为LevelObjects(在英语里面,level一词除了可以用来表示等级之外,还可以用来表示关卡,这里的LevelObjects表示的意思就是该关卡里面的全部自然物体),然后我们把所有的Stone、Floor还有Wall都放到这个物体里面。这样做是为了使我们的Hierarchy面板看起来比较干净。



然后我们将里面的全部物体全选,加上一个LevelObjects的标签。然后我们打开Bullet脚本,在void OnTriggerEnter函数里面加入:

if (other.gameObject.tag == "LevelObjects") {

Destroy(gameObject);

}

这样的话,我们的子弹碰到墙壁什么的,就可以自行摧毁啦。



现在,一个丑的要死的场景就搞定啦,哈哈~还是有点小激动啊~~

十一、平台制作

这一讲比较简单,因为我们之前已经让我们的敌人可以左右晃动了,那么,copy那一部分的代码稍微修改一下,就可以做出一个上下升腾的平台效果出来。但是直接操作的话会出现一个很奇怪的问题,那就是在我们的角色跳上平台的那一瞬间,角色就会穿过平台掉落到地面上。这个问题在我以前看过的一个教程里面并没有出现,因为上次看的那个教程里面用到的平台是3D的,而本次用到的是2D的,可能在这方面会有一定的影响吧。

算了,暂时不管了,我是打算先把这个部分的教程完整地走完一遍,然后再去进行其他的一些补充的。这个问题就暂时按照原教程里面的方法修复好就算了。

那么原教程里面的方法是怎样做的呢?

首先,我们先创建一个MovingPlatform的脚本,弄进去以下内容:

using UnityEngine;
  using System.Collections;
  
  public class MovingPlatform : MonoBehaviour {
  
   float startPos;
   float endPos;
  
   public int unitsToMove = 5;
  
   public int moveSpeed = 2;
  
   bool moveRight = true;
  
  
   void Awake(){
   startPos = transform.position.y;
   endPos = startPos + unitsToMove;
   }
  
   void Update(){
   if (moveRight) {
   transform.position += Vector3.up * moveSpeed * Time.deltaTime;
   }
   if (transform.position.y >= endPos) {
   moveRight = false;
   }
   if (moveRight==false) {
   transform.position -= Vector3.up * moveSpeed * Time.deltaTime;
   }
   if (transform.position.y <= startPos) {
   moveRight = true;
   }
   }
  }
这部分内容全部都是在Enemy2D里面搬来的,然后我去掉了里面的中文注释,并且将Rigidbody改为transform,因为我们的平台物体并不需要一个Rigidbody在配合。(你要的话也是可以的)

接下来我们在Platform物体(这个物体是刚刚创建的,类似墙壁物体一样,要去掉Mesh Collider,然后加上Box Collider)下面创建一个空物体作为它的子物体,命名为PlatformTrigger,然后再创建一个叫做StickToPlatform的脚本,并且把这个脚本扔到我们的角色上面去,编辑脚本:

using UnityEngine;
  using System.Collections;
  
  public class StickToPlatform : MonoBehaviour {
  
   void OnTriggerStay(Collider other){
   if (other.tag == "Platform") {
   this.transform.parent = other.transform;
   }
   }
  
   void OnTriggerExit(Collider other){
   if (other.tag == "Platform") {
   this.transform.parent = null;
   }
   }
  }
这个地方稍微需要解释一下,这里的意思是,当我们的Player和这个踏板接触时,就会变成踏板的一个子物体,然后跟着踏板一起移动,当我们的Player和踏板不再接触时,就会取消掉它身上的父类,这样的话我们的角色物体又变成了独立物体。



现在,一个会自动上升和下降,并且不会对角色产生干扰的平台就做好了!(请注意,上面的脚本是拖拽到我们的角色身上,而不是平台上,不要弄错,否则无法运行的。)

好啦,我的第一篇CSDN博客就整理到这里。个人水平有限,如果里面有什么说错写错的地方,欢迎大家指正和补充。如果这篇笔记可以帮到大家,那自然是最好不过啦!希望能和大家多多交流~

工程文件打包下载:http://download.csdn.net/detail/sinolzeng/7279669

第二篇继续整理和学习研究中……
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息