Langchain系列文章目录
01-玩转LangChain:从模型调用到Prompt模板与输出解析的完整指南
02-玩转 LangChain Memory 模块:四种记忆类型详解及应用场景全覆盖
03-全面掌握 LangChain:从核心链条构建到动态任务分配的实战指南
04-玩转 LangChain:从文档加载到高效问答系统构建的全程实战
05-玩转 LangChain:深度评估问答系统的三种高效方法(示例生成、手动评估与LLM辅助评估)
06-从 0 到 1 掌握 LangChain Agents:自定义工具 + LLM 打造智能工作流!
07-【深度解析】从GPT-1到GPT-4:ChatGPT背后的核心原理全揭秘
PyTorch系列文章目录
Python系列文章目录
C#系列文章目录
01-C#与游戏开发的初次见面:从零开始的Unity之旅
02-C#入门:从变量与数据类型开始你的游戏开发之旅
03-C#运算符与表达式:从入门到游戏伤害计算实践
04-从零开始学C#:用if-else和switch打造智能游戏逻辑
05-掌握C#循环:for、while、break与continue详解及游戏案例
06-玩转C#函数:参数、返回值与游戏中的攻击逻辑封装
07-Unity游戏开发入门:用C#控制游戏对象移动
08-C#面向对象编程基础:类的定义、属性与字段详解
09-C#封装与访问修饰符:保护数据安全的利器
10-如何用C#继承提升游戏开发效率?Enemy与Boss案例解析
11-C#多态性入门:从零到游戏开发实战
12-C#接口王者之路:从入门到Unity游戏开发实战 (IAttackable案例详解)
13-C#静态成员揭秘:共享数据与方法的利器
14-Unity 面向对象实战:掌握组件化设计与脚本通信,构建玩家敌人交互
15-C#入门 Day15:彻底搞懂数组!从基础到游戏子弹管理实战
16-C# List 从入门到实战:掌握动态数组,轻松管理游戏敌人列表 (含代码示例)
17-C# 字典 (Dictionary) 完全指南:从入门到游戏属性表实战 (Day 17)
18-C#游戏开发【第18天】 | 深入理解队列(Queue)与栈(Stack):从基础到任务队列实战
19-【C# 进阶】深入理解枚举 Flags 属性:游戏开发中多状态组合的利器
20-C#结构体(Struct)深度解析:轻量数据容器与游戏开发应用 (Day 20)
21-Unity数据持久化进阶:告别硬编码,用ScriptableObject优雅管理游戏配置!(Day 21)
22-Unity C# 健壮性编程:告别崩溃!掌握异常处理与调试的 4 大核心技巧 (Day 22)
23-C#代码解耦利器:委托与事件(Delegate & Event)从入门到实践 (Day 23)
24-Unity脚本通信终极指南:从0到1精通UnityEvent与事件解耦(Day 24)
25-精通C# Lambda与LINQ:Unity数据处理效率提升10倍的秘诀! (Day 25)
26-# Unity C#进阶:掌握泛型编程,告别重复代码,编写优雅复用的通用组件!(Day26)
27-Unity协程从入门到精通:告别卡顿,用Coroutine优雅处理异步与时序任务 (Day 27)
28-搞定玩家控制!Unity输入系统、物理引擎、碰撞检测实战指南 (Day 28)
文章目录
前言
欢迎来到《C# for Unity》专栏学习的第28天!经过前几周对C#基础、面向对象、数据结构以及C#进阶特性的学习,我们已经打下了坚实的编程基础。今天,我们将深入探讨Unity中构建交互式游戏体验的三大基石:输入系统(Input System)、物理系统(Physics System)以及碰撞与触发器(Collision & Trigger)。理解并熟练运用这些核心机制,是让玩家能够顺畅地控制角色、与游戏世界进行有效互动的关键。本篇将系统性地讲解新旧输入系统的使用、Rigidbody和Collider的核心概念与配置、碰撞/触发事件的处理,并通过实战案例,带你一步步实现角色移动、跳跃以及基于碰撞的物品拾取和伤害判定。准备好,让我们开始构建更生动的游戏世界吧!
一、输入系统:捕捉玩家意图的桥梁
输入系统是游戏与玩家沟通的第一个环节,它负责捕捉玩家的操作(键盘、鼠标、手柄、触摸屏等)并将其转化为游戏逻辑可以理解的信号。Unity提供了两套输入系统:传统的Input Manager和现代的Input System。
1.1 为何需要输入系统?
想象一下没有输入的互动体验——游戏将变成一部无法参与的电影。输入系统是连接玩家操作与游戏角色行为的纽带。无论是角色的移动、跳跃、攻击,还是菜单的导航、物品的选择,都离不开输入系统的支持。一个设计良好的输入系统能提供灵敏、准确且跨平台的控制体验。
1.2 旧版 Input Manager 概览
Input Manager是Unity内置的传统输入解决方案,配置简单直观,适合快速原型开发或需求相对简单的项目。
1.2.1 配置轴(Axis)与按键(Button)
在Unity编辑器中,通过 Edit > Project Settings > Input Manager
可以进行配置。这里预设了一些常用输入,如 “Horizontal”, “Vertical”, “Jump”, “Fire1” 等。你可以修改它们或添加自定义输入。
- Axes (轴): 用于处理连续变化的输入,如摇杆移动、鼠标移动。关键属性包括:
Name
: 输入的名称,在代码中通过此名称引用。Type
: 输入类型(Key / Mouse Button, Mouse Movement, Joystick Axis)。Axis
: 对应的物理轴(如 X axis, Y axis)。Positive Button / Negative Button
: 定义触发轴正负向移动的按键。Sensitivity
: 输入值变化的速度。Gravity
: 输入值回归到0的速度。
- Buttons (按键): 用于处理离散的按键事件,如跳跃、开火。
1.2.2 C#脚本获取输入
在脚本中,通常在 Update
或 FixedUpdate
方法中使用 Input
类来获取输入状态:
using UnityEngine;
public class OldInputDemo : MonoBehaviour
{
public float moveSpeed = 5f;
public float jumpForce = 10f;
private Rigidbody rb;
void Start()
{
rb = GetComponent<Rigidbody>(); // 获取Rigidbody组件
}
void Update()
{
// 1. 获取轴输入 (连续) - 返回值范围 -1 到 1
float moveHorizontal = Input.GetAxis("Horizontal"); // 获取水平轴输入
float moveVertical = Input.GetAxis("Vertical"); // 获取垂直轴输入
Vector3 movement = new Vector3(moveHorizontal, 0.0f, moveVertical);
// 注意:直接修改transform.position不符合物理模拟,稍后会用Rigidbody改进
// transform.Translate(movement * moveSpeed * Time.deltaTime, Space.World);
// 2. 获取按键按下事件 (一次性触发)
if (Input.GetButtonDown("Jump")) // 检测“Jump”按钮是否被按下
{
Debug.Log("Jump button pressed!");
// 跳跃逻辑通常在FixedUpdate中应用力
}
// 3. 获取鼠标按键按下
if (Input.GetMouseButtonDown(0)) // 0代表鼠标左键
{
Debug.Log("Mouse Left Button pressed!");
}
// 4. 获取按键状态 (持续按下)
if (Input.GetKey(KeyCode.LeftShift)) // 检测左Shift键是否被持续按下
{
Debug.Log("Left Shift is held down.");
}
}
void FixedUpdate()
{
// 物理相关的移动建议在FixedUpdate中处理
float moveHorizontal = Input.GetAxis("Horizontal");
float moveVertical = Input.GetAxis("Vertical");
Vector3 movement = new Vector3(moveHorizontal, 0.0f, moveVertical);
rb.AddForce(movement * moveSpeed); // 使用力来移动,更符合物理
if (Input.GetButtonDown("Jump")) // 在FixedUpdate检测跳跃输入可能丢失,最好在Update检测,用标志位传递
{
// 实际跳跃施加力
// rb.AddForce(Vector3.up * jumpForce, ForceMode.Impulse); // 确保只在Update检测到跳跃时执行一次
}
}
}
1.2.3 优缺点分析
- 优点: 配置简单,上手快,无需额外安装包。
- 缺点:
- 配置与代码分离,管理复杂输入方案时易混乱。
- 对多设备(手柄、触摸)支持不够灵活和强大。
- 不支持运行时的输入重绑定。
- 输入处理分散在
Update
中,可能不够清晰。
1.3 新版 Input System 详解
为了解决旧版系统的不足,Unity推出了全新的Input System包,它基于事件驱动,更加灵活、强大且可扩展。
1.3.1 引入与安装
需要通过 Window > Package Manager
搜索并安装 “Input System” 包。安装后,Unity会提示是否启用新的后端,选择“是”并重启编辑器。你也可以在 Project Settings > Player > Active Input Handling
中切换新旧系统或两者并存。
1.3.2 核心概念:Action Maps, Actions, Bindings
新系统围绕以下核心概念构建:
- Input Actions Asset (.inputactions): 一个资源文件,用于集中定义所有的输入行为。
- Action Maps (动作映射表): 对相关联的输入动作进行分组,例如 “Player” Action Map包含移动、跳跃、攻击等动作,“UI” Action Map包含导航、确认、取消等动作。同一时间通常只激活一个Action Map。
- Actions (动作): 代表一个具体的玩家意图,如 “Move”, “Jump”, “Fire”。每个Action可以设置类型(Value, Button, Pass Through)。
Value
: 用于连续变化的输入(如移动向量)。Button
: 用于触发式输入(如跳跃、开火)。Pass Through
: 不做处理,直接传递原始设备输入。
- Bindings (绑定): 将一个或多个具体的物理输入(如 W 键、手柄左摇杆、鼠标左键)映射到一个Action。可以设置多个绑定,实现多设备支持。还可以添加交互(Interactions, 如 Hold, Tap)和处理器(Processors, 如 Normalize Vector, Scale)。
1.3.3 Player Input 组件与事件驱动
Player Input
是一个方便的组件,可以直接添加到玩家对象上。
- 配置: 在Inspector中指定
Actions
资源。 - Behavior: 设置输入响应方式:
Send Messages
: 调用预定义名称的方法(如OnMove
,OnJump
)。Broadcast Messages
: 同上,但会广播到子对象。Invoke Unity Events
: 在Inspector中暴露UnityEvent
,可以拖拽脚本方法进行响应。这是推荐的方式之一,耦合度低。Invoke C# Events
: 触发C#事件,需要在代码中订阅。
1.3.4 C#脚本响应输入
(1) 使用 Player Input 组件 (Invoke Unity Events)
- 创建Input Actions Asset,定义好Action Maps, Actions, Bindings。
- 在玩家对象上添加
Player Input
组件,关联Actions资源,设置Behavior为Invoke Unity Events
。 - 在你的玩家控制脚本中,创建公共方法来响应事件:
using UnityEngine;
using UnityEngine.InputSystem; // 引入新输入系统命名空间
public class NewInputPlayerController : MonoBehaviour
{
public float moveSpeed = 5f;
public float jumpForce = 7f;
private Rigidbody rb;
private Vector2 moveInput;
private bool isGrounded; // 需要实现地面检测逻辑
void Start()
{
rb = GetComponent<Rigidbody>();
}
// Player Input组件会调用这些方法 (需在Inspector中将对应Action的Events链接到这里)
public void OnMove(InputValue value) // 方法名需与Action名匹配 (On + Action名)
{
moveInput = value.Get<Vector2>(); // 获取Vector2类型的输入值
Debug.Log($"Move Input: {moveInput}");
}
public void OnJump(InputValue value)
{
if (value.isPressed && isGrounded) // 确保按下且在地面
{
rb.AddForce(Vector3.up * jumpForce, ForceMode.Impulse); // 施加瞬时力
Debug.Log("Jump Action Triggered!");
}
}
void FixedUpdate()
{
// 根据输入应用移动力
Vector3 movement = new Vector3(moveInput.x, 0.0f, moveInput.y); // 注意这里Y是2D输入,对应世界的Z轴
rb.AddForce(movement * moveSpeed);
// 更新isGrounded状态 (需要自己实现,如射线检测)
CheckGroundStatus();
}
void CheckGroundStatus()
{
// 示例:简单的向下射线检测
float rayLength = 0.6f; // 比角色高度的一半稍长一点
isGrounded = Physics.Raycast(transform.position, Vector3.down, rayLength);
Debug.DrawRay(transform.position, Vector3.down * rayLength, isGrounded ? Color.green : Color.red);
}
}
(2) 手动引用和读取 Action
如果不使用 Player Input
组件,也可以在代码中手动管理 Input Actions。
using UnityEngine;
using UnityEngine.InputSystem;
public class ManualInputHandler : MonoBehaviour
{
public InputActionsAsset playerControls; // 在Inspector中指定Input Actions Asset
public float moveSpeed = 5f;
private InputAction moveAction;
private InputAction jumpAction;
private Rigidbody rb;
void Awake()
{
playerControls = new InputActionsAsset(); // 实例化
rb = GetComponent<Rigidbody>();
// 获取Action引用 (格式:"ActionMapName/ActionName")
moveAction = playerControls.Player.Move;
jumpAction = playerControls.Player.Jump;
// 订阅事件 (可选,也可以直接读取)
jumpAction.performed += context => Jump(); // 当动作执行时调用Jump方法
}
void OnEnable()
{
moveAction.Enable();
jumpAction.Enable();
}
void OnDisable()
{
moveAction.Disable();
jumpAction.Disable();
}
void FixedUpdate()
{
// 读取Move Action的当前值
Vector2 moveInput = moveAction.ReadValue<Vector2>();
Vector3 movement = new Vector3(moveInput.x, 0.0f, moveInput.y);
rb.AddForce(movement * moveSpeed);
}
void Jump()
{
// 这里需要添加isGrounded检查
Debug.Log("Jump Action Performed via C# Event!");
rb.AddForce(Vector3.up * jumpForce, ForceMode.Impulse);
}
}
1.3.5 新旧对比与选择建议
特性 | 旧 Input Manager | 新 Input System |
---|---|---|
配置 | Project Settings (全局) | Input Actions Asset (可多个) |
灵活性 | 低,硬编码或编辑器配置 | 高,Action Maps, Bindings, Interactions |
设备支持 | 有限,手柄映射较麻烦 | 广泛,易于添加新设备和重映射 |
运行时重绑定 | 不支持 | 支持 |
事件模型 | 无,需轮询 (Update ) | 支持事件驱动 (performed , canceled ) |
学习曲线 | 低 | 中等 |
建议: 对于新项目,强烈推荐使用新版Input System。它提供了更强大、灵活和面向未来的解决方案。对于维护旧项目或快速原型,旧版Input Manager仍然可用。
二、物理系统:赋予世界规则与重量
Unity内置了强大的NVIDIA PhysX物理引擎(用于3D)和Box2D(用于2D),它们负责模拟现实世界中的力、运动、碰撞等效果。要让一个游戏对象参与物理模拟,核心是添加Rigidbody和Collider组件。
2.1 物理引擎的核心:Rigidbody (刚体)
Rigidbody
组件赋予游戏对象物理属性,使其能够响应重力、力、扭矩和碰撞。
2.1.1 Rigidbody 组件的作用
添加了Rigidbody的对象会被物理引擎接管其运动。这意味着你不应直接通过修改 transform.position
或 transform.rotation
来移动它,而应该通过施加力 (AddForce
)、改变速度 (velocity
) 或使用物理引擎提供的移动方法 (MovePosition
, MoveRotation
)。
2.1.2 Rigidbody 类型:动态、运动学、静态
Rigidbody组件有三种主要模式(Body Type),决定了对象如何与物理世界交互:
- Dynamic (动态):
- 描述: 完全受物理引擎控制,响应力、碰撞和重力。拥有质量(Mass)。
- 用途: 玩家角色、可被推动的箱子、掉落物、炮弹等需要真实物理行为的对象。
- 注意: 需要至少一个非触发器的Collider来与其他物体互动。
- Kinematic (运动学):
- 描述: 不受力、碰撞(不会被撞开)或重力的影响,但可以通过脚本直接控制其
transform
或使用Rigidbody.MovePosition/MoveRotation
来移动。它可以影响动态Rigidbody(推开它们),并且可以接收碰撞和触发事件。 - 用途: 由动画控制的角色、玩家直接控制的平台、门、需要精确脚本控制路径但又能与其他物体交互的对象。
- 描述: 不受力、碰撞(不会被撞开)或重力的影响,但可以通过脚本直接控制其
- Static (静态):
- 描述: 技术上说,没有Rigidbody组件或者Rigidbody组件处于休眠状态的对象被视为静态。它们不移动,是物理世界中的固定障碍物。
- 用途: 地面、墙壁、固定的障碍物。
- 实现: 通常只需要添加Collider组件,不需要Rigidbody。如果一个对象既需要检测碰撞(如墙壁),又完全不动,就不需要添加Rigidbody。如果一个原本是Dynamic或Kinematic的对象需要临时变为完全静态,可以禁用Rigidbody组件或将其设置为Kinematic并停止移动。
类型 | 受力/重力影响 | 可通过脚本移动 | 能否推动Dynamic Rigidbody | 接收碰撞/触发事件 | 常见用途 |
---|---|---|---|---|---|
Dynamic | 是 | 通过力/速度 | 是 | 是 | 玩家, 可动物体, 抛射物 |
Kinematic | 否 | 是 (Transform/Move) | 是 | 是 | 动画控制角色, 移动平台, 门 |
Static | 否 | 否 | 否 | 是 (若有Collider) | 地面, 墙壁, 固定障碍物 |
2.1.3 重要属性设置
- Mass (质量): 影响物体惯性和碰撞反应。质量大的物体更难被移动。
- Drag (线性阻力): 物体移动时受到的空气阻力,影响速度衰减。
- Angular Drag (角阻力): 物体旋转时受到的空气阻力,影响角速度衰减。
- Use Gravity (使用重力): 是否受全局重力影响。
- Is Kinematic: 勾选后,Rigidbody类型变为Kinematic。
- Constraints (约束): 可以冻结在某些轴向上的移动或旋转。例如,冻结Y轴移动防止角色意外飞起,冻结XZ轴旋转防止角色摔倒。
- Collision Detection (碰撞检测模式):
Discrete
: 默认模式,性能较好,但在高速移动时可能发生“隧道效应”(穿透薄物体)。Continuous
: 用于高速移动的对象(如玩家、子弹),防止穿透,但消耗更多性能。Continuous Dynamic
: 用于高速移动且需要与其它高速移动对象精确碰撞的对象,性能消耗最大。Continuous Speculative
: 一种基于推测的连续碰撞检测,通常性能介于Discrete和Continuous之间,且能提供较好的稳定性。
2.2 定义形状:Collider (碰撞器)
Collider
组件定义了游戏对象用于物理碰撞的几何形状。它不必与模型的视觉形状完全一致,通常使用更简单的形状(如球体、胶囊体)来优化性能。
2.2.1 Collider 的必要性
没有Collider的对象无法与物理世界进行碰撞交互,即使它有Rigidbody。Collider定义了物体的物理边界。
2.2.2 常见 Collider 类型
- Box Collider: 立方体形状,简单高效。
- Sphere Collider: 球体形状,适用于球形物体或需要平滑碰撞的场合。
- Capsule Collider: 胶囊形状(圆柱体两端加半球),常用于角色控制器,因为它没有棱角,不易被卡住。
- Mesh Collider: 使用模型的实际网格作为碰撞形状。
- 优点: 精确贴合模型。
- 缺点: 性能开销大,特别是对于复杂模型。默认情况下,非凸(Concave)的Mesh Collider不能与另一个非凸的Mesh Collider碰撞。 如果需要,必须勾选
Convex
将其转换为凸包形状,但这会牺牲精度。 - 用途: 复杂的静态环境(如地形、不规则建筑),或需要精确碰撞的特殊物体(勾选Convex后也可用于动态物体)。
- Terrain Collider: 专门用于Unity地形对象的碰撞器。
选择原则: 优先使用简单的基元碰撞器(Box, Sphere, Capsule),性能最好。仅在必要时使用Mesh Collider。对于角色,Capsule Collider通常是最佳选择。
2.2.3 Is Trigger (触发器) 选项
当Collider组件的 Is Trigger
选项被勾选时:
- 该Collider不再产生物理碰撞响应(不会阻挡物体,也不会被物体阻挡)。
- 它变成了一个“感应区域”。当其他带有Rigidbody和Collider的对象进入、停留或离开这个区域时,会触发触发器事件(
OnTriggerEnter
,OnTriggerStay
,OnTriggerExit
)。 - 用途: 检测玩家是否进入某个区域(如检查点、陷阱区域、拾取物品范围)、实现非物理交互(如穿过幽灵墙)。
重要: 要使碰撞或触发事件发生,参与交互的两个对象中,至少有一个必须拥有Rigidbody组件。
三、碰撞与触发器:交互的火花
当带有Collider的对象在物理引擎的作用下相互接触时,就会发生碰撞或触发事件。这允许我们编写脚本来响应这些交互,实现游戏逻辑。
3.1 物理碰撞事件
当两个Collider发生物理接触(Is Trigger
未勾选),并且至少一方有非Kinematic的Rigidbody时,会触发碰撞事件。这些事件会传递一个 Collision
对象,包含了碰撞的详细信息。
3.1.1 OnCollisionEnter:碰撞开始
当一个Collider/Rigidbody开始接触另一个Collider/Rigidbody时,在附加了脚本的对象上调用一次。
void OnCollisionEnter(Collision collision) // 注意参数类型是 Collision
{
// collision 参数包含了碰撞信息
Debug.Log($"开始碰撞到对象: {collision.gameObject.name}"); // 获取碰撞到的对象名称
// 获取碰撞点信息
foreach (ContactPoint contact in collision.contacts)
{
Debug.DrawRay(contact.point, contact.normal, Color.white); // 绘制碰撞点法线
}
// 检查碰撞对象的标签或组件
if (collision.gameObject.CompareTag("Obstacle"))
{
Debug.Log("撞到了障碍物!");
// 执行相关逻辑,如减速、播放音效等
}
}
3.1.2 OnCollisionStay:持续碰撞
当两个Collider/Rigidbody保持接触状态时,每一物理帧都会调用。
void OnCollisionStay(Collision collision)
{
Debug.Log($"持续碰撞中: {collision.gameObject.name}");
// 适用于需要持续检测的场景,如站在移动平台上
}
3.1.3 OnCollisionExit:碰撞结束
当两个Collider/Rigidbody停止接触时调用一次。
void OnCollisionExit(Collision collision)
{
Debug.Log($"停止碰撞对象: {collision.gameObject.name}");
// 适用于离开某个区域或物体后的逻辑
}
3.1.4 获取碰撞信息 (Collision)
Collision
对象包含丰富信息:
collision.gameObject
: 发生碰撞的另一个游戏对象。collision.collider
: 发生碰撞的另一个Collider组件。collision.transform
: 发生碰撞的另一个对象的Transform组件。collision.rigidbody
: 发生碰撞的另一个对象的Rigidbody组件(如果存在)。collision.contacts
: 一个ContactPoint
数组,包含所有碰撞点的详细信息(位置point
、法线normal
等)。collision.relativeVelocity
: 两个碰撞对象间的相对线性速度。
3.2 触发器事件
当一个Collider(勾选了 Is Trigger
)与另一个Collider(至少一方有Rigidbody)发生重叠时,会触发触发器事件。这些事件传递的是进入触发区域的 Collider
对象。
3.2.1 OnTriggerEnter:进入触发区域
当另一个Collider进入这个触发器时调用一次。
void OnTriggerEnter(Collider other) // 注意参数类型是 Collider
{
Debug.Log($"对象 {other.gameObject.name} 进入了触发区域");
// 检查进入对象的标签或组件
if (other.CompareTag("Player"))
{
Debug.Log("玩家进入!");
// 执行相关逻辑,如拾取物品、触发陷阱、显示提示
}
}
3.2.2 OnTriggerStay:停留在触发区域
当另一个Collider保持在触发器区域内时,每一物理帧都会调用。
void OnTriggerStay(Collider other)
{
Debug.Log($"对象 {other.gameObject.name} 停留在触发区域内");
// 适用于持续效果,如站在治疗区域回血
}
3.2.3 OnTriggerExit:离开触发区域
当另一个Collider离开这个触发器时调用一次。
void OnTriggerExit(Collider other)
{
Debug.Log($"对象 {other.gameObject.name} 离开了触发区域");
// 适用于离开区域后的逻辑,如停止回血、关闭提示
}
3.2.4 与碰撞事件的区别
特性 | 碰撞事件 (OnCollision... ) | 触发器事件 (OnTrigger... ) |
---|---|---|
要求 | 双方Collider (Is Trigger 关闭) | 一方Collider (Is Trigger 开启) |
至少一方有非Kinematic Rigidbody | 至少一方有Rigidbody | |
物理响应 | 产生力的交互(阻挡、弹开等) | 无物理阻挡,仅检测重叠 |
事件参数 | Collision (含接触点信息) | Collider (进入/离开的对象) |
用途 | 模拟物理碰撞、伤害判定 | 区域检测、拾取、非物理交互 |
3.3 优化交互:物理材质 (Physic Material)
Physic Material
是一种资源,可以应用到Collider上,用来控制碰撞时的物理特性,主要是摩擦力(Friction)和弹性(Bounciness)。
3.3.1 物理材质的作用
- 摩擦力: 决定了物体相互接触时滑动或滚动的难易程度。高摩擦力使物体更难滑动。
- 弹性: 决定了物体碰撞后反弹的程度。值为0表示完全不反弹,值为1表示完全反弹(理论上,实际会略有能量损失)。
3.3.2 创建与应用
- 在Project窗口右键
Create > Physic Material
。 - 选中创建的物理材质资源,在Inspector中调整属性。
- 将该物理材质拖拽到游戏对象上Collider组件的
Material
字段。
3.3.3 关键属性
- Dynamic Friction (动态摩擦系数): 物体滑动时的摩擦力 (0-1)。
- Static Friction (静摩擦系数): 物体开始滑动前需要克服的摩擦力 (0-1)。通常静摩擦系数略大于动态摩擦系数。
- Bounciness (弹性系数): 碰撞反弹程度 (0-1)。
- Friction Combine (摩擦力组合模式): 当两个带有不同物理材质的物体碰撞时,如何计算它们之间的综合摩擦力(Average, Minimum, Maximum, Multiply)。
- Bounce Combine (弹性组合模式): 同上,用于计算综合弹性。
应用场景: 制作冰面(低摩擦)、橡胶球(高弹性)、粗糙表面(高摩擦)等。
四、实践:让角色动起来并与世界互动
现在,我们将运用前面学到的知识,实现一个简单的第三人称角色控制器,包含移动、跳跃,以及通过碰撞/触发机制拾取物品和受到伤害。
(假设你已设置好场景,有一个代表玩家的Capsule对象和一个代表地面的Plane对象)
4.1 基础角色移动
4.1.1 设置玩家对象
- 选中玩家Capsule对象。
- 添加
Rigidbody
组件。调整属性:Mass
: 1Drag
: 1 (或根据需要调整)Angular Drag
: 5 (或更高,防止旋转过快)Use Gravity
: 勾选Constraints
: 冻结Freeze Rotation
的 X 和 Z 轴,防止角色摔倒。
- 确保玩家对象有
Capsule Collider
组件,调整大小以匹配视觉模型。Is Trigger
不要勾选。 - 给玩家对象添加一个新的C#脚本,例如
PlayerController
。 - (可选,推荐) 添加
Player Input
组件,设置好 Actions 资源(包含 “Move” Vector2 Action 和 “Jump” Button Action),并将 Behavior 设置为Invoke Unity Events
。
4.1.2 编写移动脚本(使用新输入系统)
using UnityEngine;
using UnityEngine.InputSystem;
[RequireComponent(typeof(Rigidbody))] // 确保对象有Rigidbody
public class PlayerController : MonoBehaviour
{
public float moveSpeed = 8f;
public float jumpForce = 10f;
public Transform groundCheck; // 在玩家底部创建一个空对象作为地面检测点
public LayerMask groundLayer; // 设置哪些层是地面
public float groundDistance = 0.4f; // 地面检测距离
private Rigidbody rb;
private Vector2 moveInput;
private bool isGrounded;
private bool jumpRequested = false;
void Awake()
{
rb = GetComponent<Rigidbody>();
// 可以不在这里禁用光标,根据游戏需要决定
// Cursor.lockState = CursorLockMode.Locked;
// Cursor.visible = false;
}
// 由Player Input组件的Events调用
public void OnMove(InputValue value)
{
moveInput = value.Get<Vector2>();
}
// 由Player Input组件的Events调用
public void OnJump(InputValue value)
{
if (value.isPressed)
{
jumpRequested = true; // 在Update中标记跳跃请求
}
}
void Update()
{
// 在Update中检测是否着地
isGrounded = Physics.CheckSphere(groundCheck.position, groundDistance, groundLayer);
// Debug visualization for ground check
// Debug.DrawWireSphere(groundCheck.position, groundDistance, Color.yellow);
}
void FixedUpdate()
{
// 处理移动
// 将二维输入转换为三维世界移动方向 (相对于角色自身)
// Vector3 moveDirection = transform.right * moveInput.x + transform.forward * moveInput.y;
// 如果希望基于世界坐标移动:
Vector3 moveDirection = new Vector3(moveInput.x, 0f, moveInput.y);
rb.AddForce(moveDirection.normalized * moveSpeed, ForceMode.Force); // 使用持续力
// 处理跳跃
if (jumpRequested && isGrounded)
{
rb.AddForce(Vector3.up * jumpForce, ForceMode.Impulse); // 施加瞬时向上的力
jumpRequested = false; // 重置跳跃请求
}
else
{
jumpRequested = false; // 如果不在地面,也取消本次跳跃请求
}
}
}
设置说明:
- 创建一个空对象作为
Player
的子对象,命名为GroundCheck
,并将其放置在角色脚底稍下方。 - 在
PlayerController
组件的 Inspector 中,将GroundCheck
对象拖入Ground Check
字段。 - 创建一个新的 Layer (例如 “Ground”),并将你的地面对象的 Layer 设置为 “Ground”。
- 在
PlayerController
组件的 Inspector 中,将Ground Layer
设置为包含 “Ground” 层。 - 将此脚本添加到玩家对象,并在
Player Input
组件的Events
中,将Player
Action Map 下的Move
Action 的Performed
和Canceled
事件链接到OnMove
方法,将Jump
Action 的Performed
事件链接到OnJump
方法。
4.1.3 实现跳跃 (已包含在4.1.2脚本中)
跳跃逻辑的关键在于:
- 检测跳跃输入 (
OnJump
)。 - 检测角色是否在地面 (
isGrounded
)。 - 只有在收到输入且在地面时,才施加向上的力 (
rb.AddForce
)。 - 使用
ForceMode.Impulse
施加瞬时力。 - 在
FixedUpdate
中执行物理力的施加。 - 地面检测通常使用
Physics.CheckSphere
,Physics.Raycast
或OnCollisionStay
实现。CheckSphere
比较常用和简单。
4.2 碰撞检测应用:拾取物品
4.2.1 创建物品预制件
- 创建一个简单的3D对象(如Cube或Sphere)代表可拾取物品。
- 添加
Collider
组件(如Box Collider
)。勾选Is Trigger
。 - (可选) 添加一个脚本
CollectibleItem.cs
用于标识或处理拾取效果。 - 创建一个
Tag
(例如 “Collectible”) 并赋给这个物品对象。 - 将配置好的物品对象拖拽到 Project 窗口,创建成 Prefab。
4.2.2 编写拾取逻辑 (OnTriggerEnter)
在 PlayerController.cs
脚本中添加 OnTriggerEnter
方法:
// ... (在 PlayerController 类中) ...
void OnTriggerEnter(Collider other)
{
if (other.gameObject.CompareTag("Collectible")) // 检查进入触发器的是否是可拾取物品
{
Debug.Log("拾取了物品: " + other.gameObject.name);
// 在这里添加拾取逻辑,例如增加分数、添加到库存等
Destroy(other.gameObject); // 销毁被拾取的物品对象
}
// 可以添加对其他Tag的检测,如陷阱等
}
现在,当玩家走进(触发)带有 “Collectible” 标签且 Collider 设置为 Trigger 的物品时,该物品会被销毁,并打印日志。
4.3 碰撞检测应用:受到伤害
4.3.1 创建伤害区域/陷阱
- 创建一个代表陷阱或伤害区域的对象(如一个带刺的地板区域)。
- 添加
Collider
组件。- 方式一 (触发器): 勾选
Is Trigger
。玩家进入区域即受伤害。 - 方式二 (物理碰撞): 不勾选
Is Trigger
。玩家需要物理接触到陷阱表面才受伤害。如果陷阱本身需要是静态的,则不需要 Rigidbody;如果陷阱是移动的或需要物理交互,则添加 Rigidbody (通常设为 Kinematic)。
- 方式一 (触发器): 勾选
- 创建一个
Tag
(例如 “DamageZone”) 并赋给这个陷阱对象。 - (可选) 添加脚本
DamageZone.cs
来定义伤害值等。
4.3.2 编写受击逻辑 (OnCollisionEnter 或 OnTriggerEnter)
在 PlayerController.cs
脚本中添加相应的方法(根据陷阱是碰撞体还是触发器选择一个):
// ... (在 PlayerController 类中) ...
public int health = 100; // 示例玩家血量
// 如果陷阱是物理碰撞体 (Is Trigger 未勾选)
void OnCollisionEnter(Collision collision)
{
if (collision.gameObject.CompareTag("DamageZone"))
{
TakeDamage(10); // 假设每次碰撞受到10点伤害
Debug.Log($"因碰撞受到伤害! 当前血量: {health}");
}
// ... 可能还有其他碰撞逻辑 ...
}
// 如果陷阱是触发器 (Is Trigger 已勾选) - 需要修改上面的 OnTriggerEnter 或新增逻辑
void OnTriggerEnter(Collider other) // 合并或区分逻辑
{
if (other.gameObject.CompareTag("Collectible"))
{
Debug.Log("拾取了物品: " + other.gameObject.name);
Destroy(other.gameObject);
}
else if (other.gameObject.CompareTag("DamageZone")) // 检测伤害区域触发器
{
TakeDamage(20); // 假设进入触发区域受到20点伤害
Debug.Log($"进入伤害区域! 当前血量: {health}");
}
}
void TakeDamage(int damageAmount)
{
health -= damageAmount;
if (health <= 0)
{
Die();
}
}
void Die()
{
Debug.Log("玩家死亡!");
// 执行死亡逻辑,如游戏结束、播放死亡动画等
// gameObject.SetActive(false); // 简单禁用对象
}
确保你的陷阱对象被正确设置了 Tag (“DamageZone”) 和 Collider 类型(触发器或非触发器)。
五、常见问题与排查建议 (FAQ)
5.1 角色穿墙/抖动怎么办?
- 穿墙 (隧道效应):
- 检查 Rigidbody 的 Collision Detection 模式: 对于高速移动的角色或物体,将其设置为
Continuous
或Continuous Dynamic
。对于与其碰撞的静态环境(如墙壁),如果它们是薄片,也考虑设置对应的连续检测模式(通常Continuous Speculative
对静态物体足够)。 - 增加物理模拟频率: 在
Project Settings > Time > Fixed Timestep
中减小该值(如从默认0.02改为0.01),但这会增加CPU负担。 - 避免过大的单帧位移: 确保移动速度不会导致角色在一帧内完全穿过薄墙。
- 检查 Rigidbody 的 Collision Detection 模式: 对于高速移动的角色或物体,将其设置为
- 抖动:
- Rigidbody Interpolation (插值): 设置为
Interpolate
(根据上一物理帧平滑)或Extrapolate
(预测下一物理帧平滑)。这可以平滑视觉表现,尤其当物理更新频率(FixedUpdate
)低于渲染帧率(Update
)时。 - 在
FixedUpdate
中处理物理: 确保所有对 Rigidbody 的力、速度或MovePosition
的操作都在FixedUpdate
中进行。 - 检查碰撞器形状: 过于复杂的 Mesh Collider 或不合适的碰撞器形状可能导致卡住或抖动。尝试简化碰撞器。
- 地面检测抖动: 确保地面检测逻辑稳定,避免在接触地面边缘时快速切换
isGrounded
状态。
- Rigidbody Interpolation (插值): 设置为
5.2 碰撞/触发事件没反应?
- 检查Rigidbody: 确保发生交互的两个对象中,至少有一个附加了
Rigidbody
组件。对于碰撞事件(非Trigger),这个Rigidbody不能是 Kinematic(除非是 Kinematic Rigidbody 撞击 Dynamic Rigidbody)。对于触发器事件,只要有一方有 Rigidbody 即可。 - 检查Collider: 确保两个对象都有启用的
Collider
组件。 - 检查Is Trigger: 确认是否正确使用了
OnCollision...
(对应Is Trigger
关闭) 或OnTrigger...
(对应Is Trigger
开启)。不要混淆。 - 检查Layer Collision Matrix: 在
Project Settings > Physics
(或Physics 2D
) 中,检查 Layer Collision Matrix,确保参与碰撞的两个对象的 Layer 之间是允许碰撞的(对应格子是勾选状态)。 - 检查脚本和方法签名:
- 脚本是否已正确附加到拥有 Collider (和 Rigidbody) 的对象上?
- 事件方法的签名是否完全正确?(例如,
void OnCollisionEnter(Collision collision)
,void OnTriggerEnter(Collider other)
),包括大小写、参数类型。 - 脚本是否已启用?
- 检查对象是否被禁用或销毁: 在事件发生前,对象或脚本是否被
SetActive(false)
或Destroy()
了? - 使用
Debug.Log
: 在事件方法的第一行添加Debug.Log
,确认方法本身是否被调用。
5.3 新旧输入系统如何抉择?
- 新项目: 强烈建议使用新版 Input System。它更灵活、强大,支持现代设备和功能(如运行时重绑定、多玩家支持),且是 Unity 未来的发展方向。
- 旧项目维护: 如果项目已经深度使用旧版 Input Manager 且运行良好,不一定需要强制迁移,除非需要新系统的特定功能。
- 学习: 即使维护旧项目,也建议学习新版 Input System,因为它代表了更现代的输入处理方式。
- 两者并存: 可以在
Project Settings > Player > Active Input Handling
中选择Both
,允许项目中同时使用两种系统,方便逐步迁移或兼容旧插件。
六、总结
今天,我们深入探索了Unity中实现玩家与世界交互的三大核心机制:
- 输入系统: 学习了旧版Input Manager的配置与脚本使用 (
Input.GetAxis
,Input.GetButtonDown
等),以及新版Input System的优势、核心概念(Action Maps, Actions, Bindings)、Player Input
组件的事件驱动用法和手动脚本控制方法。新系统是未来趋势,提供了更优越的灵活性和设备支持。 - 物理系统: 理解了Rigidbody组件的作用,区分了Dynamic, Kinematic, Static三种类型及其适用场景,并掌握了重要属性(Mass, Drag, Constraints, Collision Detection)的设置。同时,了解了Collider定义物理边界的必要性,熟悉了常见的Collider类型(Box, Sphere, Capsule, Mesh)及其选择原则,并明确了
Is Trigger
的作用。 - 碰撞与触发器: 掌握了处理物理接触的碰撞事件 (
OnCollisionEnter/Stay/Exit
) 和处理区域感应的触发器事件 (OnTriggerEnter/Stay/Exit
),了解了它们各自的触发条件、参数(Collision
vsCollider
)和应用场景。 - 物理材质: 学会了使用
Physic Material
来调整碰撞的摩擦力和弹性,丰富物理交互的表现。 - 实践应用: 通过动手实践,我们成功实现了基于Rigidbody和新输入系统的角色移动与跳跃,并利用触发器实现了物品拾取,利用碰撞或触发器实现了受到伤害的逻辑。
掌握输入、物理和碰撞是构建任何交互式Unity游戏的基础。它们共同构成了游戏世界的基本规则和玩家控制角色探索这个世界的方式。希望通过今天的学习和实践,你对这些核心机制有了更深入的理解,并能将它们应用到自己的项目中去!继续努力,下一节课再见!