本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:C#游戏开发涵盖编程基础、Unity引擎使用、图形渲染、物理系统、游戏逻辑与AI、音画控制、用户界面设计、网络编程及性能优化等关键内容。本课程围绕Unity平台展开,通过实战教学帮助开发者掌握C#在游戏开发中的核心应用,涵盖从基础语法到高级功能的完整开发流程,适合希望进入游戏开发领域的学习者和开发者。

1. C#面向对象编程基础

面向对象编程(OOP)是C#语言的核心思想,其核心特性包括封装、继承与多态。理解这些特性是掌握Unity脚本开发的关键基础。类是OOP的基本构建块,用于封装数据(属性)与行为(方法)。对象则是类的具体实例。例如:

public class Player
{
    private int health;  // 封装:使用private限制外部直接访问

    public Player(int initialHealth)
    {
        health = initialHealth;  // 构造函数用于初始化对象
    }

    public void TakeDamage(int damage)
    {
        health -= damage;
    }
}

通过该类,我们可以创建多个玩家对象,各自维护独立的状态。下一节将进一步探讨继承与多态机制。

2. Unity引擎界面与核心功能

Unity引擎作为当前最流行的游戏开发平台之一,其强大的编辑器界面和丰富的核心功能模块为开发者提供了高效、灵活的开发环境。本章将深入探讨Unity编辑器的界面结构、核心功能模块以及项目设置与版本控制策略,帮助开发者熟悉并掌握Unity的基本使用方法和开发流程。

2.1 Unity编辑器界面概述

Unity编辑器的界面设计直观且模块化,便于开发者快速上手。主要由五大视图组成: 工程视图(Project View) 层级视图(Hierarchy View) 场景视图(Scene View) 游戏视图(Game View) 检查器视图(Inspector View) 。每个视图都有其特定的用途,合理使用这些视图可以提高开发效率。

2.1.1 工程视图与层级视图的使用

工程视图(Project View) 是资源管理的核心界面,用于展示项目中所有的资源文件,包括脚本、纹理、模型、音频等。通过该视图可以方便地导入、导出、组织资源。

层级视图(Hierarchy View) 展示当前场景中所有的游戏对象(GameObject),开发者可以在这里创建、删除、重命名游戏对象,也可以调整它们的父子关系。

示例:资源导入与对象创建
// 示例代码:通过脚本创建一个立方体对象并设置其父对象
using UnityEngine;

public class CreateGameObjectExample : MonoBehaviour
{
    void Start()
    {
        // 创建一个立方体
        GameObject cube = GameObject.CreatePrimitive(PrimitiveType.Cube);
        cube.name = "MyCube";

        // 创建一个空对象作为父对象
        GameObject parent = new GameObject("ParentObject");

        // 设置父子关系
        cube.transform.parent = parent.transform;

        // 设置位置
        cube.transform.position = Vector3.zero;
    }
}

逻辑分析:

  • GameObject.CreatePrimitive 方法用于创建基本的3D对象。
  • cube.name 设置对象的名称。
  • new GameObject 创建一个新的空对象作为父对象。
  • transform.parent 设置父子层级关系。
  • transform.position 设置立方体在世界坐标系中的位置。
表格:工程视图与层级视图功能对比
功能 工程视图(Project View) 层级视图(Hierarchy View)
显示内容 项目资源文件 场景中的游戏对象
主要用途 资源管理与导入 游戏对象组织与层级管理
支持操作 导入、导出、重命名 创建、删除、父子关系设置

2.1.2 场景视图与游戏视图的区别

场景视图(Scene View) 是开发者编辑和布局场景的主要区域,支持多种视图模式(如正交视图、透视视图)和编辑工具(如移动、旋转、缩放)。开发者可以在此视图中精确地调整对象的位置、旋转和缩放。

游戏视图(Game View) 是游戏运行时的预览窗口,显示的是最终玩家看到的画面。可以在此视图中测试摄像机视角、UI布局、光照效果等。

流程图:场景视图与游戏视图交互流程
graph TD
    A[编辑器启动] --> B[进入Scene View]
    B --> C[编辑对象位置/旋转/缩放]
    C --> D[切换到Game View]
    D --> E[查看运行时画面]
    E --> F[返回Scene View继续编辑]
参数说明
  • Scene View :用于构建和编辑游戏场景。
  • Game View :用于测试和查看游戏最终运行效果。
  • 切换视图 :可以通过编辑器右上角的“Game”和“Scene”按钮切换视图。

2.1.3 检查器视图与项目资源管理

检查器视图(Inspector View) 是Unity中最重要的属性编辑器,用于显示和修改当前选中对象的所有组件属性。无论是游戏对象、脚本、材质还是动画,都可以在检查器中进行配置。

示例:添加组件并配置参数
using UnityEngine;

public class AddComponentExample : MonoBehaviour
{
    void Start()
    {
        // 获取当前游戏对象
        GameObject obj = this.gameObject;

        // 添加 Rigidbody 组件
        Rigidbody rb = obj.AddComponent<Rigidbody>();

        // 设置质量
        rb.mass = 5.0f;

        // 设置是否使用重力
        rb.useGravity = true;
    }
}

逻辑分析:

  • AddComponent<Rigidbody>() 向当前对象添加物理刚体组件。
  • rb.mass 设置物体的质量。
  • rb.useGravity 控制是否受到重力影响。
表格:Inspector View常用组件说明
组件名称 功能说明 常用参数示例
Transform 控制对象的位置、旋转、缩放 Position、Rotation
Rigidbody 提供物理模拟功能 Mass、Use Gravity
Collider 定义碰撞边界 Is Trigger
AudioSource 播放音频 Clip、Play On Awake

2.2 Unity核心功能模块

Unity的核心功能模块是构建游戏逻辑和系统的基础。包括脚本组件的执行控制、时间与帧率管理、以及场景加载机制等。理解这些模块的使用方式对于构建高性能、响应及时的游戏至关重要。

2.2.1 脚本组件的添加与执行顺序

Unity中的脚本本质上是继承自 MonoBehaviour 的类,挂载到游戏对象上后,其生命周期方法(如 Start() Update() )会被引擎自动调用。

示例:控制脚本执行顺序
using UnityEngine;

[DefaultExecutionOrder(100)] // 设置执行顺序
public class ScriptOrderExample : MonoBehaviour
{
    void Start()
    {
        Debug.Log("ScriptOrderExample Start");
    }

    void Update()
    {
        Debug.Log("ScriptOrderExample Update");
    }
}

逻辑分析:

  • [DefaultExecutionOrder(100)] 设置该脚本的执行顺序为100。
  • 执行顺序值越小越早执行,默认为0。
  • 可用于控制多个脚本之间的执行顺序,避免依赖问题。
表格:常见脚本生命周期方法
方法名 调用时机
Awake() 脚本实例被加载时调用(早于Start)
Start() 在Awake之后,Update之前调用
Update() 每帧调用
FixedUpdate() 固定时间间隔调用,用于物理计算
LateUpdate() 在Update之后调用,适用于摄像机控制

2.2.2 时间控制与帧率管理

Unity提供了 Time 类来获取当前时间、帧间隔、时间缩放等信息。开发者可以通过控制 Time.timeScale 来实现游戏暂停、慢动作等效果。

示例:暂停与恢复游戏
using UnityEngine;

