个人名片:
🐼作者简介:一名大三在校生,喜欢AI编程🎋
零基础学Java——小白入门必备🔥重识C语言——复习回顾🔥计算机网络体系———深度详讲HCIP数通工程师-刷题与实战🔥🔥🔥微信小程序开发——实战开发🔥HarmonyOS 4.0 应用开发实战——实战开发🔥🔥🔥Redis快速入门到精通——实战开发🔥🔥🔥RabbitMQ快速入门🔥
🐻❄️个人主页🥇:落798.
🐼个人WeChat:hmmwx53
🕊️系列专栏:🖼️
🐓每日一句:🍭我很忙,但我要忙的有意义!
欢迎评论 💬点赞👍🏻 收藏 📂加关注+
文章目录
BallBattle简介背景设定玩法主要功能匹配对战属性同步与保存房间属性玩家属性 自定义事件其他功能消息处理控制移动同步 项目结构欢迎添加微信,加入我的核心小队,请备注来意
BallBattle
简介
《球球大作战》是一款由巨人网络Superpop&Lollipop工作室自主研发,并且免费(不包括道具)的手机网络游戏。2015年5月27日由巨人网络在中国大陆发行。
游戏以玩家间的实时互动PK为设计宗旨,通过简单的规则将玩家操作直接转化为游戏策略,体验智谋碰撞的战斗乐趣。在这个球球的世界里,每个人都化身为一颗独特的球球,大球吃小球,努力生存下来就是唯一的目标。
线上体验
背景设定
在宇宙深处一片遍布着荆棘之花的神秘星云中,生活着一群名叫“波拉哩”(译名“球球”)的奇特生物。他们外表萌萌,却有着勇敢的心。他们是天生的战斗种族,为战斗而生,为战斗而亡。
传说中,这群波拉哩的共同祖先是一只叫“塔坦”的超级波拉哩,塔坦的职责就是守护宇宙瑰宝“荆棘之花”,它拥有强大的能量,会分出分身,变化万物,唯一的弱点就是贪吃。
一天,塔坦终于禁不住诱惑,偷食了“荆棘之花”,结果身体爆裂,成为了数以亿计的小波拉哩。从此,波拉哩的族群就受到了贪食的诅咒,只能在这片星云中无休止的战斗,如果停止战斗,生命便会流失,消亡在茫茫的星空之中。在漫漫的历史长河里,只有最强大的波拉哩才能冲过这片黑暗星云,打破命运的枷锁,去寻找那传说中的光明与和平。
为了那甜蜜的希望,波拉哩们战斗着。他们必须奋力奔跑,让自己变大变大再变大,才能对抗比自己更强的存在。哪怕经历无数失败也必须重新凝聚力量,直到成为最强壮的那个。
玩法
输入房间 ID,加入房间(如果没有此房间,则创建)。
用户 ID 随机生成。
使用 ⬆️⬇️⬅️➡️ 或 WSAD 来控制小球移动,吃掉场景中的食物(三角形,方形,六边形)则会增长体重(并减少速度);遇到其他球(玩家),碰撞之后,体重较大者获胜,较小者将会死亡并重生。
右侧面板显示当前房间的玩家体重排行榜。
主要功能
匹配对战
最基础的房间 ID 匹配。
更多关于房间匹配文档
属性同步与保存
这个 demo 使用的是 Master Client 机制,但由于 Master Client 可能存在掉线等异常情况,所以需要将房间和玩家的部分数据保存至 Room Properties 和 Player Properties。
更多关于属性同步文档
房间属性
房间用时战场的食物列表食物最大 ID食物:
/** * 食物 */cc.Class({ extends: cc.Component, properties: { id: 0, type: 0 }, getProperties() { const id = this.id; const type = this.type; const { x, y } = this.node.position; return { id, type, x, y // 可能还会有能量值 }; }});
玩家属性
位置体重速度球:
const Constants = require("Constants");const Food = require("./Food");/** * 球 */cc.Class({ extends: cc.Component, properties: { nameLabel: { type: cc.Label, default: null }, infoLabel: { type: cc.Label, default: null } }, init(player) { this.player = player; }, eat() { // 计算尺寸 const { weight } = this.player.customProperties; const scale = Math.sqrt(weight) / Constants.BORN_SIZE; this.node.scale = cc.v2(scale, scale); }, reborn() { // 计算尺寸 const { weight, pos } = this.player.customProperties; const scale = Math.sqrt(weight) / Constants.BORN_SIZE; this.node.scale = cc.v2(scale, scale); // 位置 const { x, y } = pos; this.node.position = cc.v2(x, y); }, getId() { return this.player.actorId; }, getSpeed() { const { speed } = this.player.customProperties; return speed; }, getWeight() { const collider = this.node.getComponent(cc.CircleCollider); const { radius } = collider; const { scaleX, scaleY } = this.node; return Constants.PI * Math.pow(radius, 2) * scaleX * scaleY; }, // LIFE-CYCLE CALLBACKS: start() { this.nameLabel.string = this.player.userId; }, update(dt) { const { x, y } = this.node; this.infoLabel.string = `(${parseInt(x)}, ${parseInt(y)})`; }, // 碰撞 onCollisionEnter(other, self) { const { group: otherGroup } = other.node; if (otherGroup === Constants.FOOD_GROUP) { this._onCollideFood(other, self); } else if (otherGroup === Constants.BALL_GROUP) { this._onCollideBall(other, self); } }, _onCollideFood(other, self) { // 球碰食物,客户端模拟 const { node: foodNode } = other; const { x, y } = self.node.position; cc.log(`collide food: (${x}, ${y})`); const food = foodNode.getComponent(Food); foodNode.active = false; // 交由 Master 处理 const event = new cc.Event.EventCustom( Constants.BALL_AND_FOOD_COLLISION_EVENT, true ); event.detail = { ball: this, food }; this.node.dispatchEvent(event); }, _onCollideBall(other, self) { const { node: b1Node } = other; const { node: b2Node } = self; const event = new cc.Event.EventCustom( Constants.BALL_AND_BALL_COLLISION_EVENT, true ); event.detail = { b1Node, b2Node }; this.node.dispatchEvent(event); }});
自定义事件
玩家出生:对于当前玩家,执行战场初始化逻辑;对于其他玩家,执行增加玩家逻辑。吃食物:客户端移除内存中的食物节点,同步玩家体重。杀死玩家:用于同步节点间碰撞事件。玩家重生:用于重新初始化玩家数据。生成食物:同步房间内的食物数据。玩家离开:用于移除场景和 UI 对应节点。游戏结束:用于返回主菜单场景。其他功能
消息处理控制
由于从主场景加载到战斗场景,存在异步的资源加载过程,所以需要暂停 / 恢复消息队列的处理。流程如下:
加入房间暂停消息处理加载战斗场景初始化战场恢复消息队列。移动同步
移动同步实现思路是玩家在运动状态改变时,将当前运动状态同步给其他客户端,其他客户端对玩家行为进行模拟。而在运动过程中,并不同步移动数据。
运动状态包括:
位置移动方向时间戳模拟步骤:
在收到运动状态改变时,根据运动改变时的位置,方向,以及当前时间戳与运动改变时的时间戳的差值,计算出当前应该所在的位置 p0玩家节点当前实际所在位置 p1,p0 - p1(向量减法),即为校正后的运动路径对路径进行模拟,直至下次运动状态改变球控制器,当前客户端需要添加组件,由用户输入直接移动,并触发移动同步
const Ball = require("Ball");const Constants = require("../Constants");const LeanCloud = require("../LeanCloud");const { getClient } = LeanCloud;/** * 球控制器,当前客户端需要添加组件,由用户输入直接移动,并触发移动同步 */cc.Class({ extends: cc.Component, properties: {}, // LIFE-CYCLE CALLBACKS: onLoad() { cc.systemEvent.on(cc.SystemEvent.EventType.KEY_DOWN, this._onKeyDown, this); cc.systemEvent.on(cc.SystemEvent.EventType.KEY_UP, this._onKeyUp, this); this._ball = this.node.getComponent(Ball); this._direction = cc.Vec2.ZERO; }, onDestroy() { cc.systemEvent.off( cc.SystemEvent.EventType.KEY_DOWN, this._onKeyDown, this ); cc.systemEvent.off(cc.SystemEvent.EventType.KEY_UP, this._onKeyUp, this); }, start() { this._cameraNode = cc.find("Canvas/Main Camera"); }, update(dt) { const speed = this._ball.getSpeed(); const delta = this._direction.normalize().mul(speed * dt); const position = this.node.position.add(delta); const { x, y } = position; const { LEFT, RIGHT, TOP, BOTTOM } = Constants; const newPosition = cc.v2( Math.min(Math.max(x, LEFT), RIGHT), Math.min(Math.max(y, BOTTOM), TOP) ); this.node.position = newPosition; // 设置摄像机跟随 this._cameraNode.position = this.node.position; }, _onKeyDown(event) { this.running = true; let dir = this._direction.clone(); switch (event.keyCode) { case cc.macro.KEY.a: case cc.macro.KEY.left: dir.x = -1; break; case cc.macro.KEY.d: case cc.macro.KEY.right: dir.x = 1; break; case cc.macro.KEY.w: case cc.macro.KEY.up: dir.y = 1; break; case cc.macro.KEY.s: case cc.macro.KEY.down: dir.y = -1; break; default: break; } this._synchMove(dir.normalize()); }, _onKeyUp(event) { let dir = this._direction.clone(); switch (event.keyCode) { case cc.macro.KEY.a: case cc.macro.KEY.left: case cc.macro.KEY.d: case cc.macro.KEY.right: dir.x = 0; break; case cc.macro.KEY.w: case cc.macro.KEY.up: case cc.macro.KEY.s: case cc.macro.KEY.down: dir.y = 0; break; default: break; } this._synchMove(dir.normalize()); }, _synchMove(dir) { if (dir.fuzzyEquals(this._direction, 0.01)) { return; } this._direction = dir; const { x, y } = this.node.position; const { x: dx, y: dy } = this._direction; const client = getClient(); client.player.setCustomProperties({ move: { p: { x, y }, d: { x: dx, y: dy }, t: Date.now() } }); }});
项目结构
├── Animation 动画目录├── Prefabs 预制目录,主要存放球,食物预制体├── Scene 场景目录,主菜单场景,战斗场景├── Script 脚本目录│ ├── Battle 战斗相关脚本目录│ │ ├── Ball.js 球节点控制脚本│ │ ├── BallController.js 玩家控制球脚本,生成移动数据同步给其他客户端│ │ ├── BallSimulator.js 玩家运动模拟脚本,根据玩家运动数据,模拟运动行为│ │ ├── Battle.js 战场节点总控制器,用于接收并解析战斗中的自定义事件,驱动场景节点及 UI 节点变化│ │ ├── BattleHelper.js 战场工具脚本│ │ ├── Food.js 食物节点控制脚本│ │ ├── Master.js 游戏逻辑脚本,用于区分 Master 客户端与普通客户端,Master 组件用于生成房间数据及逻辑判断,只有 Master 的客户端才拥有这个组件,包括最初的房间的创建者和切换后的新房主。│ │ ├── PlayerInfoItem.js 玩家信息 UI 节点控制脚本│ │ └── UI.js UI 控制脚本│ ├── Menu主菜单相关脚本目录│ │ └── Menu.js 主菜单脚本│ ├── Constants.js 游戏中用到的常量│ └── LeanCloud.js 全局存放 LeanCloud SDK 对象的脚本├── Texture 素材资源目录└── play.js LeanCloud 实时对战服务 SDK
添加wx/微信公众回复球球大作战获取完整代码
在线体验链接
欢迎评论 💬点赞👍🏻 收藏 📂加关注+
欢迎添加微信,加入我的核心小队,请备注来意
👇👇👇👇👇👇👇👇👇👇👇👇👇👇👇