雷霆战机开发回顾
1. 项目目标
这次我们使用 Cocos Creator 3.8.8,从零开始搭建了一款单机版雷霆战机的基础版本。
当前已经完成的核心内容:
- 创建
2D + TypeScript项目 - 在
Main.scene中创建玩家飞机 - 实现玩家拖动移动
- 实现自动发射子弹
- 实现敌机定时生成
- 实现子弹与敌机碰撞
- 实现分数系统
- 实现玩家血量系统
- 实现游戏结束界面
- 实现重新开始游戏
这说明项目已经从“空场景”进入到了“可玩原型”阶段。
2. 我们做了什么
2.1 创建项目
一开始我们在 Cocos Creator 中创建了一个 2D 项目,并选择了 TypeScript 作为脚本语言。
这样做的原因:
2D更适合做雷霆战机这类竖版飞行射击游戏TypeScript比纯 JavaScript 更规范,后面项目变大时更容易维护
2.2 创建玩家飞机
我们在场景文件 Main.scene 中创建了 Player 节点,并把它放到 Canvas 下面。
要点:
- 节点是在场景里创建,不是在脚本文件里创建
Player要放在Canvas下,方便显示在 UI 场景中- 给
Player添加Sprite组件,让飞机图片显示出来
这里学到的概念:
Hierarchy:场景里有哪些节点Assets:项目里有哪些资源Inspector:当前节点的属性和组件Canvas:2D 游戏和 UI 常用的根节点
2.3 让飞机可以移动
我们创建了脚本 PlayerController.ts,并挂到 Player 节点上。
这个脚本完成了:
- 监听鼠标或手指拖动
- 根据触摸位置移动飞机
- 限制飞机不能飞出屏幕
这里学到的概念:
- 脚本需要挂到节点上才会生效
update()用于每帧执行逻辑- 可以通过
view.getVisibleSize()获取屏幕可视区域
2.4 实现自动发射子弹
我们先创建了 Bullet 节点,再做成 Bullet.prefab 预制体。
然后创建了两个脚本:
Bullet.tsPlayerShoot.ts
各自负责:
Bullet.ts:控制子弹向上飞行,飞出屏幕后销毁PlayerShoot.ts:控制玩家按固定时间间隔自动发射子弹
这里学到的概念:
Prefab是可以重复生成的模板- 运行时实例化预制体要用
instantiate - 子弹这类对象通常是动态创建的,不是手动摆满场景
2.5 实现敌机生成
我们创建了 Enemy 节点和 Enemy.prefab,然后又创建了:
Enemy.tsEnemySpawner.ts
功能分别是:
Enemy.ts:控制敌机向下飞,飞出屏幕后销毁EnemySpawner.ts:定时随机生成敌机
这里学到的概念:
- 生成器节点可以专门负责“刷怪”
- 敌机的出生位置可以随机化
- 游戏里的对象逻辑最好拆分,不要全写在一个脚本里
2.6 实现碰撞检测
我们给 Bullet、Enemy、Player 都加上了碰撞和刚体组件:
BoxCollider2DRigidBody2D
同时还学习了几个关键设置:
Type = KinematicEnabled Contact Listener要勾上Gravity Scale = 0
然后通过代码监听碰撞事件,实现:
- 子弹碰到敌机,双方销毁
- 敌机碰到玩家,玩家掉血
这里学到的概念:
- 有碰撞组件不代表一定能收到碰撞回调
RigidBody2D的接触监听必须正确开启- 调试碰撞时,要先确认组件、名字、脚本、监听都对
2.7 实现分数和血量
我们创建了 GameManager 节点和 GameManager.ts 脚本,用它统一管理游戏状态。
它负责:
- 记录分数
- 记录玩家血量
- 更新界面文字
- 判断游戏结束
同时在 Canvas 下创建了两个文本:
ScoreLabelHpLabel
这里学到的概念:
GameManager适合做全局管理- 分数和血量这类数据最好集中管理
- UI 文字通常通过
Label组件更新
2.8 实现游戏结束和重新开始
我们又做了 GameOverPanel,并在里面加入:
Game Over文字RestartButton按钮
然后在 GameManager.ts 中加入:
- 游戏结束时显示面板
- 点击按钮后重新加载
Main.scene
这里学到的概念:
Button组件本身不显示文字- 按钮文字通常由它的
Label子节点负责 director.loadScene('Main')可以重新载入场景
3. 目前项目里的核心脚本
你现在的项目,核心脚本大致可以这样理解:
PlayerController.ts
负责玩家移动。
PlayerShoot.ts
负责玩家自动发射子弹。
Bullet.ts
负责子弹移动和子弹碰撞逻辑。
Enemy.ts
负责敌机移动和敌机碰撞逻辑。
EnemySpawner.ts
负责定时生成敌机。
GameManager.ts
负责分数、血量、游戏结束、重新开始等全局逻辑。
4. 目前项目里的核心预制体
Bullet.prefab
子弹模板,发射时动态生成。
Enemy.prefab
敌机模板,刷怪时动态生成。
Explosion.prefab
爆炸模板,目前还在调试阶段。
5. 这次开发中最重要的学习点
5.1 节点和脚本是分开的
节点是场景中的对象,脚本是给对象添加行为。
简单理解:
- 节点 = 游戏里的角色或物体
- 脚本 = 这些物体怎么动、怎么打、怎么碰撞
5.2 预制体非常重要
像子弹、敌机、爆炸这种会反复出现的对象,不适合每次手动创建,所以要做成 Prefab。
5.3 游戏逻辑要拆分
不要把所有代码写进一个文件。
我们现在的拆分方式已经是比较合理的入门结构:
- 玩家逻辑一个脚本
- 子弹逻辑一个脚本
- 敌机逻辑一个脚本
- 刷怪逻辑一个脚本
- 全局管理一个脚本
5.4 先做功能,再做美术
这次开发中,我们一直优先让“功能可运行”,而不是先追求画面精致。
这是一个非常正确的开发顺序:
- 先能运行
- 再能玩
- 再好看
- 再精细优化
6. 你已经掌握的开发流程
通过这次练习,你已经实际走过了一遍最基础的游戏开发流程:
- 创建项目
- 创建场景
- 创建节点
- 添加图片显示
- 创建脚本
- 挂载脚本
- 创建预制体
- 动态生成对象
- 添加碰撞检测
- 制作基础 UI
- 管理游戏状态
这套流程以后做别的 2D 游戏也一样能用。
7. 目前还有哪些可以继续完善
你现在的项目已经是一个“能玩的原型”,但距离更完整的雷霆战机还可以继续做这些内容:
- 爆炸动画完善
- 双发子弹
- 火力升级
- 敌机血量系统
- Boss 战
- 道具掉落
- 音效和背景音乐
- 开始界面
- 暂停界面
- 关卡系统
- 不同敌机类型
- 玩家无敌时间
- 对象池优化
8. 下一阶段建议学习顺序
如果你接下来继续做,我建议按这个顺序推进:
- 先把爆炸效果调通
- 再做双发子弹
- 再做敌机血量和不同敌机
- 再做掉落道具和火力升级
- 最后再做 Boss、关卡、音效和美术优化
这样难度是逐步上升的,不容易乱。
9. 每个脚本逐段解释版
这一部分不是死抠语法,而是帮助你理解“每个脚本为什么要这样写”。
9.1 PlayerController.ts
这个脚本的目标是让玩家飞机跟着鼠标或手指移动。
常见结构可以理解成下面几段:
导入模块
import { _decorator, Component, EventTouch, input, Input, UITransform, Vec3, view } from 'cc';
这里是在从 cc 里取出后面会用到的工具。
_decorator:用来写@ccclassComponent:所有脚本组件的基础类EventTouch:触摸事件对象input和Input:监听输入事件UITransform:读取节点宽高Vec3:表示三维坐标view:获取屏幕可视区域
定义类
@ccclass('PlayerController')
export class PlayerController extends Component
这表示我们定义了一个叫 PlayerController 的组件脚本,它可以挂到节点上。
定义成员变量
private halfPlayerWidth = 0;
private halfPlayerHeight = 0;
private halfScreenWidth = 0;
private halfScreenHeight = 0;
这些变量用来记录:
- 飞机自身的一半宽高
- 屏幕可视区域的一半宽高
为什么要记录一半:
- 因为限制边界时,不能只看节点中心点
- 还要考虑飞机图片本身的大小
start()
start() 会在脚本启用后执行一次。
在这里主要做了两件事:
- 读取飞机尺寸
- 注册触摸移动事件
大致思路是:
const uiTransform = this.getComponent(UITransform);
这句是从当前节点上拿到 UITransform 组件,从而得到节点宽高。
const visibleSize = view.getVisibleSize();
这句是获取屏幕可视区域大小。
input.on(Input.EventType.TOUCH_MOVE, this.onTouchMove, this);
这句的意思是:
- 监听拖动事件
- 当拖动发生时
- 调用
onTouchMove
onDestroy()
input.off(Input.EventType.TOUCH_MOVE, this.onTouchMove, this);
这句用于取消事件监听。
为什么要取消:
- 防止节点销毁后还保留旧监听
- 避免重复监听和潜在 bug
onTouchMove(event)
这是最核心的方法,负责真正移动飞机。
const uiLocation = event.getUILocation();
获取当前鼠标或手指在 UI 坐标系里的位置。
let x = uiLocation.x - this.halfScreenWidth;
let y = uiLocation.y - this.halfScreenHeight;
这一步是在做坐标换算。
原因是:
- 触摸位置通常是以屏幕左下角为原点
- 场景里的节点位置更常以中心为参考
所以要减去半个屏幕宽高。
x = Math.max(...Math.min(...));
y = Math.max(...Math.min(...));
这两句是限制边界,防止飞机飞出屏幕。
this.node.setPosition(new Vec3(x, y, 0));
最后把飞机移动到目标位置。
这个脚本的核心理解
一句话概括:
监听拖动输入,换算坐标,并把玩家节点移动到合法范围内。
9.2 PlayerShoot.ts
这个脚本的目标是让飞机自动按固定节奏发射子弹。
预制体属性
@property(Prefab)
public bulletPrefab: Prefab | null = null;
这表示:
- 脚本里有一个可在编辑器中拖拽赋值的子弹预制体
- 运行时发射的子弹,就是根据这个预制体复制出来的
发射间隔
@property
public shootInterval = 0.2;
表示每隔 0.2 秒发射一次。
计时器
private shootTimer = 0;
用来累计经过的时间。
update(deltaTime)
this.shootTimer += deltaTime;
deltaTime 是这一帧距离上一帧经过的秒数。
把它不断累加,就能知道“已经过了多久”。
if (this.shootTimer >= this.shootInterval)
如果累计时间超过设定间隔,就发射一次。
this.shootTimer = 0;
this.shoot();
发射后把计时器清零,重新开始下一轮计时。
shoot()
const bullet = instantiate(this.bulletPrefab);
复制一个新的子弹节点。
this.node.parent.addChild(bullet);
把子弹加入当前场景。
这里用 this.node.parent,因为玩家节点和子弹节点通常都放在同一个父节点下,比如 Canvas。
const playerPos = this.node.position;
bullet.setPosition(new Vec3(playerPos.x, playerPos.y + 80, 0));
这句表示把子弹放到玩家前方一点的位置,这样视觉上就像从飞机头部发射出去。
这个脚本的核心理解
一句话概括:
用一个计时器,每隔固定时间复制出一个子弹预制体。
9.3 Bullet.ts
这个脚本控制子弹移动和命中敌机后的处理。
速度属性
@property
speed: number = 1000;
表示子弹向上飞行的速度。
监听碰撞
const collider = this.getComponent(Collider2D);
collider.on(Contact2DType.BEGIN_CONTACT, this.onBeginContact, this);
意思是:
- 先拿到当前子弹的碰撞器
- 再注册“开始接触”事件
当子弹碰到其他物体时,就会调用 onBeginContact。
update(deltaTime)
const pos = this.node.position;
this.node.setPosition(new Vec3(pos.x, pos.y + this.speed * deltaTime, pos.z));
这表示每一帧都让子弹沿着 y 轴正方向移动。
为什么要乘 deltaTime:
- 这样移动速度就和帧率无关
- 不管电脑快还是慢,子弹每秒移动距离都差不多
if (this.node.position.y > 1200) {
this.node.destroy();
}
子弹飞出屏幕后就删除,避免场景里积累太多无用节点。
onBeginContact(...)
通常逻辑是:
if (otherCollider.node.name === 'Enemy') {
GameManager.instance?.addScore(10);
GameManager.instance?.createExplosion(otherCollider.node.position.clone());
otherCollider.node.destroy();
this.node.destroy();
}
这段代码含义是:
- 如果撞到的是敌机
- 先加分
- 再创建爆炸
- 再销毁敌机
- 最后销毁子弹
这个脚本的核心理解
一句话概括:
子弹每帧向上飞,碰到敌机后触发命中处理。
9.4 Enemy.ts
这个脚本负责敌机下落以及撞到玩家后的处理。
敌机速度
@property
speed: number = 300;
表示敌机向下飞行的速度。
update(deltaTime)
this.node.setPosition(new Vec3(pos.x, pos.y - this.speed * deltaTime, pos.z));
因为敌机是往下飞,所以这里是 y - speed * deltaTime。
飞出屏幕后销毁
if (this.node.position.y < -1200) {
this.node.destroy();
}
避免敌机飞出屏幕后还一直留在场景里。
碰到玩家
敌机的碰撞逻辑通常写在 onBeginContact() 中。
if (otherCollider.node.name === 'Player') {
GameManager.instance?.takeDamage(1);
this.node.destroy();
}
意思是:
- 如果碰到的是玩家
- 玩家扣
1点血 - 当前敌机销毁
这个脚本的核心理解
一句话概括:
敌机持续向下移动,撞到玩家就让玩家掉血。
9.5 EnemySpawner.ts
这个脚本控制敌机按固定节奏生成。
敌机预制体属性
@property(Prefab)
public enemyPrefab: Prefab | null = null;
这是在编辑器中指定“要生成哪种敌机”。
生成间隔
@property
public spawnInterval: number = 1.0;
表示每 1 秒生成一次敌机。
start()
在开始时记录屏幕宽高,用于后面计算出生范围。
const visibleSize = view.getVisibleSize();
this.halfScreenWidth = visibleSize.width * 0.5;
this.halfScreenHeight = visibleSize.height * 0.5;
update(deltaTime)
和 PlayerShoot.ts 的思路很像,也是通过计时器累计时间,时间到了就生成一架敌机。
spawnEnemy()
这是真正创建敌机的方法。
const enemy = instantiate(this.enemyPrefab);
this.node.parent.addChild(enemy);
这两句表示:
- 复制一架敌机
- 把它加到场景里
const minX = -this.halfScreenWidth + 50;
const maxX = this.halfScreenWidth - 50;
const randomX = minX + Math.random() * (maxX - minX);
这几句是生成一个随机横坐标,让敌机不会每次都从同一位置出现。
const spawnY = this.halfScreenHeight + 100;
enemy.setPosition(new Vec3(randomX, spawnY, 0));
把敌机放在屏幕顶部之外一点的位置,看起来就像从上面飞进来。
这个脚本的核心理解
一句话概括:
每隔一段时间,在屏幕上方随机位置生成一架敌机。
9.6 GameManager.ts
这是整个项目中最接近“大脑”的脚本。
它主要负责:
- 记录分数
- 记录血量
- 更新 UI
- 游戏结束
- 重新开始
- 生成爆炸
静态实例
public static instance: GameManager;
这是为了让别的脚本能方便访问它。
比如:
GameManager.instance?.addScore(10);
UI 属性
@property(Label)
public scoreLabel: Label | null = null;
@property(Label)
public hpLabel: Label | null = null;
这两个属性是让你在编辑器中把界面上的文字节点拖进来。
面板属性
@property(Node)
public gameOverPanel: Node | null = null;
这是为了在游戏结束时显示失败界面。
数据变量
public playerHp: number = 3;
private score: number = 0;
private isGameOver: boolean = false;
分别表示:
- 玩家初始生命值
- 当前分数
- 当前是否已经结束
onLoad()
GameManager.instance = this;
把当前脚本实例保存到静态变量上,让别的脚本可以访问到。
start()
这里通常做初始化工作:
- 隐藏游戏结束界面
- 初始化分数显示
- 初始化血量显示
addScore(value)
表示增加分数。
典型流程:
- 如果游戏已经结束,直接返回
- 分数增加
- 更新分数字样
takeDamage(value)
表示玩家受伤。
典型流程:
- 如果已经结束,直接返回
- 扣减血量
- 限制最低不能小于 0
- 更新血量显示
- 如果血量为 0,触发游戏结束
restartGame()
director.resume();
director.loadScene('Main');
这表示:
- 先恢复被暂停的游戏
- 再重新加载主场景
gameOver()
通常负责:
- 标记已结束
- 显示
GameOverPanel - 暂停游戏
createExplosion(position)
这个方法是为了把爆炸逻辑统一交给 GameManager 管理。
它的常见流程:
- 判断
Explosion.prefab是否存在 - 实例化爆炸节点
- 把爆炸节点加入场景
- 设置爆炸出现的位置
这个脚本的核心理解
一句话概括:
统一管理全局数据、UI 状态和游戏流程。
9.7 Explosion.ts
这个脚本负责爆炸动画播放。
帧数组
@property([SpriteFrame])
public frames: SpriteFrame[] = [];
表示这个爆炸动画由多张图片组成。
帧间隔
@property
public frameInterval: number = 0.08;
表示每隔多久切换到下一张爆炸图。
start()
主要做两件事:
- 取到当前节点的
Sprite - 把第一张图设置为初始显示图
update(deltaTime)
这里是动画播放的关键。
大致逻辑是:
- 累计时间
- 到达
frameInterval后切换下一帧 - 如果帧已经播放完,就销毁节点
这个脚本的核心理解
一句话概括:
让爆炸节点按顺序播放多张图片,播放完后自动删除。
10. Cocos 常用面板和组件速查版
这一部分你可以当作入门速查表,以后忘了就回来查。
10.1 常用面板
Hierarchy
作用:
- 查看场景中的节点结构
- 创建节点
- 调整父子关系
你这次最常做的事:
- 创建
Player - 创建
GameManager - 把
Player拖到Canvas下
Assets
作用:
- 管理项目资源
- 存放图片、脚本、预制体、场景
你这次最常做的事:
- 导入飞机图片
- 导入敌机图片
- 导入爆炸图片
- 创建脚本
- 查看
spriteFrame
Inspector
作用:
- 修改选中节点的属性和组件
- 给节点挂脚本
- 调整坐标、缩放、图片、碰撞体
你这次最常做的事:
- 给节点添加
Sprite - 给节点添加
Button - 给节点添加
BoxCollider2D - 给节点拖入预制体引用
Scene
作用:
- 直接在场景中摆放节点
- 移动、缩放、查看当前布局
Console
作用:
- 查看报错
- 输出调试信息
你这次最常做的事:
- 看碰撞有没有报错
- 检查脚本有没有执行
10.2 常用节点
Canvas
作用:
- 2D 游戏常用根节点
- UI 元素和大部分 2D 节点通常都挂在它下面
普通空节点
作用:
- 用来作为逻辑节点
- 本身不显示图片
适合做:
GameManagerEnemySpawner
Label
作用:
- 显示文字
适合做:
- 分数显示
- 血量显示
Game Over
Button
作用:
- 响应点击事件
要点:
Button本身不负责显示文字- 一般要搭配子节点
Label
10.3 常用组件
cc.UITransform
作用:
- 控制节点的宽高
- 提供 UI 相关尺寸信息
你这次用它做了:
- 读取玩家节点大小
- 设置碰撞区域参考
cc.Sprite
作用:
- 显示一张图片
常用属性:
Sprite Frame:当前显示哪张图Size Mode:图片大小模式
适合做:
- 玩家飞机
- 子弹
- 敌机
- 爆炸图
cc.Button
作用:
- 给节点添加点击能力
常用属性:
TargetTransitionClick Events
Label
作用:
- 显示文字内容
常用属性:
StringFont SizeLine Height
BoxCollider2D
作用:
- 定义 2D 碰撞范围
常用属性:
SizeOffsetIs Sensor
RigidBody2D
作用:
- 参与 2D 物理和碰撞系统
这次你最常用的设置:
Type = KinematicEnabled Contact Listener = 勾选Gravity Scale = 0
自定义脚本组件
作用:
- 给节点添加自定义逻辑
例如:
PlayerControllerPlayerShootBulletEnemyEnemySpawnerGameManagerExplosion
10.4 常用资源类型
Scene
作用:
- 保存一个完整场景
例如:
Main.scene
Prefab
作用:
- 保存一个可重复生成的节点模板
例如:
Bullet.prefabEnemy.prefabExplosion.prefab
SpriteFrame
作用:
- 图片在
Sprite中真正使用的资源
要点:
- 拖到
Sprite Frame里的通常是它 - 不是原始
texture
10.5 这次开发中最常见的报错来源
以后如果再遇到问题,优先检查这几类:
- 脚本没有挂到正确节点上
- 改的是场景节点,不是预制体节点
- 预制体引用没有拖进去
- 节点名字和代码判断不一致
RigidBody2D没开Enabled Contact Listener- 资源拖进去时用了
texture,没有用spriteFrame - 节点没有放在
Canvas下,导致显示异常
11. 项目目录结构说明版
这一部分的目标是帮助你看懂“项目文件为什么这样放”,以后你自己加功能时,就知道该把资源放到哪里。
11.1 当前项目可以参考的目录结构
assets/
scenes/
Main.scene
scripts/
PlayerController.ts
PlayerShoot.ts
Bullet.ts
Enemy.ts
EnemySpawner.ts
GameManager.ts
Explosion.ts
prefabs/
Bullet.prefab
Enemy.prefab
Explosion.prefab
textures/
TF.png
enemy.png
explosion_1.png
explosion_2.png
explosion_3.png
explosion_4.png
这不是唯一正确答案,但对你现在这个阶段来说,是一个非常清晰、非常适合继续扩展的结构。
11.2 assets/ 是最重要的资源目录
assets/ 可以理解成游戏项目最核心的资源区。
你在编辑器里看到的大多数内容,最终都在这里。
例如:
- 场景文件
- 脚本文件
- 图片资源
- 预制体资源
以后你做别的功能,也基本都围绕这个目录展开。
11.3 assets/scenes/
这个目录专门放场景文件。
目前你最重要的场景是:
Main.scene
它的作用是:
- 保存主游戏场景
- 保存
Canvas - 保存玩家、UI、管理器等场景节点
为什么要单独放到 scenes/:
- 场景文件是项目结构的核心入口
- 单独管理后,不容易和图片、脚本混在一起
以后如果你继续扩展,还可能新增:
Start.scene:开始界面Select.scene:关卡选择Game.scene:正式战斗场景
11.4 assets/scripts/
这个目录专门放代码脚本。
你现在的脚本基本都在这里,例如:
PlayerController.tsPlayerShoot.tsBullet.tsEnemy.tsEnemySpawner.tsGameManager.tsExplosion.ts
这个目录里的每个脚本,都代表“某个功能逻辑”。
你可以这样理解:
- 场景节点决定“谁在场上”
- 脚本决定“这些东西会做什么”
为什么要把脚本集中放这里:
- 以后好找
- 不容易和图片资源混在一起
- 代码量变多后更方便管理
如果项目后面变大,可以继续细分成:
scripts/
player/
enemy/
bullet/
ui/
manager/
effect/
例如:
player/PlayerController.tsplayer/PlayerShoot.tsenemy/Enemy.tsenemy/EnemySpawner.tsmanager/GameManager.ts
你现在先不用急着拆这么细,但要知道后面可以这么整理。
11.5 assets/prefabs/
这个目录专门放预制体。
目前你已经有:
Bullet.prefabEnemy.prefabExplosion.prefab
为什么这些对象适合做成预制体:
- 它们会被反复生成
- 不是只在场景里出现一次
- 运行时需要动态创建
你可以把预制体理解成“模板”:
Bullet.prefab是子弹模板Enemy.prefab是敌机模板Explosion.prefab是爆炸模板
运行时不是重新手搓一个节点,而是从这个模板复制出新对象。
以后还适合放进 prefabs/ 的内容有:
- 道具预制体
- Boss 预制体
- 敌机子弹预制体
- 开始界面弹窗预制体
11.6 assets/textures/
这个目录专门放图片资源。
比如你目前的:
- 飞机图
- 敌机图
- 爆炸图
为什么单独放:
- 所有图片集中管理
- 导入资源时不容易乱
- 做替换美术时更方便
这里有个你已经接触到的重要点:
图片导入后,Cocos 不只是给你一个原图,还会生成可供 Sprite 使用的 spriteFrame。
所以以后你给 Sprite 指定图片时,要优先用:
spriteFrame
而不是:
- 原始
texture
如果以后图片更多,也可以继续细分:
textures/
player/
enemy/
bullet/
effect/
ui/
例如:
textures/player/player_1.pngtextures/enemy/enemy_small.pngtextures/effect/explosion_1.png
11.7 一个资源从导入到使用的完整过程
你可以用“敌机”为例理解整条链路:
第一步:图片进入 textures/
例如:
assets/textures/enemy.png
第二步:在场景中创建节点
例如创建 Enemy 节点,并加上 Sprite。
第三步:把图片的 spriteFrame 拖到 Sprite Frame
这样节点就显示出敌机图片了。
第四步:给节点挂脚本和组件
例如:
Enemy.tsBoxCollider2DRigidBody2D
第五步:把节点拖进 prefabs/
这样就得到:
Enemy.prefab
第六步:在生成器脚本里引用这个预制体
例如在 EnemySpawner.ts 中通过:
@property(Prefab)instantiate(enemyPrefab)
动态创建敌机。
这就是一个资源从“图片”变成“可运行游戏对象”的完整路径。
11.8 目录结构和场景结构不是一回事
这是一个很重要、很容易混淆的点。
目录结构
指的是硬盘上的文件分类。
例如:
assets/scripts/assets/prefabs/assets/textures/
场景结构
指的是 Hierarchy 里的节点父子关系。
例如:
CanvasPlayerGameManagerGameOverPanel
所以你要把这两件事分开理解:
- 文件放在哪里,是目录结构问题
- 节点挂在哪里,是场景结构问题
11.9 你现在项目中的“谁放在哪里”
为了帮助你更快记住,我把你现在项目里的核心内容重新对应一次。
放在 scenes/ 的
Main.scene
放在 scripts/ 的
- 玩家移动脚本
- 玩家射击脚本
- 子弹脚本
- 敌机脚本
- 刷怪脚本
- 游戏管理脚本
- 爆炸脚本
放在 prefabs/ 的
- 子弹模板
- 敌机模板
- 爆炸模板
放在 textures/ 的
- 玩家飞机图片
- 敌机图片
- 爆炸序列图
放在 Hierarchy 里的场景节点
CanvasPlayerEnemySpawnerGameManagerScoreLabelHpLabelGameOverPanel
这样一对应,你以后就不容易乱了。
11.10 为什么现在这样整理对你最合适
因为你现在还是初学阶段,这种结构有三个好处:
第一,容易找
你要找脚本,就进 scripts/。
你要找图片,就进 textures/。
你要找可重复生成的模板,就进 prefabs/。
第二,容易讲清楚
你以后自己复盘时,很容易说清楚:
- 这个功能对应哪个脚本
- 这个对象对应哪个预制体
- 这个外观对应哪张图片
第三,后面容易扩展
以后你新增双发子弹、Boss、道具、音效时,完全可以在现有结构上继续长,不需要推倒重来。
11.11 下一步如果继续扩展,目录可以怎么升级
当项目内容更多时,你可以把结构逐渐升级成这样:
assets/
scenes/
Main.scene
Start.scene
scripts/
player/
PlayerController.ts
PlayerShoot.ts
enemy/
Enemy.ts
EnemySpawner.ts
bullet/
Bullet.ts
manager/
GameManager.ts
effect/
Explosion.ts
ui/
StartPanel.ts
GameOverPanel.ts
prefabs/
player/
enemy/
bullet/
effect/
ui/
textures/
player/
enemy/
bullet/
effect/
ui/
audio/
bgm/
sfx/
这是更接近正式项目的写法。
你现在不用一步到位,但可以先知道方向。
11.12 这一节的核心记忆法
你可以用一句很简单的话记住:
scenes放场景scripts放逻辑prefabs放模板textures放图片
如果以后又忘了,就先回到这四个词。