public class TimeControlExample : MonoBehaviour
{
    void Update()
    {
        if (Input.GetKeyDown(KeyCode.P))
        {
            // 暂停游戏
            Time.timeScale = 0.0f;
            Debug.Log("Game Paused");
        }

        if (Input.GetKeyDown(KeyCode.R))
        {
            // 恢复游戏
            Time.timeScale = 1.0f;
            Debug.Log("Game Resumed");
        }
    }
}

逻辑分析:

  • Time.timeScale = 0.0f 暂停所有基于时间的更新(如Update、动画等)。
  • Time.timeScale = 1.0f 恢复正常时间流速。
  • 注意:物理引擎(FixedUpdate)不会受Time.timeScale影响。
表格:Time类常用属性
属性名 类型 描述
time float 当前游戏时间(秒)
deltaTime float 上一帧的时间间隔
fixedDeltaTime float FixedUpdate的时间间隔
timeScale float 时间缩放因子(0为暂停)

2.2.3 场景加载与管理机制

Unity使用SceneManager类来管理场景的加载、卸载与切换。常见的加载方式包括同步加载和异步加载。

示例:异步加载场景
using UnityEngine;
using UnityEngine.SceneManagement;

public class SceneLoader : MonoBehaviour
{
    public string sceneName = "Level1";

    IEnumerator LoadSceneAsync()
    {
        AsyncOperation asyncLoad = SceneManager.LoadSceneAsync(sceneName);

        // 允许在加载过程中进行其他操作
        asyncLoad.allowSceneActivation = false;

        while (!asyncLoad.isDone)
        {
            if (asyncLoad.progress >= 0.9f)
            {
                Debug.Log("Scene loaded, ready to activate.");
                asyncLoad.allowSceneActivation = true;
            }

            yield return null;
        }
    }

    void Start()
    {
        StartCoroutine(LoadSceneAsync());
    }
}

逻辑分析:

  • LoadSceneAsync() 异步加载指定场景。
  • allowSceneActivation = false 防止场景立即激活,用于加载进度条等。
  • isDone 判断加载是否完成。
  • yield return null 等待一帧。
表格:SceneManager常用方法
方法名 功能描述
LoadScene() 同步加载指定场景
LoadSceneAsync() 异步加载指定场景
UnloadSceneAsync() 卸载指定场景
GetActiveScene() 获取当前激活的场景
sceneCountInBuildSettings 获取构建设置中包含的场景数量

2.3 Unity项目设置与版本控制

良好的项目设置和版本控制策略是项目长期维护和团队协作的关键。

2.3.1 输入管理器配置

Unity的输入系统支持键盘、鼠标、手柄等多种输入方式。通过 Input Manager 可以自定义按键映射。

示例:读取自定义输入
using UnityEngine;

public class InputExample : MonoBehaviour
{
    void Update()
    {
        // 读取水平输入(默认A/D或←→)
        float horizontal = Input.GetAxis("Horizontal");

        // 读取垂直输入(默认W/S或↑↓)
        float vertical = Input.GetAxis("Vertical");

        // 输出输入值
        Debug.Log($"Horizontal: {horizontal}, Vertical: {vertical}");
    }
}

逻辑分析:

  • Input.GetAxis("Horizontal") 读取水平轴输入。
  • 可在Edit > Project Settings > Input Manager中自定义按键映射。

2.3.2 标签与层级设置

Unity允许为游戏对象设置标签(Tag)和层级(Layer),便于分类管理和物理碰撞过滤。

示例:使用标签查找对象
using UnityEngine;

public class FindGameObjectWithTagExample : MonoBehaviour
{
    void Start()
    {
        // 查找标签为"Player"的对象
        GameObject player = GameObject.FindGameObjectWithTag("Player");

        if (player != null)
        {
            Debug.Log("Player found: " + player.name);
        }
        else
        {
            Debug.Log("Player not found.");
        }
    }
}

逻辑分析:

  • FindGameObjectWithTag 用于查找具有特定标签的对象。
  • 标签可在Inspector中设置。
表格:Unity层级与标签用途
设置项 用途说明
Tag 对象分类标识,用于查找或逻辑判断
Layer 用于物理碰撞过滤、渲染控制(如UI层不参与渲染)

2.3.3 使用Git进行版本控制的最佳实践

Git是Unity项目版本控制的首选工具。推荐使用 .gitignore 文件排除不必要的文件,如 Library/ Temp/ 等。

示例:.gitignore文件内容
/[Ll]ibrary/
/[Tt]emp/
/[Oo]bj/
/[Bb]uild/
/[Bb]uilds/
*.sln
*.user
*.userprefs
*.pidb
*.booproj

逻辑分析:

  • 排除Unity自动生成的文件夹和缓存文件。
  • 避免将本地设置提交到版本库。
表格:Unity项目版本控制注意事项
内容 推荐操作
.gitignore 必须添加
Assets目录 应提交
ProjectSettings 应提交(包含输入设置、标签等)
Editor文件夹 应提交(包含自定义编辑器脚本)
Library目录 不应提交
Temp目录 不应提交

3. 游戏对象与组件系统

Unity引擎的核心设计理念之一是 基于游戏对象(GameObject)与组件(Component)的架构体系 。这种设计使得开发者可以将复杂的逻辑和功能模块化、可复用化,极大提升了开发效率和代码维护的便利性。在本章中,我们将深入探讨游戏对象的创建与管理机制、组件的使用与交互方式,以及如何通过自定义组件构建灵活可扩展的Unity项目架构。

3.1 游戏对象(GameObject)的创建与管理

在Unity中, GameObject 是构成游戏世界的基本单元。它可以是一个角色、一个摄像机、一个光源,甚至只是一个空对象(Empty GameObject),用于组织其他对象的层级关系。

3.1.1 实例化与销毁游戏对象

在运行时动态创建和销毁游戏对象是Unity开发中非常常见的操作。Unity提供了 Instantiate() Destroy() 方法来实现这一功能。

public class ObjectSpawner : MonoBehaviour
{
    public GameObject prefab; // 要实例化的预制体
    public Transform spawnPoint; // 实例化位置

    void Update()
    {
        if (Input.GetKeyDown(KeyCode.Space))
        {
            GameObject go = Instantiate(prefab, spawnPoint.position, spawnPoint.rotation);
            // 设置父对象
            go.transform.SetParent(transform);
        }
    }

    void OnDestroy()
    {
        // 销毁当前对象的所有子对象
        foreach (Transform child in transform)
        {
            Destroy(child.gameObject);
        }
    }
}

代码逻辑分析:

  • Instantiate(prefab, position, rotation) :该方法用于创建一个 prefab 的实例,参数分别为预制体对象、位置和旋转角度。
  • go.transform.SetParent(transform) :将新创建的对象设置为当前对象的子对象,便于层级管理。
  • Destroy(child.gameObject) :用于在对象销毁时清除其所有子对象,避免内存泄漏。

注意 Instantiate() Destroy() 都是代价较高的操作,频繁调用可能影响性能。建议在性能敏感场景使用对象池(Object Pool)技术进行优化。

3.1.2 父子对象关系与层级结构

Unity中的GameObject可以组成 父子层级结构 ,这在组织场景、控制摄像机跟随、角色骨骼动画等方面非常关键。

示例:父子关系构建
void BuildParentChild()
{
    GameObject parent = new GameObject("Parent");
    GameObject child = new GameObject("Child");

    child.transform.SetParent(parent.transform);
    child.transform.localPosition = Vector3.zero; // 设置局部坐标
}

父子对象特性:

