资讯动态

Unity3D新手必看:用C#脚本让物体动起来的全流程

发布时间:2026/10/17 7:58:36 来源:尧图企业网站定制
说个最常见的场景你在Unity3D里拖了一个Cube进场景然后想把它的位置改一下让它“动起来”于是选中物体后在Inspector面板里拖动Position数值或者直接用手把物体拽到别处。结果一按Play物体瞬间回到了原来位置。很多人会愣一下然后开始在菜单里找“运动”相关的按钮找半天发现根本没有。Unity的移动不是靠“记录坐标”实现的它需要在运行时每帧执行逻辑来改变物体位置而这段逻辑就放在一个代码文件里标题里说的就是这个事。这篇博文的目标很明确带你从零创建一个C#脚本文件把它挂到某个物体上让物体在游戏运行时动起来。先做最简单的匀速直线移动再做键盘控制移动最后聊物理刚体方案的区别。适合刚学Unity3D、对脚本还没有建立起模型的同学也适合那些能照着教程把脚本拖进物体但不知道“为什么这样就能动”的人。我尽量把原理和踩坑一起讲了。1. 先把问题讲清楚为什么“运动”必须交给代码文件1.1 手动改坐标与代码控制的本质区别手动拖动Position其实也是“移动”只是它发生在编辑阶段的静态场景里。一旦进入Play模式Unity会重新从场景初始状态开始运行你手动改过的坐标如果不被任何逻辑记录就等于没改过。换句话说编辑器里的拖拽是给美术摆场景用的不是给游戏运行时用的。代码控制则完全不同。脚本挂在物体上之后引擎会在每一帧调用脚本里的方法方法里面写一句“把当前坐标加上一小段距离”这时候物体就真的开始动了。所以很多新手会觉得“为什么移动这么麻烦”这不是麻烦而是Unity对运行时行为的基本约束一切动态行为必须有逻辑驱动美术摆不动动画扳不动只有代码最自由。1.2 三种驱动方式的定位实际项目中让物体“动起来”不只代码一种方式但适合的场景差别很大。我整理了一张对比表你先感受一下驱动方式适用场景控制精度学习成本性能特点动画Animation/Animator角色待机、走路、攻击等固定表演中等靠关键帧和曲线中等模型带动画时效率高物理引擎Rigidbody碰撞、掉落、投掷、推挤效果高但受物理参数影响较高计算量较大C#代码文件玩家控制、简单巡逻、机关移动最高可完全定制低逻辑简单时开销极小大多数玩法上的“功能性移动”比如玩家按方向键控制角色走动、子弹向前飞、敌人追踪目标其实都适合用代码文件驱动。动画适合表现动作物理适合表现“被别的东西推动”的自然效果而代码可以做到精准、可控、无意外碰撞的纯逻辑移动。1.3 一个代码文件究竟做了什么C#脚本挂到物体上后本质上是一个组件。它继承了MonoBehaviour这个基类从而被Unity的组件系统识别。它并不神秘就像给物体装了一个会定时发指令的小控制器。脚本最常见的两个方法是Start和Update。Start会在物体激活后的第一帧更新前调用一次适合做“初始化”操作Update则会在每一次画面渲染更新前被调用游戏跑60帧它就调用60次。你要做移动就是利用了Update的高频执行特性每次把一个极小的位移量加到物体坐标上连续调用下来物体自然就走起来了。2. 创建脚本这个动作比新建文档有更多的规矩2.1 场景、物体与脚本的命名规范很多人第一步就翻车不是因为代码写错而是因为命名不对。在Unity里创建C#脚本文件名必须和类名完全一致例如新建一个脚本叫MoveCube.cs那里面默认生成的类名也必须是MoveCube一个字母都不能差否则编译直接报错。我的建议是从一开始就养成习惯脚本名首字母大写用驼峰命名不要用中文名不要用“Script1”、“NewBehaviourScript”这种默认名。因为一个项目做到后面脚本几十上百个光靠名字根本无法维护。2.2 在Project面板里创建C# Script的正确路径在Project面板中找到你想存放脚本的文件夹右键选择Create - C# Script然后重命名。这里的文件夹位置不影响功能但是建议放到单独的“Scripts”文件夹里方便之后管理。创建完成后双击打开默认内容是这样的using System.Collections; using System.Collections.Generic; using UnityEngine; public class MoveCube : MonoBehaviour { // Start is called before the first frame update void Start() { } // Update is called once per frame void Update() { } }这几行自带的注释和空方法就是Unity给你留好的“运动接口”。Start写初始化逻辑Update写每帧执行的逻辑。2.3 把脚本挂到物体上的两种方式脚本创建出来只是一个文件还没有和被控制的物体产生关系。把脚本拖到Hierarchy里的Cube物体上或者选中Cube后在Inspector面板点击Add Component搜索“MoveCube”并添加。这两种方式等价。这里有个关键点需要搞明白脚本里的this.transform并不是“随便某个物体”而是挂载这个脚本的物体的Transform组件。你把这个脚本挂到Cube上它控制的就是Cube挂到Sphere上控制的就是Sphere。换句话说脚本本身不知道自己叫“MoveCube”它只知道“谁挂了我我就控制谁”。2.4 默认代码的三大关键成员如果你是第一次接触C#脚本可以先把脚本理解为三段MonoBehaviour基类提供生命周期方法Start是做一次性准备工作的Update是做每帧重复工作的。移动这种操作要不要放在Start里不行因为Start只在开始执行一次放进去变成瞬移一次。常用习惯是Update里写持续移动逻辑Start里获取组件引用或者给初值。到后面你会发现有些与物理相关的逻辑会放到FixedUpdate里那是固定频率调用的方法第三节再讲。3. 第一段能跑的移动代码从位置累加理解运动本质3.1 核心APITransform的position、Translate和localPosition物体在3D空间里的位置核心数据就是Transform组件。它有三个互补的概念position是物体在世界坐标系下的位置localPosition是相对于父物体坐标系下的位置Translate则是一个方法表示“让物体沿着某个方向移动一段距离”。新手一上来容易混淆position和localPosition因为这个Demo里物体没有父物体两者数值一样。一旦你创建了几个空物体做父子层级就会发现子物体的localPosition永远参考父物体的坐标原点想在做可嵌套物体时理解坐标最好从一开始就把这两个概念分开。3.2 最简单版本每帧给position叠加速度你要让物体匀速移动物理直觉告诉我们位移等于速度乘以时间。所以在Update里你只需要把当前位置加上“速度乘以每帧消耗的时间”using UnityEngine; public class MoveCube : MonoBehaviour { public float speed 5f; void Update() { transform.position Vector3.forward * speed * Time.deltaTime; } }这里Vector3.forward就是世界坐标的Z轴正方向也就是静态场景里蓝色箭头指向的方向。运行后物体会自动沿Z轴匀速前进你的第一个代码驱动的运动就完成了。3.3 不要忘记Time.deltaTime帧率对移动的直接影响很多第一次运行的人会做两个动作把speed调到20、去掉Time.deltaTime。然后发现物体“嗖”一下飞出屏幕或者在不同电脑上速度差很多原因就在这里。我算笔账给你看假设speed等于5游戏每秒渲染60帧每帧耗时约0.0167秒那么每帧位移量就是5乘以0.0167约0.083米一秒总位移约5米。如果游戏只有20帧每帧耗时0.05秒每帧位移0.25米一秒总位移还是5米。这就是Time.deltaTime的作用把“每帧”位移换算成“每秒”位移让速度在不同帧率下保持一致。如果不乘它60帧时一秒走300米如果哪台机器跑到了120帧一秒就变成600米。同一个游戏在不同设备上会出现完全不同的体验。所以从写第一行移动代码开始就要习惯性地带上这个参数。3.4 Transform.Translate 和直接赋值到底选哪个上面的代码用到了transform.position 这不是唯一方式。还有另一个更直观的方法transform.Translate(Vector3.forward * speed * Time.deltaTime);Translate的意思是“在当前物体的坐标系或世界坐标系中移动指定距离”写起来更像日常说话。直接赋值则更像“先算出终点再把位置放过去”。我的习惯是Demo阶段用Translate减少出错发现它内部还会帮你处理一些旋转后的方向换算高级阶段用直接赋值因为你可以先计算位移向量再考虑是否要做边界检测、是否要跟动画混合。两者本身没有优劣但搞清楚差异可以让你以后读别人代码时不犯迷糊。3.5 先跑通往目标点直线移动的最短验证写完后别急着写复杂交互先把这几步做通创建一个Cube创建脚本挂上去填好speed运行看到物体沿Z轴移动。整个链路通了再往下加键盘控制。我见过太多人连最基本的“挂载后编译成功”都没验证就开始写一整套复杂的角色控制系统。结果出错后不知道是脚本没挂上还是输入方向读错了还是坐标系用反了。调试往往是从最原始的一步开始的。4. 键盘控制让物体跟你的手指走4.1 用Input.GetAxis读取方向轴单纯的自动移动没什么交互感真正有价值的是“按键控制移动”。Unity内置的输入系统给了一套标准映射Horizontal轴对应A/D键和左右方向键Vertical轴对应W/S键和上下方向键。读取方式是用Input.GetAxis它会返回一个-1到1之间的平滑值。按D键时数值逐渐向1靠近按A键时向-1靠近松开后向0回落。这个“平滑”是内置的渐变设置适合需要柔和转向的游戏如果你希望按键瞬间响应、毫无缓冲可以用Input.GetAxisRaw它只返回-1、0、1三个值非常适合刚体移动和像素级操作。4.2 完整键盘控制脚本与逐行解释直接看代码这是我通常推荐给新手的版本using UnityEngine; public class MoveCube : MonoBehaviour { public float moveSpeed 5f; public float rotateSpeed 100f; void Update() { float horizontal Input.GetAxis(Horizontal); float vertical Input.GetAxis(Vertical); Vector3 direction new Vector3(horizontal, 0f, vertical); if (direction.sqrMagnitude 1f) { direction.Normalize(); } transform.Translate(direction * moveSpeed * Time.deltaTime, Space.World); } }前两行从输入系统读取横轴和竖轴数值。第三行把两个数值拼成一个三维向量因为物体主要在XZ平面上移动所以Y轴填0。if (direction.sqrMagnitude 1f)判断的是“斜向按两个键时向量长度是不是超过了1”。比如只按D键时向量是(1,0,0)长度为1同时按D和W时向量是(1,0,1)长度约1.414如果不处理斜着走会比直着走快41%。Normalize()就是把向量长度缩回1但方向不变。最后用Translate让物体往方向向量所指的方向移动Space.World表示这是按世界坐标方向移动而不是物体旋转后的局部方向。4.3 为什么斜方向会变快向量归一化如果你不想用sqrMagnitude也可以先判断长度后再归一化效果一样。很多人不理解为什么键盘控制这么简单的事还要处理斜向问题但你想象一下直走每秒5米斜走每秒却变成7.07米这在MOBA类游戏或者俯视角游戏里会严重影响操作手感。这里顺带提一个实际项目里常见的坑如果用GetAxisRaw按两个方向键向量长度就正好是根号2所以归一化非常关键如果用GetAxis因为数值是平滑变化的偶尔你会得到长度介于1和1.414之间的向量同样需要归一化。无论哪种读取方式处理方向向量归一化都是标准动作。4.4 补充相机视角下的前后左右用上面的代码跑起来后你会发现移动方向完全和世界坐标对齐按W是沿Z轴正方向按S是沿Z轴负方向按A/D是沿X轴负/正方向。如果相机固定在这个轴上看起来没问题如果相机旋转到45度画面里的“向上”就不再是世界Z轴了。控制方向跟随相机需要把方向向量旋转到相机坐标系里核心做法是用Camera.main.transform的朝向和右向构建一个新的方向向量。这个内容稍微进阶但你现在心里留个概念就好键盘方向永远只代表“输入值”最终移动向量怎么映射到世界取决于你的相机坐标系。5. 进阶让移动受到物理约束5.1 为什么要碰刚体一直用Transform移动物体会无视碰撞体直接穿过墙壁因为position 本质上是“瞬移一小段”不等同于“推动一个物理物体”。假如你在做推箱子游戏或者想让物体受重力掉到地上就必须引入Rigidbody组件这是Unity物理引擎的核心载体。Rigidbody负责重力、摩擦力、碰撞响应。物体一旦挂上Rigidbody并开启重力播放模式下它就会往地面掉。如果此时你还在Update里给它用Transform改坐标就会产生严重冲突物理引擎算出物体在这个位置你下一秒又把它瞬移到另一个位置结果物体要么抖动要么穿模。5.2 AddForce方案给物体一个力物理方案的第一种常见做法是AddForce也就是给物体施加一个持续的力public class PhysicMove : MonoBehaviour { public float force 10f; private Rigidbody rb; void Start() { rb GetComponentRigidbody(); } void FixedUpdate() { float h Input.GetAxis(Horizontal); float v Input.GetAxis(Vertical); Vector3 dir new Vector3(h, 0f, v).normalized; rb.AddForce(dir * force); } }这串代码把GetComponentRigidbody()放在了Start里初始化每次利用FixedUpdate的稳定帧率来施力。注意物理相关代码不要放进Update因为物理计算有自己的时间步长Update的调用频率可能忽高忽低放进FixedUpdate才能和其他物理插值匹配。5.3 Transform方案与刚体方案怎么选我用一张表把关键差异列出来对比项Transform.TranslateRigidbody.AddForce/Velocity受重力影响不受受会掉落与碰撞体交互基本无视碰撞会碰撞、反弹对帧率依赖依赖Time.deltaTime依赖FixedUpdate固定步长控制手感硬、直接、响应快有惯性、需要调参数典型场景UI动画、机关移动、视觉表现角色控制、投掷物、可破坏物如果你是做一个纯粹的视觉展示比如旋转的齿轮、来回移动的平台用Transform就够了。如果你做的是玩家角色、敌人角色、或者任何需要和场景里的墙壁门框发生实际交互的物体那必须考虑刚体。5.4 配合速度参数Rigidbody.velocity更可控AddForce的问题在于它会累积力角色容易越跑越快然后像个冰面一样刹不住。很多动作游戏使用的是更“硬”的做法直接设置刚体速度让速度严格等于方向和速度的乘积void FixedUpdate() { Vector3 dir new Vector3(h, 0f, v).normalized; rb.velocity new Vector3(dir.x * moveSpeed, rb.velocity.y, dir.z * moveSpeed); }这个做法的巧妙之处在于它只修改X和Z方向的速度保留Y方向速度这样重力仍然能正常把人往下拉同时地面移动又非常跟手。我接触过的很多Demo项目都是这样控制角色的虽然不是最精细的物理模拟但做大作业、做原型验证非常够用。6. 我实际跑这个Demo时踩过的坑与排查思路6.1 物体完全不动脚本没挂上或编译失败最常见的情况是写完代码挂上组件按Play物体纹丝不动。这时候不要怀疑代码逻辑先按下面顺序排查第一看Hierarchy里选中的物体在Inspector里有没有MoveCube这个组件没有就说明根本没挂成功。第二看Console窗口有没有红色报错只要有报错旧脚本默认不会运行。第三双击查看到底是哪个文件报错最常见的错误是类名和文件名不一致。一个很有用的调试动作是在Update里加一行Debug.Log(running);如果运行时Console有输出说明脚本确实在跑问题在移动逻辑本身如果完全没有输出说明脚本执行入口受阻问题在挂载或编译。6.2 “Input.GetAxis找不到”新旧输入系统冲突很多新版本Unity3D创建项目时默认开启了新版输入系统。在这个模式下旧版的Input.GetAxis是不会被编译通过的Console会报类似“Input.GetAxis is not defined”的错误。这时候你有两个选择改成新版输入系统API或者干脆在Player Settings里把Active Input Handling设为“Input Manager (Old)”或“Both”。个人建议只是学这个Demo的话切回旧输入管理器最省事因为教程里大部分传统移动例子都基于老API如果项目已经全面用了新输入系统那就借这个机会学一下新版输入事件但不要混着用。6.3 物体运动时抖动或穿透用Transform移动时物体速度过快会直接穿过薄墙或触发频繁的碰撞抖动。这是因为Transform移动类似“跳跃式插值”物理引擎来不及响应。如果你是物理方案注意调高Collider的Collision Detection模式从Discrete改成Continuous或Continuous Dynamic代价是性能稍高。如果你坚持用Transform做移动又要防止穿透可以改用Rigidbody.MovePosition方法它在物理内部计算位移而不是直接把坐标拽过去。这个API兼顾了Transform的直观和物理引擎的稳定性。6.4 旋转相关的坑欧拉角与四元数移动做顺之后很多人会顺手给物体加一个旋转逻辑用transform.rotation new Vector3(0, angle, 0)结果发现不仅报错物体还会出现各种奇怪的姿态。这不是偶然transform.rotation的类型是Quaternion四元数不能直接用Vector3赋值。正确写法有两个要么用transform.Rotate每次叠加旋转角要么用Quaternion.Euler(0, angle, 0)构造出四元数再赋值。以后涉及到更复杂的旋转插值、角色朝向时你会发现四元数这东西绕不开但在这个Demo阶段只需要记得一条红线旋转的本质是四元数不要用欧拉角向量直接赋值。6.5 运行后Cube往下掉还有一个非常“经典”的翻车明明只给物体写了移动代码结果一按Play物体直接掉落甚至掉出场景。原因很简单——你的Cube上挂了一个Rigidbody且Use Gravity勾选了。如果你只是想演示键盘控制移动最简单的办法是直接删除Rigidbody组件如果你想保留物理又要让代码控制位置那就把Rigidbody的Interpolate启用、Constraints里的Freeze Position Y勾上或者干脆把Is Kinematic打开。Kinematic刚体不会被重力推着走但会通过代码移动去推动其他动态刚体是一个好用的中间状态。7. 后续扩展方向与一点个人心得这个Demo跑通之后你完全可以在它基础上继续扩展。最简单的几个方向是给物体加一个跳跃逻辑按下空格时修改Y方向速度把移动代码从Update搬到FixedUpdate观察手感差异把速度和输入读取独立成单独的脚本组件化地组合到角色身上。还有一个很推荐的做法把MoveCube挂到摄像机父物体上用方向键控制相机飞旋你会立刻感受到坐标系选择带来的手感差异。我在实际项目里最常见的教训是过度关注“用哪种方式移动”这个表面问题忽略了三件底层事——坐标系、更新周期、是否参与物理。这个Demo本身只有几十行代码但只要你把这三件事理清了以后控制人物、车辆、飞弹都不会再迷路。做实验阶段尽量用变量把速度暴露在Inspector上因为“手感”不是一次调出来的而是要反复试出来的。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