Skip to content

Unity

基本

概念

  • 场景由许多游戏对象GameObject(如摄像机、球体)构成;一个游戏对象由许多组件构成(如transform移动组件、mesh renderer渲染组件)。
  • 可以直接在左侧菜单拖拽一个节点到另一个作为联动子节点

场景

UGUI

  • 基本组成
  • Canvas:所有的 UI 元素都需要放在 Canvas 下。Canvas 可以是屏幕空间、世界空间或摄像机空间。
  • Rect Transform:替代了传统的 Transform,用于 2D 界面定位。
  • UI 组件:例如 Button、Text、Image、Slider 等。
  • 事件系统:UGUI 有自己的事件系统,用于处理如点击、拖放、滚动等交互。
  • Raycasting:用于确定用户点击了哪个 UI 元素。
  • 布局组件:如 Grid Layout Group 和 Vertical Layout Group,它们可以自动安排其子对象。

布局

  1. Vertical Layout GroupHorizontal Layout Group: 这两个组件会自动将其子元素排列成垂直或水平的线性序列。
  2. Grid Layout Group: 将其子元素组织成一个网格。你可以定义格子的大小、间距等。
  3. Layout Element: 允许你为单个元素指定布局属性,例如最小、最大和首选尺寸。
  4. Content Size Fitter: 调整其矩形大小以适应其内容。
  5. Aspect Ratio Fitter: 使 GameObject 保持特定的宽高比。

事件系统

  • 事件绑定,如按钮
    public Button myButton;
    
    void Start()
    {
        myButton.onClick.AddListener(OnClick);
    }
    
    void OnClick()
    {
        Debug.Log("Button was clicked!");
    }
    

UI组件

脚本c

对象

  • 获取对象
  • 通过组件获取组件.gameObject
  • 获取被绑定的对象this.gameObject
  • 通过名称获取:GameObject myObject = GameObject.Find("ObjectName");
  • 通过标签获取:GameObject myObject = GameObject.Find("ObjectName");
    • 也可以获取全部:GameObject[] objectsWithTags = GameObject.FindGameObjectsWithTag("TagName");
  • 图形界面拖拽:

    • 在你的脚本中定义一个公共(public)变量,这样它就会在图形面板中出现。如public GameObject targetObject;
    • 将这个脚本拖拽到一个GameObject上,或者直接在GameObject上添加这个脚本作为组件。
    • 直接从窗口中直接拖拽一个GameObject到相应属性上,即可完成绑定。
  • 通过对象可以直接获得属性Transform objTransform = myObject.transform;

  • 激活对象
  • gameObject.SetActive(false);
  • 获取状态
    • activeSelf直接返回对象的激活状态
    • activelnHierarchy检查是否是真的活跃(如果任何父对象被停用,则在场景中实际上是不活跃的)
  • 删除对象Destroy(gameObject);
  • 同样也可以放入组件,删除组件

组件

  • 获取组件
  • 获取对象上的组件MyComponent component = myObject.GetComponent<MyComponent>();
    • 如果一个脚本挂载到了对象上,可以更简便的获取改对象的组件GetComponent<Renderer>(),也可以使用this获取自身的属性(这里的this就是指向被绑定的对象)
  • 获取子对象上的组件MyComponent childComponent = myObject.GetComponentInChildren<MyComponent>();
    • 全部子对象:MyComponent[] allChildComponents = myObject.GetComponentsInChildren<MyComponent>();
  • 父对象:MyComponent parentComponent = myObject.GetComponentInParent<MyComponent>();
  • 动态添加组件MyComponent newComponent = myObject.AddComponent<MyComponent>();
  • 启用和禁用组件:设置enabled属性
  • 可以直接使用.来访问脚本的属性(包含脚本组件)

