cover
UNITY SCRIPTING

Unity
脚本设计

全书10章教学课件

[美] Alan Thorn 著 清华大学出版社有限公司

专业教材配套课件

API使用详解

01-10
CONTENTS

全书目录

01

Unity中的C#语言

C#基础语法、变量、条件语句、循环、函数、面向对象、属性、SendMessage

02

调试机制

编译错误、Debug.Log、可视化调试、Profiler性能分析、断点调试

03

单例模式、GameObject与场景世界

重点API:GetComponent、Find、Time类、DontDestroyOnLoad、单例模式实现

04

事件驱动程序设计

重点API:委托delegate、event关键字、EventManager架构、MonoBehaviour事件

05

相机、渲染和场景

Camera组件API、投影模式、可见性检测、后处理、相机震动

06

与Mono协同工作

重点API:List/Dictionary/Stack、LINQ、协程、正则表达式、文件操作

07

人工智能

NavMesh导航、NavMeshAgent API、有限状态机FSM、AI行为实现

08

定制Unity编辑器

编辑器扩展、自定义Inspector、PropertyDrawer、反射、本地化系统

09

纹理、模型和2D元素

天空盒、程序化网格Mesh、UV动画、纹理绘制系统

10

资源控制和其他

Git版本控制、Resources加载、AssetBundles、数据持久化

第3、4、6章为重点API章节,将详细讲解相关API使用方法

共132页
chapter1
01

Unity中的
C#语言

C#基础语法与Unity脚本入门

脚本创建
面向对象
组件通信
CHAPTER 01

为何选择C#语言

类型安全

C#是强类型语言,编译时进行类型检查,减少运行时错误。相比动态类型语言,更容易捕获bug,提高代码可靠性。

面向对象

完整的OOP支持:封装、继承、多态。适合构建复杂的游戏系统,代码可重用性高,易于维护和扩展。

与Unity深度集成

Unity引擎原生支持C#,提供完整的API访问。MonoBehaviour基类、组件系统、生命周期函数都是为C#设计的。

跨平台支持

一次编写,多平台运行。支持Windows、macOS、Linux、iOS、Android、WebGL、主机平台等20+平台。

丰富的类库

.NET Framework/.NET Core提供海量功能:文件IO、网络通信、数据结构、LINQ、多线程、加密等。

语言对比

C# vs UnityScript (JavaScript) C#性能更好,类型更安全
C# vs Boo C#社区更大,文档更丰富
C# vs C++ C#开发效率更高,内存更安全
Unity 2017+版本已弃用UnityScript和Boo,C#成为唯一官方支持语言
CHAPTER 01

创建脚本文件

创建流程

1

在Project窗口中右键点击

选择 Create → C# Script

2

输入脚本名称

命名规范:使用 PascalCase,如 PlayerController

3

双击脚本打开编辑器

默认使用 Visual Studio 或 VS Code

4

将脚本附加到 GameObject

拖拽脚本到 Inspector 窗口或 Add Component

命名规范

  • 类名使用 PascalCase:PlayerController, EnemyAI
  • 脚本文件名与类名必须一致
  • 避免使用特殊字符和空格
  • 使用有意义的描述性名称

自动生成的脚本模板

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class PlayerController : MonoBehaviour
{
    // Start is called before 
    // the first frame update
    void Start()
    {
        
    }

    // Update is called once per frame
    void Update()
    {
        
    }
}

继承 MonoBehaviour

使脚本可以作为组件附加到 GameObject

Start() - 初始化方法

在第一次 Update 前调用一次

Update() - 每帧更新

每帧调用一次,用于常规更新逻辑

CHAPTER 01

脚本的实例化操作

MonoBehaviour 生命周期

Awake()

脚本实例化时调用,用于初始化自身

执行时机:对象创建时(仅一次)

OnEnable()

对象启用时调用

执行时机:对象激活或脚本启用时

Start()

第一次 Update 前调用,用于初始化依赖

执行时机:第一次帧更新前(仅一次)

Update()

每帧调用,用于常规逻辑更新

执行时机:每帧(帧率相关)

FixedUpdate()

固定时间间隔调用,用于物理更新

执行时机:固定时间步长(默认0.02s)

生命周期代码示例

public class LifecycleDemo : MonoBehaviour
{
    void Awake()
    {
        Debug.Log("1. Awake - 初始化自身");
    }

