《Cocos游戏开发入门一本通》第四章
在现代游戏引擎中,组件系统是构建游戏对象功能的核心机制。它采用 “组合优于继承” 的设计理念,通过将不同功能封装为独立组件,再将组件挂载到节点上,实现游戏对象的多样化行为。这种设计不仅提高了代码的复用性和灵活性,还降低了系统的耦合度,使游戏开发更加高效。本章将全面探讨组件系统的概念、常用内置组件、自定义组件开发以及组件的生命周期,帮助开发者深入理解并灵活运用组件系统构建复杂的游戏功能。
4.1 组件(Component)概念
组件是游戏引擎中封装特定功能的模块化单元,它无法独立存在,必须依附于节点(Node)才能发挥作用。组件与节点的结合构成了游戏对象的完整功能,节点提供空间属性和层级关系,组件提供具体的行为逻辑。
4.1.1 组件的定义与核心作用
组件可以被定义为 “封装了特定功能逻辑、属性数据和交互方式的代码模块”。它的核心作用是将游戏对象的功能拆解为独立的、可复用的单元,使开发者能够通过组合不同组件快速构建复杂的游戏对象。
在传统的面向对象开发中,游戏对象的功能往往通过类继承实现,例如 “玩家角色” 继承 “角色基类”,“敌人角色” 也继承 “角色基类”,再各自添加专属功能。但这种方式存在明显缺陷:当功能需求复杂时,继承链会变得冗长,子类可能继承大量不需要的功能,且功能复用困难(如 “飞行” 功能既可能属于玩家,也可能属于敌人或道具,难以通过继承共享)。
组件系统则通过 “组合” 解决这一问题:无论玩家、敌人还是道具,都可以通过挂载 “飞行组件” 获得飞行能力,通过挂载 “碰撞检测组件” 获得碰撞功能。这种方式使功能与对象解耦,同一个组件可以被多个不同节点复用,极大提高了开发效率。
例如,在一款平台跳跃游戏中:
- 一个 “金币” 节点可以挂载 “碰撞组件”(检测玩家接触)+“收集逻辑组件”(触发加分)+“动画组件”(旋转动画);
- 一个 “陷阱” 节点可以挂载 “碰撞组件”(检测玩家接触)+“伤害组件”(扣除玩家生命值)+“粒子组件”(触发爆炸特效)。
两者通过组合不同组件实现了完全不同的功能,但都复用了 “碰撞组件” 这一基础模块。
4.1.2 组件的核心特性
组件系统之所以被现代游戏引擎广泛采用,源于其独特的特性:
- 封装性:组件将功能逻辑、属性数据和交互方式封装在内部,对外只暴露必要的接口(如方法、属性)。开发者无需关心组件内部实现,只需通过接口调用即可使用功能。例如,“动画组件” 封装了骨骼动画的加载、播放、暂停等逻辑,开发者只需调用play("run")即可让角色播放跑步动画。
- 复用性:同一组件可以挂载到多个不同节点上,实现功能的批量复用。例如,“发光组件” 可以同时挂载到金币、道具箱、传送门上,使这些对象都具备发光效果,无需为每个对象单独编写发光逻辑。
- 组合性:通过组合不同组件可以构建复杂功能。一个节点可以挂载任意数量的组件,组件之间通过节点或事件系统通信,形成协同工作的功能集合。例如,“玩家角色” 节点通过 “移动组件”+“攻击组件”+“生命值组件”+“音效组件” 的组合,实现移动、攻击、受击和音效反馈的完整功能。
- 独立性:组件之间尽可能保持独立,一个组件的修改或移除不会影响其他组件的基本功能(除非存在显式依赖)。例如,移除 “玩家角色” 的 “音效组件”,只会导致角色失去音效反馈,移动、攻击等功能仍能正常运行。
- 可扩展性:支持开发者自定义组件扩展引擎功能。当引擎内置组件无法满足需求时,开发者可以编写自定义组件,通过引擎提供的接口接入组件系统,与内置组件无缝协作。
4.1.3 组件与节点的关系
组件与节点是 “依附与被依附” 的关系,二者共同构成了游戏对象的完整概念:
- 节点是组件的载体:节点为组件提供存在的容器和空间属性(位置、旋转、缩放),组件必须挂载到节点上才能被引擎识别和执行。没有节点,组件无法独立运行;没有组件,节点只是一个空的空间坐标。
- 组件为节点赋予功能:节点本身仅包含基础的空间变换属性,而组件为节点添加了具体功能。例如,一个空节点挂载 “Sprite 组件” 后成为可见的图像,挂载 “Button 组件” 后成为可交互的按钮,挂载 “Rigidbody 组件” 后获得物理运动能力。
- 组件间通过节点通信:同一节点上的组件可以通过节点实例相互访问。例如,“攻击组件” 可以通过节点获取 “武器组件” 的攻击力数据,“生命值组件” 可以通知 “音效组件” 播放受伤音效。
- 组件共享节点的生命周期:组件的生命周期与所挂载的节点同步,节点被激活时组件开始运行,节点被销毁时组件也随之销毁。
这种关系可以类比为 “汽车与零件”:节点如同汽车的车架,提供基础结构和空间位置;组件如同发动机、轮胎、方向盘等零件,各自提供特定功能,组合后形成完整的汽车功能。
4.1.4 组件系统的优势
组件系统相比传统的继承式开发,具有显著的优势:
- 降低耦合度:组件之间通过接口或事件通信,避免了类继承带来的强耦合。修改一个组件的逻辑不会影响其他组件,使代码维护更加轻松。
- 提高开发效率:通过复用组件,开发者无需重复编写相同功能的代码。例如,开发多个 UI 按钮时,只需为每个按钮挂载相同的 “Button 组件” 并配置不同参数,无需为每个按钮单独开发点击逻辑。
- 灵活应对需求变化:当游戏需求变更时,只需添加、移除或替换组件即可实现功能调整。例如,若需要将 “地面敌人” 改为 “飞行敌人”,只需移除 “地面移动组件” 并挂载 “飞行组件”,无需重构整个敌人类。
- 简化团队协作:不同开发者可以负责不同组件的开发,组件之间通过明确的接口协作,减少了团队成员之间的代码冲突。例如,美术负责制作动画组件的资源,程序负责编写动画逻辑,策划负责配置组件参数。
- 便于调试与测试:单个组件可以独立进行测试,定位问题时只需检查相关组件的逻辑,无需排查整个对象的所有代码。例如,角色无法移动时,只需重点检查 “移动组件” 的参数和逻辑,而不必关注攻击、音效等无关组件。
4.1.5 组件系统的实现原理
虽然不同游戏引擎的组件系统实现细节有所差异,但核心原理一致:
- 组件注册机制:引擎通过注册表管理所有组件类型,每个组件类型需注册到引擎中,声明其名称、接口和生命周期函数。当节点挂载组件时,引擎从注册表中查找组件类型并创建实例。
- 组件管理器:引擎内部存在组件管理器,负责组件的创建、销毁、更新和事件分发。在游戏运行的每一帧,组件管理器会遍历所有激活的组件,调用其更新函数(如Update),确保组件逻辑按帧执行。
- 消息分发系统:组件之间的通信主要通过消息(事件)系统实现。组件可以发送事件(如 “玩家受伤”),其他组件可以注册监听该事件并执行响应逻辑。这种方式避免了组件之间的直接引用,降低了耦合度。
- 序列化与反序列化:组件的属性数据(如位置、速度、颜色等)需要被序列化存储在场景文件或预制体中。当场景加载时,引擎通过反序列化将数据恢复到组件实例中,确保组件状态与保存时一致。
例如,Unity 引擎的组件系统通过MonoBehaviour类作为所有组件的基类,提供Awake、Start、Update等生命周期方法;Cocos Creator 则通过Component基类,提供onLoad、start、update等方法,两者都通过组件管理器实现对组件生命周期的统一管理。
4.2 常用内置组件(Sprite、Label、Button、Animation 等)
游戏引擎通常提供丰富的内置组件,涵盖渲染、交互、物理、动画等核心功能。这些组件经过优化,能够满足大多数游戏开发需求。本节将详细介绍常用内置组件的功能、属性、使用场景及代码示例。
4.2.1 Sprite 组件
Sprite 组件是 2D 游戏中用于渲染 2D 图像的核心组件,支持静态图片、精灵图集、动画帧等多种资源类型,广泛应用于角色、道具、UI 元素等视觉表现。
核心功能
- 加载并渲染 2D 图像资源;
- 支持图像的颜色调整、翻转、裁切等效果;
- 控制图像的渲染层级和显示优先级;
- 配合精灵图集实现高效渲染。
关键属性
- Sprite 资源(Sprite Asset/Sprite Frame):指定要渲染的图像资源。在 Unity 中为Sprite类型,在 Cocos Creator 中为SpriteFrame类型,支持从精灵图集中选择单个精灵。
- 颜色(Color):设置图像的叠加颜色,通过 RGBA 通道调整色调和透明度。例如,将 alpha 值设为 0.5 可使图像半透明,将 RGB 设为红色可使图像显示为红色调。
- 翻转(Flip):包括水平翻转(Flip X)和垂直翻转(Flip Y)。常用于实现角色左右转向(水平翻转)或镜像效果(垂直翻转)。
- 绘制模式(Draw Mode):
- 简单模式(Simple):直接渲染原始图像,适用于大多数静态图像;
- 平铺模式(Tiled):将图像重复平铺以填充指定区域,适用于背景、地面等需要无缝拼接的图像;
- 拉伸模式(Sliced):保持图像边缘不变,拉伸中间区域以适应大小,适用于按钮、面板等 UI 元素(避免边缘变形)。
- 排序层级(Sorting Layer/Order in Layer):控制 Sprite 的渲染顺序。Sorting Layer用于设置层级(层级高的优先渲染),Order in Layer用于同一层级内的排序(值大的优先渲染)。
使用场景
- 显示角色、NPC、道具等游戏对象的 2D 外观;
- 绘制 UI 界面的背景、图标、按钮等元素;
- 实现 2D 动画(通过切换 Sprite 资源实现帧动画);
- 创建粒子效果的渲染图像(配合粒子系统组件)。
代码示例
Unity(C#):动态修改 Sprite 图像和颜色
using UnityEngine;
using UnityEngine.UI;
public class SpriteExample : MonoBehaviour
{
// 引用SpriteRenderer组件(2D对象)或Image组件(UI对象)
public SpriteRenderer spriteRenderer;
public Image uiImage;
// 备用Sprite资源
public Sprite newSprite;
void Start()
{
// 为2D对象设置新图像
if (spriteRenderer != null)
{
spriteRenderer.sprite = newSprite;
spriteRenderer.color = new Color(1, 0.5f, 0.5f, 1); // 淡红色
spriteRenderer.flipX = true; // 水平翻转
}
// 为UI对象设置新图像
if (uiImage != null)
{
uiImage.sprite = newSprite;
uiImage.color = new Color(1, 1, 1, 0.8f); // 半透明白色
}
}
}
Cocos Creator(JavaScript):设置 Sprite 绘制模式和排序
const { ccclass, property } = cc._decorator;
@ccclass
export default class SpriteExample extends cc.Component {
@property(cc.Sprite)
sprite: cc.Sprite = null;
@property(cc.SpriteFrame)
newFrame: cc.SpriteFrame = null;
start() {
if (this.sprite) {
// 设置新精灵帧
this.sprite.spriteFrame = this.newFrame;
// 设置为拉伸模式(适用于UI按钮)
this.sprite.type = cc.Sprite.Type.SLICED;
// 设置颜色为蓝色半透明
this.sprite.node.color = new cc.Color(0, 0, 255, 128);
// 设置渲染层级(Cocos中通过zIndex控制,值大的优先渲染)
this.sprite.node.zIndex = 10;
}
}
}
4.2.2 Label 组件
Label 组件用于在游戏中显示文本内容,支持字体、大小、颜色、对齐方式等多种样式设置,是 UI 界面、剧情对话、数据显示等场景的核心组件。
核心功能
- 渲染文本字符串;
- 支持自定义字体、字号、颜色等样式;
- 处理文本换行、溢出和对齐;
- 支持富文本格式(部分引擎)。
关键属性
- 字体(Font):指定文本使用的字体资源。支持系统字体和自定义字体(需导入 TTF/OTF 字体文件),自定义字体需生成字体图集以提高渲染效率。
- 文本内容(Text/String):设置要显示的文本字符串,可通过代码动态修改(如显示玩家得分、生命值等实时数据)。
- 字体大小(Font Size):设置文本的字号,单位为像素(2D/UI)或世界单位(3D)。
- 颜色(Color):设置文本颜色,通过 RGBA 通道调整,支持透明文本。
- 对齐方式(Alignment):控制文本在 Label 区域内的对齐方式,包括水平对齐(左、中、右)和垂直对齐(上、中、下)。
- 行距(Line Spacing):设置文本行与行之间的距离,单位为像素,影响多行文本的可读性。
- 字距(Character Spacing):设置字符之间的间距,可正可负(负值使字符紧凑)。
- 溢出处理(Overflow):当文本长度超过 Label 显示区域时的处理方式:
- 截断(Clip):直接截断超出区域的文本;
- 省略号(Ellipsis):在文本末尾显示 “...” 表示截断;
- 滚动(Scroll):文本在区域内水平或垂直滚动显示;
- 自适应大小(Resize):自动调整 Label 大小以适应文本长度。
- 富文本(Rich Text):部分引擎(如 Unity、Cocos)支持富文本标签,可在文本中插入<color>、<size>、<b>等标签实现局部样式修改(如<color=red>警告</color>显示红色 “警告” 文字)。
使用场景
- 显示 UI 菜单的按钮文本、标题文本;
- 展示游戏内数据(得分、金币、生命值、倒计时等);
- 呈现剧情对话、任务描述、提示信息;
- 创建动态文本效果(如打字机效果、文本闪烁)。
代码示例
Unity(C#):动态更新文本内容和样式
using UnityEngine;
using TMPro; // 使用TextMeshPro组件(Unity推荐的文本组件)
public class LabelExample : MonoBehaviour
{
public TextMeshProUGUI scoreText; // UI文本组件
public TextMeshPro worldText; // 3D世界文本组件
private int currentScore = 0;
void Start()
{
// 初始化文本样式
if (scoreText != null)
{
scoreText.text = "得分: 0";
scoreText.color = Color.yellow;
scoreText.fontSize = 24;
scoreText.alignment = TextAlignmentOptions.Center;
}
// 启动计分计时器
InvokeRepeating("AddScore", 1f, 1f);
}
void AddScore()
{
currentScore += 10;
// 动态更新文本内容
if (scoreText != null)
{
scoreText.text = $"得分: {currentScore}";
// 得分超过100时文本变色
if (currentScore > 100)
{
scoreText.color = Color.red;
}
}
}
}
Cocos Creator(JavaScript):实现打字机效果
const { ccclass, property } = cc._decorator;
@ccclass
export default class LabelExample extends cc.Component {
@property(cc.Label)
dialogueLabel: cc.Label = null;
// 完整对话文本
private fullText = "欢迎来到组件系统教程!本章将带你学习Label组件的使用方法。";
// 打字速度(字符/秒)
@property
typeSpeed = 10;
private currentText = "";
private textIndex = 0;
start() {
if (this.dialogueLabel) {
this.dialogueLabel.string = "";
// 启动打字机效果
this.schedule(this.typeNextChar, 1 / this.typeSpeed);
}
}
typeNextChar() {
if (this.textIndex < this.fullText.length) {
this.currentText += this.fullText[this.textIndex];
this.dialogueLabel.string = this.currentText;
this.textIndex++;
} else {
// 停止调度
this.unschedule(this.typeNextChar);
}
}
}
4.2.3 Button 组件
Button 组件是处理用户点击交互的核心组件,通常与 Sprite、Label 组件组合使用(Sprite 作为背景,Label 作为文字),用于实现菜单按钮、功能按钮、交互按钮等可点击元素。
核心功能
- 检测用户的点击输入(鼠标点击、触摸点击);
- 提供不同状态的视觉反馈(正常、高亮、按下、禁用);
- 触发自定义点击事件(如加载场景、打开面板、执行逻辑)。
关键属性
- 过渡效果(Transition):定义按钮在不同状态下的视觉变化方式:
- 颜色过渡(Color Tint):通过改变颜色区分状态(如正常为蓝色,按下为深蓝色);
- 精灵过渡(Sprite Swap):通过切换不同精灵图片区分状态(如正常显示按钮图片,按下显示按下图片);
- 动画过渡(Animation):通过播放动画区分状态(如按下时播放缩放动画)。
- 状态属性:针对不同过渡效果设置对应状态的属性:
- 正常状态(Normal):按钮未被交互时的属性(颜色、精灵、动画);
- 高亮状态(Highlighted):鼠标悬停(PC)或触摸停留(移动)时的属性;
- 按下状态(Pressed):按钮被点击但未松开时的属性;
- 禁用状态(Disabled):按钮不可交互时的属性(通常为灰色)。
- 点击事件(On Click/Click Events):设置按钮被点击时触发的事件列表。每个事件包含目标节点和目标组件的方法,点击时引擎会自动调用该方法。
- 交互区域(Target Graphic):指定按钮的交互碰撞区域,通常为按钮的背景 Sprite,确保点击按钮图像区域才会触发事件。
- 禁用状态(Interactable):控制按钮是否可交互。设为false时按钮进入禁用状态,不响应点击且显示禁用视觉效果。
使用场景
- 主菜单中的 “开始游戏”“设置”“退出” 按钮;
- 游戏内的 “攻击”“跳跃”“拾取” 等功能按钮;
- UI 面板中的 “确认”“取消”“关闭” 按钮;
- 道具、技能图标等可点击交互元素。
代码示例
Unity(C#):为按钮添加点击事件与动态控制
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.SceneManagement;
public class ButtonExample : MonoBehaviour
{
public Button startButton;
public Button settingsButton;
public Button closeButton;
void Start()
{
// 代码方式添加点击事件(推荐,更灵活)
if (startButton != null)
{
startButton.onClick.AddListener(OnStartGame);
}
if (settingsButton != null)
{
settingsButton.onClick.AddListener(OnOpenSettings);
}
if (closeButton != null)
{
closeButton.onClick.AddListener(OnClosePanel);
}
// 禁用开始按钮(例如需要满足条件才能点击)
if (startButton != null)
{
startButton.interactable = false;
// 3秒后启用按钮
Invoke("EnableStartButton", 3f);
}
}
void EnableStartButton()
{
if (startButton != null)
{
startButton.interactable = true;
}
}
void OnStartGame()
{
// 加载游戏场景
SceneManager.LoadScene("GameScene");
}
void OnOpenSettings()
{
Debug.Log("打开设置面板");
// 实际项目中此处会显示设置面板
}
void OnClosePanel()
{
// 关闭当前面板(禁用面板节点)
gameObject.SetActive(false);
}
}
Cocos Creator(JavaScript):按钮状态控制与事件绑定
const { ccclass, property } = cc._decorator;
@ccclass
export default class ButtonExample extends cc.Component {
@property(cc.Button)
attackButton: cc.Button = null;
@property(cc.Node)
skillPanel: cc.Node = null;
onLoad() {
if (this.attackButton) {
// 绑定点击事件
this.attackButton.node.on('click', this.onAttack, this);
// 初始禁用攻击按钮
this.attackButton.interactable = false;
}
// 2秒后启用攻击按钮
this.scheduleOnce(() => {
if (this.attackButton) {
this.attackButton.interactable = true;
}
}, 2);
}
onAttack() {
console.log("执行攻击逻辑");
// 播放攻击动画、扣血逻辑等
}
// 在编辑器中绑定到技能按钮的点击事件
onOpenSkillPanel() {
if (this.skillPanel) {
this.skillPanel.active = true;
}
}
// 在编辑器中绑定到技能面板的关闭按钮
onCloseSkillPanel() {
if (this.skillPanel) {
this.skillPanel.active = false;
}
}
}
4.2.4 Animation 组件
Animation 组件用于控制动画的播放、暂停、停止等操作,支持骨骼动画、帧动画、属性动画等多种动画类型,是实现角色动作、UI 过渡、场景动画的核心组件。
核心功能
- 加载并管理动画剪辑(Animation Clip);
- 控制动画的播放、暂停、停止、重播等;
- 支持动画混合、过渡和层级权重;
- 触发动画事件(如动画播放结束时执行逻辑)。
关键属性
- 动画剪辑(Animations/Clips):组件关联的动画剪辑列表,每个剪辑包含一段完整的动画数据(如 “idle”“run”“attack”)。
- 默认动画(Default Clip):设置组件激活时自动播放的动画剪辑。
- 播放速度(Speed):控制动画播放速度,1 为正常速度,0.5 为半速,2 为双倍速,负值为倒放。
- 循环模式(Loop):设置动画是否循环播放。角色的待机、跑步动画通常需要循环,攻击、死亡动画通常不循环。
- 动画事件(Events):在动画时间轴的特定帧添加事件,当动画播放到该帧时触发指定函数(如攻击动画末尾触发伤害判定)。
- 混合模式(Blending):用于多动画同时播放时的混合方式(如角色跑步时同时播放挥手动画),通过权重控制各动画的影响程度。
使用场景
- 角色的待机、行走、跑步、攻击等动作动画;
- UI 元素的弹出、关闭、缩放等过渡动画;
- 道具的旋转、发光、爆炸等特效动画;
- 场景物体的移动、旋转、变形等动画(如门的开关)。
代码示例
Unity(C#):动画播放控制与事件监听
using UnityEngine;
public class AnimationExample : MonoBehaviour
{
private Animator animator; // Unity中常用Animator组件(基于状态机)
// 动画参数哈希(优化性能)
private int isRunningHash;
private int attackHash;
void Start()
{
animator = GetComponent<Animator>();
if (animator != null)
{
// 初始化动画参数哈希
isRunningHash = Animator.StringToHash("IsRunning");
attackHash = Animator.StringToHash("Attack");
}
}
void Update()
{
// 按W键播放跑步动画
if (animator != null)
{
bool isRunning = Input.GetKey(KeyCode.W);
animator.SetBool(isRunningHash, isRunning);
// 按空格键播放攻击动画
if (Input.GetKeyDown(KeyCode.Space))
{
animator.SetTrigger(attackHash);
}
}
}
// 动画事件回调(需在动画剪辑中设置事件并关联此方法)
public void OnAttackHit()
{
Debug.Log("攻击命中,执行伤害判定");
// 实际项目中此处会触发敌人扣血逻辑
}
public void OnAttackEnd()
{
Debug.Log("攻击动画结束,恢复待机状态");
}
}
Cocos Creator(JavaScript):帧动画控制与速度调整
const { ccclass, property } = cc._decorator;
@ccclass
export default class AnimationExample extends cc.Component {
@property(cc.Animation)
animation: cc.Animation = null;
private isRunning = false;
start() {
if (this.animation) {
// 注册动画事件
this.animation.on('finished', this.onAnimationFinish, this);
// 播放待机动画
this.animation.play('idle');
}
}
update(dt) {
if (this.animation) {
// 按A键切换跑步/待机状态
if (cc.input.keyboard.isPressed(cc.macro.KEY.a) && !this.isRunning)
{
this.animation.play('run');
this.isRunning = true;
// 设置跑步动画速度为1.2倍
this.animation.setSpeed(1.2);
}
else if (!cc.input.keyboard.isPressed(cc.macro.KEY.a) && this.isRunning)
{
this.animation.play('idle');
this.isRunning = false;
// 恢复正常速度
this.animation.setSpeed(1);
}
// 按S键播放跳跃动画
if (cc.input.keyboard.isPressed(cc.macro.KEY.s))
{
this.animation.play('jump');
}
}
}
onAnimationFinish(state: cc.AnimationState) {
// 跳跃动画结束后自动播放待机动画
if (state.name === 'jump')
{
this.animation.play('idle');
this.isRunning = false;
}
}
}
4.2.5 Rigidbody 组件
Rigidbody(刚体)组件是物理引擎的核心组件,用于模拟物体的物理运动,如重力、碰撞、加速度等,使物体遵循物理规律运动,广泛应用于需要真实物理效果的游戏场景。
核心功能
- 使物体受物理引擎控制,遵循重力、力和扭矩;
- 支持质量、阻力、角速度等物理属性调整;
- 与碰撞体(Collider)配合实现碰撞检测与响应;
- 提供物理运动控制接口(施加力、设置速度等)。
关键属性
- 质量(Mass):物体的质量(单位为千克),影响物体的惯性和对其他物体的作用力(质量越大,越难推动)。
- 重力缩放(Gravity Scale/Gravity Scale):控制物体受重力影响的程度。值为 1 时受正常重力,0 时不受重力,2 时受双倍重力。
- 线性阻力(Linear Drag):物体线性运动时的阻力,值越大,物体减速越快(模拟空气阻力、摩擦力)。
- 角速度阻力(Angular Drag):物体旋转运动时的阻力,影响物体的旋转减速速度。
- 是否受重力(Use Gravity):控制物体是否受重力影响(勾选则受重力,适合掉落物体;不勾选则不受重力,适合飞行物体)。
- 是否运动学(Is Kinematic):设为true时物体不受物理引擎控制,仅通过代码或动画控制运动(常用于平台、电梯等需要精确控制的物体)。
- 碰撞检测模式(Collision Detection):控制碰撞检测的精度:
- 离散检测(Discrete):默认模式,性能高但快速移动物体可能穿透碰撞体;
- 连续检测(Continuous):对快速移动物体进行连续检测,避免穿透(性能消耗较高)。
使用场景
- 角色的物理运动(如受重力下落、跳跃后的抛物线运动);
- 道具的掉落、滚动、弹跳(如金币掉落、箱子滚动);
- 车辆、炮弹等高速移动物体的物理模拟;
- 布料、绳索等柔性物体的物理效果(配合特定物理组件)。
代码示例
Unity(C#):施加力与物理运动控制
using UnityEngine;
public class RigidbodyExample : MonoBehaviour
{
private Rigidbody rb;
public float moveSpeed = 5f;
public float jumpForce = 7f;
private bool isGrounded = false;
void Start()
{
rb = GetComponent<Rigidbody>();
// 确保物体有碰撞体(否则不会触发碰撞事件)
if (GetComponent<Collider>() == null)
{
gameObject.AddComponent<BoxCollider>();
}
}
void Update()
{
// 跳跃输入(空格键)
if (Input.GetKeyDown(KeyCode.Space) && isGrounded && rb != null)
{
// 施加向上的力实现跳跃
rb.AddForce(Vector3.up * jumpForce, ForceMode.Impulse);
isGrounded = false;
}
}
// 物理更新在FixedUpdate中处理(与物理引擎帧率同步)
void FixedUpdate()
{
if (rb != null && !rb.isKinematic)
{
// 水平移动输入(WASD或方向键)
float horizontal = Input.GetAxis("Horizontal");
float vertical = Input.GetAxis("Vertical");
Vector3 movement = new Vector3(horizontal, 0f, vertical) * moveSpeed;
// 移动刚体(保持原有Y轴速度,避免覆盖重力效果)
rb.velocity = new Vector3(movement.x, rb.velocity.y, movement.z);
}
}
// 检测是否接地(碰撞到地面)
void OnCollisionEnter(Collision collision)
{
if (collision.gameObject.CompareTag("Ground"))
{
isGrounded = true;
}
}
}
Cocos Creator(JavaScript):物理运动与碰撞响应
const { ccclass, property } = cc._decorator;
@ccclass
export default class RigidbodyExample extends cc.Component {
@property(cc.RigidBody)
rb: cc.RigidBody = null;
@property
moveSpeed = 200; // 移动速度(像素/秒)
@property
jumpForce = 500; // 跳跃力
private isGrounded = false;
onLoad() {
if (this.rb) {
// 设置刚体类型为动态(受物理控制)
this.rb.type = cc.RigidBodyType.Dynamic;
// 添加碰撞体(如果没有)
if (!this.node.getComponent(cc.Collider)) {
this.node.addComponent(cc.BoxCollider);
}
}
}
update(dt) {
if (this.rb && this.rb.type === cc.RigidBodyType.Dynamic) {
// 跳跃输入(空格键)
if (cc.input.keyboard.isPressed(cc.macro.KEY.space) && this.isGrounded) {
// 施加跳跃力
this.rb.applyForce(cc.v2(0, this.jumpForce), cc.Vec2.ZERO, true);
this.isGrounded = false;
}
}
}
// 物理更新回调(与物理引擎同步)
fixedUpdate(dt) {
if (this.rb && this.rb.type === cc.RigidBodyType.Dynamic) {
// 水平移动输入(左右箭头)
let velocity = this.rb.linearVelocity;
if (cc.input.keyboard.isPressed(cc.macro.KEY.right)) {
velocity.x = this.moveSpeed;
} else if (cc.input.keyboard.isPressed(cc.macro.KEY.left)) {
velocity.x = -this.moveSpeed;
} else {
// 停止移动时X轴速度归零
velocity.x = 0;
}
this.rb.linearVelocity = velocity;
}
}
// 碰撞开始回调
onBeginContact(contact: cc.PhysicsContact, selfCollider: cc.Collider, otherCollider: cc.Collider) {
// 判断碰撞对象是否为地面
if (otherCollider.tag === 1) { // 假设地面标签为1
// 获取碰撞法线,判断是否从上方碰撞(接地)
let normal = contact.getWorldManifold().normal;
if (normal.y > 0.5) { // 法线向上,说明从上方碰撞地面
this.isGrounded = true;
}
}
}
}
4.2.6 Collider 组件
Collider(碰撞体)组件用于定义物体的碰撞形状,与 Rigidbody 配合实现碰撞检测。碰撞体本身不产生物理运动,但其形状决定了物体之间如何碰撞和交互。
核心功能
- 定义物体的碰撞边界形状;
- 与其他碰撞体检测碰撞;
- 触发碰撞事件(开始碰撞、持续碰撞、结束碰撞);
- 支持触发器模式(仅检测碰撞不产生物理响应)。
常见碰撞体类型
- 盒形碰撞体(Box Collider):立方体形状,适用于箱子、墙壁、角色等规则矩形物体。
- 球形碰撞体(Sphere Collider):球形形状,适用于球体、角色头部、弹珠等圆形物体。
- 胶囊碰撞体(Capsule Collider):胶囊形状(圆柱体两端加半球),适用于角色控制器(避免角色因旋转卡住)。
- 网格碰撞体(Mesh Collider):基于模型网格的碰撞体,适用于复杂形状物体(但性能消耗较高)。
- 2D 碰撞体:包括 BoxCollider2D、CircleCollider2D、PolygonCollider2D 等,用于 2D 游戏中的碰撞检测。
关键属性
- 中心(Center):碰撞体相对于节点的中心偏移量,用于调整碰撞体位置。
- 大小(Size):碰撞体的尺寸(盒形、胶囊形)或半径(球形)。
- 是否为触发器(Is Trigger):设为true时,碰撞体变为触发器,仅触发碰撞事件但不产生物理碰撞响应(如检测玩家进入区域)。
- 碰撞层(Layer):指定碰撞体所属的碰撞层,通过碰撞矩阵设置与其他层是否产生碰撞(优化性能,减少不必要的碰撞检测)。
使用场景
- 角色与地面的碰撞(使角色站立在地面上);
- 攻击判定区域(武器碰撞体检测是否击中敌人);
- 触发区域(玩家进入区域后触发事件,如开门、播放剧情);
- 边界碰撞(防止角色或物体超出游戏区域)。
代码示例
Unity(C#):碰撞与触发器事件处理
using UnityEngine;
public class ColliderExample : MonoBehaviour
{
// 角色碰撞体(非触发器)
private Collider characterCollider;
// 道具触发器
public Collider itemTrigger;
void Start()
{
characterCollider = GetComponent<Collider>();
if (itemTrigger != null)
{
// 设置道具为触发器
itemTrigger.isTrigger = true;
}
}
// 非触发器碰撞开始(物理碰撞)
void OnCollisionEnter(Collision collision)
{
// 碰撞到敌人
if (collision.gameObject.CompareTag("Enemy"))
{
Debug.Log("与敌人发生碰撞,受到伤害");
// 执行受伤逻辑
}
// 碰撞到地面
else if (collision.gameObject.CompareTag("Ground"))
{
Debug.Log("接触地面");
}
}
// 触发器进入事件
void OnTriggerEnter(Collider other)
{
// 进入道具区域
if (other.CompareTag("Item"))
{
Debug.Log("拾取道具:" + other.gameObject.name);
// 执行拾取逻辑(加分、获取道具)
Destroy(other.gameObject); // 销毁道具
}
// 进入危险区域
else if (other.CompareTag("DangerZone"))
{
Debug.Log("进入危险区域,持续掉血");
}
}
// 触发器持续事件
void OnTriggerStay(Collider other)
{
if (other.CompareTag("DangerZone"))
{
Debug.Log("在危险区域内,掉血中...");
// 每帧执行掉血逻辑
}
}
// 触发器离开事件
void OnTriggerExit(Collider other)
{
if (other.CompareTag("DangerZone"))
{
Debug.Log("离开危险区域,停止掉血");
}
}
}
Cocos Creator(JavaScript):2D 碰撞与触发事件
const { ccclass, property } = cc._decorator;
@ccclass
export default class ColliderExample extends cc.Component {
@property(cc.Collider2D)
playerCollider: cc.Collider2D = null;
@property(cc.Collider2D)
coinTrigger: cc.Collider2D = null;
onLoad() {
if (this.playerCollider) {
// 设置玩家碰撞体为非触发器
this.playerCollider.isTrigger = false;
}
if (this.coinTrigger) {
// 设置金币为触发器
this.coinTrigger.isTrigger = true;
// 注册碰撞事件(Cocos中需手动注册)
this.coinTrigger.node.on('trigger-enter', this.onCoinTriggerEnter, this);
}
// 注册玩家碰撞事件
this.playerCollider.node.on('collision-enter', this.onPlayerCollisionEnter, this);
}
// 玩家碰撞事件(物理碰撞)
onPlayerCollisionEnter(other: cc.Collider2D, self: cc.Collider2D) {
if (other.tag === 2) { // 敌人标签为2
console.log("玩家碰撞到敌人,受到伤害");
// 执行受伤逻辑
}
}
// 金币触发器进入事件
onCoinTriggerEnter(other: cc.Collider2D, self: cc.Collider2D) {
if (other.node === this.playerCollider.node) {
console.log("玩家拾取金币");
// 加分逻辑
this.coinTrigger.node.destroy(); // 销毁金币
}
}
onDestroy() {
// 移除事件监听(避免内存泄漏)
if (this.coinTrigger) {
this.coinTrigger.node.off('trigger-enter', this.onCoinTriggerEnter, this);
}
this.playerCollider.node.off('collision-enter', this.onPlayerCollisionEnter, this);
}
}
4.2.7 Camera 组件
Camera(摄像机)组件是游戏的 “眼睛”,负责捕捉场景中的图像并渲染到屏幕上,控制玩家看到的游戏画面,支持视角调整、投影模式切换、渲染层控制等功能。
核心功能
- 定义游戏画面的视角范围和投影方式;
- 控制画面的背景颜色、清晰度和渲染层级;
- 支持多摄像机叠加渲染(如 UI 摄像机、游戏摄像机);
- 提供画面特效接口(如模糊、色彩校正)。
关键属性
- 投影模式(Projection):
- 透视投影(Perspective):符合人眼视觉,物体近大远小,具有立体感,用于 3D 游戏;
- 正交投影(Orthographic):物体大小不随距离变化,无立体感,用于 2D 游戏和 UI。
- 视角 / 大小(Field of View/Size):
- 透视模式下为 “视角(FOV)”,控制可见范围角度(角度越大,视野越广);
- 正交模式下为 “大小(Size)”,控制 Y 轴方向的可见范围(单位为世界单位或像素)。
- 近裁剪面 / 远裁剪面(Near/Far Clip Plane):定义摄像机可见的距离范围,小于近裁剪面或大于远裁剪面的物体不会被渲染(优化性能)。
- 背景颜色(Background):设置摄像机渲染画面的背景颜色,透视模式下可设置为天空盒(Skybox)。
- 渲染层(Culling Mask):指定摄像机渲染的图层,仅渲染勾选图层中的物体(如 UI 摄像机只渲染 UI 层,游戏摄像机只渲染游戏层)。
- 深度(Depth):控制多摄像机渲染的叠加顺序,深度值大的摄像机画面显示在上方(如 UI 摄像机深度大于游戏摄像机,确保 UI 显示在游戏画面上方)。
使用场景
- 主摄像机:渲染游戏场景的主要画面;
- UI 摄像机:专门渲染 UI 元素,确保 UI 始终显示在最上层;
- 迷你地图摄像机:渲染俯视视角的场景缩略图,作为迷你地图;
- 特效摄像机:渲染特定特效(如全屏 bloom 效果)并叠加到主画面。
代码示例
Unity(C#):摄像机视角控制与切换
using UnityEngine;
public class CameraExample : MonoBehaviour
{
public Camera mainCamera;
public Camera uiCamera;
private bool isPerspective = true;
void Start()
{
if (mainCamera != null)
{
// 设置主摄像机渲染游戏层(Layer 0)
mainCamera.cullingMask = 1 << 0;
mainCamera.depth = 0; // 深度低于UI摄像机
}
if (uiCamera != null)
{
// 设置UI摄像机渲染UI层(Layer 5)
uiCamera.cullingMask = 1 << 5;
uiCamera.depth = 1; // 确保UI显示在上方
uiCamera.clearFlags = CameraClearFlags.Depth; // 不清除颜色缓冲,只渲染UI
}
}
void Update()
{
// 按V键切换投影模式
if (Input.GetKeyDown(KeyCode.V) && mainCamera != null)
{
isPerspective = !isPerspective;
mainCamera.orthographic = !isPerspective;
if (isPerspective)
{
// 切换回透视模式
mainCamera.fieldOfView = 60;
Debug.Log("切换为透视投影");
}
else
{
// 切换为正交模式
mainCamera.orthographicSize = 5;
Debug.Log("切换为正交投影");
}
}
// 鼠标滚轮调整视角
if (mainCamera != null && isPerspective)
{
float fov = mainCamera.fieldOfView;
fov -= Input.GetAxis("Mouse ScrollWheel") * 20;
fov = Mathf.Clamp(fov, 30, 90); // 限制视角范围
mainCamera.fieldOfView = fov;
}
}
}
Cocos Creator(JavaScript):摄像机跟随与图层控制
const { ccclass, property } = cc._decorator;
@ccclass
export default class CameraExample extends cc.Component {
@property(cc.Camera)
gameCamera: cc.Camera = null;
@property(cc.Node)
player: cc.Node = null; // 跟随目标
@property
followSpeed = 0.1; // 跟随平滑度
onLoad() {
if (this.gameCamera) {
// 设置摄像机渲染游戏图层(假设游戏物体在Layer 1)
this.gameCamera.cullingMask = 1 << 1;
// 设置背景颜色为深蓝色
this.gameCamera.backgroundColor = new cc.Color(10, 10, 50);
}
}
lateUpdate(dt) {
// 摄像机跟随玩家
if (this.gameCamera && this.player) {
let targetPos = this.player.position;
// 限制Y轴视角(避免摄像机跟随玩家上下移动)
targetPos.y = Mathf.clamp(targetPos.y, -5, 5);
// 平滑移动摄像机
let currentPos = this.gameCamera.node.position;
let newPos = cc.v3(
Mathf.lerp(currentPos.x, targetPos.x, this.followSpeed),
Mathf.lerp(currentPos.y, targetPos.y, this.followSpeed),
currentPos.z
);
this.gameCamera.node.position = newPos;
}
}
// 切换摄像机渲染图层(在编辑器中绑定到按钮)
toggleRenderLayer(layerIndex: number) {
if (this.gameCamera) {
this.gameCamera.cullingMask = 1 << layerIndex;
console.log(`切换摄像机渲染图层:${layerIndex}`);
}
}
}
4.3 自定义组件开发
虽然引擎提供了丰富的内置组件,但实际开发中往往需要实现特定功能,此时自定义组件就成为关键。自定义组件允许开发者封装专属逻辑,通过引擎接口接入组件系统,与内置组件协同工作。本节将详细介绍自定义组件的开发流程、设计原则、实现方法及实例。
4.3.1 自定义组件的开发流程
自定义组件开发需遵循规范的流程,确保组件的可用性、可维护性和性能。完整流程包括需求分析、设计、编码、测试和优化五个阶段。
需求分析
在开发前需明确组件的功能目标、使用场景和交互方式:
- 功能目标:组件需要实现什么核心功能?例如 “玩家控制器组件” 需实现移动、跳跃、攻击等功能;
- 使用场景:组件将被挂载到哪些类型的节点上?例如 “对话组件” 通常挂载到 NPC 节点;
- 交互方式:组件如何与其他组件、节点或系统交互?例如 “伤害组件” 需要与 “生命值组件” 通信;
- 参数配置:哪些属性需要暴露给编辑器供策划配置?例如 “攻击组件” 的攻击力、攻击间隔。
需求分析示例:
组件名称 | 核心功能 | 挂载节点类型 | 依赖组件 | 可配置参数 |
玩家控制器 | 移动、跳跃、互动 | 玩家角色节点 | Rigidbody、Collider | 移动速度、跳跃高度、互动距离 |
任务追踪器 | 显示 / 更新任务进度 | UI 面板节点 | Label | 任务列表、完成图标、提示文本 |
设计阶段
设计阶段需确定组件的接口、属性、方法和内部逻辑结构:
- 接口设计:定义组件对外提供的方法和事件(如StartTask()、OnTaskComplete事件);
- 属性设计:区分私有属性(内部逻辑使用)和公有属性(编辑器配置或外部访问);
- 依赖管理:明确组件依赖的其他组件(如依赖 Rigidbody 则需在代码中检测并获取);
- 生命周期规划:确定在哪些生命周期函数中执行初始化、更新、清理逻辑。
设计示例(玩家控制器组件):
属性:
- 公有:moveSpeed(移动速度)、jumpForce(跳跃力)、interactDistance(互动距离)
- 私有:isGrounded(是否接地)、rigidbody(刚体引用)、animator(动画组件引用)
方法:
- 公有:Move(Vector2 direction)、Jump()、Interact()
- 私有:CheckGrounded()、FindInteractableObject()
生命周期:
- onLoad/Start:初始化组件引用、设置初始状态
- update/FixedUpdate:处理输入、更新移动逻辑
- onDestroy:清理事件监听
编码阶段
编码需遵循引擎的组件开发规范,使用引擎支持的脚本语言(如 C#、JavaScript、TypeScript)实现逻辑:
- 继承基础组件类:所有自定义组件需继承引擎的基础组件类(如 Unity 的MonoBehaviour、Cocos 的Component);
- 声明属性与方法:使用引擎提供的特性标记公有属性(使其在编辑器中可见);
- 实现生命周期方法:重写基础类的生命周期方法(如Start()、Update());
- 处理依赖组件:在初始化阶段获取依赖的组件,若缺失则提示错误;
- 实现核心逻辑:按照设计的功能目标编写具体逻辑代码。
测试阶段
测试是确保组件功能正确的关键步骤,包括:
- 单元测试:测试组件的独立方法(如调用Jump()方法是否正确施加力);
- 集成测试:将组件挂载到节点上,测试与其他组件的协同工作(如移动时动画是否同步);
- 边界测试:测试极端情况(如移动速度为 0、跳跃力过大时的表现);
- 性能测试:检测组件是否存在性能问题(如 Update 中执行大量计算导致帧率下降)。
优化阶段
根据测试结果优化组件:
- 性能优化:减少不必要的计算(如将频繁调用的方法结果缓存)、避免在 Update 中使用复杂逻辑;
- 容错处理:添加异常检测(如依赖组件缺失时输出警告而非崩溃);
- 易用性优化:增加属性说明、设置合理的默认值、添加编辑器菜单快捷创建方式;
- 文档完善:编写组件说明文档,包括功能描述、属性含义、使用示例。
4.3.2 自定义组件的核心要素
一个高质量的自定义组件需具备以下核心要素,确保其可用性、可维护性和扩展性。
清晰的功能边界
组件应专注于单一功能,遵循 “单一职责原则”,避免设计多功能的 “万能组件”。例如,“攻击组件” 应仅负责攻击逻辑(检测目标、计算伤害、播放动画),而不应包含移动或生命值管理功能。
反例:一个 “角色组件” 同时处理移动、攻击、生命值、任务交互等所有功能,导致代码臃肿、难以维护。
正例:将角色功能拆分为 “移动组件”“攻击组件”“生命值组件”“交互组件”,每个组件专注于单一功能,通过事件通信协同工作。
可配置的属性参数
将组件的关键参数暴露为可配置属性,允许通过编辑器或代码动态调整,提高组件的灵活性。例如,“敌人 AI 组件” 可暴露 “巡逻范围”“攻击距离”“警戒时间” 等参数,使同一组件适用于不同类型的敌人。
在 Unity 中使用[SerializeField]标记私有可配置属性,使用[Tooltip]添加说明:
[Tooltip("敌人巡逻的最大范围(单位:米)")]
[SerializeField] private float patrolRange = 10f;
[Tooltip("敌人发现玩家后的警戒时间(单位:秒)")]
[SerializeField] private float alertTime = 5f;
在 Cocos Creator 中使用@property装饰器:
@property({
type: cc.Float,
tooltip: "敌人巡逻的最大范围(单位:像素)"
})
patrolRange = 500;
@property({
type: cc.Float,
tooltip: "敌人发现玩家后的警戒时间(单位:秒)"
})
alertTime = 5;
完善的依赖管理
组件应明确依赖的其他组件,并在初始化阶段进行检测和获取,确保依赖存在或优雅处理缺失情况。避免在逻辑代码中直接假设依赖组件存在,否则可能导致运行时错误。
依赖管理示例(Unity):
private NavMeshAgent navAgent;
private Animator animator;
void Start()
{
// 获取导航组件,若缺失则提示错误
navAgent = GetComponent<NavMeshAgent>();
if (navAgent == null)
{
Debug.LogError("EnemyAI组件依赖NavMeshAgent组件,请添加!", this);
enabled = false; // 禁用组件避免后续错误
return;
}
// 获取动画组件,若缺失则输出警告(非必需依赖)
animator = GetComponent<Animator>();
if (animator == null)
{
Debug.LogWarning("EnemyAI组件未找到Animator组件,动画功能将禁用", this);
}
}
事件驱动的通信方式
组件之间应通过事件(消息)进行通信,而非直接引用调用,降低组件间的耦合度。例如,“攻击组件” 攻击命中目标时发送 “OnAttackHit” 事件,“生命值组件” 监听该事件并执行扣血逻辑。
事件系统实现示例(Cocos Creator):
// 攻击组件
const { ccclass, property } = cc._decorator;
@ccclass
export default class AttackComponent extends cc.Component {
// 攻击命中时发送事件
private onHitTarget(target: cc.Node, damage: number) {
// 发送事件(带参数)
this.node.emit('attack-hit', { target: target, damage: damage });
}
}
// 生命值组件
@ccclass
export default class HealthComponent extends cc.Component {
@property(cc.Integer)
maxHealth = 100;
private currentHealth: number;
onLoad() {
this.currentHealth = this.maxHealth;
// 监听攻击命中事件
this.node.on('attack-hit', this.takeDamage, this);
}
private takeDamage(eventData: { target: cc.Node, damage: number }) {
// 仅处理自身的伤害
if (eventData.target === this.node) {
this.currentHealth -= eventData.damage;
if (this.currentHealth <= 0) {
this.node.emit('die'); // 发送死亡事件
}
}
}
onDestroy() {
// 移除事件监听避免内存泄漏
this.node.off('attack-hit', this.takeDamage, this);
}
}
规范的生命周期管理
组件应在正确的生命周期阶段执行初始化、更新和清理逻辑,遵循引擎的生命周期调用顺序,避免逻辑冲突或资源泄漏。
例如:
- 初始化资源、获取组件引用应在Awake()/onLoad()中执行;
- 开始逻辑(如启动 AI 巡逻)应在Start()/start()中执行;
- 帧更新逻辑(如输入检测、状态更新)应在Update()/update()中执行;
- 物理相关逻辑应在FixedUpdate()/fixedUpdate()中执行;
- 清理资源、移除事件监听应在OnDestroy()/onDestroy()中执行。
4.3.3 自定义组件开发实例(基础篇)
本节通过 “玩家控制器组件” 和 “道具拾取组件” 两个实例,展示基础自定义组件的开发过程,涵盖属性定义、依赖管理、输入处理和事件通信。
实例 1:玩家控制器组件(2D)
功能目标:实现 2D 玩家的移动、跳跃和互动功能,依赖 Rigidbody2D 和 Collider2D 组件,支持参数配置和动画同步。
开发步骤:
- 定义属性与依赖:
// Unity(C#)
using UnityEngine;
using UnityEngine.Events;
[RequireComponent(typeof(Rigidbody2D), typeof(Collider2D))] // 强制依赖组件
public class PlayerController2D : MonoBehaviour
{
[Header("移动设置")]
[Tooltip("移动速度(单位:米/秒)")]
[SerializeField] private float moveSpeed = 5f;
[Header("跳跃设置")]
[Tooltip("跳跃力")]
[SerializeField] private float jumpForce = 7f;
[Tooltip("检测接地的射线长度")]
[SerializeField] private float groundCheckDistance = 0.1f;
[Tooltip("接地检测的图层")]
[SerializeField] private LayerMask groundLayer;
[Header("互动设置")]
[Tooltip("互动检测距离")]
[SerializeField] private float interactDistance = 1f;
[Tooltip("互动检测的标签")]
[SerializeField] private string interactableTag = "Interactable";
// 私有属性
private Rigidbody2D rb;
private Animator anim;
private bool isGrounded;
private Vector2 movementInput;
// 事件(供外部监听)
public UnityEvent onJump;
public UnityEvent onInteract;
}
// Cocos Creator(TypeScript)
const { ccclass, property, requireComponent } = cc._decorator;
@ccclass
@requireComponent(cc.RigidBody2D, cc.Collider2D) // 强制依赖组件
export default class PlayerController2D extends cc.Component {
@property({
type: cc.Float,
tooltip: "移动速度(单位:像素/秒)"
})
moveSpeed = 300;
@property({
type: cc.Float,
tooltip: "跳跃力"
})
jumpForce = 500;
@property({
type: cc.Float,
tooltip: "检测接地的射线长度"
})
groundCheckDistance = 10;
@property({
type: cc.LayerMask,
tooltip: "接地检测的图层"
})
groundLayer = 1 << 8;
@property({
type: cc.Float,
tooltip: "互动检测距离"
})
interactDistance = 100;
@property({
type: cc.String,
tooltip: "互动检测的标签"
})
interactableTag = "Interactable";
// 私有属性
private rb: cc.RigidBody2D = null;
private anim: cc.Animation = null;
private isGrounded = false;
private movementInput: cc.Vec2 = cc.v2(0, 0);
// 事件(供外部监听)
@property({ type: cc.Event.EventHandler })
onJump: cc.Event.EventHandler[] = [];
@property({ type: cc.Event.EventHandler })
onInteract: cc.Event.EventHandler[] = [];
}
- 初始化组件与资源:
// Unity:在Start中初始化
void Start()
{
// 获取依赖组件
rb = GetComponent<Rigidbody2D>();
anim = GetComponent<Animator>();
// 检测组件是否存在
if (rb == null)
{
Debug.LogError("PlayerController2D需要Rigidbody2D组件!", this);
}
if (anim == null)
{
Debug.LogWarning("PlayerController2D未找到Animator组件,动画功能将禁用", this);
}
}
// Cocos Creator:在onLoad中初始化
onLoad() {
// 获取依赖组件
this.rb = this.getComponent(cc.RigidBody2D);
this.anim = this.getComponent(cc.Animation);
// 检测组件是否存在
if (!this.rb) {
cc.error("PlayerController2D需要Rigidbody2D组件!");
}
if (!this.anim) {
cc.warn("PlayerController2D未找到Animation组件,动画功能将禁用");
}
}
- 实现核心逻辑(移动、跳跃、互动):
// Unity:输入处理与移动
void Update()
{
// 获取输入
movementInput.x = Input.GetAxisRaw("Horizontal");
movementInput.y = 0;
// 检测接地
CheckGrounded();
// 跳跃输入
if (Input.GetButtonDown("Jump") && isGrounded)
{
Jump();
}
// 互动输入
if (Input.GetKeyDown(KeyCode.E))
{
Interact();
}
// 更新动画
UpdateAnimation();
}
// 物理移动在FixedUpdate中处理
void FixedUpdate()
{
Move();
}
private void Move()
{
// 设置水平速度
Vector2 velocity = rb.velocity;
velocity.x = movementInput.x * moveSpeed;
rb.velocity = velocity;
}
private void Jump()
{
// 施加跳跃力
rb.velocity = new Vector2(rb.velocity.x, jumpForce);
isGrounded = false;
// 触发跳跃事件
onJump?.Invoke();
}
private void CheckGrounded()
{
// 发射射线检测是否接地
RaycastHit2D hit = Physics2D.Raycast(
transform.position,
Vector2.down,
groundCheckDistance,
groundLayer
);
isGrounded = hit.collider != null;
}
private void Interact()
{
// 发射射线检测前方可互动物体
RaycastHit2D hit = Physics2D.Raycast(
transform.position,
Vector2.right * transform.localScale.x, // 沿面向方向检测
interactDistance,
LayerMask.GetMask(interactableTag)
);
if (hit.collider != null)
{
// 调用互动物体的互动方法
IInteractable interactable = hit.collider.GetComponent<IInteractable>();
interactable?.Interact();
// 触发互动事件
onInteract?.Invoke();
}
}
private void UpdateAnimation()
{
if (anim == null) return;
// 更新移动动画参数
anim.SetFloat("Speed", Mathf.Abs(movementInput.x));
// 更新跳跃动画参数
anim.SetBool("IsGrounded", isGrounded);
}
// 绘制Gizmos辅助调试
private void OnDrawGizmos()
{
// 绘制接地检测射线
Gizmos.color = Color.green;
Gizmos.DrawLine(transform.position, transform.position + Vector3.down * groundCheckDistance);
// 绘制互动检测射线
Gizmos.color = Color.blue;
Gizmos.DrawLine(transform.position, transform.position + Vector3.right * transform.localScale.x * interactDistance);
}
// Cocos Creator:输入处理与移动
update(dt: number) {
// 获取输入
this.movementInput.x = 0;
if (cc.input.keyboard.isPressed(cc.macro.KEY.right)) {
this.movementInput.x = 1;
} else if (cc.input.keyboard.isPressed(cc.macro.KEY.left)) {
this.movementInput.x = -1;
}
// 检测接地
this.checkGrounded();
// 跳跃输入
if (cc.input.keyboard.justPressed(cc.macro.KEY.space) && this.isGrounded) {
this.jump();
}
// 互动输入
if (cc.input.keyboard.justPressed(cc.macro.KEY.e)) {
this.interact();
}
// 更新动画
this.updateAnimation();
}
// 物理移动在fixedUpdate中处理
fixedUpdate(dt: number) {
this.move();
}
private move() {
if (!this.rb) return;
// 设置水平速度
let velocity = this.rb.linearVelocity;
velocity.x = this.movementInput.x * this.moveSpeed;
this.rb.linearVelocity = velocity;
}
private jump() {
if (!this.rb) return;
// 施加跳跃力
let velocity = this.rb.linearVelocity;
velocity.y = this.jumpForce;
this.rb.linearVelocity = velocity;
this.isGrounded = false;
// 触发跳跃事件
cc.Component.EventHandler.emitEvents(this.onJump, this.node);
}
private checkGrounded() {
// 发射射线检测是否接地
let startPos = this.node.position;
let endPos = startPos.add(cc.v2(0, -this.groundCheckDistance));
let hit = cc.director.getPhysicsManager().rayCast(startPos, endPos, this.groundLayer);
this.isGrounded = hit.length > 0;
}
private interact() {
// 计算检测方向(面向右侧或左侧)
let direction = this.movementInput.x !== 0 ? this.movementInput.x : 1;
let startPos = this.node.position;
let endPos = startPos.add(cc.v2(direction * this.interactDistance, 0));
// 射线检测可互动物体
let hits = cc.director.getPhysicsManager().rayCast(startPos, endPos, 1 << this.node.layer);
for (let hit of hits) {
let target = hit.collider.node;
if (target.tag === this.interactableTag) {
// 调用互动组件的方法
let interactable = target.getComponent<InteractableComponent>(InteractableComponent);
if (interactable</doubaocanvas>