碰撞器

  • 设置为触发器:不会产生物理碰撞
  • OnTriggerEnterOnTriggerStayOnTriggerExit等事件就会被触发。

    using UnityEngine;
    
    public class Cannon : MonoBehaviour
    {
        private void OnTriggerEnter(Collider other)
        {
            if (other.CompareTag("Enemy"))
            {
                // 敌人进入了攻击范围
                Debug.Log("Enemy in range!");
            }
        }
    
        private void OnTriggerExit(Collider other)
        {
            if (other.CompareTag("Enemy"))
            {
                // 敌人离开了攻击范围
                Debug.Log("Enemy out of range!");
            }
        }
    }
    

  • 使用标签标识

  • 在Unity的主界面的顶部,点击Edit
  • 从下拉菜单中选择Project Settings
  • 在打开的窗口中选择Tags and Layers
  • 在Tags部分,点击+按钮添加一个新的tag,例如"Enemy"。
  • 设定image-20231009001609387

  • 是否发生碰撞的设定

  • 在项目设置中可以全局设定可以放生碰撞的层
  • 在碰撞器组件中可以进行单独自定义设置

    • image-20231014161001824
  • Mesh Colider:

  • 默认条件下会对模型所有的面添加碰撞
  • 开启凸包选项之后则只有模型的凸包(最外层)具有碰撞

  • 层级通常用于:摄像机渲染控制、光照控制、物理交互(如是否碰撞)

  • 标签通常用于:快速访问对象(筛选),逻辑分组(如敌我识别)

Rigidbody

脚本文件间交互

  • 访问其他类的成员变量
  • 对于public属性,可以直接获取对象再获取脚本组件,使用属性的形式获取

预制件prefab

  • 创建预制件的克隆体Instantiate
  • 要创建一个 Prefab,只需将场景中的游戏物体拖拽到 Project 视图中即可。这会创建一个 Prefab 资产,并且场景中的游戏物体现在是该 Prefab 的一个实例。

  • 实例化

  • 可以指定位置、角度(省略时在默认位置生成)
    Vector3 position = new Vector3(0, 0, 0);
    Quaternion rotation = Quaternion.identity;
    GameObject instance = Instantiate(myPrefab, position, rotation);
    
  • Instantiate 方法的返回值是一个 Object 类型。

生命周期函数

  • image-20231025094712073

初始化阶段

  • Awake
  • 加载包含脚本的游戏对象时调用,无论该对象是否启用。
  • 总是在任何Start方法之前被调用。
  • 即使脚本是禁用的,这个方法也会被调用。
  • 初始化变量或状态。配置不依赖于其他游戏对象的设置。

  • OnEnable

  • 当脚本被初始化或者当所在的GameObject被设置为active时调用。在脚本对象被激活时调用
  • 这发生在所有对象的 Awake 之后、任何对象的 Start 之前。
  • 注册事件监听器或广播消息。开始协程。对外部资源进行订阅或更新状态。

  • Start

  • Update之前的第一个帧中被调用。
  • 仅被调用一次
  • 如果 MonoBehaviour 被禁用,则不会被调用。
  • 初始化依赖于其他对象的变量或状态。在场景中查找其他游戏对象。设置依赖于场景中其他对象或组件的初始化代码。

更新阶段

  • FixedUpdate
  • 用于(时间)固定的帧率的更新,与物理相关的代码应该放在这里。 可以保证物理模拟的稳定性

    • 由于物理模拟需要在各种硬件和帧率下保持一致性,使用固定时间间隔可以确保物理模拟的行为是可预测的,不受帧率波动的影响。
  • Update

  • 都被调用。
  • 主要用于普通的游戏逻辑。
  • 获取帧时间 Time.deltaTime

  • LateUpdate

  • 在每帧在所有 Update 方法调用后被调用
  • 主要用于跟随摄像机的移动、后处理、修正、反馈等操作。
  • 如果你有一个摄像机跟随一个角色,你可能想要确保角色在 Update() 中首先移动,然后摄像机在 LateUpdate() 中进行更新以跟随角色。这确保了无论角色如何移动或如何被其他系统影响,摄像机都能够在该帧结束时准确地跟随它。

渲染阶段

  • OnPreRender:在摄像机开始渲染之前调用
  • OnRenderObject:在所有常规渲染完成后,用于自定义渲染操作。
  • OnPostRender:在摄像机完成渲染后调用。
  • OnGUI:用于渲染和处理GUI事件。每帧可能被调用多次。

禁用或销毁

  • OnDisable
  • 当脚本被禁用或 GameObject 变为非 active 时调用。

  • OnDestroy

  • 当 MonoBehaviour 将被销毁、或者关联的 GameObject 被销毁时调用。

补充

  • 子类的会覆盖掉父类,也就是说只会执行子类的Update等函数
  • 可以使用base.Update()手动调用

