Unity最大的优点:可视化的工作流和跨平台的支持。Unity基于component的设计,使得一个component能被重复的使用。
Unity的缺点有:查找功能不够强大,有时候在项目中查找脚本比较麻烦;不支持链接到第三方库,要使用时必须手动拷贝到工程中来;prefab是Unity独有的重要功能,但是编辑prefab又不太方便。这些都希望在以后的版本中得到改进。
Unity开发的产品横跨三大平台(主机、PC、手机),比较著名的有:炉石传说、王者荣耀、神庙逃亡、捣蛋猪(愤怒的小鸟)、高尔夫俱乐部等。
Unity推荐的官方开发语言有C#和JavaScript,这里的JavaScript与标准版有所不同。
所有的Unity脚本都要继承MonoBehaviour,它提供了Start()和Update()方法以供重载。前者是在脚本关联的对象被激活时(也就是对象所在层级被加载时)调用。
构建一个FPS游戏场景需要:创建场景和人物,放置光源,将camera与人物绑定,处理移动脚本。
以大拇指为x轴,食指y轴,中指z轴,可将三维坐标系分为:左手系和右手系。Unity使用的是左手系,OpenGL用的是右手系。左手系和右手系区别在于:左手系的z轴由近及远,右手系的z轴由远及近。
Unity的对象层级结构是树状的,对象与对象之间呈父子关系。当没有合适的根对象时我们可以创建空的根对象。这里指的对象都是GameObject,它实际上是一系列component的容器,装载的component实际上决定了GameObject的作用。
Unity支持三种类型的光源:点光源(point)、聚光灯(spot)、平行光(directional)。分别类似于灯泡、手电筒、阳光。point和spot可以指定光照范围,它们比directional带来更多的内存消耗。离point越近照得越亮。
创建的Cube对象会自带如下基础component:Transform,Mesh Filter(几何形状),Box Collider(处理碰撞),Mesh Renderer(负责网格的渲染)。
Capsule默认自带capsule collider,这里移除并替换成character controller,为了更好地模拟人的动作。
CharacterController是Unity官方提供的模拟人动作的组件,特点是不受力的控制,只受碰撞影响。
将camera绑定在角色身上,设置位置为(0, 0.5, 0),让其跟着角色移动。
Rotate()方法默认使用本地坐标系,若需使用全局坐标系需额外注明:Rotate(0, speed, 0, Space.World)
下面是视角随鼠标旋转的代码:
// x轴上的变化对应的是y轴上的旋转
float rotationY = transform.localEulerAngles.y + Input.GetAxis("Mouse X") * sensitivityHor;
_rotationX -= Input.GetAxis("Mouse Y") * sensitivityVert;
// 对于x轴上的旋转,应控制范围
_rotationX = Mathf.Clamp(_rotationX, minimumVert, maximumVert);
// 将新的向量赋值给欧拉角
transform.localEulerAngles = new Vector3(_rotationX, rotationY, 0);
localRotation使用的是四元数,localEulerAngles使用的是欧拉角,两者可以互相转换。四元数的优点是:避免万向节锁,提供平滑插值,表达效率高。
Rigidbody中的freezeRotation属性可以禁用物理模拟引起的转动。
下面是位置随键盘输入移动的代码:
void Start() {
// 要做碰撞必须使用CharacterController
_charController = GetComponent<CharacterController>();
}
void Update() {
float deltaX = Input.GetAxis("Horizontal") * speed;
float deltaZ = Input.GetAxis("Vertical") * speed;
Vector3 movement = new Vector3(deltaX, 0, deltaZ);
// 斜线运动时不能超过最大速度
movement = Vector3.ClampMagnitude(movement, speed);
// 避免飞起来
movement.y = gravity;
// 考虑避免不同帧数的影响
movement *= Time.deltaTime;
// 从本地坐标系转成全局坐标系(若是用Translate()方法则是用本地坐标系,但是没有碰撞)
movement = transform.TransformDirection(movement);
_charController.Move(movement);
}
可以通过RequireComponent和AddComponentMenu声明来标注component依赖关系,使得component之间能够相互调用。
子弹的轨迹是由2D坐标屏幕的camera位置射向3D坐标世界的某个点(沿视椎体方向)。
Unity中的协程不是异步的,yield之后退出,下一帧再接着执行。执行时机是在update()和lateUpdate()之间。适合用于耗时操作:如等待几秒后销毁,文件加载好后处理。
发射子弹并击中目标的代码:
void Start() {
_camera = GetComponent<Camera>();
// 鼠标锁定且隐藏,按ESC解除锁定
Cursor.lockState = CursorLockMode.Locked;
Cursor.visible = false;
}
// 在每一帧完成3D场景渲染后调用
void OnGUI() {
int size = 12;
float posX = _camera.pixelWidth/2 - size/4;
float posY = _camera.pixelHeight/2 - size/2;
// 这里用基础GUI构造了一个TextLabel
GUI.Label(new Rect(posX, posY, size, size), "*");
}
void Update() {
if (Input.GetMouseButtonDown(0)) {
// 从camera中心发出
Vector3 point = new Vector3(_camera.pixelWidth/2, _camera.pixelHeight/2, 0);
// 这个方法垂直屏幕发出射线
Ray ray = _camera.ScreenPointToRay(point);
RaycastHit hit;
if (Physics.Raycast(ray, out hit)) {
// 击中的对象
GameObject hitObject = hit.transform.gameObject;
ReactiveTarget target = hitObject.GetComponent<ReactiveTarget>();
// 若找到相应脚本,则认为是敌人,否则是普通物体
if (target != null) {
target.ReactToHit();
} else {
StartCoroutine(SphereIndicator(hit.point));
}
}
}
}
// 调用协程方法
private IEnumerator SphereIndicator(Vector3 pos) {
GameObject sphere = GameObject.CreatePrimitive(PrimitiveType.Sphere);
sphere.transform.position = pos;
// 暂停1秒再销毁(直接return, Unity会延迟1秒再执行下面的Destroy)
yield return new WaitForSeconds(1);
Destroy(sphere);
}
tweens可以让敌人被击中后倾斜的动作更加顺滑。
敌人AI代码:
void Start() {
_alive = true;
}
void Update() {
if (_alive) {
// 1. 向前移动(rotate()和translate()都默认使用本地坐标系)
transform.Translate(0, 0, speed * Time.deltaTime);
Ray ray = new Ray(transform.position, transform.forward);
RaycastHit hit;
if (Physics.SphereCast(ray, 0.75f, out hit)) {
GameObject hitObject = hit.transform.gameObject;
// 2. 若发射线击中物体了
if (hitObject.GetComponent<PlayerCharacter>()) {
// 若没有火球则产生火球
if (_fireball == null) {
_fireball = Instantiate(fireballPrefab) as GameObject;
_fireball.transform.position = transform.TransformPoint(Vector3.forward * 1.5f);
_fireball.transform.rotation = transform.rotation;
}
}
// 3. 若在一定范围内
else if (hit.distance < obstacleRange) {
// 4. 则旋转随机角度
float angle = Random.Range(-110, 110);
transform.Rotate(0, angle, 0);
}
}
}
}
prefab是可被不同场景重复使用的GameObject,它可以看做是一种asset,在场景中需要使用时动态加载,这时产生的一个copy叫做instance。关键词:动态加载,重复使用。
敌人死亡后重生的做法是:创建一个空GameObject,下面挂载一个脚本SceneController,每帧判断下当前场景中是否有enemy prefab的instance,没有则创建。
火球运动的代码:
void Update() {
transform.Translate(0, 0, speed * Time.deltaTime);
}
// 碰撞时触发(需要设置rigidBody)
void OnTriggerEnter(Collider other) {
PlayerCharacter player = other.GetComponent<PlayerCharacter>();
if (player != null) {
// 执行扣血代码
player.Hurt(damage);
}
Destroy(this.gameObject);
}
敌人和玩家子弹的处理不一样,一个是用prefab,可以看到移动过程;另一个是raycast,瞬间击中。
art asset的类型分类:material, texture, model, animation, particle system。
Unity支持的2D图形文件类型:PNG,JPG,GIF,BMP,TGA,TIFF,PICT,PSD。其中PNG是无损压缩,JPG和GIF是有损压缩。Unity中推荐使用的格式是:PNG,TGA和PSD。
将texture应用到model的方法:可以直接拖拽到model上,这时material会自动生成;也可以先创建material,再将texture和material关联。
Unity支持的3D模型格式有:FBX,Collada,OBJ,3DS,DXF,Maya,3ds Max,Blender。推荐的是FBX和Collada,它们同时支持mesh和animation。
粒子系统用于表现大量物体的运动(如火焰、烟雾、水)。
sprite是直接在屏幕上显示的2D图片,而非贴在3D模型表面(texture)。拖入2D场景时自动设为sprite类型。
SendMessage()可以向指定对象通信,效率低于直接调用指定对象的方法,但是无需知道它的类型信息。
Application.LoadLevel()可以用来重新加载场景。
展示2D图形需要用orthographic(正交)摄像机。
对于pixel-perfect的图形,摄像机的大小应该是屏幕高度一半。
sprite若要支持点击,则需要在其上加2D collider。
UI text有多种创建方法,其中一种是通过创建3D text对象。
翻牌游戏相关代码片段:
初始化所有牌的代码:
// place cards in a grid
for (int i = 0; i < gridCols; i++) {
for (int j = 0; j < gridRows; j++) {
MemoryCard card;
// use the original for the first grid space
if (i == 0 && j == 0) {
card = originalCard;
} else {
// Instantiate方法可以clone自定义类型的对象
card = Instantiate(originalCard) as MemoryCard;
}
// next card in the list for each grid space
int index = j * gridCols + i;
int id = numbers[index];
card.SetCard(id, images[id]);
float posX = (offsetX * i) + startPos.x;
float posY = -(offsetY * j) + startPos.y;
card.transform.position = new Vector3(posX, posY, startPos.z);
}
}
检查是否匹配的代码:
private IEnumerator CheckMatch() {
// increment score if the cards match
if (_firstRevealed.id == _secondRevealed.id) {
_score++;
scoreLabel.text = "Score: " + _score;
}
// otherwise turn them back over after .5s pause
else {
yield return new WaitForSeconds(.5f);
_firstRevealed.Unreveal();
_secondRevealed.Unreveal();
}
_firstRevealed = null;
_secondRevealed = null;
}
GUI系统分为immediate mode和retained mode两种:前者每帧都会重绘(在OnGUI()方法中),使用简单但功能单一;后者是更新的方法,需要使用Canvas在场景中编辑,可支持更复杂的功能。
Canvas是Unity中用来绘制UI的特殊object。创建它时会自动加入EventSystem component。
UI的anchor可设置相对屏幕角的位置,这样屏幕缩放后相对位置也不会变。
sliced image:九宫格切片,切成九块,中间缩放,其余不变。这里的弹出窗口就是sliced image。
PlayerPrefs可以用来持久化存储应用的数据。
使用event来处理UI和场景之间的交互,好处是降低耦合性。如何使用event:1. 定义event;2.A模块广播event;3.B模块监听event并做处理。
阴影分为实时阴影和光照贴图。前者效果好,但消耗计算量大;后者是由烘焙出来的纹理贴图而成。一般来说,静态物体适合用光照贴图,动态物体(如人物)适合用实时阴影。
LateUpdate()调用时机是在所有对象的Update()调用完成之后。
在悬崖和边缘处用射线来处理地面探测,真实性更好。
animation由animation clip作为基本元素组成,animation controller是状态机,定义了各状态之间的转换条件。
TPS中camera绑定的代码:
// Use this for initialization
void Start() {
_rotY = transform.eulerAngles.y;
_offset = target.position - transform.position;
}
// Update is called once per frame
// 用LateUpdate是因为要跟着人来走
void LateUpdate() {
// 无论是转视角还是水平移动,都需要绕着player旋转camera
float horInput = Input.GetAxis("Horizontal");
if (horInput != 0) {
_rotY += horInput * rotSpeed;
} else {
_rotY += Input.GetAxis("Mouse X") * rotSpeed * 3;
}
Quaternion rotation = Quaternion.Euler(0, _rotY, 0);
// 根据player的位置和旋转角度,来调整camera位置,并保持相对位置
transform.position = target.position - (rotation * _offset);
// camera转向player
transform.LookAt(target);
}
TPS中player绑定的代码:
// Update is called once per frame
void Update() {
// start with zero and add movement components progressively
Vector3 movement = Vector3.zero;
// x z movement transformed relative to target
float horInput = Input.GetAxis("Horizontal");
float vertInput = Input.GetAxis("Vertical");
if (horInput != 0 || vertInput != 0) {
movement.x = horInput * moveSpeed;
movement.z = vertInput * moveSpeed;
movement = Vector3.ClampMagnitude(movement, moveSpeed);
Quaternion tmp = target.rotation;
target.eulerAngles = new Vector3(0, target.eulerAngles.y, 0);
// 将基于camera的移动转成global的移动
movement = target.TransformDirection(movement);
target.rotation = tmp;
// face movement direction
//transform.rotation = Quaternion.LookRotation(movement);
// 往移动方向转向
Quaternion direction = Quaternion.LookRotation(movement);
transform.rotation = Quaternion.Lerp(transform.rotation,
direction, rotSpeed * Time.deltaTime);
}
// 设置speed属性,触发进入动画的walk状态
_animator.SetFloat("Speed", movement.sqrMagnitude);
// raycast down to address steep slopes and dropoff edge
bool hitGround = false;
RaycastHit hit;
// 利用射线更精确地做地面接触检测
if (_vertSpeed < 0 && Physics.Raycast(transform.position, Vector3.down, out hit)) {
float check = (_charController.height + _charController.radius) / 1.9f;
hitGround = hit.distance <= check; // to be sure check slightly beyond bottom of capsule
}
// y movement: possibly jump impulse up, always accel down
// could _charController.isGrounded instead, but then cannot workaround dropoff edge
// 若射线检测在地面
if (hitGround) {
if (Input.GetButtonDown("Jump")) {
_vertSpeed = jumpSpeed;
} else {
_vertSpeed = minFall;
// 落到地面后停止jump动作
_animator.SetBool("Jumping", false);
}
} else {
_vertSpeed += gravity * 5 * Time.deltaTime;
if (_vertSpeed < terminalVelocity) {
_vertSpeed = terminalVelocity;
}
// 实际没有碰撞
if (_contact != null ) { // not right at level start
_animator.SetBool("Jumping", true);
}
// workaround for standing on dropoff edge
// 实际在地面上(悬崖边)
if (_charController.isGrounded) {
if (Vector3.Dot(movement, _contact.normal) < 0) {
movement = _contact.normal * moveSpeed;
} else {
movement += _contact.normal * moveSpeed;
}
}
}
movement.y = _vertSpeed;
movement *= Time.deltaTime;
_charController.Move(movement);
}
// store collision to use in Update
void OnControllerColliderHit(ControllerColliderHit hit) {
_contact = hit;
}
trigger可以定义触发操作,它不是真实的物体,可以被穿透。
Awake()比Start()更早执行,它可以做一些初始化操作。
Resources.Load()可以从Resources目录加载一些assets。
人和场景中物体交互的一些代码:
Player上面绑定的代码:
void Update() {
// 若按下command键
if (Input.GetButtonDown("Fire3")) {
Collider[] hitColliders = Physics.OverlapSphere(transform.position, radius);
foreach (Collider hitCollider in hitColliders) {
Vector3 direction = hitCollider.transform.position - transform.position;
if (Vector3.Dot(transform.forward, direction) > .5f) {
// 则向周围一定半径的物体发送Operate消息
hitCollider.SendMessage("Operate", SendMessageOptions.DontRequireReceiver);
}
}
}
}
门的trigger上绑定的代码:
void OnTriggerEnter(Collider other) {
if (requireKey && Managers.Inventory.equippedItem != "key") {
return;
}
foreach (GameObject target in targets) {
// 碰撞后,对门发送Activate消息
target.SendMessage("Activate");
}
}
void OnTriggerExit(Collider other) {
foreach (GameObject target in targets) {
target.SendMessage("Deactivate");
}
}
门上绑定的代码:
// 无论是通过按键,还是碰撞,都可以实现门的开闭
public void Operate() {
if (_open) {
Vector3 pos = transform.position - dPos;
transform.position = pos;
} else {
Vector3 pos = transform.position + dPos;
transform.position = pos;
}
_open = !_open;
}
public void Activate() {
if (!_open) {
Vector3 pos = transform.position + dPos;
transform.position = pos;
_open = true;
}
}
public void Deactivate() {
if (_open) {
Vector3 pos = transform.position - dPos;
transform.position = pos;
_open = false;
}
}
场景中散落的道具上绑定的代码:
void OnTriggerEnter(Collider other) {
// 碰撞之后ui上增加,并将自己销毁
Managers.Inventory.AddItem(itemName);
Destroy(this.gameObject);
}
UI相关的代码:
// 实时绘制UI
void OnGUI() {
int posX = 10;
int posY = 10;
int width = 100;
int height = 30;
int buffer = 10;
List<string> itemList = Managers.Inventory.GetItemList();
if (itemList.Count == 0) {
GUI.Box(new Rect(posX, posY, width, height), "No Items");
}
foreach (string item in itemList) {
int count = Managers.Inventory.GetItemCount(item);
Texture2D image = Resources.Load<Texture2D>("Icons/"+item);
GUI.Box(new Rect(posX, posY, width, height), new GUIContent("(" + count + ")", image));
posX += width+buffer;
}
string equipped = Managers.Inventory.equippedItem;
if (equipped != null) {
posX = Screen.width - (width+buffer);
Texture2D image = Resources.Load("Icons/"+equipped) as Texture2D;
GUI.Box(new Rect(posX, posY, width, height), new GUIContent("Equipped", image));
}
posX = 10;
posY += height+buffer;
foreach (string item in itemList) {
// 创建button同时判断button是否被按下
if (GUI.Button(new Rect(posX, posY, width, height), "Equip "+item)) {
Managers.Inventory.EquipItem(item);
}
if (item == "health") {
if (GUI.Button(new Rect(posX, posY + height+buffer, width, height), "Use Health")) {
Managers.Inventory.ConsumeItem("health");
Managers.Player.ChangeHealth(25);
}
}
posX += width+buffer;
}
}
Trigger和Collider的区别:Collider是物理碰撞,isTrigger是Collider上的一个属性,勾选后只检测碰撞,但没有物理效果。