    void OnEnable()
    {
        Debug.Log("2. OnEnable - 对象启用");
    }

    void Start()
    {
        Debug.Log("3. Start - 初始化依赖");
    }

    void Update()
    {
        // 每帧执行
    }

    void OnDisable()
    {
        Debug.Log("OnDisable - 对象禁用");
    }

    void OnDestroy()
    {
        Debug.Log("OnDestroy - 对象销毁");
    }
}

执行顺序控制

Edit → Project Settings → Script Execution Order

通过设置 Execution Order 控制脚本执行优先级

数值越小,执行越早(默认值为 0)

重要提示

  • Awake 和 Start 都只执行一次
  • Awake 在 Start 之前执行
  • 对象未激活时 Awake 仍会执行
CHAPTER 01

变量与数据类型

基本数据类型

int

整数

-2,147,483,648 到 2,147,483,647

float

单精度浮点数

7位精度,后缀 f

double

双精度浮点数

15-16位精度

bool

布尔值

true 或 false

string

字符串

Unicode字符序列

char

字符

单个 Unicode 字符

值类型 vs 引用类型

值类型

int, float, bool, struct

存储在栈上,赋值时复制

引用类型

string, class, array

存储在堆上,赋值时引用

Unity 特有数据类型

Vector3

三维向量 (x, y, z)

Vector3 position = new Vector3(1f, 2f, 3f);

Vector2

二维向量 (x, y)

Vector2 uv = new Vector2(0.5f, 0.5f);

Quaternion

四元数,表示旋转

Quaternion rotation = Quaternion.identity;

Color

颜色 (r, g, b, a)

Color red = Color.red;

变量声明与初始化

// 声明变量
public int health = 100;
private float speed = 5.0f;
public string playerName = "Player";

// Unity 类型
public Vector3 startPosition;
public Color playerColor = Color.blue;
CHAPTER 01

条件语句

if-else 语句

if (condition)
{
    // 条件为 true 时执行
}
else if (otherCondition)
{
    // 其他条件为 true 时执行
}
else
{
    // 以上条件都不满足时执行
}

游戏应用示例

if (health <= 0)
{
    Die();
}
else if (health < 30)
{
    ShowWarning();
}
else
{
    UpdateUI();
}

条件运算符

== 等于
!= 不等于
>, < 大于,小于
>=, <= 大于等于,小于等于
&& 逻辑与
|| 逻辑或

switch 语句

switch (expression)
{
    case value1:
        // 执行代码
        break;
    case value2:
        // 执行代码
        break;
    default:
        // 默认执行
        break;
}

游戏状态示例

switch (gameState)
{
    case GameState.Menu:
        ShowMenu();
        break;
    case GameState.Playing:
        UpdateGame();
        break;
    case GameState.Paused:
        ShowPauseUI();
        break;
    default:
        break;
}

三元运算符

// 语法:condition ? trueValue : falseValue

string status = health > 0 ? "Alive" : "Dead";

int maxHealth = isBoss ? 1000 : 100;

使用建议

  • 条件较少时用 if-else
  • 多值判断时用 switch
  • 简单赋值用三元运算符
CHAPTER 01

数组与集合基础

数组声明与初始化

// 声明数组
int[] scores;

// 初始化数组
scores = new int[5];

// 声明并初始化
string[] names = new string[3];

// 直接赋值初始化
int[] health = { 100, 80, 60, 40, 20 };

// Unity 对象数组
public GameObject[] enemies;

数组访问与属性

int first = health[0]; // 访问第一个元素

health[2] = 50; // 修改第三个元素

int count = health.Length; // 获取数组长度

数组遍历

// for 循环遍历
for (int i = 0; i < health.Length; i++)
{
    Debug.Log("Health: " + health[i]);
}

// foreach 循环遍历
foreach (int hp in health)
{
    Debug.Log("Health: " + hp);
}

Unity 中常见的数组应用

存储游戏对象

public GameObject[] waypoints;

管理资源引用

public Material[] materials;

配置数据

public float[] damageValues;

注意事项

  • 数组长度固定,声明后不能改变
  • 索引从 0 开始,最大索引为 Length-1
  • 访问越界会抛出异常
CHAPTER 01

循环结构

for 循环

for (初始化; 条件; 迭代)
{
    // 循环体
}

示例:遍历敌人