特性 描述
局部坐标 子对象的位置是相对于父对象的局部坐标
变换继承 父对象的旋转、缩放、移动会作用于子对象
层级清理 销毁父对象时,所有子对象也会被销毁

3.1.3 对象查找与引用方式

在实际开发中,经常需要获取其他GameObject或组件的引用。Unity提供了多种方式实现这一目的。

常用引用方式对比:
方式 说明 适用场景
Inspector拖拽引用 在Inspector面板中手动拖入对象 静态对象引用
GameObject.Find("Name") 通过名称查找对象 动态查找,注意性能
transform.Find("Path") 通过路径查找子对象 层级结构中查找
GetComponentInChildren<T>() 获取子对象上的组件 复杂结构中查找特定组件

示例:

public class FindReference : MonoBehaviour
{
    public Transform target;

    void Start()
    {
        if (target == null)
        {
            target = transform.Find("Gun/Barrel");
        }

        // 或者使用 FindGameObjectWithTag
        GameObject player = GameObject.FindWithTag("Player");
    }
}

最佳实践 :避免在 Update() 中频繁调用 Find 类方法,建议在 Start() Awake() 中缓存引用。

3.2 组件(Component)的使用与交互

在Unity中, 组件(Component)是功能的最小单元 。每个GameObject可以附加多个组件,例如 Transform Rigidbody Collider 等,这些组件共同决定了对象的行为。

3.2.1 常用组件概述(Transform、Rigidbody等)

组件名称 功能描述
Transform 控制对象的位置、旋转、缩放
Rigidbody 物理引擎组件,控制物体的运动和受力
Collider 碰撞检测组件,用于物理交互
Renderer 控制对象的视觉渲染,如MeshRenderer、SpriteRenderer
AudioSource 播放音效组件
Animator 控制动画状态机
示例:Rigidbody与Transform配合
public class MoveWithRigidbody : MonoBehaviour
{
    public float speed = 5f;
    private Rigidbody rb;

    void Awake()
    {
        rb = GetComponent<Rigidbody>();
    }

    void FixedUpdate()
    {
        float moveX = Input.GetAxis("Horizontal");
        float moveZ = Input.GetAxis("Vertical");

        Vector3 movement = new Vector3(moveX, 0, moveZ);
        rb.AddForce(movement * speed);
    }
}

代码逻辑分析:

  • GetComponent<Rigidbody>() :从当前GameObject获取Rigidbody组件。
  • AddForce() :向物体施加力,使其在物理引擎中移动。
  • 使用 FixedUpdate() 而非 Update() ,因为物理更新应与固定时间步长同步。

3.2.2 组件的添加、获取与移除

组件的添加可以通过代码动态控制,适用于运行时扩展功能。

示例:添加与移除组件
void AddRemoveComponent()
{
    // 添加组件
    Rigidbody rb = gameObject.AddComponent<Rigidbody>();
    rb.useGravity = true;

    // 获取组件
    Renderer renderer = GetComponent<Renderer>();
    if (renderer != null)
    {
        renderer.material.color = Color.red;
    }

    // 移除组件
    Destroy(rb);
}

注意 Destroy() 不仅用于销毁GameObject,也可以用来移除组件。如果希望立即移除,可以使用 DestroyImmediate() (仅在编辑器中可用)。

3.2.3 组件间的消息传递机制

Unity组件之间可以通过事件系统或直接调用方法实现通信。

使用 SendMessage 实现组件通信
// PlayerController.cs
public class PlayerController : MonoBehaviour
{
    void Update()
    {
        if (Input.GetKeyDown(KeyCode.E))
        {
            // 向当前对象所有组件发送 "Interact" 消息
            gameObject.SendMessage("Interact", SendMessageOptions.DontRequireReceiver);
        }
    }
}

// Interactable.cs
public class Interactable : MonoBehaviour
{
    public void Interact()
    {
        Debug.Log("Object interacted!");
    }
}

优缺点分析:

优点 缺点
简单易用 性能开销大
无需引用 缺乏类型安全
快速原型开发 不适合复杂项目

3.3 自定义组件开发

Unity允许开发者继承 MonoBehaviour 类,创建自定义组件,实现高度模块化的功能复用。

3.3.1 MonoBehaviour类的生命周期方法

MonoBehaviour 是所有Unity脚本的基类,它定义了组件在生命周期中的各个阶段。

生命周期流程图(Mermaid)
graph TD
    A[Awake] --> B[OnEnable]
    B --> C[Start]
    C --> D[Update]
    D --> E[FixedUpdate]
    D --> F[LateUpdate]
    E --> D
    F --> D
    G[OnDisable] --> H[OnDestroy]
    D -->|Destroy| G
各阶段方法说明:
方法 调用时机 常见用途
Awake() 所有对象加载后调用 初始化变量、建立引用
OnEnable() 组件启用时调用 启动监听器、注册事件
Start() 第一次Update前调用 初始化与游戏逻辑相关的数据
Update() 每帧调用 用户输入、动画更新
FixedUpdate() 固定时间间隔调用 物理更新
LateUpdate() 所有Update后调用 摄像机跟随、后处理
OnDisable() 组件禁用时调用 清理资源、取消注册
OnDestroy() 对象销毁时调用 内存释放、资源卸载

3.3.2 开发可复用的自定义组件

创建一个可复用的组件,可以帮助多个对象共享相同的功能逻辑。

示例:可复用的健康组件
public class Health : MonoBehaviour
{
    public float maxHealth = 100f;
    private float currentHealth;

    void Awake()
    {
        currentHealth = maxHealth;
    }

    public void TakeDamage(float damage)
    {
        currentHealth -= damage;
        if (currentHealth <= 0)
        {
            Die();
        }
    }

    private void Die()
    {
        Debug.Log(gameObject.name + " has died.");
        Destroy(gameObject);
    }
}

使用方式:

  • 将该脚本附加到任何需要“生命值”功能的对象上。
  • 其他组件可以通过调用 TakeDamage() 触发伤害逻辑。

3.3.3 协程(Coroutine)与异步操作

Unity协程是一种轻量级的异步编程方式,特别适合处理延迟、等待、动画播放等任务。

示例:使用协程实现延迟攻击
public class Attack : MonoBehaviour
{
    public float attackInterval = 1f;

    void Start()
    {
        StartCoroutine(AttackCoroutine());
    }

    IEnumerator AttackCoroutine()
    {
        while (true)
        {
            Debug.Log("Attack!");
            yield return new WaitForSeconds(attackInterval); // 等待1秒
        }
    }
}

代码逻辑分析:

  • StartCoroutine() :启动协程。
  • yield return new WaitForSeconds() :暂停协程执行一段时间。
  • 协程不会阻塞主线程,也不会影响其他逻辑。
协程与异步操作对比:
方式 是否阻塞主线程 是否适合复杂逻辑
协程
Invoke()
async/await 否(需配合Unity Jobs)
线程(Thread) 否(Unity API非线程安全)

本章内容从游戏对象的创建、管理、引用,到组件的添加、使用与交互,再到自定义组件与协程的应用,全面构建了Unity中对象与组件系统的知识体系。下一章我们将进入Unity图形渲染与Shader编程的世界,探索如何通过视觉表现提升游戏体验。

4. 图形渲染与Shader编程

4.1 Unity渲染管线基础

4.1.1 渲染流程概述(几何阶段、光栅化阶段)

Unity渲染管线是图形渲染的基础,它定义了从三维模型到最终屏幕像素的转换过程。整个渲染流程可以分为几个主要阶段,其中最关键的两个阶段是 几何阶段(Geometry Stage) 光栅化阶段(Rasterization Stage)

