全书10章教学课件
专业教材配套课件
API使用详解
C#基础语法、变量、条件语句、循环、函数、面向对象、属性、SendMessage
编译错误、Debug.Log、可视化调试、Profiler性能分析、断点调试
重点API:GetComponent、Find、Time类、DontDestroyOnLoad、单例模式实现
重点API:委托delegate、event关键字、EventManager架构、MonoBehaviour事件
Camera组件API、投影模式、可见性检测、后处理、相机震动
重点API:List/Dictionary/Stack、LINQ、协程、正则表达式、文件操作
NavMesh导航、NavMeshAgent API、有限状态机FSM、AI行为实现
编辑器扩展、自定义Inspector、PropertyDrawer、反射、本地化系统
天空盒、程序化网格Mesh、UV动画、纹理绘制系统
Git版本控制、Resources加载、AssetBundles、数据持久化
第3、4、6章为重点API章节,将详细讲解相关API使用方法
C#基础语法与Unity脚本入门
C#是强类型语言,编译时进行类型检查,减少运行时错误。相比动态类型语言,更容易捕获bug,提高代码可靠性。
完整的OOP支持:封装、继承、多态。适合构建复杂的游戏系统,代码可重用性高,易于维护和扩展。
Unity引擎原生支持C#,提供完整的API访问。MonoBehaviour基类、组件系统、生命周期函数都是为C#设计的。
一次编写,多平台运行。支持Windows、macOS、Linux、iOS、Android、WebGL、主机平台等20+平台。
.NET Framework/.NET Core提供海量功能:文件IO、网络通信、数据结构、LINQ、多线程、加密等。
在Project窗口中右键点击
选择 Create → C# Script
输入脚本名称
命名规范:使用 PascalCase,如 PlayerController
双击脚本打开编辑器
默认使用 Visual Studio 或 VS Code
将脚本附加到 GameObject
拖拽脚本到 Inspector 窗口或 Add Component
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() - 每帧更新
每帧调用一次,用于常规更新逻辑
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)
int
整数
float
单精度浮点数
double
双精度浮点数
bool
布尔值
string
字符串
char
字符
值类型
int, float, bool, struct
存储在栈上,赋值时复制
引用类型
string, class, array
存储在堆上,赋值时引用
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;
if (condition) { // 条件为 true 时执行 } else if (otherCondition) { // 其他条件为 true 时执行 } else { // 以上条件都不满足时执行 }
游戏应用示例
if (health <= 0) { Die(); } else if (health < 30) { ShowWarning(); } else { UpdateUI(); }
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;
// 声明数组 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); }
存储游戏对象
public GameObject[] waypoints;
管理资源引用
public Material[] materials;
配置数据
public float[] damageValues;
for (初始化; 条件; 迭代) { // 循环体 }
示例:遍历敌人
for (int i = 0; i < enemies.Length; i++) { enemies[i].TakeDamage(10); }
适用场景:已知循环次数,需要索引
while (condition) { // 条件为 true 时执行 }
示例:等待条件
while (playerHealth > 0) { GameLoop(); }
适用场景:未知循环次数,条件驱动
foreach (元素类型 变量 in 集合) { // 处理每个元素 }
示例:遍历玩家列表
foreach (Player player in players) { player.UpdateScore(); }
适用场景:遍历集合,不需要索引
break;
立即退出循环
continue;
跳过当前迭代,继续下一次
// 错误示例 while (true) // 没有退出条件! { // 死循环 }
// 访问修饰符 返回类型 函数名(参数) public void Attack() { // 函数体 } private int CalculateDamage(int baseDamage) { return baseDamage * 2; }
// 同名不同参数 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);
void Start() { }
void Update() { }
void FixedUpdate() { }
// 声明委托类型 public delegate void OnHealthChanged(int newHealth); // 创建委托实例 public OnHealthChanged healthChangedCallback; // 订阅事件 void Start() { healthChangedCallback += OnHealthChangedHandler; } // 事件处理函数 void OnHealthChangedHandler(int newHealth) { Debug.Log("Health: " + newHealth); }
// 声明事件 public event OnHealthChanged OnHealthChangedEvent; // 触发事件 public void TakeDamage(int damage) { health -= damage; if (OnHealthChangedEvent != null) { OnHealthChangedEvent(health); } }
event 的优势:
只能在声明类中触发,外部只能订阅/取消订阅
OnEnable / OnDisable
对象启用/禁用时
OnDestroy
对象销毁时
OnCollisionEnter / OnTriggerEnter
碰撞检测事件
// 类定义 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(); // 调用各自的重写方法 }
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; } }
封装组件数据
public Vector3 Position { get { return transform.position; } set { transform.position = value; } }
数据验证
set { health = Mathf.Clamp(value, 0, maxHealth); }
向同一 GameObject 上的其他组件发送消息
// 发送消息 void TakeDamage(int damage) { health -= damage; // 通知其他组件 SendMessage("OnHealthChanged", health, SendMessageOptions.DontRequireReceiver); } // 接收消息的组件 void OnHealthChanged(int newHealth) { UpdateUI(newHealth); }
SendMessageOptions:
RequireReceiver - 必须有接收者
DontRequireReceiver - 可以没有接收者
向自身及所有子对象广播消息
// 广播消息给所有子对象 void OnGamePaused() { // 通知自身和所有子对象 BroadcastMessage("Pause", SendMessageOptions.DontRequireReceiver); } // 子对象中的接收方法 void Pause() { isPaused = true; }
向父对象发送消息
// 子对象通知父对象 void OnClicked() { SendMessageUpwards("OnButtonClicked", this.name); }
为后续章节学习奠定语言基础
掌握C#基础语法,为Unity API学习做好准备
下一章
调试机制
掌握Unity项目调试技巧与错误排查
Error 错误
阻止程序运行的严重问题,必须修复
Warning 警告
潜在问题,不会阻止运行但建议修复
Log 信息
普通输出信息,用于调试
; expected
缺少分号
} expected
缺少右大括号
The type or namespace could not be found
缺少using语句或程序集引用
Cannot implicitly convert type
类型不匹配
错误格式
Assets/Scripts/Player.cs(15,23): error CS1002: ; expected
文件路径(行号,列号): 错误类型 错误描述
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
在Scene视图中绘制线段
void Update() { // 绘制从起点到终点的红线 Debug.DrawLine( startPoint, endPoint, Color.red); // 持续时间1秒 Debug.DrawLine( transform.position, target.position, Color.green, 1.0f); }
绘制射线
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); } }
在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.DrawLine()
绘制线段
Gizmos.DrawWireSphere()
绘制线框球体
Gizmos.DrawWireCube()
绘制线框立方体
Gizmos.DrawIcon()
绘制图标
Window → Analysis → Profiler
CPU Usage
CPU使用情况,脚本执行时间
GPU Usage
GPU渲染时间
Rendering
渲染统计:Draw Calls、Triangles
Memory
内存使用情况
减少Draw Calls
使用批处理、图集、合并网格
优化脚本
避免在Update中频繁分配内存
资源优化
压缩纹理、优化模型面数
物理优化
减少碰撞体复杂度
启用Deep Profile查看每个函数的详细耗时
注意:Deep Profile会显著降低性能,仅在调试时使用
在Visual Studio/VS Code中
1. 点击代码行号左侧设置断点
2. 按F9切换断点
3. 红色圆点表示断点已设置
附加到Unity
1. 点击"附加到Unity"按钮
2. 选择运行的Unity实例
3. 在Unity中运行游戏
监视变量值的变化
使用方法
1. 在Watch窗口点击添加
2. 输入变量名或表达式
3. 实时查看值的变化
查看函数调用顺序
Update() → Move() → CalculatePosition()
点击函数名跳转到对应代码
执行即时代码,测试表达式
? player.health > 100 ? player.transform.position > (0, 1, 0)
建立系统的错误排查思路,提高开发效率
掌握多种调试工具,快速定位和解决问题
下一章
单例模式、GameObject与场景世界
核心API使用:GameObject操作、组件交互、场景管理
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是功能模块
• Transform:位置、旋转、缩放
• Renderer:渲染
• Collider:碰撞
• 自定义脚本组件
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有一定开销
频繁调用会影响性能
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方法的问题
替代方案
public class GameManager : MonoBehaviour { public GameObject player; // Inspector赋值 private GameObject[] enemies; void Start() { // 只在Start中调用一次 enemies = GameObject.FindGameObjectsWithTag("Enemy"); } }
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.frameCount
总帧数
Time.realtimeSinceStartup
不受timeScale影响的实时
Time.captureFramerate
捕获帧率设置
使对象在场景切换时不被销毁
public class GameManager : MonoBehaviour { void Awake() { // 使对象在场景切换时保持 DontDestroyOnLoad(gameObject); } }
常用场景
单例模式与DontDestroyOnLoad结合
public class GameManager : MonoBehaviour { public static GameManager Instance; void Awake() { // 检查是否已存在实例 if (Instance != null) { Destroy(gameObject); return; } Instance = this; DontDestroyOnLoad(gameObject); } }
确保类只有一个实例,并提供全局访问点
实现步骤
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);
优点
缺点
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
下一章
事件驱动程序设计
核心API使用:事件系统、委托、EventManager架构
// 声明委托类型 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
及时取消订阅避免内存泄漏
无返回值的泛型委托
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<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;
返回bool的委托,用于条件判断
Predicate<int> isEven = (num) => num % 2 == 0; bool result = isEven(4); // true
// 编辑器脚本 using UnityEditor; public class BuildAssetBundles { [MenuItem("Assets/Build AssetBundles")] static void BuildAllAssetBundles() { BuildPipeline.BuildAssetBundles( "Assets/AssetBundles", BuildAssetBundleOptions.None, BuildTarget.StandaloneWindows); } }
// 从文件加载 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");
}
}
简单的键值存储
// 保存数据 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();
[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
结构化数据,可读性好
二进制
体积小,安全性高
Unity脚本编程的完整技能体系
从C#基础到高级API,从调试技巧到资源管理,全面掌握Unity脚本开发
Unity脚本设计
全书10章教学课件
C#基础
核心API
事件系统
AI系统
资源管理
[美] Alan Thorn 著 | 清华大学出版社有限公司