for (int i = 0; i < enemies.Length; i++)
{
    enemies[i].TakeDamage(10);
}

适用场景:已知循环次数,需要索引

while 循环

while (condition)
{
    // 条件为 true 时执行
}

示例:等待条件

while (playerHealth > 0)
{
    GameLoop();
}

适用场景:未知循环次数,条件驱动

foreach 循环

foreach (元素类型 变量 in 集合)
{
    // 处理每个元素
}

示例:遍历玩家列表

foreach (Player player in players)
{
    player.UpdateScore();
}

适用场景:遍历集合,不需要索引

循环控制语句

break;

立即退出循环

continue;

跳过当前迭代,继续下一次

⚠️ 避免无限循环

// 错误示例
while (true)  // 没有退出条件!
{
    // 死循环
}
CHAPTER 01

函数与方法

函数声明语法

// 访问修饰符 返回类型 函数名(参数)
public void Attack()
{
    // 函数体
}

private int CalculateDamage(int baseDamage)
{
    return baseDamage * 2;
}

访问修饰符

public 公开访问
private 仅类内部访问
protected 类及子类访问

方法重载

// 同名不同参数
public void TakeDamage(int damage)
{
    health -= damage;
}

public void TakeDamage(int damage, bool isCritical)
{
    int finalDamage = isCritical ? 
        damage * 2 : damage;
    health -= finalDamage;
}

静态方法

public static class MathUtils
{
    public static int Clamp(int value, 
        int min, int max)
    {
        return value < min ? min : 
               (value > max ? max : value);
    }
}

// 调用
int result = MathUtils.Clamp(health, 0, 100);

Unity 生命周期方法

void Start() { }

void Update() { }

void FixedUpdate() { }

CHAPTER 01

事件与委托基础

委托 delegate

// 声明委托类型
public delegate void OnHealthChanged(int newHealth);

// 创建委托实例
public OnHealthChanged healthChangedCallback;

// 订阅事件
void Start()
{
    healthChangedCallback += OnHealthChangedHandler;
}

// 事件处理函数
void OnHealthChangedHandler(int newHealth)
{
    Debug.Log("Health: " + newHealth);
}

委托的特点

  • 类型安全的函数指针
  • 支持多播(多个方法)
  • 实现回调机制

event 关键字

// 声明事件
public event OnHealthChanged OnHealthChangedEvent;

// 触发事件
public void TakeDamage(int damage)
{
    health -= damage;
    if (OnHealthChangedEvent != null)
    {
        OnHealthChangedEvent(health);
    }
}

event 的优势:

只能在声明类中触发,外部只能订阅/取消订阅

Unity 内置事件

OnEnable / OnDisable

对象启用/禁用时

OnDestroy

对象销毁时

OnCollisionEnter / OnTriggerEnter

碰撞检测事件

CHAPTER 01

面向对象程序设计

类的定义与对象创建

// 类定义
public class Player
{
    public string name;
    public int health;
    
    public void Attack()
    {
        Debug.Log(name + " attacks!");
    }
}

// 创建对象
Player player = new Player();
player.name = "Hero";
player.health = 100;
player.Attack();

构造函数

public class Enemy
{
    public int health;
    
    // 构造函数
    public Enemy(int initialHealth)
    {
        health = initialHealth;
    }
}

// 使用
Enemy enemy = new Enemy(50);

继承

// 基类
public class Character
{
    public int health;
    public virtual void Move()
    {
        Debug.Log("Moving");
    }
}

// 派生类
public class Player : Character
{
    public override void Move()
    {
        base.Move();
        Debug.Log("Player moving");
    }
}

多态

public class Enemy : Character
{
    public override void Move()
    {
        Debug.Log("Enemy moving");
    }
}

// 多态使用
Character[] characters = new Character[2];
characters[0] = new Player();
characters[1] = new Enemy();

foreach (Character c in characters)
{
    c.Move(); // 调用各自的重写方法
}
CHAPTER 01

C#属性与访问器

属性的定义

public class Player
{
    private int health;
    
    // 属性定义
    public int Health
    {
        get { return health; }
        set 
        { 
            if (value >= 0)
                health = value; 
        }
    }
}

使用属性

Player player = new Player();
player.Health = 100; // 调用 set
int hp = player.Health; // 调用 get

自动属性

// 自动实现属性
public string PlayerName { get; set; }

// 只读属性
public int MaxHealth { get; private set; }