几何阶段

几何阶段主要负责处理三维模型的顶点数据,包括以下几个子阶段:

  • 模型变换(Model Transformation) :将物体的局部坐标转换为世界坐标。
  • 视图变换(View Transformation) :将世界坐标转换为摄像机坐标(观察坐标系)。
  • 投影变换(Projection Transformation) :将观察坐标系转换为裁剪坐标系,通常是透视投影或正交投影。
  • 裁剪(Clipping) :将超出视景体(Frustum)范围的几何体部分裁剪掉。
  • 屏幕映射(Screen Mapping) :将裁剪坐标系映射到屏幕坐标系。
光栅化阶段

光栅化阶段负责将几何数据转换为像素,并进行光照、纹理采样等操作,主要包括:

  • 三角形设置(Triangle Setup) :为每个三角形建立扫描线区间。
  • 三角形遍历(Triangle Traversal) :判断每个像素是否在三角形内。
  • 片元着色(Fragment Shading) :对每个像素进行颜色计算,包括光照、纹理采样等。
  • 测试与混合(Tests and Blending) :进行深度测试、模板测试、颜色混合等操作,最终输出像素颜色。

下面是一个简化版的渲染管线流程图:

graph TD
    A[三维模型] --> B(模型变换)
    B --> C(视图变换)
    C --> D(投影变换)
    D --> E(裁剪)
    E --> F(屏幕映射)
    F --> G(三角形设置)
    G --> H(三角形遍历)
    H --> I(片元着色)
    I --> J(测试与混合)
    J --> K[屏幕输出]

4.1.2 材质与着色器的基本关系

材质(Material)和着色器(Shader)是Unity中控制物体外观的两个核心概念。

  • Shader :定义了如何计算物体表面的颜色。它是一段运行在GPU上的程序,通常使用HLSL语言编写。
  • Material :是Shader的实例化,包含了Shader中所需的参数(如颜色、纹理等)。

例如,我们可以创建一个简单的材质,并将一个着色器应用到该材质上:

// 创建材质
Material material = new Material(Shader.Find("Custom/MyShader"));

// 设置材质颜色
material.color = Color.red;

// 将材质应用到物体的渲染器上
Renderer renderer = GetComponent<Renderer>();
renderer.material = material;

参数说明
- Shader.Find("Custom/MyShader") :查找名为 Custom/MyShader 的着色器。
- material.color :设置材质的基本颜色。
- renderer.material :将材质应用到物体的渲染组件上。

通过这种方式,我们可以动态控制物体的外观。着色器可以定义多个属性,如纹理、法线贴图、反射系数等,这些都可以在材质中进行设置。

4.1.3 渲染模式与混合方式

Unity支持多种渲染模式(Rendering Mode),主要包括:

渲染模式 描述 使用场景
Opaque 不透明,不启用混合 固体物体(如墙体)
Cutout 剪裁模式,通过透明通道裁剪像素 树叶、铁网等
Fade 透明,启用混合但不写入深度缓冲 玻璃、半透明材质
Transparent 透明,使用更复杂的混合方式 动态透明效果(如火焰)
混合方式(Blending)

混合方式决定了物体颜色如何与背景颜色进行混合。常见的混合模式包括:

  • Blend SrcAlpha OneMinusSrcAlpha :标准透明混合,适用于大多数透明物体。
  • Blend One One :加法混合,适用于发光效果。
  • Blend DstColor Zero :调制混合,常用于光照贴图。

我们可以在Shader中设置渲染模式和混合方式,如下所示:

SubShader {
    Tags { "Queue"="Transparent" "RenderType"="Transparent" }
    LOD 200

    Pass {
        Blend SrcAlpha OneMinusSrcAlpha
        ZWrite Off

        CGPROGRAM
        #pragma vertex vert
        #pragma fragment frag

        fixed4 frag (v2f i) : SV_Target {
            return fixed4(1, 0, 0, 0.5); // 半透明红色
        }
        ENDCG
    }
}

代码逻辑分析
- Tags :设置渲染队列和渲染类型,这里设置为透明队列。
- Blend SrcAlpha OneMinusSrcAlpha :启用标准透明混合模式。
- ZWrite Off :关闭深度写入,避免透明物体遮挡其他物体。
- fixed4(1, 0, 0, 0.5) :输出颜色为红色,透明度为0.5。

通过灵活设置渲染模式和混合方式,我们可以实现丰富的视觉效果,从玻璃、火焰到粒子系统等。

4.2 Shader编程入门

4.2.1 HLSL语言基础与语法结构

HLSL(High-Level Shader Language)是Unity中用于编写Shader的语言,语法类似于C语言。它主要用于编写顶点着色器(Vertex Shader)和片段着色器(Fragment Shader)。

基本结构

一个最简单的Shader结构如下:

Shader "Custom/BasicShader" {
    SubShader {
        Tags { "RenderType"="Opaque" }

        Pass {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            struct appdata {
                float4 vertex : POSITION;
            };

            struct v2f {
                float4 pos : SV_POSITION;
            };

            v2f vert (appdata v) {
                v2f o;
                o.pos = UnityObjectToClipPos(v.vertex);
                return o;
            }

            fixed4 frag (v2f i) : SV_Target {
                return fixed4(1, 1, 1, 1); // 白色
            }
            ENDCG
        }
    }
}

代码逻辑分析
- Shader "Custom/BasicShader" :定义Shader名称。
- appdata :顶点输入结构,包含顶点位置信息。
- v2f :顶点输出结构,包含裁剪空间中的位置。
- vert() :顶点着色器函数,将顶点从模型空间转换到裁剪空间。
- frag() :片段着色器函数,返回每个像素的颜色。

常用变量类型
类型 说明
float 单精度浮点数
float2 二维向量
float3 三维向量
float4 四维向量
fixed 固定精度浮点数,适用于颜色
half 半精度浮点数,适用于光照计算

通过HLSL语言,我们可以实现从简单的颜色输出到复杂的光照、阴影、法线贴图等效果。

4.2.2 表面着色器(Surface Shader)与顶点片段着色器(Vertex Fragment Shader)对比

在Unity中,主要有两种Shader编写方式: 表面着色器(Surface Shader) 顶点片段着色器(Vertex Fragment Shader)

表面着色器(Surface Shader)
  • 特点:使用 SurfaceFunction 描述材质属性(如Albedo、Normal、Emission等),Unity自动处理光照计算。
  • 优点:代码简洁,适合快速实现标准光照效果。
  • 缺点:灵活性较低,难以实现复杂光照或非标准效果。

示例代码:

Shader "Custom/SurfaceShaderExample" {
    SubShader {
        Tags { "RenderType"="Opaque" }

        CGPROGRAM
        #pragma surface surf Lambert

        struct Input {
            float2 uv_MainTex;
        };

        sampler2D _MainTex;

        void surf (Input IN, inout SurfaceOutput o) {
            fixed4 c = tex2D(_MainTex, IN.uv_MainTex);
            o.Albedo = c.rgb;
            o.Alpha = c.a;
        }
        ENDCG
    }
}
顶点片段着色器(Vertex Fragment Shader)
  • 特点:完全控制顶点和片段处理流程,适合实现复杂效果。
  • 优点:高度灵活,适合高级光照、粒子系统、后处理等。
  • 缺点:需要手动处理光照、变换等计算,代码复杂度高。

示例代码:

Shader "Custom/VertexFragmentShader" {
    SubShader {
        Pass {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            struct appdata {
                float4 vertex : POSITION;
                float3 normal : NORMAL;
            };

            struct v2f {
                float4 pos : SV_POSITION;
                float3 worldNormal : TEXCOORD0;
            };

            v2f vert (appdata v) {
                v2f o;
                o.pos = UnityObjectToClipPos(v.vertex);
                o.worldNormal = UnityObjectToWorldNormal(v.normal);
                return o;
            }

            fixed4 frag (v2f i) : SV_Target {
                float3 normal = normalize(i.worldNormal);
                float3 lightDir = normalize(_WorldSpaceLightPos0.xyz);
                float diff = max(0, dot(normal, lightDir));
                return fixed4(diff, diff, diff, 1); // 漫反射光照
            }
            ENDCG
        }
    }
}

对比总结

特性 表面着色器 顶点片段着色器
编写难度 简单 复杂
光照处理 自动 手动
适用场景 标准光照、快速开发 高级效果、自定义光照
可控性

根据项目需求和开发经验,选择合适的Shader编写方式是关键。

4.2.3 编写第一个光照着色器

我们来编写一个简单的 漫反射光照Shader ,模拟物体在灯光下的明暗变化。

实现步骤:
  1. 定义顶点输入结构 :包含顶点位置和法线信息。
  2. 顶点着色器 :将顶点转换到裁剪空间,并将法线转换到世界空间。
  3. 片段着色器 :计算漫反射光照强度,并输出颜色。

完整Shader代码如下:

Shader "Custom/DiffuseShader" {
    Properties {
        _Color ("Diffuse Color", Color) = (1, 1, 1, 1)
    }

    SubShader {
        Tags { "LightMode"="ForwardBase" }

        Pass {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityLightingCommon.cginc"

            struct appdata {
                float4 vertex : POSITION;
                float3 normal : NORMAL;
            };

            struct v2f {
                float4 pos : SV_POSITION;
                float3 worldNormal : TEXCOORD0;
            };

            fixed4 _Color;

            v2f vert (appdata v) {
                v2f o;
                o.pos = UnityObjectToClipPos(v.vertex);
                o.worldNormal = UnityObjectToWorldNormal(v.normal);
                return o;
            }

            fixed4 frag (v2f i) : SV_Target {
                float3 normal = normalize(i.worldNormal);
                float3 lightDir = normalize(_WorldSpaceLightPos0.xyz);
                float diff = max(0, dot(normal, lightDir));
                return _Color * diff;
            }
            ENDCG
        }
    }
}

参数说明
- _Color :控制物体的基础颜色。
- _WorldSpaceLightPos0 :Unity内置变量,表示当前主光源在世界空间中的方向。
- dot(normal, lightDir) :计算光照强度,值越大越亮。

应用效果:
  • 当物体表面法线与光源方向一致时,亮度最大(白色)。
  • 当法线与光源方向垂直时,亮度为0(黑色)。
  • 当法线与光源方向相反时,亮度也为0。

通过这个简单的光照Shader,我们掌握了光照计算的基本原理,为进一步学习高级光照、阴影、反射等打下基础。

5. 3D图形基础:顶点、法线、摄像机

在Unity中构建3D游戏世界,理解三维图形的基础知识至关重要。本章将深入探讨3D图形的核心概念,包括坐标系统、顶点与法线的处理方式,以及摄像机的控制机制。这些内容不仅构成了Unity图形系统的基础,也为后续的Shader编程、光照计算和物理渲染提供了支撑。我们将从三维空间的数学表示开始,逐步深入到Mesh组件的结构、动态几何体的生成,以及摄像机视角的控制逻辑。

5.1 三维空间与坐标系统

5.1.1 世界坐标、局部坐标与摄像机坐标

在Unity中,对象的位置、方向和缩放等信息是通过坐标系统来表达的。Unity使用右手坐标系,其中X轴指向右,Y轴指向上,Z轴指向屏幕外(向前)。

  • 世界坐标(World Space) :全局坐标系,所有物体的位置最终都会被转换为世界坐标。
  • 局部坐标(Local Space) :相对于父对象的坐标系,常用于父子对象之间的相对变换。
  • 摄像机坐标(View Space) :以摄像机为中心的坐标系统,用于渲染时的视图变换。
// 示例:获取不同坐标系下的位置
Transform objTransform = GetComponent<Transform>();
Vector3 worldPos = objTransform.position; // 获取世界坐标
Vector3 localPos = objTransform.localPosition; // 获取局部坐标
Vector3 cameraPos = Camera.main.transform.InverseTransformPoint(worldPos); // 转换为摄像机坐标

代码解析:
- position 返回物体在世界空间中的位置。
- localPosition 返回物体相对于其父物体的位置。
- InverseTransformPoint 用于将世界坐标转换为相对于某个变换的坐标(此处为摄像机坐标)。

5.1.2 向量运算与矩阵变换

向量和矩阵是3D图形学中的基本数学工具。向量用于表示方向或位移,矩阵用于进行坐标变换(平移、旋转、缩放)。

// 示例:使用向量与矩阵进行坐标变换
Vector3 direction = new Vector3(1, 0, 0); // 定义一个方向向量
Quaternion rotation = Quaternion.Euler(0, 45, 0); // 绕Y轴旋转45度
Vector3 rotatedDirection = rotation * direction; // 旋转向量

Matrix4x4 translationMatrix = Matrix4x4.Translate(new Vector3(5, 0, 0)); // 构造平移矩阵
Vector4 transformedVector = translationMatrix.MultiplyPoint(direction); // 应用变换

逻辑分析:
- Quaternion.Euler 用于创建绕指定轴的旋转。
- rotation * direction 表示将方向向量应用旋转。
- Matrix4x4.Translate 创建一个平移矩阵, MultiplyPoint 将向量进行变换。

变换类型 Unity API 说明
平移 Matrix4x4.Translate 沿指定方向移动物体
旋转 Quaternion.Euler 使用欧拉角构造旋转
缩放 Matrix4x4.Scale 对物体进行缩放

5.1.3 视口与投影矩阵

Unity中的摄像机通过 视口(View Frustum) 来定义可见区域,并使用 投影矩阵(Projection Matrix) 将3D坐标转换为2D屏幕坐标。

// 获取摄像机的投影矩阵
Camera cam = Camera.main;
Matrix4x4 projectionMatrix = cam.projectionMatrix;
Debug.Log("Projection Matrix:\n" + projectionMatrix);

参数说明:
- projectionMatrix 是摄像机用于将视图空间坐标转换为裁剪空间坐标的矩阵。
- 通常包括正交投影和透视投影两种模式,可通过 Camera.orthographic 切换。

graph TD
    A[世界坐标] --> B[视图坐标]
    B --> C[裁剪坐标]
    C --> D[屏幕坐标]
    D --> E[最终像素]

5.2 顶点与法线数据处理

5.2.1 Mesh组件结构解析

在Unity中,Mesh(网格)是构建3D模型的基础。一个Mesh通常由以下部分组成:

  • 顶点数组(Vertices) :表示模型几何形状的点集合。
  • 三角形数组(Triangles) :定义顶点如何组成三角形。
  • 法线数组(Normals) :用于光照计算的方向向量。
  • UV数组(UVs) :用于纹理映射的坐标。
// 创建一个简单的三角形网格
Mesh mesh = new Mesh();
Vector3[] vertices = new Vector3[]
{
    new Vector3(0, 0, 0),
    new Vector3(1, 0, 0),
    new Vector3(0, 1, 0)
};

