前言
6月底开始学习Unity3D,断断续续,算是开始入门了.
相比于cocos2d-x,在用Unity3D写游戏时,在界面上用编辑器拖拉修改,而且可以实时查看效果,但cocos2d-x需要用代码去设计,而且每次查看修改后的效果都需要重新编译运行,十分耗时;此外,Unity3D,使用C#脚本(还有JavaScript)来编写逻辑,逻辑编写效率很高,而且不需要去担心C++里恼人的内存管理工作.
总之,Unity3D作为一套完整的游戏开发解决方案,开发游戏十分高效.自己甚至后悔在去年9月时,选择cocos2d-x去开发游戏.但,cocos2d-x的开发和实习经验,让自己对游戏开发,和部分底层的原理有了一些了解,这在自己学习Unity3D的过程中,或多或少有一定的帮助.
希望现在开始学习Unity3D还不晚,加油!
坦克大战
在此之前,已经花费时间学习了Unity3D简单的编辑器操作,对C#也简单学习了一下语法(自己C++基础尚可,对于C#只是简单的了解了一下语法和语言特性,更多的特性打算在开发过程中需使用时再去了解).
为了更加高效地入门,自己购买了Siki的一套完整的Unity教程,并照着教程视频来逐步开发游戏,第一个游戏名叫《坦克大战》(Unity官方教程之一).
以下,是学习该游戏过程中的一些Tips和总结.
1.刚体(Rigidbody)
作为一个组件(component),刚体可以使物体受Unity物理引擎的控制,而且不用添加任何的代码,就可以让物体受重力影响,并且能对碰撞做出反应.
当然,刚体组件提供了脚本API,这样就能在脚本中对刚体进行控制,例如对刚体添加力,或者控制器运动路径.
最后,应该在FixedUpdate函数中对刚体添加力或者改变刚体的属性,这是因为Update函数每秒运行次数和机器本身相关,而物理模拟应该是和时间相关,而不是和帧更新的速度相关.
重要的几个API:
(1)void OnCollisionEnter(Collision collision):
刚体本身和collision开始发生碰撞时被调用;
(2)void OnCollisionExit(Collision collision):
刚体本身和collision碰撞结束时被调用;
(3)void OnCollisionStay(Collision collision):
刚体本身和collision碰撞状态保持时被调用(每帧调用一次);
(4)void SendMessage(string methodName, object value = null, SendMessageOptions options = SendMessageOptions.RequireReceiver);
这里对最后一个参数不做了解,第一个参数是方法名,第二个参数是传递的方法参数(可以通过数组来实现多个参数的传递);
这个函数的作用是,接收一个方法名,然后在刚体的脚本中找到名为methodName的方法,并传递参数调用.
注:个人觉得这是使用了C#里的反射机制;
2.CharacterController
使用刚体,在发生碰撞的时候,会产生力的作用,这在大多数场景下是合适的;但在某些场景下,是不适合用刚体的,例如使用摄像机游览场景的时候,虽然需要进行碰撞检测,以避免穿墙,同时又要求摄像机和其他物体碰撞之后不能对它产生任何力的作用,同时摄像机本身也不需要模拟真实碰撞的反作用力.
在这种情况下,CharacterController被提出来,以满足这种需求.CharacterController本身自带一个碰撞器,无需刚体即可完成触发(Trigger)和碰撞(Collision)功能.
要使用CharacterController,首先需要将CharacterController组件挂载在目标对象下,然后添加一个脚本,在脚本中,获取(GetComponent< CharacterController >())CharacterController组件实例,并在Update或FixedUpdate函数中调用Move(暂时还未用过)或SimpleMove来更新CharacterController的位置,如果与其他的碰撞去发生碰撞,CharacterController就会被阻挡前进,同时触发OnControllerColliderHit函数.
3.Prefab
概念:Prefab是一种资源类型—存储在项目视图中的一种可以反复使用的游戏对象.因而当游戏中需要非常多反复使用的对象、资源等时,Prefab就有了用武之地.它拥有下面的特点:
(1)能够放到多个场景中,也能够在同一个场景中放置多次;
(2)当加入一个Prefab到场景中时,就创建了它的一个实例;
(3)全部的Prefab实例链接到原始的Prefab,本质上是原始Prefab的克隆;
(4)不论项目中存在多少实例,仅仅要对Prefab进行改动,全部Prefab实例都将随之发生变化.
创建填充Prefab:为创建一个Prefab,必须使用菜单构造一个新的空白Prefab.不包括游戏对象,因而不能创建它的一个实例.
此时这个新的Prefab就像一个空的容器,想要使用它.必须用游戏对象的数据来填充它,让它不空.创建方法为:Project视图中,Create->Prefab(或者,Assets->Create->Prefab),此时能够看到Project视图中多了一个灰色的小立方体,这就是创建的Prefab.
填充Prefab非常简单,仅仅需要在Hierarchy视图中选择要成为Prefab的游戏对象,然后拖动该对象到刚才创建的Prefab上就可以.此时,能够看到灰色的小立方体变成蓝色.此时,游戏对象和其全部的子对象就已经拷贝到了Prefab的数据中.而Hierarchy视图中的原始游戏对象已经成为了该Prefab的一个实例.
脚本实例化Prefab(执行时实例化Prefab):在执行时,通过Prefab实例化复杂的游戏物体是非常方便的.实例化Prefab的替代方法就是从一开始就是用代码来创建游戏物体.而实例化Prefab比替代方法有非常多的优势:
(1)能够用一行代码来实例化一个具有所有同样功能的Prefab;而用代码创建等效的游戏对象却要花费平均5行代码甚至更多;
(2)能够非常简单地在Scene和Inspector视图中高速建立、测试和改动Prefab;
(3)能够改动实例化的Prefab,却不须改变实例化它的代码;
Unity中Object类实例化函数:
static Object Instantiate(Object original, Vector3 position, Quaternion rotation);
第一个参数:实例化对象模板;
第二个参数:实例化对象的位置(例如炮弹,可以在场景中建立一个EmptyObject作为发射器,将该发射器位置作为position);
第三个参数:实例化对象的旋转.
using UnityEngine;
using System.Collections;
public class TankAttack : MonoBehaviour {
public GameObject shellPrefab;
public KeyCode fireKey = KeyCode.Space;
private Transform firePosition;
public float shellSpeed = 10.0f;
public AudioClip shootAudio;
void Start () {
firePosition = transform.Find("FirePosition");
}
void Update () {
if(Input.GetKeyDown(fireKey))
{
AudioSource.PlayClipAtPoint(shootAudio, transform.position);
GameObject go = GameObject.Instantiate(shellPrefab, firePosition.position, firePosition.rotation) as GameObject;
go.GetComponent<Rigidbody>().velocity = go.transform.forward * shellSpeed;
}
}
}
Instantiate函数返回的是Object类型,需要将其向下转型,C#里提供as关建志完成这个操作,如果转型失败,不会抛出异常,而是返回null;
上述代码中,在编辑器里将弹药(Shell)的Prefab拖到Tank脚本TankAttack的shellPrefab变量上,使用Instantiate函数即可完成实例化;
继承Prefab:继承是指任何时候当源Prefab发生变化时,这些变化将应用于全部已链接到该Prefab的游戏对象.比如,假设加入一个新的脚本到该Prefab,全部已经链接到该Prefab的游戏对象都将立马包括该脚本.
源的位置和旋转将不被应用,因为这样会影响到实例的绝对位置并把全部实例放在同一个位置.
4.InputManager(输入管理器)
在坦克大战游戏中,两个坦克分别通过WASD和上下左右键来控制前进后退以及左右旋转.在Unity中,WASD和上左下右一一对应,也就是按W和上是相同的效果,原因如下图:
这里,我们要将WASD和上下左右分别控制两个坦克,因此需要自定义Axes.
Edit->ProjectSetting->Input,打开InputManager,对于Horizontal,新建HorizontalPlayer1和HorizontalPlayer2,对应坦克1和坦克2在左右旋转上的按键输入控制,设置如下图:
这样AD控制坦克1,左右控制坦克2,对于Vertical,重复以上类似操作即可.
using UnityEngine;
using System.Collections;
public class TankMovement : MonoBehaviour {
public float speed = 5.0f;
public float angularSpeed = 10;
public int playerNum = 1;
private Rigidbody rigidbody;
void Start () {
rigidbody = GetComponent<Rigidbody>();
}
void FixedUpdate()
{
float v = Input.GetAxis("VerticalPlayer" + playerNum);
rigidbody.velocity = transform.forward * v * speed;
float h = Input.GetAxis("HorizontalPlayer" + playerNum);
rigidbody.angularVelocity = transform.up * h * angularSpeed;
}
}
通过以上代码即可控制坦克的前进后退,以及左右旋转,这里用playerNum来判断是坦克1还是坦克2,playerNum的值在编辑器里设置.而且利用v和h变量,来控制坦克移动旋转的进行和停止.
5.Audio(音频)
1.AudioListener:音频接收者组件,一般挂载在摄像机或者玩家身上;
2.AudioSource:音频源组件,放置在需要播放声音的物体上;
3.AudioClip:音频源文件类;
使用方法:
1.在脚本中,建立一个public的Audio类,并在编辑器中将音频文件拖拉赋值,然后在脚本中可以如下播放:
using UnityEngine;
using System.Collections;
public class TankAttack : MonoBehaviour {
public GameObject shellPrefab;
public KeyCode fireKey = KeyCode.Space;
private Transform firePosition;
public float shellSpeed = 10.0f;
public AudioClip shootAudio;
void Start () {
firePosition = transform.Find("FirePosition");
}
// Update is called once per frame
void Update () {
if(Input.GetKeyDown(fireKey))
{
AudioSource.PlayClipAtPoint(shootAudio, transform.position);
...
}
}
}
如上,使用AudioSource.PlayClipAtPoint播放音乐,参数分别为音频文件类实例和播放的位置;
2.同1,但在脚本中,额外增加一个private AudioSource实例,在播放时,用AudioSource实例播放具体的音频文件实例,如下:
using UnityEngine;
using System.Collections;
public class TankMovement : MonoBehaviour {
...
private Rigidbody rigidbody;
public AudioClip idleAudio;
public AudioClip drivingAudio;
private AudioSource audio;
// Use this for initialization
void Start () {
rigidbody = GetComponent<Rigidbody>();
audio = GetComponent("AudioSource") as AudioSource;
}
// Update is called once per frame
void Update () {
}
void FixedUpdate()
{
...
if (Mathf.Abs(h) > 0.1f || Mathf.Abs(v) > 0.1f)
{
audio.clip = drivingAudio;
}
else
{
audio.clip = idleAudio;
}
if(audio.isPlaying == false)
{
audio.Play();
}
}
}
上述代码是坦克在不动和运动时的声音播放切换,因为Update调用频率很高,为了避免声音重复播放,使用
if(audio.isPlaying == false)
{
audio.Play();
}
避免重复的播放;声音的切换只需要对AudioSource实例的clip成员变量进行赋值即可!
两者的区别:
(1)对于第2种方法,存在一个问题,就是播放音频是需要时间的,如果在播放的同一时刻,所挂载的GameObject被销毁,声音也就无法继续播放,此时就只能使用第1中方法;
(2)第2种方法适合根据不同状态,对多个声音的切换播放.
注:背景音乐的播放,一般是游戏开始就自动播放的,这需要将AudioSource的Play On Awake选项进行勾选,不然无法自动播放;
总结
初学Unity,很多内容都是跟着教程走的,上面的总结难免会有考虑不充分或者错误的地方,还望指出~