// 计算属性
public float HealthPercent
{
    get { return (float)Health / MaxHealth; }
}

Unity 中使用属性的最佳实践

封装组件数据

public Vector3 Position 
{
    get { return transform.position; }
    set { transform.position = value; }
}

数据验证

set { health = Mathf.Clamp(value, 0, maxHealth); }
CHAPTER 01

SendMessage与BroadcastMessage

SendMessage

向同一 GameObject 上的其他组件发送消息

// 发送消息
void TakeDamage(int damage)
{
    health -= damage;
    
    // 通知其他组件
    SendMessage("OnHealthChanged", 
                health, 
                SendMessageOptions.DontRequireReceiver);
}

// 接收消息的组件
void OnHealthChanged(int newHealth)
{
    UpdateUI(newHealth);
}

SendMessageOptions:

RequireReceiver - 必须有接收者

DontRequireReceiver - 可以没有接收者

BroadcastMessage

向自身及所有子对象广播消息

// 广播消息给所有子对象
void OnGamePaused()
{
    // 通知自身和所有子对象
    BroadcastMessage("Pause", 
                     SendMessageOptions.DontRequireReceiver);
}

// 子对象中的接收方法
void Pause()
{
    isPaused = true;
}

SendMessageUpwards

向父对象发送消息

// 子对象通知父对象
void OnClicked()
{
    SendMessageUpwards("OnButtonClicked", 
        this.name);
}

⚠️ 性能注意事项

  • SendMessage 使用反射,性能较低
  • 避免在 Update 中频繁调用
  • 大型项目建议使用委托或事件系统
CHAPTER 01

第1章小结

脚本基础

  • C#脚本创建流程
  • MonoBehaviour生命周期
  • Awake/Start/Update等
  • 变量与数据类型
  • Unity特有类型

语法结构

  • 条件语句 if/switch
  • 循环结构 for/while/foreach
  • 数组声明与遍历
  • 函数定义与调用
  • 方法重载

面向对象

  • 类与对象
  • 继承与多态
  • 属性封装
  • 委托与事件
  • SendMessage通信

为后续章节学习奠定语言基础

掌握C#基础语法,为Unity API学习做好准备

下一章

调试机制

chapter2
02

调试机制

掌握Unity项目调试技巧与错误排查

错误诊断
性能分析
断点调试
CHAPTER 02

编译错误与控制台

Unity控制台

Error 错误

阻止程序运行的严重问题,必须修复

Warning 警告

潜在问题,不会阻止运行但建议修复

Log 信息

普通输出信息,用于调试

控制台操作

  • 点击错误跳转到代码位置
  • Clear 清除所有日志
  • Collapse 合并相同日志
  • 过滤按钮筛选错误类型

常见编译错误

; expected

缺少分号

} expected

缺少右大括号

The type or namespace could not be found

缺少using语句或程序集引用

Cannot implicitly convert type

类型不匹配

错误信息解读

错误格式

Assets/Scripts/Player.cs(15,23): error CS1002: ; expected

文件路径(行号,列号): 错误类型 错误描述

CHAPTER 02

Debug.Log调试输出

Debug类方法

Debug.Log(message)

输出普通信息(白色)

Debug.Log("Player health: " + health);

Debug.LogWarning(message)

输出警告(黄色)

Debug.LogWarning("Low health!");

Debug.LogError(message)

输出错误(红色)

Debug.LogError("Player not found!");

格式化输出

// 字符串拼接
Debug.Log("Health: " + health + "/" + maxHealth);

// string.Format
Debug.Log(string.Format("Health: {0}/{1}", 
    health, maxHealth));

// 字符串插值 (C# 6.0+)
Debug.Log($"Health: {health}/{maxHealth}");

条件编译

// 只在编辑器中输出
[Conditional("UNITY_EDITOR")]
void LogDebug(string message)
{
    Debug.Log(message);
}

// 使用宏定义
#if DEBUG
    Debug.Log("Debug info");
#endif

调试最佳实践

  • 使用有意义的日志信息
  • 避免在Update中频繁输出
  • 区分Log/Warning/Error级别
  • 发布前移除或禁用调试代码
CHAPTER 02

可视化调试工具

Debug.DrawLine

在Scene视图中绘制线段