int[] triangles = new int[] { 0, 1, 2 };
Vector3[] normals = new Vector3[]
{
    Vector3.forward,
    Vector3.forward,
    Vector3.forward
};

mesh.vertices = vertices;
mesh.triangles = triangles;
mesh.normals = normals;

逻辑分析:
- vertices 定义三个顶点构成一个三角形。
- triangles 数组表示顶点索引,构成三角形面片。
- normals 定义每个顶点的法线方向,影响光照效果。

Mesh属性 类型 作用
vertices Vector3[] 定义顶点位置
triangles int[] 定义三角形面片
normals Vector3[] 定义法线方向
uv Vector2[] 定义纹理坐标

5.2.2 动态生成几何体

Unity允许在运行时动态生成Mesh,这对于程序化建模、地形生成等非常有用。

// 动态生成一个矩形网格
void GenerateQuad()
{
    Mesh mesh = new Mesh();
    Vector3[] vertices = new Vector3[4]
    {
        new Vector3(0, 0, 0),
        new Vector3(1, 0, 0),
        new Vector3(0, 1, 0),
        new Vector3(1, 1, 0)
    };

    int[] triangles = new int[6]
    {
        0, 1, 2,
        2, 1, 3
    };

    Vector2[] uvs = new Vector2[4]
    {
        new Vector2(0, 0),
        new Vector2(1, 0),
        new Vector2(0, 1),
        new Vector2(1, 1)
    };

    mesh.vertices = vertices;
    mesh.triangles = triangles;
    mesh.uv = uvs;
    mesh.RecalculateNormals(); // 自动计算法线
    GetComponent<MeshFilter>().mesh = mesh;
}

逻辑分析:
- 使用两个三角形组成一个矩形。
- uv 定义纹理映射坐标。
- RecalculateNormals() 自动根据顶点与三角形生成法线数据。

graph LR
    A[定义顶点] --> B[定义三角形]
    B --> C[定义UV]
    C --> D[生成Mesh]
    D --> E[应用到MeshFilter]

5.2.3 法线方向与光照计算

法线方向决定了物体表面如何与光源交互。Unity中可以通过 Mesh.normals 设置法线,也可以通过 RecalculateNormals() 自动生成。

// 自定义法线方向
Vector3[] customNormals = new Vector3[]
{
    new Vector3(0, 0, 1),
    new Vector3(0, 0, 1),
    new Vector3(0, 0, 1),
    new Vector3(0, 0, 1)
};
mesh.normals = customNormals;

逻辑分析:
- 所有顶点法线均指向Z轴正方向,模拟一个面向摄像机的平面。
- 光照系统将根据此方向计算漫反射与高光。

法线来源 说明 适用场景
手动设置 精确控制法线方向 特殊光照效果
RecalculateNormals 自动根据几何结构计算 通用模型处理
导入模型自带 来自3D建模软件 高精度模型

5.3 摄像机控制与视角管理

5.3.1 摄像机的移动与旋转控制

Unity摄像机可以通过脚本实现自由控制,适用于第一人称或第三人称视角。

// 基础摄像机控制脚本
public class CameraController : MonoBehaviour
{
    public Transform target;
    public float distance = 5.0f;
    public float xSpeed = 120.0f;
    public float ySpeed = 60.0f;

    private float x = 0.0f;
    private float y = 0.0f;

    void Start()
    {
        Vector3 angles = transform.eulerAngles;
        x = angles.y;
        y = angles.x;
    }

    void LateUpdate()
    {
        x += Input.GetAxis("Mouse X") * xSpeed * Time.deltaTime;
        y -= Input.GetAxis("Mouse Y") * ySpeed * Time.deltaTime;

        y = Mathf.Clamp(y, -20, 80); // 限制上下视角

        Quaternion rotation = Quaternion.Euler(y, x, 0);
        transform.position = rotation * new Vector3(0, 0, -distance) + target.position;
        transform.LookAt(target);
    }
}

逻辑分析:
- 使用 Input.GetAxis("Mouse X/Y") 获取鼠标输入。
- Quaternion.Euler 构造旋转角度。
- transform.LookAt 保持摄像机始终朝向目标。

5.3.2 多摄像机切换与叠加

Unity支持多个摄像机同时工作,可以实现UI叠加、小地图、画中画等效果。

// 多摄像机切换脚本
public class CameraSwitcher : MonoBehaviour
{
    public Camera[] cameras;

    private int currentCam = 0;

    void Update()
    {
        if (Input.GetKeyDown(KeyCode.C))
        {
            SwitchCamera();
        }
    }

    void SwitchCamera()
    {
        cameras[currentCam].enabled = false;
        currentCam = (currentCam + 1) % cameras.Length;
        cameras[currentCam].enabled = true;
    }
}

逻辑分析:
- 通过启用/禁用不同摄像机实现切换。
- KeyCode.C 触发切换逻辑。

摄像机叠加方式 说明 应用场景
层级叠加 多摄像机渲染到同一渲染目标 UI叠加
分屏渲染 渲染到不同视口 双人游戏
渲染到纹理 渲染到RenderTexture 镜面、监控

5.3.3 第三人称视角与跟随机制

第三人称视角通常用于角色扮演游戏或动作游戏,需要实现摄像机围绕角色旋转并保持一定距离。

// 第三人称摄像机跟随
public class ThirdPersonCamera : MonoBehaviour
{
    public Transform target;
    public float distance = 5.0f;
    public float height = 2.0f;
    public float smoothSpeed = 0.125f;

    void LateUpdate()
    {
        Vector3 desiredPosition = target.position - transform.forward * distance + Vector3.up * height;
        Vector3 smoothedPosition = Vector3.Lerp(transform.position, desiredPosition, smoothSpeed);
        transform.position = smoothedPosition;
        transform.LookAt(target);
    }
}

逻辑分析:
- desiredPosition 计算目标位置后方加高度。
- Lerp 实现摄像机平滑移动。
- LookAt 保持摄像机始终对准角色。

graph TD
    A[获取目标位置] --> B[计算期望摄像机位置]
    B --> C[平滑移动到目标]
    C --> D[摄像机始终看向角色]

本章内容从三维空间的数学基础出发,深入解析了Unity中Mesh组件的结构、顶点与法线数据的处理方式,以及摄像机控制机制。通过代码示例与流程图结合,帮助读者掌握3D图形处理的核心技术,为后续高级图形编程打下坚实基础。

6. Unity物理引擎:刚体与碰撞器

Unity的物理引擎是游戏开发中实现真实物理交互的核心模块之一,它基于NVIDIA PhysX引擎,提供了丰富的API和组件,能够处理刚体运动、碰撞检测、触发器响应、关节连接、力的施加等。本章将深入讲解Unity物理系统的基本组成,重点围绕 刚体(Rigidbody) 碰撞器(Collider) 展开,涵盖其工作原理、使用方法、事件处理以及性能优化技巧。

6.1 物理引擎基本概念

Unity的物理系统由多个核心组件构成,主要包括刚体(Rigidbody)、碰撞器(Collider)、物理材质(Physics Material)和物理管理器(Physics Manager)。理解这些基本概念是掌握物理交互的前提。

6.1.1 刚体动力学基础

刚体(Rigidbody)是Unity中控制物体物理行为的核心组件。当一个GameObject添加了Rigidbody组件后,它将受到物理引擎的控制,包括重力、速度、加速度、力的作用等。