物理系统

  • 物理材质
  • 物理材质定义了物体表面的摩擦和反弹特性。
  • 重力及空气阻力
  • gravity:定义在 Physics 菜单中,全局影响所有 Rigidbody 的重力方向和大小。
  • dragangularDrag:定义 Rigidbody 的空气阻力和角阻力。
  • 关节
  • unity 提供了一系列的关节组件(如 Hinge Joint, Spring Joint 等)允许物体之间有约束关系。

基本组件

  • 物理组件:Rigidbody使游戏物体受物理引擎控制
  • 要使一个对象受物理系统影响需要添加该组件
  • 碰撞:Colider,有多种碰撞器(形状)
  • 碰撞检测
    • OnCollisionEnter, OnCollisionStay, OnCollisionExit:当 Rigidbody 与其他物体发生碰撞时触发的事件。
    • OnTriggerEnter, OnTriggerStay, OnTriggerExit:当 Collider 设置为 "Trigger" 并与其他 Collider 重叠时触发的事件。

常用方法

  • AddForce(Vector3 force):向 Rigidbody 添加力。例如,向前添加力:rb.AddForce(transform.forward * forceAmount);
  • AddTorque(Vector3 torque):向 Rigidbody 添加扭矩,使其旋转。
  • velocity:直接设置或获取 Rigidbody 的速度。
  • angularVelocity:直接设置或获取 Rigidbody 的角速度。
  • isKinematic:如果设置为 true,Rigidbody 将不受物理引擎控制,但仍可以与其他非 Kinematic Rigidbody 交互。
  • useGravity:决定物体是否受重力影响。

矢量数学

基本

  • 左手坐标系
  • image-20231008121916368

  • 向量表示

  • Vector3 vec = new Vector3(x, y, z);

  • 常量

           Vector3 right = Vector3.right;    // (1, 0, 0)
           Vector3 up = Vector3.up;          // (0, 1, 0)
           Vector3 forward = Vector3.forward; // (0, 0, 1)
           Vector3 zero = Vector3.zero;      // (0, 0, 0)
           Vector3 one = Vector3.one;        // (1, 1, 1)
    

  • 此外还有 Vector2

  • 方法

  • 获取模Vector3.normalized
  • 计算Vector3.Dot(a, b), Vector3.Cross(a, b)
  • 距离Vector3.Distance(a, b)
  • 获取坐标transform.position

坐标变换

  • 平移
  • 直接加上一个向量transform.Translate(Vector3.forward * moveSpeed * Time.deltaTime);

  • 旋转

  • 指定向量作为方向,以及旋转的角度transform.Rotate(Vector3.up, turnSpeed * Time.deltaTime);

  • LookAt

  • 使一个游戏对象的变换组件面向另一个游戏对象的变换组件
  • 让摄像机指向一个对象
    using UnityEngine;
    using System.Collections;
    
    public class CameraLookAt : MonoBehaviour
    {
        public Transform target;
    
        void Update ()
        {
            transform.LookAt(target);
        }
    }
    
  • 可以在图形界面拖拽绑定对象组件

  • 使用四元数管理旋转

  • 允许连续的旋转运算而不失稳定性。使用欧拉角旋转物体时,可能会遇到一个称为“万向锁”的问题,其中某个旋转轴“消失”了。通过使用四元数,可以避免这种情况。
  • 万向节锁:先围绕 Z 轴旋转90度,然后尝试围绕 Y 轴旋转,这种情况下 X 轴会和 Z 轴对齐,导致原本独立的旋转轴合并(此时 Z 轴与 X 轴重合,旋转剩下的 X 与之前的 Z 等价)即损失了一个自由度
  • 操作
  • 单位四元数:表示没有旋转或旋转360°的四元数。
  • 四元数乘法:你可以通过乘法组合两个四元数,从而组合两个旋转。
  • 四元数的逆:逆旋转。
  • 四元数的插值:例如,Quaternion.LerpQuaternion.Slerp可以用于在两个旋转之间平滑过渡。

  • 创建

    • Quaternion.Euler(x, y, z):从欧拉角创建四元数。
    • Quaternion.AngleAxis(angle, axis):在给定的轴上创建一个角度的旋转。
    • Quaternion.LookRotation(forward, up):查看一个方向并指定向上的方向。
    • Quaternion.identity:这是一个单位四元数,表示无旋转。
  • 应用

    • 物体旋转:transform.rotation = Quaternion.Euler(45, 0, 0);
            //面向物体
            Vector3 directionToTarget = target.position - transform.position;
            transform.rotation = Quaternion.LookRotation(directionToTarget);
      