void Update()
{
    // 绘制从起点到终点的红线
    Debug.DrawLine(
        startPoint, 
        endPoint, 
        Color.red);
    
    // 持续时间1秒
    Debug.DrawLine(
        transform.position,
        target.position,
        Color.green,
        1.0f);
}

Debug.DrawRay

绘制射线

void Update()
{
    // 绘制向前的射线
    Debug.DrawRay(
        transform.position,
        transform.forward * 10f,
        Color.blue);
    
    // 检测并绘制射线
    RaycastHit hit;
    if (Physics.Raycast(ray, out hit))
    {
        Debug.DrawLine(ray.origin, hit.point, 
                      Color.yellow);
    }
}

Gizmos类

在Scene视图中绘制辅助图形

void OnDrawGizmos()
{
    // 绘制线框立方体
    Gizmos.color = Color.yellow;
    Gizmos.DrawWireCube(
        transform.position, 
        new Vector3(1, 1, 1));
    
    // 绘制球体
    Gizmos.color = new Color(1, 0, 0, 0.5f);
    Gizmos.DrawSphere(transform.position, 2f);
}

常用Gizmos方法

Gizmos.DrawLine()

绘制线段

Gizmos.DrawWireSphere()

绘制线框球体

Gizmos.DrawWireCube()

绘制线框立方体

Gizmos.DrawIcon()

绘制图标

CHAPTER 02

性能分析工具Profiler

Profiler窗口

Window → Analysis → Profiler

CPU Usage

CPU使用情况,脚本执行时间

GPU Usage

GPU渲染时间

Rendering

渲染统计:Draw Calls、Triangles

Memory

内存使用情况

性能优化建议

减少Draw Calls

使用批处理、图集、合并网格

优化脚本

避免在Update中频繁分配内存

资源优化

压缩纹理、优化模型面数

物理优化

减少碰撞体复杂度

Deep Profile深度分析

启用Deep Profile查看每个函数的详细耗时

注意:Deep Profile会显著降低性能,仅在调试时使用

CHAPTER 02

断点调试

设置断点

在Visual Studio/VS Code中

1. 点击代码行号左侧设置断点

2. 按F9切换断点

3. 红色圆点表示断点已设置

附加到Unity

1. 点击"附加到Unity"按钮

2. 选择运行的Unity实例

3. 在Unity中运行游戏

调试快捷键

F5 继续执行
F10 单步跳过
F11 单步进入
Shift+F5 停止调试

Watch窗口

监视变量值的变化

使用方法

1. 在Watch窗口点击添加

2. 输入变量名或表达式

3. 实时查看值的变化

调用栈 Call Stack

查看函数调用顺序

Update() → Move() → CalculatePosition()

点击函数名跳转到对应代码

Immediate窗口

执行即时代码,测试表达式

? player.health
> 100
? player.transform.position
> (0, 1, 0)
CHAPTER 02

第2章小结

控制台调试

  • Debug.Log/Warning/Error
  • 错误类型识别
  • 格式化输出
  • 条件编译

可视化调试

  • Debug.DrawLine/Ray
  • Gizmos绘制
  • 场景视图辅助
  • 运行时可视化

高级调试

  • Profiler性能分析
  • 断点调试
  • Watch监视窗口
  • 调用栈分析

建立系统的错误排查思路,提高开发效率

掌握多种调试工具,快速定位和解决问题

下一章

单例模式、GameObject与场景世界

chapter3
03

单例模式、GameObject
与场景世界

核心API使用:GameObject操作、组件交互、场景管理

GameObject
GetComponent
Time类
单例模式
CHAPTER 03 - 重点API

GameObject对象核心API

关键属性

name

对象名称

gameObject.name = "Player";

tag

对象标签

gameObject.tag = "Enemy";

layer

层级

gameObject.layer = LayerMask.NameToLayer("UI");

activeSelf

自身激活状态

bool isActive = gameObject.activeSelf;

transform

Transform组件引用

transform.position = Vector3.zero;

核心方法

SetActive(bool value)

设置激活状态

gameObject.SetActive(false);

GetComponent<T>()

获取组件

Renderer renderer = GetComponent<Renderer>();

SendMessage(string methodName)

发送消息

SendMessage("OnDamage", 10);

GameObject与Component关系

GameObject是容器,Component是功能模块

• Transform:位置、旋转、缩放

• Renderer:渲染

• Collider:碰撞

• 自定义脚本组件

CHAPTER 03 - 重点API