// 示例:通过脚本为物体添加刚体并施加力
using UnityEngine;

public class RigidbodyExample : MonoBehaviour
{
    public float force = 10f;

    private Rigidbody rb;

    void Start()
    {
        rb = GetComponent<Rigidbody>();
        rb.AddForce(Vector3.forward * force, ForceMode.Impulse);
    }
}

逐行解析:

  • rb = GetComponent<Rigidbody>(); :获取当前物体的Rigidbody组件。
  • rb.AddForce(Vector3.forward * force, ForceMode.Impulse); :向物体施加一个瞬时力,使其沿Z轴向前移动。

参数说明:

  • Vector3.forward :表示Z轴正方向。
  • ForceMode.Impulse :表示施加的是瞬时冲量,适用于爆炸、跳跃等瞬间动作。

刚体的运动模式分为 动力学刚体 运动学刚体

  • 动力学刚体(isKinematic = false) :受物理引擎控制,可响应力、碰撞等。
  • 运动学刚体(isKinematic = true) :不受物理引擎影响,适用于手动控制移动的物体,如门、电梯等。

6.1.2 物理材质与摩擦力设置

Unity允许通过 物理材质(Physics Material) 来模拟不同表面之间的摩擦力和弹性。例如,金属表面比橡胶表面更滑。

创建物理材质步骤:

  1. 在Project窗口中右键 → Create → Physics Material。
  2. 设置 Dynamic Friction (动态摩擦)、 Static Friction (静态摩擦)和 Bounciness (弹性系数)。
  3. 将该材质拖到碰撞器的Material字段中。
参数名称 描述
Dynamic Friction 物体滑动时的摩擦力
Static Friction 静止时防止滑动的摩擦力
Bounciness 碰撞时反弹的程度

注意: Unity默认的碰撞材质是 PhysicMaterial.defaultMaterial ,如果没有显式指定材质,将使用默认值。

6.1.3 重力与力的施加方式

Unity中刚体是否受重力影响可通过 useGravity 属性控制:

rb.useGravity = false; // 关闭重力

施加力的方式有四种模式,可通过 ForceMode 枚举指定:

ForceMode 枚举 描述
Force 持续力,适合模拟引擎推力
Acceleration 直接施加加速度
Impulse 瞬时力,适合模拟爆炸、击打
VelocityChange 直接改变速度,忽略质量

示例:连续施加力

void Update()
{
    if (Input.GetKey(KeyCode.Space))
    {
        rb.AddForce(Vector3.up * force);
    }
}

6.2 碰撞检测与触发器

碰撞检测是物理系统中最常见的功能之一,Unity通过碰撞器组件来定义物体的物理边界,并通过事件函数处理碰撞和触发逻辑。

6.2.1 碰撞器类型与形状设置

Unity提供了多种碰撞器组件,适用于不同的几何形状:

碰撞器组件 描述
BoxCollider 盒状碰撞器,适用于立方体
SphereCollider 球形碰撞器,适用于圆形物体
CapsuleCollider 胶囊形碰撞器,常用于角色
MeshCollider 使用网格模型的形状进行碰撞检测,性能较高
CharacterController 专为角色设计的碰撞器,不与物理引擎完全集成

注意事项:

  • MeshCollider应尽量与低多边形模型配合使用,以避免性能问题。
  • 多个碰撞器可以组合使用,实现复杂碰撞形状(如角色模型)。

6.2.2 OnCollision与OnTrigger事件区别

Unity提供了两种碰撞响应机制:

  • OnCollisionEnter/Stay/Exit :用于物理碰撞,需要至少一个物体带有Rigidbody。
  • OnTriggerEnter/Stay/Exit :用于触发检测,不产生物理反应,需勾选碰撞器的 Is Trigger 选项。
// 碰撞事件示例
private void OnCollisionEnter(Collision collision)
{
    Debug.Log("碰撞到了:" + collision.gameObject.name);
}

// 触发事件示例
private void OnTriggerEnter(Collider other)
{
    Debug.Log("触发到了:" + other.gameObject.name);
}

区别总结:

类型 是否产生物理反应 是否需要Rigidbody 是否触发事件
OnCollision
OnTrigger

6.2.3 碰撞信息的获取与处理

OnCollisionEnter 中,可以通过 Collision 对象获取碰撞信息,如碰撞点、法线方向、相对速度等。

private void OnCollisionEnter(Collision collision)
{
    foreach (ContactPoint contact in collision.contacts)
    {
        Debug.DrawRay(contact.point, contact.normal, Color.red, 2f);
        Debug.Log("接触点:" + contact.point);
        Debug.Log("法线方向:" + contact.normal);
    }
}

Collision对象常用属性:

  • contacts :碰撞点集合。
  • relativeVelocity :两个物体的相对速度。
  • gameObject :发生碰撞的另一个物体。

6.3 物理模拟优化

Unity物理模拟的性能优化是确保游戏流畅运行的关键,尤其是在多物体交互、复杂碰撞检测等场景中。

6.3.1 固定时间步长设置

Unity物理模拟是基于固定时间步长(Fixed Timestep)进行更新的,默认为0.02秒(即每秒50次)。可以通过 Edit → Project Settings → Time 调整。

graph TD
    A[Update] --> B[FixedUpdate]
    B --> C[物理模拟]
    C --> D[碰撞检测]
    D --> E[力的计算]

优化建议:

  • 对性能要求高的游戏,可适当增加Fixed Timestep(如0.03秒),但可能影响物理精度。
  • 对物理精度要求高的游戏,建议保持默认或更低的值。

6.3.2 射线检测与物理查询

Unity提供了一套物理查询API,如 Physics.Raycast Physics.SphereCast 等,用于非碰撞事件的物理检测。

void Update()
{
    Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
    RaycastHit hit;

    if (Physics.Raycast(ray, out hit))
    {
        Debug.Log("点击到了:" + hit.collider.name);
        Debug.DrawLine(ray.origin, hit.point, Color.green);
    }
}

参数说明:

  • ray :从摄像机发射的射线。
  • hit :射线命中信息。
  • Physics.Raycast 返回布尔值,表示是否命中。

6.3.3 多物体交互与关节系统

Unity支持多种关节组件(Joint),用于模拟物体之间的连接关系,如铰链、弹簧、固定关节等。

常见关节类型:

关节类型 描述
HingeJoint 铰链关节,限制旋转轴
SpringJoint 弹簧关节,提供拉力
FixedJoint 固定关节,绑定两个物体
CharacterJoint 用于角色骨骼系统
ConfigurableJoint 可配置关节,高度自定义

示例:添加铰链关节

HingeJoint hinge = gameObject.AddComponent<HingeJoint>();
hinge.connectedBody = otherRigidbody;
hinge.axis = Vector3.up;
hinge.useSpring = true;

参数说明:

  • connectedBody :连接的刚体。
  • axis :旋转轴。
  • useSpring :是否启用弹簧效果。

6.3.4 物理性能优化技巧

技巧 说明
使用Layer Collision Matrix 控制不同层之间的碰撞关系,避免不必要的碰撞检测
减少Rigidbody数量 非动态物体尽量不加Rigidbody
使用静态碰撞器 静态物体使用MeshCollider并勾选 Static
合理使用Is Kinematic 对手动控制物体禁用物理计算
限制FixedUpdate频率 避免物理计算过于频繁

通过本章的学习,读者应能熟练掌握Unity物理引擎的核心组件与事件处理机制,并具备对物理性能进行调优的能力。下一章将深入探讨游戏逻辑设计与状态机实现,进一步提升游戏开发的结构化思维与工程实践能力。