线性插值

  • float result = Mathf.Lerp (3f, 5f, 0.5f);会得到4,参数为:起点终点百分比
  • 此外也有Color.Lerp 和 Vector3.Lerp等不同的插值函数

  • 实现灯光的渐变插值

    void Update ()
    {
        light.intensity = Mathf.Lerp(light.intensity, 8f, 0.5f * Time.deltaTime);
    }
    

Slerp四元数插值

  • lerp 会考虑四元数的球形几何特性,从而提供更自然和正确的旋转插值。Slerp 的速度在开始和结束时会减慢,这使得旋转看起来更自然。
    Quaternion startRotation = Quaternion.Euler(0, 0, 0);
    Quaternion endRotation = Quaternion.Euler(0, 90, 0);
    float t = 0.5f; // t 可以是时间或其他变量,它决定了插值的位置:0 是开始旋转,1 是结束旋转。
    
    transform.rotation = Quaternion.Slerp(startRotation, endRotation, t*Time.deltaTime);
    

操作输入

键盘

  • Input.GetKey(KeyCode): 在键被按下的每一帧中返回true
  • Input.GetKeyDown(KeyCode): 在键第一次被按下的那一帧返回true
  • Input.GetKeyUp(KeyCode): 在键释放的那一帧返回true
  • KeyCode
  • 对于字母:KeyCode.W

  • 传统方式就是在 Update 中进行轮询

    void Update() {
        if (Input.GetKeyDown(KeyCode.Space)) {
            Debug.Log("Space key was pressed.");
        }
    
        float horizontal = Input.GetAxis("Horizontal");
        float vertical = Input.GetAxis("Vertical");
    }
    

鼠标

  • Input.GetMouseButton(int): 在按钮被按下的每一帧中返回true
  • Input.GetMouseButtonDown(int): 在按钮第一次被按下的那一帧返回true
  • Input.GetMouseButtonUp(int): 在按钮释放的那一帧返回true
  • 获取鼠标的位置Input.mousePosition: 返回一个屏幕上的鼠标位置的Vector3
  • 获取滚轮状态`Input.mouseScrollDelta: 返回一个表示鼠标滚轮滚动的Vector2。`

按钮

  • 通过抽象的“虚拟”按钮进行输入,允许代码与实际的物理输入设备抽象出来,从而更容易地支持多种控制方案和设备。
  • 按钮的设置
  • 打开Unity编辑器。
  • 选择Edit > Project Settings > Input。
  • 在“Input Manager”窗口中,你会看到一个名为“Axes”的列表。每个轴实际上可以作为一个按钮使用。例如,默认的设置中已经有一个名为“Jump”的按钮,它映射到了键盘的空格键。
  • 输入检测:使用Input.GetButton(), Input.GetButtonDown(), 和 Input.GetButtonUp()

GetAxis

  • 线性输入(如摇杆、线性扳机),一个-1~1范围的值,可以实现更加平滑的移动
  • 返回平滑值(浮点数)Input.GetAxis()
  • 返回-1、0、1:Input.GetAxisRaw()
  • 常用名称
  • "Horizontal": 通常映射到左右移动。
  • "Vertical": 通常映射到上下移动。
  • "Mouse X": 鼠标在X轴上的移动。
  • "Mouse Y": 鼠标在Y轴上的移动。
  • 在Unity的Input Manager中,可以自定义轴或修改现有轴的配置。(Edit > Project Settings > Input。)
  • 敏感度和死区: 在 Input Manager 中,还可以为每个轴设置敏感度和死区,这在处理游戏手柄摇杆输入时特别有用,可以帮助消除不希望的微小移动