组件交互:GetComponent API详解

GetComponent系列方法

GetComponent<T>()

泛型方法获取组件

Rigidbody rb = GetComponent<Rigidbody>();

GetComponent(typeof(T))

非泛型版本

Component comp = GetComponent(typeof(Renderer));

GetComponents<T>()

获取多个同类型组件

Collider[] colliders = GetComponents<Collider>();

GetComponentInChildren<T>()

在子对象中查找

Renderer childRenderer = GetComponentInChildren<Renderer>();

GetComponentInParent<T>()

在父对象中查找

Rigidbody parentRb = GetComponentInParent<Rigidbody>();

完整示例

public class PlayerController : MonoBehaviour
{
    private Rigidbody rb;
    private Animator animator;
    
    void Start()
    {
        // 获取刚体组件
        rb = GetComponent<Rigidbody>();
        
        // 获取动画组件(可能在子对象)
        animator = GetComponentInChildren<Animator>();
        
        // 检查是否获取成功
        if (rb == null)
        {
            Debug.LogError("Rigidbody not found!");
        }
    }
}

⚠️ 性能注意事项

避免在Update中频繁调用

应在Start或Awake中缓存组件引用

GetComponent有一定开销

频繁调用会影响性能

CHAPTER 03 - 重点API

场景中的GameObject操作API

查找GameObject

GameObject.Find(string name)

按名称查找(全局搜索)

GameObject player = GameObject.Find("Player");

GameObject.FindWithTag(string tag)

按标签查找单个对象

GameObject enemy = GameObject.FindWithTag("Enemy");

GameObject.FindGameObjectsWithTag(string tag)

按标签查找所有对象

GameObject[] enemies = GameObject.FindGameObjectsWithTag("Enemy");

按类型查找对象

Object.FindObjectOfType<T>()

查找场景中指定类型的对象

PlayerController player = Object.FindObjectOfType<PlayerController>();

Object.FindObjectsOfType<T>()

查找所有同类型对象

EnemyAI[] enemies = Object.FindObjectsOfType<EnemyAI>();

⚠️ 性能警告

Find方法的问题

  • • 遍历场景中所有对象,性能开销大
  • • 避免在Update中调用
  • • 建议在Start/Awake中缓存引用

替代方案

  • • 使用Inspector拖拽赋值
  • • 使用单例模式管理
  • • 使用事件系统通信

最佳实践示例

public class GameManager : MonoBehaviour
{
    public GameObject player; // Inspector赋值
    private GameObject[] enemies;
    
    void Start()
    {
        // 只在Start中调用一次
        enemies = GameObject.FindGameObjectsWithTag("Enemy");
    }
}
CHAPTER 03 - 重点API

时间与更新:Time类API

Time类核心属性

Time.deltaTime

上一帧耗时(秒)

// 用于帧率无关的运动
transform.Translate(Vector3.forward * speed * Time.deltaTime);

Time.time

游戏开始后的总时间

float elapsedTime = Time.time;

Time.timeScale

时间缩放(0=暂停,1=正常)

Time.timeScale = 0; // 暂停游戏
Time.timeScale = 0.5f; // 慢动作

Time.fixedDeltaTime

固定时间间隔(默认0.02s)

float fixedTimeStep = Time.fixedDeltaTime;

帧率无关的运动

规则1:所有运动都乘以Time.deltaTime

// 错误:帧率相关
transform.Translate(Vector3.forward * speed);

// 正确:帧率无关
transform.Translate(Vector3.forward * speed * Time.deltaTime);

规则2:速度 = 距离/时间

// 每秒旋转90度
transform.Rotate(Vector3.up * 90f * Time.deltaTime);

其他Time属性

Time.frameCount

总帧数

Time.realtimeSinceStartup

不受timeScale影响的实时

Time.captureFramerate

捕获帧率设置

CHAPTER 03 - 重点API

永久对象:DontDestroyOnLoad

DontDestroyOnLoad方法

使对象在场景切换时不被销毁

public class GameManager : MonoBehaviour
{
    void Awake()
    {
        // 使对象在场景切换时保持
        DontDestroyOnLoad(gameObject);
    }
}

常用场景

  • • 游戏管理器(GameManager)
  • • 音频管理器(AudioManager)
  • • 玩家数据(PlayerData)
  • • 设置管理器(SettingsManager)

避免重复创建

