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

这是我参与「第五届青训营」伴学笔记创作活动的第 17 天

实例项目:太空飞船大战

0x1 3D 实体搭建

  1. 3D 实体

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

  2. 3D 实体的位姿态

    • 位置Position(x, y, z)
    • 旋转Rotation(x, y, z)
    • 缩放Scale(x, y, z)

    在 Unity 中,绝大部分情况下,是先缩放,再旋转,后平移

  3. 3D 实体的创建

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

    1. 材质
    2. 颜色
    3. 纹理
  5. 预制体

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

0x2 相机、光照、天空盒

  1. 相机

    1. Clear Flag
    2. 背景颜色
    3. Culling Mask
    4. 投影
      1. 透视投影(Perspective):具有近大远小的投影特点,如 写实类游戏
      2. 正交投影(Orthographic):不具备景深上近大远小的差别,如 策略类游戏
  2. 光照

    1. 光照类型
      1. 点光源
      2. 平行光
      3. 聚光灯
      4. 面积光
    2. 颜色
    3. 强度
    4. 阴影类型
  3. 天空盒

    1. 相机的清除标识为“天空盒”

    2. 窗口 - 渲染 - 照明设置

    3. 环境 - 天空盒材质

    更多天空盒

0x3 控制与碰撞

    1. 实时游戏时序

      1. 时序

         
    1.  
       
       
       
       
       
      1 帧
      初始化
      物理
      输入
      游戏逻辑
      渲染
      停顿
      销毁
    2. 为主角飞船添加控制逻辑

      1. 添加刚体组件

        • Add Component > Physics > Rigidbody
        • Use Gravity 设置为 false,忽略重力的影响
        • isKinematic 设置为 true,飞船通过脚本而非力影响运动属性
        • 设置 Constraints,冻结 Z 轴位移以及 XYZ 轴旋转
      2. 添加自定义脚本

        • Add Component > New Script

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

          Start():在首次调用任何 Update 方法之前在帧上调用 Start

          Update():每帧调用 Update

          FixedUpdate():用于物理计算且独立于帧率

          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);
              }
          }
          
    3. 添加敌机

      • 为每架敌机预制体添加一个刚体

        • 选中敌机预制体,在菜单栏执行 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

    4. 随机生成敌机

      新建一个名为 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);
          }
      }
      
  1. 输入管理器和Input.GetAxis()

    1. 输入管理器(InputManager)是 Unity 设置输入响应方式的管理列表 Edit > Project > Setting > Input
    2. Input.GetAxis():从 InputManager 中获取值,其中 GetAxis 方法用于获取坐标值
  2. 设置标签、图层和物理规则

    游戏中存在不同类型的游戏对象,它们需要防止在不同的图层中,并与其他游戏对象发生不同的交互

    • 主角飞船会与敌机、敌机炮弹、升级道具等碰撞,但不会与主角飞船的炮弹碰撞

    • 主角飞船的炮弹会与敌机、敌机炮弹等碰撞

    • 敌机

    • 敌机的炮弹

    • 升级道具

    1. 标签和图层管理器

      1. Edit > Project Setting > Tags and Layers
      2. 打开 Tags 左侧的三角形展开按钮,在点击 + 号后输入标签名称
      3. 点击 Layers 旁边的三角形展开按钮,从User Layer 8 开始依次输入图层名称
    2. 物理管理器

      Edit > Project Setting > Physics

    3. 为游戏对象指定合适的图层

      1. 在层级面板中选择 _Hero,在检视面板中从 Layer 下拉菜单中选择 Hero 选项
      2. 在检视面板中,在 Tags 下拉菜单中选择 Hero 选项,为 _Hero 设置标签
      3. 从项目面板中,选择所有敌机预设并设置图层为 Enemy
  3. 碰撞

    1. 敌机碰撞主角飞船

      将主角飞船与敌机飞船及其子组件上已有的碰撞盒去掉,每个对象的母体上 Add Component > Sphere Collider

    2. 添加碰撞代码

      在 Hero 类中添加代码:

      void OnTriggerEnter(Collider other)
      {
          // print("触发碰撞事件:" + other.gameObject.name);
          if(other.tag == "Enemy")
              Destory(this.gameObject);
      }
      

0x4 玩法逻辑与 UI

  1. 主角飞船增加射击功能

    • 新建预制体,命名为 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
  2. 实例化炮弹

    // 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;
    }
    
  3. 为炮弹添加碰撞事件

    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);
            }
        }
    }
    
  4. 设置计分板

    • GameObject > UI > Text
      • Canvas:匹配游戏面板尺寸的画布
      • EventSystem:用于运转按钮等交互元素
    • 选择 Text 对象,修改名称为 Score
    • 设置 Text 对象
  5. 得分机制

    每次消灭敌机加 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;
            }
        }
    }
    
  6. 游戏重新开始

    主角飞船被消灭 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);
            }
        }
    }
    
posted @ 2023-02-15 20:12  SRIGT  阅读(6)  评论(0编辑  收藏  举报  来源