input system 新输入系统

  • 多设备支持:新输入系统支持从多种输入设备接收输入,如键盘、鼠标、游戏手柄、触摸屏等,甚至可以处理更复杂的设备如 VR 控制器。
  • 输入动作:用户可以定义输入动作(Input Actions),这些动作抽象了具体的键位或控制器按钮,允许开发者基于动作而不是具体的按键或按钮编程,这有助于提高代码的可移植性和易用性。
  • 事件驱动:新系统采用事件驱动模型,允许开发者订阅并响应特定的输入事件,而不是在每个更新周期中检查输入状态。
    using UnityEngine;
    using UnityEngine.InputSystem;
    
    public class PlayerController : MonoBehaviour
    {
        public InputActionAsset inputActions; // 在Inspector中分配
    
        private InputAction moveAction;
    
        private void Awake()
        {
            // 获取动作
            var gameplayActionMap = inputActions.FindActionMap("Gameplay");
            moveAction = gameplayActionMap.FindAction("Move");
    
            // 启用动作
            moveAction.Enable();
    
            // 订阅事件
            moveAction.performed += OnMovePerformed;
        }
    
        private void OnMovePerformed(InputAction.CallbackContext context)
        {
            Vector2 moveInput = context.ReadValue<Vector2>();
            // 处理移动逻辑
        }
    
        private void OnDestroy()
        {
            // 清理
            moveAction.Disable();
        }
    }
    

事件&广播

  • 事件本身是一种特殊的委托(多播)
    //通过委托定义事件格式
    public delegate void MyEventHandler(string message);
    //声明事件
    public event MyEventHandler MyEvent;
    
  • 可以讲一个参数匹配的方法赋值给委托,即相当于用这个方法实现了这个委托

  • 触发事件MyEvent()

  • 订阅事件someInstance.MyEvent += MyHandlerMethod;
  • 注意一定还得取消注册事件someInstance.MyEvent -= MyHandlerMethod;
  • 通常写在生命周期函数中
          private void OnEnable()
          {
              someEvent += MyEventHandler;
          }
    
          private void OnDisable()
          {
              someEvent -= MyEventHandler;
          }
      //广播
      someEvent?.Invoke();
    