单例模式与DontDestroyOnLoad结合

public class GameManager : MonoBehaviour
{
    public static GameManager Instance;
    
    void Awake()
    {
        // 检查是否已存在实例
        if (Instance != null)
        {
            Destroy(gameObject);
            return;
        }
        
        Instance = this;
        DontDestroyOnLoad(gameObject);
    }
}

注意事项

  • DontDestroyOnLoad只对根对象有效
  • 子对象需要单独设置
  • 过多的持久对象会占用内存
CHAPTER 03 - 重点API

单例模式实现详解

单例模式概念

确保类只有一个实例,并提供全局访问点

实现步骤

  1. 私有构造函数
  2. 静态实例引用
  3. 公共访问属性

Unity单例最佳实践

public class GameManager : MonoBehaviour
{
    private static GameManager _instance;
    
    public static GameManager Instance
    {
        get
        {
            if (_instance == null)
            {
                _instance = FindObjectOfType<GameManager>();
            }
            return _instance;
        }
    }
    
    void Awake()
    {
        if (_instance != null && _instance != this)
        {
            Destroy(gameObject);
        }
        else
        {
            _instance = this;
            DontDestroyOnLoad(gameObject);
        }
    }
}

使用单例

// 从任何脚本访问GameManager
GameManager.Instance.AddScore(100);

// 访问属性
int score = GameManager.Instance.CurrentScore;

// 调用方法
GameManager.Instance.LoadLevel(2);

单例的优缺点

优点

  • • 全局访问
  • • 只有一个实例
  • • 延迟初始化

缺点

  • • 隐藏依赖关系
  • • 难以单元测试
  • • 违反单一职责原则
CHAPTER 03 - 重点API

第3章小结与API速查

GameObject查找

GameObject.Find()

FindWithTag()

FindGameObjectsWithTag()

FindObjectOfType()

组件获取

GetComponent<T>()

GetComponents<T>()

GetInChildren<T>()

GetInParent<T>()

时间管理

Time.deltaTime

Time.timeScale

Time.time

Time.fixedDeltaTime

场景持久化

DontDestroyOnLoad(target)

设计模式

单例模式 / 静态模式

建立API使用规范

掌握GameObject操作、组件交互、时间管理核心API

下一章

事件驱动程序设计

chapter4
04

事件驱动程序设计

核心API使用:事件系统、委托、EventManager架构

委托
事件
EventManager
CHAPTER 04 - 重点API

委托delegate基础

委托声明与使用

// 声明委托类型
public delegate void OnHealthChanged(int newHealth);

public class Player : MonoBehaviour
{
    // 创建委托实例
    public OnHealthChanged healthChangedCallback;
    
    public void TakeDamage(int damage)
    {
        health -= damage;
        
        // 触发委托
        if (healthChangedCallback != null)
        {
            healthChangedCallback(health);
        }
    }
}

订阅与取消订阅

public class UIManager : MonoBehaviour
{
    public Player player;
    
    void OnEnable()
    {
        // 订阅事件
        player.healthChangedCallback += OnHealthChanged;
    }
    
    void OnDisable()
    {
        // 取消订阅
        player.healthChangedCallback -= OnHealthChanged;
    }
    
    void OnHealthChanged(int newHealth)
    {
        healthText.text = $"Health: {newHealth}";
    }
}

多播委托

// 多个方法订阅同一个委托
player.healthChangedCallback += UpdateUI;
player.healthChangedCallback += PlaySoundEffect;
player.healthChangedCallback += CheckDeath;

// 触发时所有方法都会执行
player.TakeDamage(10);
// 执行:UpdateUI → PlaySoundEffect → CheckDeath

委托的特点

  • 类型安全的函数指针
  • 支持多播(多个方法)
  • 实现回调机制
  • 松耦合通信

⚠️ 注意事项

触发前检查是否为null

及时取消订阅避免内存泄漏

CHAPTER 04 - 重点API

Action与Func内置委托

Action委托

无返回值的泛型委托

Action

无参数,无返回值

Action onGameStart = () => Debug.Log("Game Start!");

Action<T>

1个参数,无返回值

Action<int> onScoreChanged = (score) => UpdateUI(score);

Action<T1, T2>

2个参数,无返回值(最多16个)

Action<string, int> onItemCollected = (item, count) => { };

Func委托

有返回值的泛型委托

Func<TResult>

