平时做技术实践时,很多问题不是概念不会,而是细节没串起来。拿“Unity新手必看:如何编写第一个C#脚本让游戏物体动起来?”来说,它看着像小点,放到项目里常会牵出环境、配置、兼容性和维护成本。下面按实际采用顺序,把思路、关键写法和容易踩坑的地方讲清楚,便于大家直接对照操作。
落到代码里,大家好,欢迎回到Unity教程系列。前面十五篇文章中,我们一直在学习场景搭建、组件操作、编辑器采用——这些都是"静态"的内容。从今天开始,我们要让游戏真正"动起来"了。而让游戏动起来的关键,就是编写C#脚本。
实际处理时,脚本是游戏逻辑的载体。角色的移动、敌人的AI、UI的交互、音效的播放——游戏中的一切动态行为,都来源于脚本。对于很多新手来说,第一次写脚本可能会感到紧张——别担心,这篇文章我会从最基础的操作开始,手把手带你进入Unity脚本编程的世界。
一、脚本在Unity中的角色
1.1 脚本是什么
从实现思路看,在Unity里,脚本(Script)是一种特殊的Component。当你新建一个C#脚本同时将其挂载到GameObject上时,这个脚本就成为该GameObject的一个组件,拥有该组件的所有特性——能够在Inspector中查看、能够启用/禁用、能够借助GetComponent拿到。
理解"脚本就是组件"这个观念很重要。它意味着:
- 脚本继承了MonoBehaviour(而MonoBehaviour继承了Component)
- 脚本像其他组件一样,有生命周期(新建、启用、更新、禁用、销毁)
- 脚本能够借助GetComponent被其他组件访问
1.2 脚本与GameObject的关系
在这个场景下,每个挂载了脚本的GameObject都是该脚本的一个实例。如果你将同一个脚本挂载到10个不同的GameObject上,运行时会新建10个脚本实例,每个实例有自己独立的变量值。
Player对象 ──── PlayerController脚本实例(moveSpeed=5)
Enemy_A对象 ── EnemyController脚本实例(moveSpeed=3)
Enemy_B对象 ── EnemyController脚本实例(moveSpeed=4)
二、新建脚本
2.1 在Unity中新建C#脚本
新建脚本有多种方式:
方式一:Project窗口右键
在Project窗口中进入Scripts文件夹
右键 → Create → C# Script
新脚本以蓝色图标出现,名称处于可编辑状态
输入脚本名称(如"MyFirstScript"),按回车确认
方式二:Assets菜单
Assets → Create → C# Script
方式三:GameObject的Add Component菜单
选中GameObject
Inspector底部 → Add Component
输入"New Script"
点击"New Script",输入脚本名称
落到代码里,第三种方式会同时在Project窗口的Assets根目录新建脚本,同时自动将其挂载到选中的GameObject上。
2.2 脚本命名规范
C#脚本的命名有几个重要规则:
文件名与类名必须一致:新建的脚本文件名为"MyFirstScript.cs",那么内部的类名也必须是"MyFirstScript"。如果你手动修改了文件名,必须同时修改类名,否则Unity会报编译错误。
采用PascalCase命名法:每个单词首字母大写,如 PlayerController、EnemyAI、WeaponManager。
不采用中文和特殊字符:脚本名只能采用英文、数字和下划线,且不能以数字开头。
名称要有意义:避免 Script1、Test2 这样的无意义命名。好的命名能让人一眼看出脚本的用途。
2.3 打开和编辑脚本
双击脚本文件:在Project窗口中双击C#脚本文件,Unity会在你设置的代码编辑器中打开它。默认是Visual Studio,你也能够在 Edit > Preferences > External Tools 中更换为Rider或VS Code。
在Inspector中打开:如果脚本已经挂载到GameObject上,选中该物体,在Inspector中点击脚本组件名称右侧的齿轮图标或直接点击脚本名称行,都能够跳转到脚本文件。
三、脚本的基本结构
3.1 默认生成的代码模板
新建新脚本后,Unity会自动生成以下模板代码:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class MyFirstScript : MonoBehaviour
{
// Start is called before the first frame update
void Start()
{
}
// Update is called once per frame
void Update()
{
}
}
让我们逐段理解这个模板:
3.2 using语句
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using 语句相当于"导入命名空间",让你能够采用其他代码库中的类:
UnityEngine:Unity引擎的核心命名空间,包含MonoBehaviour、GameObject、Transform、Debug等所有Unity基础类。几乎每个脚本都需它。System.Collections和System.Collections.Generic:C#标准库的集合类型命名空间,提供List、Dictionary、Queue等数据结构。
3.3 类定义
public class MyFirstScript : MonoBehaviour
这行代码定义了一个名为MyFirstScript的类,它继承自MonoBehaviour。
public:访问修饰符,表示这个类能够被其他代码访问。
class MyFirstScript:定义类名。
: MonoBehaviour:继承关系。MonoBehaviour是Unity中所有脚本组件的基类。继承MonoBehaviour后,你的类才能被挂载到GameObject上,才能采用Start、Update等生命周期方法。
3.4 Start方法
void Start()
{
// 初始化代码
}
实际处理时,Start是Unity生命周期中的一个方法,在脚本实例被启用后、第一次Update之前调用。它只被调用一次,适合用来初始化——拿到组件引用、设置初始变量值、建立初始状态等。
3.5 Update方法
void Update()
{
// 每帧执行的代码
}
落到代码里,Update在游戏的每一帧都会被调用。它是游戏逻辑的核心方法——输入检测、移动更新、状态检查等需持续执行的逻辑都放在Update中。
四、将脚本挂载到GameObject
4.1 挂载的方式
方式一:拖拽(最常用)
结合项目来看,将脚本从Project窗口拖到Hierarchy中的GameObject上,或拖到Scene视图中的物体上。
方式二:拖到Inspector
从实现思路看,选中GameObject,将脚本从Project窗口拖到Inspector的空白区域(组件列表下方)。
方式三:Add Component按钮
选中GameObject
Inspector底部 → Add Component
搜索脚本名称
点击添加
方式四:代码中动态添加
gameObject.AddComponent<MyFirstScript>();
4.2 验证脚本挂载成功
落到代码里,脚本成功挂载到GameObject后,在Inspector中能够看到脚本作为一个组件出现:
- 组件名称显示为脚本的类名
- 脚本名称左侧有启用/禁用复选框
- public变量显示在Inspector中,可直接编辑
- 如果有[SerializeField]标记的private变量也会显示
4.3 一个物体能够有多个脚本
从实现思路看,同一个GameObject上能够挂载任意数量的脚本组件,每个脚本独立运作:
Player
├── Transform
├── MeshRenderer
├── Rigidbody
├── PlayerController(处理输入和移动)
├── HealthComponent(管理生命值)
├── WeaponComponent(管理武器)
└── AnimationController(管理动画)
五、编写第一个脚本——让物体旋转
5.1 新建Rotator脚本
让我们写一个最轻松的脚本:让物体不停地旋转。
using UnityEngine;
public class Rotator : MonoBehaviour
{
// 旋转速度,可在Inspector中调整
public float rotationSpeed = 30f;
void Update()
{
// 每帧绕Y轴旋转
// Time.deltaTime使旋转速度与帧率无关
transform.Rotate(Vector3.up * rotationSpeed * Time.deltaTime);
}
}
5.2 代码解析
public float rotationSpeed = 30f:
public关键字让这个变量在Inspector中可见可编辑float是浮点数类型rotationSpeed是变量名= 30f设置默认值
transform.Rotate(…):
transform是当前脚本所在GameObject的Transform组件引用Rotate是Transform的一个方法,让物体旋转Vector3.up是Y轴正方向的简写(即(0,1,0))* rotationSpeed乘以旋转速度* Time.deltaTime乘以帧间隔时间,使旋转速度与帧率无关
5.3 测试脚本
新建一个Cube
将Rotator脚本拖拽到Cube上
点击Play按钮
观察Cube在Scene视图和Game视图中旋转
理解这一步时,在Inspector中修改rotationSpeed的值(如改为100),观察旋转速度变化
把Vector3.up改为new Vector3(1,1,0),观察旋转方向的变化
结合项目来看,恭喜!你刚刚完成了第一个Unity脚本,让一个物体在你的代码控制下动了起来!
六、让物体动起来——移动脚本
6.1 键盘控制移动
using UnityEngine;
public class SimpleMover : MonoBehaviour
{
public float moveSpeed = 5f;
void Update()
{
// 获取水平输入(A/D键或左右方向键)
float horizontal = Input.GetAxis("Horizontal");
// 获取垂直输入(W/S键或上下方向键)
float vertical = Input.GetAxis("Vertical");
// 构建移动方向向量
Vector3 movement = new Vector3(horizontal, 0, vertical);
// 移动物体
transform.Translate(movement * moveSpeed * Time.deltaTime);
}
}
6.2 Input.GetAxis解析
Input.GetAxis("Horizontal") 从Unity的输入管理器拿到"Horizontal"轴的值。这个值在-1到1之间平滑过渡:
- 按A键或左方向键:值逐渐变化到-1
- 按D键或右方向键:值逐渐变化到1
- 不按键:值逐渐回到0
"Vertical"轴对应W/S或上下方向键。
实际处理时,平滑过渡意味着物体不会"突然启动突然停止",而是有一个自然的加速和减速过程——这就是你在游戏中感受到的"手感"。
6.3 跳跃功能
using UnityEngine;
public class PlayerMover : MonoBehaviour
{
public float moveSpeed = 5f;
public float jumpForce = 10f;
private Rigidbody rb;
void Start()
{
// 获取Rigidbody组件引用
rb = GetComponent<Rigidbody>();
}
void Update()
{
// 水平移动
float horizontal = Input.GetAxis("Horizontal");
float vertical = Input.GetAxis("Vertical");
Vector3 movement = new Vector3(horizontal, 0, vertical);
transform.Translate(movement * moveSpeed * Time.deltaTime);
// 跳跃
if (Input.GetKeyDown(KeyCode.Space))
{
rb.AddForce(Vector3.up * jumpForce, ForceMode.Impulse);
}
}
}
理解这一步时,采用Rigidbody实现跳跃,需在GameObject上添加Rigidbody组件。另外,上述代码没有做"在地面上才能跳"的判断——玩家能够无限在空中跳。后续章节会添加地面检测来完善。
七、脚本的调试
7.1 采用Debug.Log输出信息
实际处理时,你的脚本行为不符合预期时,第一件事就是在关键位置添加Debug.Log输出:
void Start()
{
Debug.Log("脚本已启动!");
Debug.Log($"物体的初始位置是:{transform.position}");
}
void Update()
{
if (Input.GetKeyDown(KeyCode.Space))
{
Debug.Log("玩家按下了空格键!");
}
}
输出的信息会显示在Console窗口中。不同级别的日志:
Debug.Log("信息"):普通白色消息Debug.LogWarning("警告"):黄色警告消息Debug.LogError("错误"):红色错误消息
7.2 常用编译错误
“; expected”:某行代码末尾缺少分号。
“does not contain a definition”:调用了不存在的方法或访问了不存在的属性。可能是拼写错误或缺少using。
“cannot convert”:类型不匹配。比如把string赋值给int。
“The name ‘xxx’ does not exist”:采用了未定义的变量。
理解这一步时,遇到错误时,双击Console窗口中的错误消息,Unity会直接打开对应的脚本同时跳转到出错行号。
八、脚本文件管理
8.1 建议的文件组织
AssetsScripts
├── Player
│ ├── PlayerController.cs
│ ├── PlayerHealth.cs
│ └── PlayerInput.cs
├── Enemies
│ ├── EnemyAI.cs
│ └── EnemyPatrol.cs
├── Managers
│ ├── GameManager.cs
│ └── AudioManager.cs
├── UI
│ ├── MainMenu.cs
│ └── HealthBar.cs
└── Utils
├── ObjectPool.cs
└── Singleton.cs
8.2 脚本重命名
在这个场景下,重命名脚本要小心!在Project窗口中重命名脚本文件后,必须打开脚本文件同步修改类名。文件名和类名必须一致。
在这个场景下,若只修改了文件名而没改类名,Unity会显示错误:“Script class does not match file name”。
九、本篇总结
本文核心知识点回顾:
- 落到代码里,C#脚本在Unity中是一种特殊的Component,继承自MonoBehaviour
- 脚本文件名必须与内部的类名一致
- 在这个场景下,Start()在脚本启用后调用一次(初始化),Update()每帧调用(持续逻辑)
- public变量自动显示在Inspector中,可实时编辑
- transform是脚本所在GameObject的Transform组件的快捷引用
- 结合项目来看,Input.GetAxis()拿到平滑的输入值,Input.GetKeyDown()检测单次按键
- Time.deltaTime让移动速度与帧率无关
- Debug.Log()在Console窗口输出调试信息
结合项目来看,总的来说,Unity脚本适合结合实际项目边做边理解。先抓住核心思路,再逐步补上细节和边界处理,最后效果会更稳定,也更容易复用。