ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Unity2D开发核心API详解:从Transform到物理碰撞的实战指南

Unity2D开发核心API详解:从Transform到物理碰撞的实战指南 1. 项目概述为什么Unity2D的API值得你花时间啃如果你刚接触Unity2D可能觉得它就是个拖拖拽拽就能出游戏的“可视化”工具。但当你真正想实现一个角色精准跳跃、子弹抛物线飞行或者让UI按钮有灵动的反馈效果时你会发现不写代码几乎寸步难行。而写代码本质上就是在调用Unity引擎提供给你的各种“工具”——也就是我们今天要聊的API。API全称应用程序编程接口听起来高大上其实你可以把它理解成Unity这个“超级工具箱”里一件件功能明确的“工具”。Transform.Translate是移动工具Input.GetKey是按键监听工具Instantiate是复制生成工具。你的游戏逻辑就是用C#脚本把这些工具按顺序、按条件组合起来的过程。所以掌握常用API就等于掌握了用代码驱动游戏世界的“语法”和“词汇”。很多新手会一头扎进复杂的游戏框架或者炫酷的Shader里结果连让一个方块平滑移动都磕磕绊绊。我的建议是先把基础工具箱里的“螺丝刀”和“扳手”认全、用熟。这篇内容就是带你系统性地认识Unity2D开发中最核心、最高频的那一批API。我们不求面面俱到但求讲透每一个你几乎在每个2D项目里都会用到的函数和属性让你知其然更知其所以然。2. 核心模块一操控游戏对象的基石——Transform在Unity的世界里万物皆“对象”。而每一个对象无论它是一个精灵、一个UI图片还是一个空物体都必然挂载着一个Transform组件。它是游戏对象在场景中存在的“身份证”和“定位器”所有与位置、旋转、缩放相关的操作都绕不开它。2.1 位置Position让物体动起来Transform.position属性代表对象在世界坐标系中的位置。这是最直接的移动方式。// 直接设置位置瞬间移动 transform.position new Vector3(10, 5, 0); // 每帧向右移动1个单位与帧率相关不稳定 transform.position Vector3.right * Time.deltaTime;但直接修改position是“瞬移”缺乏过程感。对于需要平滑移动的场景我们更常用Translate方法。它基于对象自身的坐标系默认或世界坐标系进行位移。void Update() { // 每秒向右移动5个单位。Time.deltaTime是关键它使得移动速度与帧率解耦在任何机器上速度一致。 float moveSpeed 5f; transform.Translate(Vector3.right * moveSpeed * Time.deltaTime); }实操心得Time.deltaTime是灵魂永远记住在Update中做连续运动或变化时乘以Time.deltaTime。这个值代表上一帧到当前帧的时间间隔秒。不加它你的移动速度将取决于游戏运行的帧率FPS。在60帧的机器上每秒移动60单位在30帧的机器上就只剩30单位这会导致体验不一致。Translate的坐标系Translate(Vector3, Space.Self)是默认值沿物体自身坐标轴移动比如“前进”是物体的面朝方向。而Space.World则是沿世界坐标轴移动。处理玩家控制时常用Space.Self处理环境移动如背景滚动时可能用Space.World更直观。2.2 旋转Rotation与缩放Scale在纯2D游戏中我们通常只绕Z轴旋转因为屏幕是XY平面。Transform.rotation四元数和Transform.eulerAngles欧拉角都可以控制旋转对于2D入门使用欧拉角更直观。// 设置Z轴旋转90度欧拉角 transform.eulerAngles new Vector3(0, 0, 90); // 每帧绕Z轴旋转90度/秒 float rotateSpeed 90f; transform.Rotate(0, 0, rotateSpeed * Time.deltaTime);缩放Transform.localScale则直接影响对象的大小。它是一个Vector3即使在2D中Z值通常也保持为1不影响2D渲染。// 让物体宽度变为2倍高度不变 transform.localScale new Vector3(2f, 1f, 1f); // 实现一个心跳效果的缩放简略版 void Update() { float pulse Mathf.Sin(Time.time * 5f) * 0.1f 1f; // 在0.9到1.1之间正弦波动 transform.localScale Vector3.one * pulse; }常见问题排查物体移动“抽搐”或不平滑检查是否在Update中同时使用了物理系统如Rigidbody2D和直接修改Transform。这两者是冲突的。对于需要受物理影响的物体如受重力、碰撞应通过Rigidbody2D.MovePosition或给刚体施加力来移动。旋转方向不对记住Unity中旋转的正方向遵循左手定则绕轴。对于Z轴从屏幕外看向屏幕内逆时针为正方向。如果你想要顺时针旋转就使用负的角度或速度。3. 核心模块二获取玩家输入——Input没有交互的游戏就像一部默剧。Input类是连接玩家与游戏的桥梁。在Unity2D中我们主要处理键盘、鼠标和手柄的输入。3.1 键盘输入离散与连续键盘输入有两种主要检测方式Input.GetKeyDown(KeyCode)在按键被按下的那一帧返回true。用于触发一次性动作如跳跃、射击、打开菜单。if (Input.GetKeyDown(KeyCode.Space)) { Jump(); }Input.GetKey(KeyCode)在按键被按住期间的每一帧都返回true。用于持续动作如移动、蓄力。float horizontal 0f; if (Input.GetKey(KeyCode.A)) horizontal - 1f; if (Input.GetKey(KeyCode.D)) horizontal 1f; // 结合到移动中 transform.Translate(Vector3.right * horizontal * moveSpeed * Time.deltaTime);注意事项避免在FixedUpdate中检测GetKeyDownFixedUpdate是按固定物理时间步长调用的频率可能与渲染帧率(Update)不同。GetKeyDown可能在一段物理更新期间只在一帧为true导致在FixedUpdate中检测不到。处理输入响应尤其是离散动作最好放在Update中。使用虚拟轴对于移动更推荐使用Input.GetAxis(“Horizontal”)或Input.GetAxisRaw(“Horizontal”)。它们在Edit - Project Settings - Input Manager中预定义可以同时映射多个键如A/D和左右箭头并且返回值是平滑的-1到1或原始的-1 0 1方便直接用于移动计算。float horizontal Input.GetAxisRaw(“Horizontal”); // 立即响应无平滑 float vertical Input.GetAxis(“Vertical”); // 带平滑过渡3.2 鼠标输入点击与位置鼠标输入在2D游戏中至关重要尤其是对于点选、拖拽等操作。Input.GetMouseButtonDown(0)检测鼠标左键按下0-左键1-右键2-中键。常用于点击选择或攻击。Input.mousePosition返回鼠标在屏幕上的像素坐标左下角为(00)右上角为(Screen.width Screen.height)。关键技巧将鼠标屏幕坐标转换为游戏世界坐标这是2D开发中的一个核心技巧。因为mousePosition是屏幕坐标而游戏对象存在于世界坐标系中直接使用会导致错位。void Update() { if (Input.GetMouseButtonDown(0)) { // 将鼠标屏幕坐标转换为世界坐标 Vector3 mouseWorldPos Camera.main.ScreenToWorldPoint(Input.mousePosition); // 由于是2D我们通常只关心XY平面将Z轴设为0或相机Z坐标的负值 mouseWorldPos.z 0; // 现在可以让一个物体移动到鼠标点击的世界位置了 // transform.position mouseWorldPos; // 或者判断是否点击到了某个Collider2D RaycastHit2D hit Physics2D.Raycast(mouseWorldPos, Vector2.zero); if (hit.collider ! null) { Debug.Log(“点击到了: ” hit.collider.gameObject.name); } } }实操心得ScreenToWorldPoint的Z值这个函数的参数和返回值都是Vector3。对于正交相机Orthographic Camera 2D游戏常用传入的mousePosition的Z值会影响转换结果。一个常见的做法是传入时设置Z值为相机到你想放置物体的平面的距离的负值例如Camera.main.ScreenToWorldPoint(new Vector3(Input.mousePosition.x Input.mousePosition.y -Camera.main.transform.position.z))。但更简单的做法是像上面代码一样转换后再将Z轴设为一个固定值如0。Physics2D.Raycast用于点击检测这是检测鼠标是否点击到带有2D碰撞体对象的标准方法。Vector2.zero作为方向参数意味着从点击点发射一条长度为0的射线实际上就是检测该点上有哪些碰撞体。4. 核心模块三时间控制大师——TimeTime类是一个静态类它提供了与游戏时间相关的关键信息。用好它是让游戏行为帧率独立、表现稳定的关键。4.1Time.deltaTime帧率独立的法宝我们已经多次提到它。它的核心价值在于将“每帧变化量”转换为“每秒变化量”。错误示范帧率依赖transform.position Vector3.right * 5f;// 每帧移动5单位帧率高移动快。正确示范帧率独立transform.position Vector3.right * 5f * Time.deltaTime;// 每秒移动5单位与帧率无关。计算过程解析假设你的移动速度变量speed单位是“米/秒”。在每一帧里物体应该移动的距离是speed * 这一帧经过的时间。Time.deltaTime就是“这一帧经过的时间秒”。所以位移 speed * Time.deltaTime。这样无论游戏是跑在30帧还是144帧物体每秒移动的总距离都是speed米。4.2Time.time与Time.timeScaleTime.time从游戏开始到现在所经过的时间秒只读且随时间持续增长。它是制作计时器、循环动画结合Mathf.Sin的利器。// 生成一个在0到1之间循环的值周期为2秒 float pingPong Mathf.PingPong(Time.time 1.0f); // 让物体在两点间来回移动 float startX 0f endX 5f; float currentX Mathf.Lerp(startX endX pingPong); transform.position new Vector3(currentX transform.position.y 0);Time.timeScale时间的缩放系数。默认是1.0真实时间。设置为0可以暂停游戏所有基于Time.deltaTime的更新和物理模拟都会停止。设置为0.5则产生慢动作效果。注意它不影响Time.realtimeSinceStartup真实时间也不影响协程中yield return new WaitForSeconds()的行为除非使用WaitForSecondsRealtime。注意事项Time.deltaTime在FixedUpdate中在FixedUpdate里Time.deltaTime等于固定的物理时间步长默认为0.02s即50次/秒。如果你在FixedUpdate中进行物理相关的移动计算使用Time.fixedDeltaTime更准确但通常Time.deltaTime在此时也会返回相同的固定值。暂停游戏时的UI动画当你设置Time.timeScale 0来暂停游戏时所有使用Time.deltaTime的动画都会停止。如果你希望UI如暂停菜单的动画继续播放需要使用Time.unscaledDeltaTime它不受timeScale影响。5. 核心模块四物理引擎的触角——Physics2D 与 Collider2D2D物理是让游戏世界产生交互感的核心。Unity的2D物理系统相对独立位于Physics2D类下对应的组件也带有2D后缀。5.1 碰撞检测OnCollisionEnter2D与OnTriggerEnter2D这是两个最常用的碰撞回调函数但它们触发的条件不同OnCollisionEnter2D(Collision2D collision)当两个物体都带有Collider2D如BoxCollider2D CircleCollider2D且至少有一个带有Rigidbody2D并且**两者的Collider2D都没有勾选Is Trigger**时物理引擎会计算碰撞并产生物理效果如反弹同时调用此函数。OnTriggerEnter2D(Collider2D other)当两个物体中**至少有一个的Collider2D勾选了Is Trigger**时物理引擎会忽略它们的物理碰撞彼此穿透但会检测到它们发生了重叠并调用此函数。常用于制作拾取区域、伤害区域、传送门等。// 示例角色碰到敌人物理碰撞 void OnCollisionEnter2D(Collision2D collision) { if (collision.gameObject.CompareTag(“Enemy”)) { TakeDamage(10); // 可以获取碰撞点、法线等信息 // Vector2 hitPoint collision.contacts[0].point; } } // 示例角色进入金币拾取范围触发器 void OnTriggerEnter2D(Collider2D other) { if (other.CompareTag(“Coin”)) { CollectCoin(other.gameObject); // 收集金币 Destroy(other.gameObject); // 销毁金币对象 } }5.2 射线检测Physics2D.Raycast除了碰撞回调主动进行射线检测是更灵活的方式。它可以用来检测视线、鼠标点选、地面检测用于跳跃判断等。// 示例角色前方地面检测用于判断是否可跳跃 public float groundCheckDistance 0.2f; public LayerMask groundLayer; // 在Inspector中指定哪些层是地面 void CheckGrounded() { // 从脚底位置向下发射一条短射线 Vector2 origin transform.position; // 假设脚本挂在角色上 RaycastHit2D hit Physics2D.Raycast(origin Vector2.down groundCheckDistance groundLayer); if (hit.collider ! null) { // 碰到了地面层上的碰撞体 isGrounded true; // Debug.DrawRay(origin Vector2.down * groundCheckDistance Color.green); // 可视化射线 } else { isGrounded false; // Debug.DrawRay(origin Vector2.down * groundCheckDistance Color.red); } }关键参数解析origin射线起点世界坐标。direction射线方向。distance射线长度。layerMask层掩码。这是一个极其重要的优化和过滤手段。你可以通过LayerMask.GetMask(“Ground” “Wall”)来生成一个只检测“Ground”和“Wall”层的掩码避免射线检测到不必要的物体如自己、特效等提升性能并简化逻辑。实操心得与避坑指南Rigidbody2D的类型Rigidbody2D的Body Type有三种Dynamic动态受力和碰撞影响、Kinematic运动学只通过代码移动不受力影响但能影响其他动态物体、Static静态完全不受物理影响用于固定地形。角色通常用Dynamic移动平台用Kinematic静态地形用Static。碰撞回调不触发请按以下清单排查双方都有Collider2D吗至少一方有Rigidbody2D吗对于Static类型有时双方都需要有刚体或者一方是Kinematic/Dynamic如果是触发器回调是否至少有一方勾选了Is Trigger物体的层级Layer是否被物理引擎忽略检查Edit - Project Settings - Physics 2D查看Layer Collision Matrix确保对应层之间的碰撞是勾选的。碰撞体是否太小或位置偏移导致实际上没有接触性能提示Raycast和Overlap系列函数如Physics2D.OverlapCircle每帧调用成本较高。避免在Update中对大量物体进行全场景检测。对于频繁的检测如地面检测可以限定距离和层。对于AI的视野检测可以考虑每几帧进行一次而不是每帧。6. 核心模块五对象生命周期与生成销毁——GameObject 与 Instantiate/Destroy游戏世界是动态的敌人不断涌现子弹飞射而出特效绽放又消失。这一切都离不开对象的创建与销毁。6.1 创建对象InstantiateInstantiate方法用于克隆实例化一个预制体或场景中已有的对象。public GameObject bulletPrefab; // 在Inspector中拖入子弹预制体 public Transform firePoint; // 发射点 void Fire() { // 1. 最简单的实例化生成在原点无旋转 // GameObject newBullet Instantiate(bulletPrefab); // 2. 指定位置和旋转常用 GameObject newBullet Instantiate(bulletPrefab firePoint.position firePoint.rotation); // 3. 实例化后可以立即获取其组件并进行配置 // BulletScript bulletScript newBullet.GetComponentBulletScript(); // if (bulletScript ! null) bulletScript.damage 20; // 4. 实例化后设置父物体用于UI或对象层级管理 // newBullet.transform.SetParent(bulletContainer.transform); }关键技巧使用预制体永远不要直接实例化场景中已有的对象。应该将配置好的对象如子弹、敌人拖入Project视图做成预制体然后实例化这个预制体。这保证了每次生成的对象都是一致的。管理生成点不要用Vector3.zero作为生成位置。创建一个空的GameObject作为firePoint或spawnPoint将其作为子物体放在发射者如枪口位置这样即使角色旋转发射点也会随之移动和旋转。6.2 销毁对象DestroyDestroy用于从场景中移除一个游戏对象、组件或资产。// 销毁这个脚本所在的游戏对象 Destroy(gameObject); // 延迟2秒后销毁 Destroy(gameObject 2f); // 销毁某个特定的组件 BulletScript myScript GetComponentBulletScript(); if (myScript ! null) Destroy(myScript);注意事项Destroy不是立即生效的Destroy调用后对象会在当前帧的更新循环结束后才被真正销毁。这意味着在调用Destroy之后、本帧结束之前你仍然可以访问该对象及其组件但这通常是不安全的。避免空引用一个常见错误是在对象被销毁后其他地方还保留着对它的引用并在后续尝试访问它导致MissingReferenceException。对于像子弹、敌人这类频繁创建销毁的对象使用对象池是更优的性能解决方案。销毁与禁用如果对象需要频繁“出现”和“消失”如敌人、子弹与其反复Instantiate和Destroy不如使用SetActive(false)将其禁用并放入一个对象池中需要时再SetActive(true)并重置状态。这能极大减少GC垃圾回收带来的性能卡顿。7. 核心模块六查找与获取对象——GameObject.Find 与 GetComponent如何在代码中找到并控制其他对象这是构建游戏逻辑联系的基础。7.1 查找对象GameObject.Find与Transform.FindGameObject.Find(string name)通过对象在场景中的名称查找。性能开销较大因为它会遍历场景中所有激活的对象。不推荐在Update等每帧调用的函数中使用。通常用于在Start或Awake中初始化时查找一次。void Start() { GameObject player GameObject.Find(“Player”); if (player ! null) { // 找到了Player对象 } }Transform.Find(string name)在当前对象的子物体中查找指定名称的对象。效率比GameObject.Find高但仅限于子物体。路径支持使用“/”进行层级查找如transform.Find(“Arm/Hand/Weapon”)。更优的查找方案公共字段序列化这是最推荐、最高效的方式。直接将需要引用的对象在Inspector面板中拖拽赋值。public GameObject targetEnemy; // 在Inspector中拖入使用标签GameObject.FindWithTag(string tag)。可以为一类对象如“Player” “Enemy” “Coin”设置标签然后通过标签查找。比GameObject.Find稍好但同样避免在Update中使用。使用单例模式或静态访问点对于全局唯一的对象如GameManager AudioManager可以将其自身实例保存在一个静态属性中供其他脚本直接访问。7.2 获取组件GetComponent、GetComponentInChildren与GetComponentInParent获取组件是与对象交互的核心。GetComponentT()从当前游戏对象上获取类型为T的组件。如果对象上没有该组件则返回null。Rigidbody2D rb GetComponentRigidbody2D(); if (rb ! null) { rb.velocity new Vector2(10f rb.velocity.y); }GetComponentInChildrenT()从当前对象及其所有子物体中查找深度优先第一个找到的类型为T的组件。GetComponentInParentT()从当前对象及其所有父物体中查找第一个找到的类型为T的组件。性能优化与缓存GetComponent调用本身有一定开销。如果一个组件在脚本生命周期内需要被多次访问务必将其缓存起来。public class PlayerController : MonoBehaviour { // 声明私有变量来缓存组件 private Rigidbody2D rb; private Animator animator; private SpriteRenderer spriteRenderer; void Start() { // 在Start或Awake中获取一次并缓存 rb GetComponentRigidbody2D(); animator GetComponentAnimator(); spriteRenderer GetComponentSpriteRenderer(); // 错误示范在Update中每帧获取性能浪费 // void Update() { // GetComponentRigidbody2D().velocity ...; // } } void Update() { // 使用缓存的引用高效 if (rb ! null) { // 操作rb } } }常见问题GetComponent返回null首先检查脚本是否确实挂在了正确的游戏对象上。其次检查组件名称是否拼写正确注意大小写。对于泛型方法GetComponentT()确保类型T是存在的组件类名。查找不到子物体Transform.Find只查找直接激活的子物体。如果子物体本身被禁用了或者你要查找的物体在更深层级且中间有未激活的物体可能会查找失败。对于复杂查找考虑使用公共字段拖拽赋值或者通过标签、发送消息等方式进行通信。
返回列表