游戏截图
教学视频地址: https://www.bilibili.com/video/av21926233/index_1.html
场地建立
- 将地表以及玩家的资源文件拖动到Hierarchy窗口,并调整其大小。(每次添加新GameObj重置坐标到原点)
- 选择对应的Layer层,从低到高依次渲染,所以最高的Layer将覆盖低的显示。
- 相机调整,增加视野,2D游戏默认使用正交相机。
- 初步的对象建立完毕。
控制玩家
- 所有物体移动都是通过物理系统控制的,所以需要添加刚体通过外力控制玩家移动。
- 玩家游戏对象下通过C#脚本对物理添加外力,每个脚本初始都会有一些默认函数:
Update
:在渲染一帧前被调用,所以一般存放Unity需要每帧执行代码的容器 ;FixedUpdate
:执行任何物理计算前被调用,所以一般存放物理操作代码;
- 查询API通过Input获取操作杆的输入,也就是键盘控制上下左右,然后对刚体使用AddForce增加外力。
- 获取刚体对象
GetComponent<Rigidbody2D>();
- 此时会发现移动的很慢,可以在脚本中添加公共变量speed来控制,此时会发现添加的公共变量会出现在Inspector中,可以直接改变数值生效,不用改动脚本代码了。(感觉很厉害)
- 获取刚体对象
- 调整到玩家正常移动。
加入碰撞
为了防止玩家出界,所以需要添加边界碰撞。
玩家以及地面添加Collider碰撞器组件。
碰撞器是一个形状或者容器,供物理引擎判断碰撞事件,一个游戏对象可以绑定多个碰撞器。
物理碰撞至少一方须要有Rigidbody刚体组件,玩家已经有,所以地面不需要添加。
因为玩家是球形,所以添加一个圆形的Collider,调整大小到刚好合适玩家。
地面是正方形所以添加Box Collider,如下图:
此时运行时发现玩家直接飞出去了,为什么了?因为两个对象占据了同样的空间,玩家在地面的碰撞器盒子里面,物理引擎就会直接给所有相关的Rigidbody施力,所有玩家(有Rigidbody)被弹开飞出去了。
所有不应该直接放一个大的碰撞体,而是应该换成一系列小的碰撞器来代表墙体:
PS:虽然我们添加的碰撞器有重叠,但是我们没有Rigidbody组件,所以不会被弹开
测试可以正常的移动并且不会跑出地图边界。
相机跟随玩家移动
- 绑定相机和玩家,直接拖动相机到玩家的作为玩家的子对象,此时相机就会跟随玩家。
- 测试确实是跟随玩家移动,但是但玩家碰撞到墙壁发生旋转时,此时相机也会旋转,但这是我们不想要的结果也是不合理的,所以相机不能作为玩家子对象。
- 所以我们只能在相机上添加脚本,通过脚本控制。
- Start计算相机和玩家的位置偏移:相机位置-玩家位置,记录下来。
- LateUpdate(也是每帧运行,不过是运行Update之后才执行)更新相机的位置为:玩家位置+偏移。
- 测试移动玩家相机跟随移动。
创建收集物
- 同理拖动收集物到Hierarchy中,并重置坐标到原点。
- 为了能与玩家碰撞并且搜集,可以添加圆形Collider碰撞器组件。
- 添加一些动态变化吸引玩家注意达到提醒效果,比如收集物的旋转:
transform.Rotate(new Vector3(0, 0, 45) * Time.deltaTime);
Rotate
:根据一个三维的欧拉角对应x,y,z轴进行旋转。Time.deltaTime
:一帧对应的时间,所以乘上整体可以看做一秒按z轴旋转45度。
- 此时一个收集物创建完成,但是我们需要很多收集物,难道每个都要这样做么,这时候就需要Prefab(预制):
- 预制:一个包含模板的资源或者游戏对象组,可以通过现有的游戏对象或者游戏对象组来创建预制。
- 预制相当于克隆,一个预制对象的改变所有对象都会改变。
- 创建好预制对象后,直接复制对象即可生成很多收集物,改变每个的位置即可。
拾起收集物
想要玩家碰撞之后拾起收集物,就需要确定玩家何时碰撞到收集物,所以查阅Collider的API文档,知道使用OnTriggerEnter2D:当另一个物体进入与这个物体相连的触发碰撞器(只有2D物理)时发送 。
因为地面不会被搜集,所以需要判断是不是收集物,然后才收集隐藏。判断通过tag比较:
if (collision.gameObject.CompareTag("PickUp")) collision.gameObject.SetActive(false);
完成后进行运行测试发现玩家与收集物接触时被弹开了,原因上面也提到过物理引擎在计算静态与动态对象发送碰撞时,会将动态对象(玩家)弹开。
静态对象:任何带有2D碰撞器以及没有Rigidbody2D的游戏对象 ;
动态对象:任何带有2D碰撞器和Rigidbody2D的游戏对象 ;
物理引擎允许穿透或者重叠的2D碰撞器区域。这时的2D引擎仍然会计算重叠区域并且会追踪碰撞器的重叠,但是不会有物理行为发生在重叠区域中也就是不会造成碰撞。
实现方式:将收集物的2D碰撞器变为触发器
此时可以正常拾起收集物,但是有一个2D物理引擎的优化问题:当我们使用2D物理时,所有静态或者不移动的碰撞器都被计算成一个单体,这能提高性能。如果我们移动了一个引擎认为不会动的碰撞器,那么静态碰撞器或者所有的不移动的对象都将被重新计算,如果我们每一帧都这样做,游戏将会有损失性能的风险。
这里我们旋转了收集物(移动了静态对象),所有每一帧都会重新计算。所以我们需要将收集物转为动态对象,也就是添加Rigidbody(注意添加时去掉重力)。
此时完成了收集物的拾起。
积分面板
通过增加UI的Text控件,显示当前分数,然后调整到自己喜爱的位置。
脚本中用一个变量来记录当前分数(因为不可人为改变,所以设为私有变量),每次收集金币时计数加1,并显示当前得分,当全部收集完毕显示你赢了!.
count += 1; countText.text = "数量:" + count.ToString(); if (count >= 9) WinText.text = "你赢了!";
此时一个完整的UFO收集金币游戏完成了。
完整的C#脚步
玩家控制器脚本
using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.UI; public class PlayerController : MonoBehaviour { private Rigidbody2D rb2d; private int count = 0; public float speed; public Text countText, WinText; // Use this for initialization void Start() { rb2d = GetComponent<Rigidbody2D>(); WinText.text = ""; } private void FixedUpdate() { float moveHorizontal = Input.GetAxis("Horizontal"); float moveVertical = Input.GetAxis("Vertical"); Vector2 movement = new Vector2(moveHorizontal, moveVertical); rb2d.AddForce(movement * speed); } private void OnTriggerEnter2D(Collider2D collision) { if (collision.gameObject.CompareTag("PickUp")) { collision.gameObject.SetActive(false); SetCount(); } } void SetCount() { count += 1; countText.text = "数量:" + count.ToString(); if (count >= 9) WinText.text = "你赢了!"; } }
控制相机跟随玩家移动脚本
using System.Collections; using System.Collections.Generic; using UnityEngine; public class CameraController : MonoBehaviour { private Vector3 offset; public GameObject Player; // Use this for initialization void Start () { offset = transform.position - Player.transform.position; } // Update is called once per frame void LateUpdate () { transform.position = offset + Player.transform.position; } }
控制金币旋转脚本
using System.Collections; using System.Collections.Generic; using UnityEngine; public class Pickup : MonoBehaviour { // 旋转 void Update () { transform.Rotate(new Vector3(0, 0, 45) * Time.deltaTime); } }