3D 游戏实战开发 | 青训营笔记
这是我参与「第五届青训营」伴学笔记创作活动的第 17 天
实例项目:太空飞船大战
0x1 3D 实体搭建
-
3D 实体
3D 游戏是由一个个具有形状的实体组成的,每个实体在空间存在于特定的位置,有特定的旋转角度
-
3D 实体的位姿态
- 位置
Position(x, y, z)
- 旋转
Rotation(x, y, z)
- 缩放
Scale(x, y, z)
在 Unity 中,绝大部分情况下,是先缩放,再旋转,后平移
- 位置
-
3D 实体的创建
- 通过加载 3D 模型创建,如 fbx、gltf、obj
- 通过组合参数化的基本几何体创建
-
3D 实体的绘制
- 材质
- 颜色
- 纹理
-
预制体
- 预制体(Prefab)就是将游戏对象保存在工程中,在需要的时候创建出来
- 预制体存储着一个游戏对象,包括游戏对象的所有组件以及其下的所有子游戏对象
0x2 相机、光照、天空盒
-
相机
- Clear Flag
- 背景颜色
- Culling Mask
- 投影
- 透视投影(Perspective):具有近大远小的投影特点,如 写实类游戏
- 正交投影(Orthographic):不具备景深上近大远小的差别,如 策略类游戏
-
光照
- 光照类型
- 点光源
- 平行光
- 聚光灯
- 面积光
- 颜色
- 强度
- 阴影类型
- 光照类型
-
天空盒
-
相机的清除标识为“天空盒”
-
窗口 - 渲染 - 照明设置
-
环境 - 天空盒材质
-
0x3 控制与碰撞
-
实时游戏时序
-
时序
-
-
1 帧初始化物理输入游戏逻辑渲染停顿销毁
-
为主角飞船添加控制逻辑
-
添加刚体组件
- Add Component > Physics > Rigidbody
- Use Gravity 设置为 false,忽略重力的影响
- isKinematic 设置为 true,飞船通过脚本而非力影响运动属性
- 设置 Constraints,冻结 Z 轴位移以及 XYZ 轴旋转
-
添加自定义脚本
-
Add Component > New Script
-
MonoBehaviour 是一个基类,所有 Unity 脚本都派生自该类
Start()
:在首次调用任何 Update 方法之前在帧上调用 StartUpdate()
:每帧调用 UpdateFixedUpdate()
:用于物理计算且独立于帧率LateUpdate()
:在每一次调用 Update 函数后调用OnGUI()
:渲染和处理 GUI 事件OnDisable()
:在对象被禁用时调用,对象销毁时也会销毁该函数OnEnable()
:在对象变为启用和激活状态时调用using System.Collections; using System.Collections.Generic; using UnityEngine; public class Hero : MonoBehaviour { static public Hero S; // 单例对象 [Header("Set in Inspector")] public float speed = 30; // 控制飞船的运动 public float rollMult = -45; public float pitchMult = 30; [Header("Set Dynamically")] public float shieldLevel = 1; void Start() { if(S == null) S = this; // 设置单例对象 else Debug.LogError("尝试重复设置 Hero 实例"); } void Update() { // 获取输入的信息 float xAxis = Input.GetAxis("Horizontal"); float yAxis = Input.GetAxis("Vertical"); // 修改 transform.position Vector3 pos = transform.position; pos.x += xAxis * speed * Time.deltaTime; pos.y += yAxis * speed * Time.deltaTime; transform.position = pos; // 发生位移时旋转一个角度 transform.rotation = Quaternion.Euler(yAxis * pitchMult, xAxis * rollMult, 0); } }
-
-
-
添加敌机
-
为每架敌机预制体添加一个刚体
- 选中敌机预制体,在菜单栏执行 Component > Physics > Rigidbody
- 在新添加的刚体组件中,将 Use Gravity 设置为 false
- 将 isKinematic 设置为 true
- 打开 Constraints 旁边的三角形展开图标,冻结 Z 轴的坐标和 XYZ 轴的旋转
-
建立敌机的脚本 Enemy.ts
using System.Collections; using System.Collections.Generic; using UnityEngine; public class Enemy : MonoBehaviour { [Header("Set in Inspector: Enemy")] public float speed = 10f; // 运动速度,米每秒 public float fireRate = 0.3f; // 发射频率 public float health = 10; // 生命值 public float score = 100; // 得分 public Vector3 pos { get { return(this.transform.position); } set { this.transform.position = value; } } void Update() { Move(); } public virtual void Move() { Vector3 tempPos = pos; tempPos.y -= speed * Time.deltaTime; pos = tempPos; } }
-
为每架敌机预制体添加脚本 Enemy.ts
-
-
随机生成敌机
新建一个名为 Main 的 C# 脚本,绑定到 Main Camera 上
using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.SceneManagement; // 用于加载和重载场景 public class Main : MonoBehaviour { private float camWidth; // 游戏界面呈现的相机宽度 private float camHeight; // 游戏界面呈现的相机高度 static public Main S; // 设置 Main 单例 [Header("set in Inpector")] public GameObject[] prefabEnemies; // Enemy 预设数组 public float enemySpawnPerSecond = 0.5f; // 每秒产生敌机数 public float enemySpawnPadding = 1.5f; // 填充敌机举例地图左右边界的位置 void Start() { S = this; camHeight = Camera.main.orthographicSize; // 只有正交投影时有效 camWidth = camHeight * Camera.main.aspect; Invoke("SpawnEnemy", 1f/enemySpawnPerSecond); } void Update() { // 随机选取一架敌机预设并实例化 int ndx = Random.Range(0, prefabEnemies.Length); GameObject go = Instantiate<GameObject>(prefabEnemies(ndx)); // 使用随机生成的 x 坐标,将敌机置于屏幕上方 Vector3 pos = Vector3.zero; float xMax = camWidth - enemySpawnPadding; float xMin = -camWidth + enemySpawnPadding; pos.x = Random.Range(xMin, xMax); pos.y = camHeight + enemySpawnPadding; go.transform.position = pos; Invoke("SpawnEnemy", 1f/enemySpawnPerSecond); } }
-
-
输入管理器和
Input.GetAxis()
- 输入管理器(InputManager)是 Unity 设置输入响应方式的管理列表 Edit > Project > Setting > Input
Input.GetAxis()
:从 InputManager 中获取值,其中 GetAxis 方法用于获取坐标值
-
设置标签、图层和物理规则
游戏中存在不同类型的游戏对象,它们需要防止在不同的图层中,并与其他游戏对象发生不同的交互
-
主角飞船会与敌机、敌机炮弹、升级道具等碰撞,但不会与主角飞船的炮弹碰撞
-
主角飞船的炮弹会与敌机、敌机炮弹等碰撞
-
敌机
-
敌机的炮弹
-
升级道具
-
标签和图层管理器
- Edit > Project Setting > Tags and Layers
- 打开 Tags 左侧的三角形展开按钮,在点击 + 号后输入标签名称
- 点击 Layers 旁边的三角形展开按钮,从User Layer 8 开始依次输入图层名称
-
物理管理器
Edit > Project Setting > Physics
-
为游戏对象指定合适的图层
- 在层级面板中选择 _Hero,在检视面板中从 Layer 下拉菜单中选择 Hero 选项
- 在检视面板中,在 Tags 下拉菜单中选择 Hero 选项,为 _Hero 设置标签
- 从项目面板中,选择所有敌机预设并设置图层为 Enemy
-
-
碰撞
-
敌机碰撞主角飞船
将主角飞船与敌机飞船及其子组件上已有的碰撞盒去掉,每个对象的母体上 Add Component > Sphere Collider
-
添加碰撞代码
在 Hero 类中添加代码:
void OnTriggerEnter(Collider other) { // print("触发碰撞事件:" + other.gameObject.name); if(other.tag == "Enemy") Destory(this.gameObject); }
-
0x4 玩法逻辑与 UI
-
主角飞船增加射击功能
- 新建预制体,命名为 ProjectileHero,模型为立方体,Position 与 Rotation 均为 [0, 0, 0],Scale 为 [0.25, 1, 0.5],保留默认的 Box Collider 并设置其 Size.z 为 10
- 新建材质,命名为 Mat_Projectile,将着色器指定为 ProtoTools > UnlitAlpha,并将新材质运用到 ProjectileHero 上
- 为 ProjectileHero 游戏对象添加一个新的刚体组件:
- Use Gravity:false
- isKinematic:false
- Collision Detection:Continuous
- Constraints 冻结 Z 轴与 XYZ 轴旋转
- Tag 和 Layer 设置为 ProjectileHero
-
实例化炮弹
// Hero.ts public GameObject projectilePrefab; public float projectileSpeed = 40; void Update() { // 按下空格键开火 if(Input.GetKeyDown(KeyCode.Space)) TempFire(); } void TempFire() { GameObject projGO = Instantiate<GameObject>(projectilePrefab); projGO.transform.position = transform.position; Rigidbody rigidB = projGO.GetComponent<Rigidbody>(); rigidB.velocity = Vector3.up * projectileSpeed; }
-
为炮弹添加碰撞事件
public class ProjectileHero: MonoBehaviour { private float camHeight; void Start() { this.camHeight = Camera.main.orthographicSize; } void Update() { // 飞出屏幕的炮弹自动销毁 if(transform.position.y > this.camHeight) Destroy(this.gameObject); } void OnCollisionEnter(Collision other) { if(other.gameObject.tag == 'Enemy') { Destroy(other.gameObject); Destroy(this.gameObject); } } }
-
设置计分板
- GameObject > UI > Text
- Canvas:匹配游戏面板尺寸的画布
- EventSystem:用于运转按钮等交互元素
- 选择 Text 对象,修改名称为 Score
- 设置 Text 对象
- GameObject > UI > Text
-
得分机制
每次消灭敌机加 50 分
using UnityEngine.UI; using TMPro; public class Hero : MonoBehaviour { static public Hero S; public TMP_Text scoreGT; public int score; } //======================\\ public class ProjectileHero : MonoBehaviour { private float camHeight; void OnCollisionEnter(Collision other) { if(other.gameObject.tag == 'Enemy') { Hero.S.score += 50; Hero.S.scoreGT.text = "Score: " + Hero.S.score; } } }
-
游戏重新开始
主角飞船被消灭 2 秒后重新开始游戏
using UnityEngine.SceneManagement; public class Main : MonoBehaviour { public void DelayedRestart(float delay) { Invoke("Restart", delay); } public void Restart() { SceneManager.LoadScene("SimpleScene"); } } //======================\\ public class Hero : MonoBehaviour { static public Hero S; void OnTriggerEnter(Collider other) { if(other.tag == "Enemy") { Main.S.DelayedRestart(2f); } } }