补充

  • 延时执行函数invoke
  • 只能调用不需要参数并且无返回值的函数
  • Invoke ("函数名", time);
  • 周期调用InvokeRepeating(函数名", 初次延时, 周期调用间隔);

  • 属性标志

  • Range限制Inspector窗口中变量的取值范围。

    [Range(0, 10)]
    public int myNumber;
    

  • ExecuteInEditMode将此特性放在MonoBehaviour派生的类之前时,该类中的代码将在编辑模式下运行,而不是仅在播放模式下。

    [ExecuteInEditMode]
    public class MyScript : MonoBehaviour
    {
        void Update()
        {
            // 这个代码将在编辑器模式下每帧执行
        }
    }
    

  • 存在副作用:使用 ExecuteInEditMode 可能会导致场景数据在编辑模式下被更改,如果您没有注意到这些更改并保存了场景,这可能会导致您丢失某些所需的配置或数据。(永久改变)

高级

文件系统

持久化存储

  • playerprefs:存储简单的少量数据,不适用于大量数据或复杂数据结构(类似简答键值对)

    // 保存数据
    PlayerPrefs.SetInt("Level", 10);
    PlayerPrefs.SetFloat("Health", 75.0f);
    PlayerPrefs.SetString("PlayerName", "Alice");
    PlayerPrefs.Save(); // 确保数据写入
    
    // 加载数据
    int level = PlayerPrefs.GetInt("Level", 1); // 默认值为1
    float health = PlayerPrefs.GetFloat("Health", 100.0f); // 默认值为100.0f
    string playerName = PlayerPrefs.GetString("PlayerName", "DefaultName"); // 默认值为DefaultName
    

  • 文件存储(JSON,XML,Binary)

    • Unity 支持 JSON 和 XML 的序列化和反序列化,也可以使用二进制格式存储数据以提高效率和安全性。
      [System.Serializable]
      public class GameData
      {
          public int level;
          public float health;
          public string playerName;
      }
      
      // 保存数据
      GameData data = new GameData { level = 10, health = 75.0f, playerName = "Alice" };
      string json = JsonUtility.ToJson(data);
      System.IO.File.WriteAllText(Application.persistentDataPath + "/savefile.json", json);
      
      // 加载数据
      string loadedJson = System.IO.File.ReadAllText(Application.persistentDataPath + "/savefile.json");
      GameData loadedData = JsonUtility.FromJson<GameData>(loadedJson);
      
  • SQLite 数据库

    • 对于需要管理大量数据或复杂查询的游戏,使用 SQLite 数据库是一个好选择。Unity 可以通过第三方库(如 SQLite4Unity3d)来集成 SQLite 数据库。

序列化与反序列化

  • 对象序列化是指将对象的状态信息转换成可存储或可传输的形式(如二进制流、XML、JSON 等格式),以便在需要时(如存储到文件、发送到另一台机器等)能够重建对象。
    • 持久化存储
    • 网络通信
    • 深拷贝
  • 可以序列化的对象
    [Serializable]
    public class Player {
        public string Name;
        public int Score;
        [NonSerialized]
        private int secretValue; // 这个字段不会被序列化
    }
    
  • 使用 JSON 实现(兼容性好,适用于网络传输)
    using System.Text.Json; // .NET Core 3.0 以上版本引入的命名空间
    
    public class Player
    {
        public string Name { get; set; }
        public int Score { get; set; }
    }
    
    // 序列化
    Player player = new Player { Name = "Alice", Score = 100 };
    string jsonString = JsonSerializer.Serialize(player);
    
    // 反序列化
    Player deserializedPlayer = JsonSerializer.Deserialize<Player>(jsonString);
    
  • 使用二进制实现(效率更高,适合用于本地存储)
    // 序列化
    Player player = new Player { Name = "Alice", Score = 100 };
    BinaryFormatter formatter = new BinaryFormatter();
    using (Stream stream = new FileStream("player.dat", FileMode.Create, FileAccess.Write, FileShare.None))
    {
        formatter.Serialize(stream, player);
    }
    
    // 反序列化
    using (Stream stream = new FileStream("player.dat", FileMode.Open, FileAccess.Read, FileShare.Read))
    {
        Player deserializedPlayer = (Player)formatter.Deserialize(stream);
    }
    

物理系统

射线

  • 用于检测是否有遮挡、从摄像机发射射线到平面获取点击位置
  • 可以设置屏蔽吗,屏蔽&指定图层

    public static bool RayTrigger(Vector3 from, GameObject to , string tag)
        {
            Vector3 rayOrigin = from;
            Vector3 rayDirection = to.transform.position - from;
            float rayDistance = rayDirection.magnitude;
            RaycastHit hit;
            int layerMask = ~(1 << LayerMask.NameToLayer(tag));//屏蔽码
            // 发送射线
            if (Physics.Raycast(rayOrigin, rayDirection.normalized, out hit, rayDistance, layerMask))
            {
                // 如果射线只碰撞到敌方或没有碰撞到其他物体
                if (hit.collider.gameObject == to)
                {
                    return true;
                }
            }
            return false;
        }
    

  • 获得点击点的坐标hit.point

摄像机

多摄像机渲染

  • 可以用于调整图层的渲染顺序,通过设置摄像机的深度,可以设置摄像机的渲染顺序,此外再指定摄像机渲染的图层就可以实现分层渲染
  • 混合模式
  • Skybox:摄像机会先渲染天空盒,然后在其上渲染其他物体。
  • Solid Color:摄像机会用一个固定颜色(可以在"Background"属性中设置)清除渲染目标,然后在这个背景上渲染物体。
  • Depth Only:摄像机不会清除之前的任何内容,而只会渲染新的内容。这意味着摄像机只更新深度缓冲区,所以渲染的物体会根据深度信息覆盖之前的内容。
  • Don't Clear:摄像机不会清除之前的内容,也不更新深度缓冲区,新的渲染内容将直接绘制在之前的内容上。

粒子系统

灯光

  • unity 中的灯光种类
    • 方向光:从犯远方发出的平行光,如太阳光,没有位置只有方向
    • 点光源:从一个点向所有方向均匀发光,随着距离减弱,模拟灯泡火把等局部光源
    • 聚光灯:从一个点放出光纤,在特定角度内形成锥形照明区域,随距离减弱,如手电筒、舞台聚光灯
    • 面光源:从矩形、圆形区域发出光纤,柔和且均匀,模拟窗户光线等大面积光源,常用于室内
    • 环境光:影响整个场景的光照,提供基础光照

对象池

  • 对象池通过预先创建和维护一定数量的对象实例来工作。这些对象在需要时被激活,不需要时被禁用,而不是被销毁。这样,当游戏需要新对象时,可以直接从池中获取一个已经存在的禁用对象,激活并使用它,而不是每次需要时都创建新的对象。
  • 基本过程
    • 创建池管理器:创建一个管理对象池的类,用于处理对象的请求和回收。
    • 预先实例化对象:在游戏开始或对象池初始化时,根据需要预先创建足够数量的对象实例。
    • 管理对象的获取与返回:提供方法来让其他游戏组件请求对象和返回对象。获取对象时,检查池中是否有可用的禁用对象;返回对象时,禁用该对象并将其放回池中。(注意在回收时或启用时进行初始化,因为会带有原先的属性数据)
  • 优点
    • 性能提升:减少运行时的实例化和销毁操作,降低内存分配和回收的压力,从而提高性能。
    • 内存使用优化:通过重复使用对象减少内存消耗,避免频繁的内存分配和碎片化
      using System.Collections.Generic;
      using UnityEngine;
      
      public class ObjectPool : MonoBehaviour
      {
          public GameObject prefab; // 预设对象
          public int poolSize = 10; // 池的初始大小
          private Queue<GameObject> objectPool = new Queue<GameObject>(); // 使用队列存储池中的对象
      
          void Start()
          {
              // 初始化池
              for (int i = 0; i < poolSize; i++)
              {
                  GameObject obj = Instantiate(prefab); // 创建对象
                  obj.SetActive(false); // 禁用对象
                  objectPool.Enqueue(obj); // 将对象添加到池中
              }
          }
      
          // 从池中获取对象
          public GameObject GetObject()
          {
              if (objectPool.Count > 0)
              {
                  GameObject obj = objectPool.Dequeue(); // 获取对象
                  obj.SetActive(true); // 激活对象
                  return obj;
              }
              else
              {
                  // 池为空,根据需要可以扩展池
                  GameObject obj = Instantiate(prefab);
                  obj.SetActive(true);
                  return obj;
              }
          }
      
          // 将对象返回池中
          public void ReturnObject(GameObject obj)
          {
              obj.SetActive(false); // 禁用对象
              objectPool.Enqueue(obj); // 将对象放回池中
          }
      }
      

杂项

  • 打印调试Debug.Log()

摄像机动画

  • [Unity Cinemachine & Timeline 制作镜头动画
  • brain组件放置到主摄像机上,创建虚拟摄像机(虚拟摄像机本质上是作为主摄像机上进行移动,即可以被附身)
  • 使用image-20231027173210290
  • 在脚本中进行播放
  • 可以使用timeline文件存储动画,左侧拖入主摄像机,右侧剪辑虚拟摄像机,即不同时间使用不同的虚拟摄像机作为显示

MonoBehaviour

  • MonoBehaviour 是Unity提供的一个基类,它允许为游戏对象创建行为脚本。
  • 作为组件附加到游戏对象上:只有继承自MonoBehaviour的脚本才可以直接附加到游戏对象。
  • 利用Unity生命周期方法:例如Start(), Update(), Awake(), FixedUpdate(), OnCollisionEnter(), 等等。这些方法在特定的时刻由Unity自动调用,允许开发者编写游戏逻辑。
  • 访问其他游戏对象和组件MonoBehaviour提供了方法如GetComponent<T>(),允许脚本与其所在的或其他游戏对象的组件交互。
  • 与Unity编辑器集成:通过MonoBehaviour,你可以使用诸如[SerializeField][HideInInspector]这样的属性标签,来定制组件在Inspector视图中的显示。
  • 使用协程 (Coroutines)MonoBehaviour允许使用StartCoroutine方法,这使得你可以编写协程来执行延迟、重复或基于时间的任务,而不必完全依赖Update方法。

版本控制PlasticSCM

分支与合并

  • 从主线创建新分支
  • image-20231008195802790
  • 分支切换
  • image-20231008200041156
  • 合并分支
  • image-20231008200524733
  • 冲突处理
  • image-20231008201857186
  • 对于二进制文件冲突:Unity 提供了一个叫做 Smart Merge(也称为 UnityYAMLMerge)的工具,专门用来解决像 Prefabs 和 Scenes 这样的 YAML 文件冲突。

游戏构建

教程

配置

  • 统一unity editor版本:2022.3.8f1c1
  • editor更换中文(hub更换中文直接设置里改)
  • https://new-translate.unity3d.jp/v1/live/54/{version}/zh-hans 把{version}换成你的版本,比如说如果你的是2022.3.8f1c1那就填2022.3. 其他同理。下完之后放到 Unity 安装目录下 Data 里的 Localization 文件夹里,如果没有就新建一个。放完之后在界面左上角找到 Edit,然后在下面找到 preferences 选项,找到 language,在 editor language 那一栏选简体中文。

文件结构

  1. Assets:所有的游戏资源都放在这里。
  2. Scenes:存放所有场景文件。
  3. Scripts:存放所有C#脚本。
  4. Prefabs:预制体文件,如常用的游戏对象或UI元素。
  5. Materials:存放材质和材质球。
  6. Textures:存放所有的纹理和图片资源。
  7. Models:存放3D模型,如.obj或.fbx文件。
  8. Animations:存放动画及相关的资源。
  9. Audio:音效和音乐文件。
    • Music:背景音乐文件。
    • SFX:音效文件。
  10. Shaders:自定义的shader文件。
  11. Fonts:字体文件。
  12. Plugins:外部插件和库。
  13. Resources:需要动态加载的资源。
  14. Editor:与Unity编辑器相关的脚本和工具。
  15. Gizmos:自定义的编辑器图标。
  16. Packages:存放Unity Package Manager使用的包。
  17. ProjectSettings:存放Unity项目的设置文件。这些设置包括输入、标签、图层、物理、渲染和其他全局设置。
  18. UserSettings:存放某些用户特定的设置。

问题及解决

  • 移动碰撞后不正常
  • 使用物理刚体线性速度代替直接设置transform
public class InputMove : MonoBehaviour
{
    private float _speed;
    private Rigidbody rb;

    void Start()
    {
        _speed = GetComponent<Hero>().moveSpeed;
        rb = GetComponent<Rigidbody>();
    }

    void FixedUpdate() // 使用FixedUpdate而不是Update处理物理相关操作
    {
        float moveHorizontal = Input.GetAxis("Horizontal");
        float moveVertical = Input.GetAxis("Vertical");

        Vector3 movement = new Vector3(moveHorizontal, 0.0f, moveVertical);
        rb.velocity = movement * _speed;
    }
}

静态脚本

  • 用于进行一些批量化处理及操作
  • 将路径下所有网格设置为可以读取

    using UnityEngine;
    using UnityEditor;
    using System.Collections.Generic;
    
    public class MeshReadWriteSetter
    {
        [MenuItem("Tools/Set Meshes to Read/Write Enabled")]
        public static void SetMeshesToReadWriteEnabled()
        {
            string folder = "Assets/GameMain/Packages"; // 替换 "YourFolderPath" 为你的文件夹路径
    
            // 获取文件夹中的所有模型
            string[] guids = AssetDatabase.FindAssets("t:Model", new[] { folder });
            List<ModelImporter> importersToModify = new List<ModelImporter>();
    
            foreach (string guid in guids)
            {
                string assetPath = AssetDatabase.GUIDToAssetPath(guid);
                ModelImporter importer = AssetImporter.GetAtPath(assetPath) as ModelImporter;
    
                if (importer && !importer.isReadable)
                {
                    importersToModify.Add(importer);
                }
            }
    
            // 设置每个模型为可读/写
            foreach (var importer in importersToModify)
            {
                importer.isReadable = true;
                importer.SaveAndReimport();
            }
    
            Debug.Log($"Set {importersToModify.Count} meshes to Read/Write Enabled.");
        }
    }
    

  • 为所有子对象添加mesh碰撞器

using UnityEngine;
using UnityEditor;

public static class MeshColliderEditorUtility
{
    [MenuItem("GameObject/Add MeshCollider to Children", false, 30)]
    public static void AddMeshColliders()
    {
        GameObject selectedObj = Selection.activeGameObject;

        if (selectedObj == null)
        {
            Debug.LogWarning("No object selected. Please select an object first.");
            return;
        }

        RecursiveAddMeshCollider(selectedObj);
        Debug.Log("MeshColliders added to " + selectedObj.name + " and its children.");
    }

    private static void RecursiveAddMeshCollider(GameObject obj)
    {
        if (obj.GetComponent<MeshFilter>() && !obj.GetComponent<MeshCollider>())
        {
            obj.AddComponent<MeshCollider>();
        }

        foreach (Transform child in obj.transform)
        {
            RecursiveAddMeshCollider(child.gameObject);
        }
    }
}