无参数,有返回值

Func<int> getScore = () => currentScore;

Func<T, TResult>

1个参数,有返回值

Func<int, bool> isAlive = (health) => health > 0;

Func<T1, T2, TResult>

2个参数,有返回值(最多16个参数)

Func<int, int, int> add = (a, b) => a + b;

Predicate委托

返回bool的委托,用于条件判断

Predicate<int> isEven = (num) => num % 2 == 0;
bool result = isEven(4); // true
="fas fa-save text-[#C8A265] text-xl"> 数据持久化
CHAPTER 10

AssetBundles资源包

构建AssetBundles

// 编辑器脚本
using UnityEditor;

public class BuildAssetBundles
{
    [MenuItem("Assets/Build AssetBundles")]
    static void BuildAllAssetBundles()
    {
        BuildPipeline.BuildAssetBundles(
            "Assets/AssetBundles",
            BuildAssetBundleOptions.None,
            BuildTarget.StandaloneWindows);
    }
}

加载AssetBundles

// 从文件加载
AssetBundle bundle = AssetBundle.LoadFromFile(
    Path.Combine(Application.streamingAssetsPath, "mybundle"));

// 加载资源
GameObject prefab = bundle.LoadAsset<GameObject>("Enemy");
Instantiate(prefab);

// 卸载AssetBundle
bundle.Unload(false); // false不卸载已加载资源

网络加载

IEnumerator DownloadAndLoad()
{
    string url = "http://example.com/bundle";
    
    UnityWebRequest request = 
        UnityWebRequestAssetBundle.GetAssetBundle(url);
    yield return request.SendWebRequest();
    
    if (request.result == UnityWebRequest.Result.Success)
    {
        AssetBundle bundle = DownloadHandlerAssetBundle
            .GetContent(request);
        GameObject obj = bundle.LoadAsset<GameObject>("Asset");
    }
}

AssetBundle优势

  • 减小包体大小
  • 支持热更新
  • 动态加载资源
  • 资源分包管理
CHAPTER 10

数据持久化与游戏保存

PlayerPrefs

简单的键值存储

// 保存数据
PlayerPrefs.SetInt("HighScore", 1000);
PlayerPrefs.SetString("PlayerName", "Hero");
PlayerPrefs.SetFloat("Volume", 0.8f);

// 读取数据
int score = PlayerPrefs.GetInt("HighScore", 0);
string name = PlayerPrefs.GetString("PlayerName", "");

// 删除数据
PlayerPrefs.DeleteKey("HighScore");
PlayerPrefs.DeleteAll();

JSON序列化

[System.Serializable]
public class SaveData
{
    public int level;
    public int score;
    public Vector3 playerPosition;
}

// 保存
SaveData data = new SaveData { level = 5, score = 1000 };
string json = JsonUtility.ToJson(data);
File.WriteAllText(savePath, json);

// 加载
string json = File.ReadAllText(savePath);
SaveData data = JsonUtility.FromJson<SaveData>(json);

存档管理器

public class SaveManager
{
    private static string SavePath => 
        Path.Combine(Application.persistentDataPath, "save.json");
    
    public static void SaveGame(SaveData data)
    {
        string json = JsonUtility.ToJson(data, true);
        File.WriteAllText(SavePath, json);
    }
    
    public static SaveData LoadGame()
    {
        if (!File.Exists(SavePath)) return null;
        
        string json = File.ReadAllText(SavePath);
        return JsonUtility.FromJson<SaveData>(json);
    }
}

数据存储方案对比

PlayerPrefs

简单设置,适合小数据

JSON

结构化数据,可读性好

二进制

体积小,安全性高

CHAPTER 10

第10章小结与全书总结

版本控制

  • Git基础
  • Unity配置
  • 常用命令

AssetBundles

  • 构建AB包
  • 加载资源
  • 热更新

数据持久化

  • PlayerPrefs
  • JSON序列化
  • 存档系统

全书回顾

  • C#基础
  • 核心API
  • 事件系统
  • AI与资源

Unity脚本编程的完整技能体系

从C#基础到高级API,从调试技巧到资源管理,全面掌握Unity脚本开发

final
THANK YOU

谢谢观看

Unity脚本设计

全书10章教学课件

C#基础

核心API

事件系统

AI系统

资源管理

[美] Alan Thorn 著 | 清华大学出版社有限公司