7. 游戏逻辑设计与状态机实现

在Unity游戏开发中,游戏逻辑的设计直接决定了游戏的可扩展性、可维护性和可读性。良好的逻辑结构可以有效避免“意大利面条式”代码的出现,提升团队协作效率。本章将围绕 游戏状态管理 AI行为逻辑实现 以及 游戏主循环与事件系统 展开深入讲解,帮助开发者构建结构清晰、易于维护的游戏逻辑系统。

7.1 游戏状态管理

游戏状态管理是构建复杂游戏逻辑的核心部分。通过状态机(State Machine)的设计,可以清晰地划分不同阶段的行为逻辑,如游戏菜单、游戏进行中、暂停、胜利/失败等。

7.1.1 状态模式设计原则

状态模式是一种行为设计模式,它允许对象在其内部状态改变时改变其行为。其核心设计原则包括:

  • 单一职责原则 :每个状态类只负责一个状态的行为逻辑。
  • 开闭原则 :新增状态时无需修改已有状态类。
  • 依赖倒置原则 :状态管理器应依赖于状态接口,而非具体实现。

7.1.2 游戏状态机的结构设计

我们可以使用枚举定义状态,并通过接口和具体类实现状态逻辑:

// 定义状态枚举
public enum GameState
{
    MainMenu,
    Playing,
    Paused,
    GameOver
}

// 定义状态接口
public interface IGameState
{
    void Enter();
    void Update();
    void Exit();
}

状态管理器负责管理当前状态并调用相应的方法:

public class GameStateManager : MonoBehaviour
{
    private IGameState currentState;

    public void ChangeState(IGameState newState)
    {
        if (currentState != null)
            currentState.Exit();

        currentState = newState;
        if (currentState != null)
            currentState.Enter();
    }

    private void Update()
    {
        if (currentState != null)
            currentState.Update();
    }
}

7.1.3 状态之间的切换与过渡逻辑

PlayingState 切换到 PausedState 为例:

public class PlayingState : IGameState
{
    public void Enter()
    {
        Time.timeScale = 1f;
        Debug.Log("游戏开始");
    }

    public void Update()
    {
        if (Input.GetKeyDown(KeyCode.Escape))
        {
            GameManager.Instance.ChangeState(new PausedState());
        }
    }

    public void Exit()
    {
        Debug.Log("退出游戏状态");
    }
}

通过这种方式,状态之间的切换清晰明了,且易于扩展和维护。

7.2 AI行为逻辑实现

在游戏开发中,AI的行为逻辑决定了NPC的智能化程度。常见的AI逻辑包括巡逻、追击、攻击等行为,而状态机和行为树是实现这些行为的两种主流方式。

7.2.1 简单AI状态切换(巡逻、追击、攻击)

我们以一个简单的敌人AI为例,展示状态切换逻辑:

public enum AIState
{
    Patrol,
    Chase,
    Attack
}

public class EnemyAI : MonoBehaviour
{
    private AIState currentState;

    private Transform player;
    private float chaseRange = 10f;

    void Start()
    {
        player = GameObject.FindGameObjectWithTag("Player").transform;
        currentState = AIState.Patrol;
    }

    void Update()
    {
        switch (currentState)
        {
            case AIState.Patrol:
                Patrol();
                if (Vector3.Distance(transform.position, player.position) < chaseRange)
                    currentState = AIState.Chase;
                break;
            case AIState.Chase:
                Chase();
                if (Vector3.Distance(transform.position, player.position) < 2f)
                    currentState = AIState.Attack;
                break;
            case AIState.Attack:
                Attack();
                if (Vector3.Distance(transform.position, player.position) > 3f)
                    currentState = AIState.Chase;
                break;
        }
    }

    void Patrol() { /* 巡逻逻辑 */ }
    void Chase() { /* 追击逻辑 */ }
    void Attack() { /* 攻击逻辑 */ }
}

7.2.2 使用行为树实现复杂AI

行为树是一种更高级的AI逻辑结构,适用于需要多个条件判断和行为组合的场景。其基本结构如下:

graph TD
    A[Root] --> B{玩家可见?}
    B -- 是 --> C{距离近?}
    C -- 是 --> D[攻击]
    C -- 否 --> E[追击]
    B -- 否 --> F[巡逻]

Unity中可以使用插件(如Behavior Designer)或自定义行为树节点来实现。

7.2.3 状态机与行为树的对比分析

特性 状态机 行为树
实现复杂度 较低 较高
可读性 中等
可扩展性 中等
适合场景 简单有限状态切换 复杂条件判断与行为组合

状态机适合小型AI逻辑,而行为树更适合需要复杂逻辑判断的NPC AI设计。

7.3 游戏主循环与事件系统

游戏主循环是整个游戏运行的核心流程,事件系统则是实现模块间通信的重要手段。

7.3.1 游戏启动、运行与结束流程

Unity游戏的主循环流程如下:

  1. 初始化阶段 :加载主菜单、初始化系统配置。
  2. 运行阶段 :处理用户输入、更新游戏状态、渲染画面。
  3. 结束阶段 :清理资源、保存数据、退出游戏。
public class GameManager : MonoBehaviour
{
    public static GameManager Instance;

    void Awake()
    {
        if (Instance == null)
            Instance = this;
        else
            Destroy(gameObject);

        DontDestroyOnLoad(gameObject);
    }

    void Start()
    {
        LoadMainMenu();
    }

    void LoadMainMenu()
    {
        SceneManager.LoadScene("MainMenu");
    }

    public void StartGame()
    {
        SceneManager.LoadScene("MainGame");
    }

    public void ExitGame()
    {
        Application.Quit();
    }
}

7.3.2 自定义事件系统的实现

事件系统可以帮助我们解耦游戏逻辑模块。我们可以使用C#的 Action 委托实现简单的事件系统:

public class EventManager
{
    public static Action OnPlayerDeath;

    public static void PlayerDied()
    {
        OnPlayerDeath?.Invoke();
    }
}

// 订阅事件
EventManager.OnPlayerDeath += HandlePlayerDeath;

// 触发事件
EventManager.PlayerDied();

通过这种方式,不同组件可以响应同一事件,而无需相互引用。

7.3.3 异步加载与资源加载管理

Unity提供了 SceneManager.LoadSceneAsync 来实现异步加载,避免游戏卡顿:

IEnumerator LoadSceneAsync(string sceneName)
{
    AsyncOperation asyncLoad = SceneManager.LoadSceneAsync(sceneName);

    while (!asyncLoad.isDone)
    {
        float progress = Mathf.Clamp01(asyncLoad.progress / 0.9f);
        Debug.Log($"加载进度:{progress * 100}%");
        yield return null;
    }
}

资源加载方面,可使用 Resources.Load Addressables 系统实现按需加载,提高内存管理效率。

下一章节将围绕UI系统与交互设计展开,继续探讨如何将游戏逻辑与界面进行有效结合。

本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:C#游戏开发涵盖编程基础、Unity引擎使用、图形渲染、物理系统、游戏逻辑与AI、音画控制、用户界面设计、网络编程及性能优化等关键内容。本课程围绕Unity平台展开,通过实战教学帮助开发者掌握C#在游戏开发中的核心应用,涵盖从基础语法到高级功能的完整开发流程,适合希望进入游戏开发领域的学习者和开发者。


本文还有配套的精品资源,点击获取
menu-r.4af5f7ec.gif

更多推荐