阅读前请先下载项目源码,边读边看源码以加深理解和实操,
源码地址已放于文章末尾!
Hello,各位代码世界的冒险家们!欢迎来到第三章!在上一章,我们成功地为游戏注入了灵魂,让它从一堆静态文件变成了一个能够跑起来的“活物”。但是,一个没有主角的世界是孤独的,今天,我们就来创造这个世界的主角——我们那支英勇无畏、所向披靡的玩家队伍!

准备好了吗?咱们要开始“捏人”了!
第一步:谁负责创建玩家?—— RoleLayer登场!
在咱们的“施工队”里,有一个专门负责管理所有角色(包括玩家和敌人)的“包工头”,它就是 Game/Script/Custom/RoleLayer.ts。当关卡管家 LevelManager 吹响开工的号角时,就会调用 RoleLayer 的 setData 方法,命令它开始创建角色。
assets/Game/Script/Custom/RoleLayer.ts - setRoles 方法:
setRoles(d?) {
// 每次新开一局,都把计数器清零
this.roleSumCount = 0;
this.normalRoleNum = 0;
// 从“仓库”(存档)里拿出玩家数据
let asset = StorageSystem.getData().userAssets;
this.curWeapon = asset.chooseWeapon; // 玩家上次选了什么武器
// --- 根据存档数据,计算要创建多少小兵、多少巨人 ---
// 比如,巨人等级是5级,每3级合成一个满级巨人,那就有 5/3=1个满级巨人,和 5%3=2个2级巨人
let giantLv = asset.props.GiantLv - 1;
let maxLvGiantNum = Math.floor(giantLv / 3);
let lastGiantLv = giantLv % 3;
let hasUnMaxLvGiant = lastGiantLv != 0;
// 小兵数量直接从存档读
let roleNum = asset.props.RoleNumLv;
// --- 开始创建!---
this.createRoles(roleNum, false); // 先创建普通小兵 (isGiant = false)
this.createRoles(maxLvGiantNum, true, 3); // 再创建满级(3级)巨人
if (hasUnMaxLvGiant) {
this.createRoles(1, true, lastGiantLv); // 最后创建那个未满级的巨人
}
}
代码解读:
setRoles 的逻辑就像一位将军在阅兵前清点自己的部队:
- 清点人数:先把当前的总人数、小兵人数清零。
- 读取档案:从
StorageSystem(你可以理解为小程序的wx.getStorageSync)里获取玩家的“兵力”数据。 - 排兵布阵:精打细算地计算出应该上场多少小兵、多少满级巨人和多少“半成品”巨人。
- 执行创建:分批调用
createRoles方法,把光秃秃的“数字”变成活生生的角色。
第二步:“捏人”的艺术 —— createRoles 详解
createRoles 方法就像一个高效的“兵工厂”,你给它一个数量和类型,它就能“唰唰唰”地给你造出一队士兵来。
assets/Game/Script/Custom/RoleLayer.ts - createRoles 方法:
createRoles(n: number, isGiant = false, GaintLv = 1) {
let maxLen = GlobalConfig.Formation.length; // 阵型表里定义了最多能站多少人
if (this.roleSumCount >= maxLen) {
console.warn('玩家已达最大数量:', maxLen);
return;
}
// 所有角色都围绕着一个中心点(followPos),这个中心点会沿着道路前进
let initPos = v3(this.followPos).add(GlobalTmpData.Player.offset);
// 根据是否是巨人,选择不同的“模具”(预制体名字)
let name = isGiant ? 'playerGiant' : 'player';
for (let i = 0; i < n; i++) {
if (this.roleSumCount >= maxLen) break; // 防止中途超出人口上限
// 1. 从对象池里拿出预制体实例
let e = GlobalPool.get(name);
e.parent = this.node; // 把它放到场景里
// 2. 计算它在队伍里的位置(阵型)
const p = GlobalConfig.Formation[this.roleSumCount];
// p是从配置里读出来的2D坐标,需要转换成3D空间里的偏移量
let offset = v3(p.x * GlobalConfig.Scale2DTo3D, 0, -p.y * GlobalConfig.Scale2DTo3D).multiplyScalar(GlobalConfig.FormationScale);
// 3. 创建角色的“灵魂” (逻辑类实例)
let role = isGiant ? new RoleGiant() : new Role();
// 4. 初始化角色的各种属性
role.init(e, initPos, offset, this.pathLineVec, isGiant);
role.setWeapon(this.curWeapon); // 给他发武器
if (isGiant) {
role.setGiantLv(GaintLv); // 如果是巨人,设置等级
}
// 5. 登记在册
this.allRoles[e.uuid] = role;
this.roleSumCount++; // 总人口+1
}
}
这个“捏人”的过程,就像开一个手办工厂:
- 取出模具:从
GlobalPool里get一个预制体实例。 - 安排工位:根据当前队伍的总人数,从
GlobalConfig.Formation(一个预设的坐标数组)里查出新成员应该站的相对位置offset。 - 注入灵魂:
new Role()!注意,这里的Role是一个普通的 TypeScript 类,而不是 Cocos 的组件。我们用它来管理角色的所有逻辑和数据,而 Cocos 的Node对象(e)只负责显示。这种数据与视图分离的模式,在复杂项目里非常好用,能让你的代码结构更清晰。 - 出厂设置:调用
role.init()方法,把角色的所有初始状态都设置好。 - 登记入库:把
role逻辑实例存到this.allRoles这个大对象里,方便之后RoleLayer在update里统一驱动它们。
第三步:角色的“灵魂”—— Role.init 方法
role.init() 就像是角色的“新生儿洗礼”,它决定了这个角色“从哪里来,要到哪里去”。
assets/Game/Script/Custom/Role.ts - init 方法:
init(node: Node, initPos: Vec3, offset: Vec3, pathLineVec, isGiant) {
this.isGiant = isGiant; // 记录自己是不是巨人
this.node = node; // 持有自己的“身体”(视图Node)
// 获取挂在Node上的脚本,用来做一些视觉表现,比如修改顶点颜色
this.modfiyMsCmp = this.node.getComponent(BasicModifyMesh);
// 记录自己的阵型偏移和初始位置
this.offset.set(offset);
this.pathLineVec = pathLineVec; // 记录当前道路的前进方向
this.curPos.set(initPos); // 设置自己的当前世界坐标
// 初始化动画控制器
this.roleAnim = new RoleAnim(this.node);
this.roleAnim.init(this);
// 初始化自身属性(血量、攻速等)
this.initProp();
// 获取并初始化手部武器控制器
this.roleHand = this.node.getComponent(RoleHand);
this.roleHand.init(...);
// 初始化移动和状态机
this.initMoveData();
this.setStateData(); // 重点:设置角色的初始状态
this.setCollider(); // 创建碰撞体
this.initShadow(); // 创建脚底的阴影
}
init 方法做的事情非常饱满,它把一个 Role 实例所需的所有外部依赖(视图Node、初始位置、配置等)和内部模块(动画、状态机、碰撞体等)全部准备就绪。执行完 init,这个 role 对象就成了一个功能完备、随时可以战斗的单位。
第四步:让角色听指挥 —— 触摸控制的秘密
现在角色已经出现在屏幕上了,但还是一群“木头人”。怎么让他们响应我们的操作呢?秘密就藏在 Game/Script/Common/Touch/HorizonTouch.ts 里。
这其实是一个非常经典的输入控制模型,我称之为“皮筋模型”。想象一下,你手里牵着一根无形的“皮筋”,皮筋的另一头连着咱们的角色队伍中心。
assets/Game/Script/Common/Touch/HorizonTouch.ts 核心代码:
@ccclass('HorizonTouch')
export class HorizonTouch {
// ... 各种属性定义 ...
ctrlPos = v3(); // 皮筋拉着的那个点,也是队伍的实际中心偏移
touchDistX = 0; // 手指期望皮筋被拉到的目标X坐标
minX = -3; maxX = 3; // 道路的左右边界
// 构造函数,在RoleLayer里被new
constructor(initPos: Vec3, minX: number, maxX: number) {
// ...
this.onEvents(); // 开始监听触摸事件
}
// 监听到手指移动
protected onTouchMove(p1, p2) { // p1是上一帧位置, p2是当前位置
this.isTouch = true;
this.isMoveFinished = false;
// 计算手指从按下时到现在的总横向位移
let subX = p2.x - this.touchStartPos.x;
// 核心公式:目标位置 = 队伍初始位置 + 手指总位移 * 灵敏度
this.touchDistX = subX * 0.02 + this.prePos.x;
// 把目标位置限制在道路边界内
this.touchDistX = clamp(this.touchDistX, this.minX, this.maxX);
}
// 每帧调用
public update(dt) {
this.move(dt); // 平滑移动
// ...
}
protected move(dt) {
// 如果当前位置和目标位置不一致
if (this.ctrlPos.x != this.touchDistX) {
let sign = this.touchDistX > this.ctrlPos.x ? 1 : -1; // 判断方向
// 根据距离动态调整速度,实现“皮筋”的拉扯感
let dist = Math.abs(this.touchDistX - this.ctrlPos.x);
let rate = dist / 3;
this.curSpdX = this.spdX * clamp(rate, 0.1, 10);
// 计算这一帧应该移动到的位置
let tx = this.curSpdX * sign * dt + this.ctrlPos.x;
// 防止移动过头
if (sign > 0 && tx > this.touchDistX || sign < 0 && tx < this.touchDistX) {
tx = this.touchDistX;
}
// 更新实际位置
this.ctrlPos.x = tx;
}
}
}
代码解读:
- 我们并没有直接在
onTouchMove里去修改角色的坐标,而是设置一个“目标点”touchDistX。 - 然后在
update里,通过move方法,让实际位置ctrlPos去平滑地追赶这个目标。 - 追赶的速度还不是恒定的,而是和距离成正比,这就完美模拟了“皮筋”越长,拉力越大的效果,手感非常好。
这个 ctrlPos.x 最终会被 RoleLayer 用来计算 GlobalTmpData.Player.offset,也就是整个队伍的横向偏移量,从而驱动所有 Role 实例的位置更新。
今日总结
今天我们完成了最激动人心的部分——创造了游戏的主角!我们从“项目经理” RoleLayer 开始,追踪到了“兵工厂” createRoles,学习了如何使用对象池和阵型表来高效地创建和管理我们的玩家队伍。我们还深入 Role.init,了解了一个角色是如何被“初始化”的。最后,我们揭开了触摸控制的秘密,理解了如何通过一个巧妙的“皮筋模型”来实现丝滑的角色操控。
现在,我们的英雄们已经整装待发,听候调遣了!但是,没有武器的英雄怎么能叫英雄呢?
下一期,咱们就要给他们“发枪”了!我们将深入探讨武器系统和子弹的实现,让我们的队伍真正拥有“biubiubiu”的能力。准备好迎接枪林弹雨了吗?
敬请期待:《手把手教你写抖音爆火小游戏之激战突围(四):火力全开!biubiubiu的子弹系统》!咱们下期见!
游戏源码下载地址:
https://wwuj.lanzoul.com/i4N1n33e9dib
验证码hack

被折叠的 条评论
为什么被折叠?



