3D 游戏实战开发 | 青训营笔记

87 阅读5分钟

这是我参与[第五届青训营]笔记创作活动的第十八天

本课堂重点内容:

  1. 3D 实体搭建
  2. 相机,光照,天空盒
  3. 控制与碰撞
  4. 玩法逻辑与 UI

详细知识点介绍:

3D 实体搭建:

3D 实体

3D游戏是由一个个具有形状的实体组成的。每个实体在空间中存在于特定的位置,有特定的姿态(旋转角度)

3D 实体的位姿态:

  1. 位置 Position(x,y,z),是一个三维向量坐标
  2. 旋转 Rotation(x,y,z),是一个三维向量坐标
  3. 缩放 Scale(x,y,z),是一个三维向量坐标
    在 Unity 中,绝大部分情况下,是先缩放,后旋转,最后平移

3D 实体的创建:

  1. 通过加载 3D 模型创建,如 fbx,obj,gltf
  2. 通过组合参数化的基本几何体创建

3D 实体的绘制:

  1. 材质
  2. 颜色
  3. 纹理

预制体:

  • 将游戏对象保存在工程中,在需要的时候创建出来,这就是预制体
  • 预制体存储着一个游戏对象,包括游戏对象的所有组件以及其下的所有子游戏对象

相机,光照,天空盒

相机

  1. Clear Flag
  2. 背景颜色
  3. Culling Mask
  4. 投影(透视、正交)

光照

  1. 常用类型:点光源、平行光
  2. 颜色
  3. 强度
  4. 阴影类型

天空盒

  1. 相机的清除标志设置为“天空盒”
  2. 窗口 - 渲染 - 照明设置
  3. 环境 - 天空盒材质

控制与碰撞

为主角飞船添加控制逻辑

  1. 添加刚体组件
  • Add Component > Physics > Rigidbody
  • Use Gravity 设置为 false,忽略重力影响
  • isKinematic 设置为 true,飞船通过脚本而非力影响运动属性
  • 设置 Constraints, 冻结Z轴位移以及 X、Y、Z 轴旋转
  1. 添加自定义脚本
  • Add Component > New Script

MonoBehaviour

MonoBehaviour 是一个基类,所有的 Unity 脚本都派生自该类

  • Start() 在首次调用任何 Update 方法之前在帧上调用 Start
  • Update() 每帧调用 Update
  • FixedUpdate() 用于物理计算且独立于帧率。具有物理系统的帧率;每个固定帧率帧调用该函数
  • LateUpdate() 在每一次调用 Update 函数后调用
  • OnGUI() 系统调用 OnGUI 来渲染和处理 GUI 事件。它实现可以每帧调用多次(每个事件调用一次)
  • OnDisable() 该函数在对象被禁用时调用。对象销毁时也会调用该函数
  • OnEnable() 该函数在对象变为启用和激活状态时调用

Input.GetAxis() 和输入管理器(InputManager)

  • InputManager 是 Unity 设置输入响应方式的管理列表,位置在 Edit > Project Setting > Input 中

添加敌机

  • 为每架敌机预制体添加一个刚体
  • 建立敌机的脚本 Enemey.cs
  • 为每架敌机预制体均添加脚本 Enemey.cs

随机生成敌机

新建一个名为 Main的 C#脚本,绑定到 Main Camera 上

设置标签、图层和物理规则

  • 主角(Hero):会与敌机、敌机炮弹、升级道具碰撞,但不会与主角自身的炮弹碰撞
  • 主角炮弹(ProjectileHero):与敌机、敌机炮弹碰撞
  • 敌机(Enemy):与主角和主角炮弹碰撞
  • 敌机炮弹(ProjectileEnemy):只与主角飞船碰撞
  • 升级道具(PowerUp):只与主角飞船碰撞

实践练习例子:

主角飞船 Hero.cs

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;
    // Start is called before the first frame update
    void Start()
    {
        if(S==null){
            S = this; // 设置单例对象
        } else {
            Debug.LogError("尝试重复设置 Hero 实例")
        }
    }
    // Update is called once per frame
    void Update()
    {
        // 从 input(用户输入)类中获取信息
        float xAxis = Input.GetAxis("Horizontal");
        float yAxis = Input.GetAxis("Vertical");
        // 基于获取的水平轴和竖直轴信息,修改 transofm.position
        Vector3 pos = transofm.position;
        pos.x += xAxis * speed * Time.deltaTime;
        pos.y += yAxis * speed * Time.deltaTime;
        transofm.position = pos;
        // 位置变化时让飞船旋转一个角度,让飞船更有动感
        transofm.rotation = Quaternion.Euler(yAxis*pitchMult, xAxis*rollMult,0);
        }
    }

敌机 Enemey.cs

using System.Collections; 
using System.Collections.Generic; 
using UnityEngine; 
public class Hero : MonoBehaviour 
{
  [Header("Set in Inspector: Enemy")] 
  public float speed = 10f; // 运动速度 单位为 m/s
  public float fireRate = 0.3f; // 发射频率
  public float health = 10;
  public float score = 100; // 击毁敌机获得的分数
  // pos 是一个属性,即行为表现与字段相似的方法
  public Vector3 pos {
    get {
        return(this.transofm.position);
    }
    set {
        this.transofm.position = value;
    }
  }
  // Update is called once per frame
  Void Update() 
  {
    Move();
  }
  public virtual void Move() {
    Vector3 tempPos = pos;
    tempPos.y -= speed * Time.deltaTime;
    pos = tempPos;
  }
 }

随机生成 Main

using System.Collections; 
using System.Collections.Generic; 
using UnityEngine; 
using UnityEngine.SceneManagement; // 用于加载和重载场景
public class Hero : MonoBehaviour 
{
  private float camWidth; // 游戏界面呈现的相机宽度
  private float camHeight; // 游戏界面呈现的相机高度
  static public Main S; // Main 单例
  [Header("Set in Inspector")]
  public GameObject[] prefabEnemies; // Enemy 预设数组
  public float enemySpawnPerSecond = 0.5f; // 每秒产生的敌机数量
  public float enemySpawnPadding = 1.5f; // 填充敌机距离地图左右边界的位置
  // Start is called before the first frame update
  void Start()
  {
    S = this;
    camHeight = Camera.main.orthographicSize; // 只有正交投影下有效
    camWidth = camHeight * Camera.main.aspect;
    // 调用一次 SpawnEnemy() 默认值是每两秒生成一个
    Invoke("SpawnEnemy", 1f/enemySpawnPerSecond);
  }
  public void SpawnEnemy() {
    // 随机选取一架敌机预设,实例化
    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;
    // 再次调用 SpawnEnemy()
    Invoke("SpawnEnemy", 1f/enemySpawnPerSecond);
  }

个人课后总结:

  • 理解实时游戏 update 的时序控制
  • 通过Input.GetAxis()与InputManager 监听玩家输入
  • Instantiate() 动态生成游戏实体实例
  • Camera.main 获得相机参数,设置场景中的物体在画面中的位置
  • 利用标签、图层管理器区分实体种类并设置物理规则
  • 通过 Collider 组件与 OnTrigger 函数添加碰撞事件
  • 通过图形用户界面管理游戏界面