前言
这个项目差不多5月份就已经没有再更新了,6月初正式从公司离职开始专心做独立游戏了。差不多到现在已经一个月了,工作也慢慢进入了正轨,这两天手头暂时闲下来了,也差不多该把这个系列完结掉了,了却我一桩心愿233
服务端主要游戏逻辑
上次说到主要逻辑是由各个具体的Controller来实现的,这个游戏分为两个Controller:UserController
和BattleController
。
前者主要负责用户的登陆登出等等,逻辑比较简单,我们主要来看BattleController
的逻辑
这个Controller只处理一个协议,就是SFRequestMsgUnitSync
同步状态协议,里面包含4个参数,移动方向,鼠标朝向和是否释放了技能。
处理协议的逻辑如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
|
const onUserSyncInfo = function (req) { let retCode = 0; do { const battleId = userData.onlineUserList[req.uid].battleId; const battle = battleData.battleList[battleId]; const userItem = battle.users[req.uid]; userItem.accX = req["moveX"] * userItem.accPower / userItem.mass; userItem.accY = req["moveY"] * userItem.accPower / userItem.mass; userItem.rotation = req["rotation"]; userItem.skillId = req["skillId"]; } while (false); const resp = {pid: 3, retCode: retCode}; m_pusher([req.uid], JSON.stringify(resp)); };
|
这里由于篇幅不便太大,我删去了错误处理的部分,大概就是把信息记录在内存里,稍后我们会用到
状态逻辑更新
因为我们的同步方式是一种状态同步,所有的逻辑运算全部在服务端计算,所以我的方案是服务端有一个定时器,每个固定时间服务器根据当前场上单位的状态信息进行一次逻辑更新,包括位置更新,速度衰减,物体碰撞等。然后把最新的状态信息同步给客户端,客户端收到状态信息后通过插值表现出连续的运动轨迹(插值一是因为服务器协议传输可能会由于网络波动而不连续,二是因为客户端画面刷新率60fps会远高于服务器逻辑更新频率25fps)
update方法如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79
|
const onUpdate = function () { const dt = commonConf.updateDt / 1000; const battleList = battleData.battleList; for (const battleId in battleList) { if (battleList.hasOwnProperty(battleId)) { const battle = battleList[battleId]; (function (battle) { battle.runTime += 40; utils.traverse(battle.users, function (userItem) { calcSpeedLimit(userItem.speedX, userItem.speedY, userItem.topSpeed); userItem.posX += userItem.speedX * dt; userItem.posY += userItem.speedY * dt; if ((userItem.accX <= 0 && userItem.accY <= 0) || k < 1) { userItem.speedX *= 0.9; userItem.speedY *= 0.9; } });
utils.traverse(battle.users, function (userItem) { if (userItem.skillId != 0) { if (userItem.skillId & 1 > 0) { battle.addBall(userItem); } } });
utils.traverse(battle.balls, function (ballItem) { ballItem.posX += ballItem.speedX * dt; ballItem.posY += ballItem.speedY * dt; ballItem.life -= dt; });
const colliders = {}; Object.assign(colliders, battle.users, battle.walls, battle.balls); utils.traverse(colliders, function (item1) { utils.traverse(colliders, function (item2) { checkCollision(battle, item1, item2, dt); }); });
let users = []; let infos = []; let ballsInfo = []; const resp = { pid: 4, retCode: 0, runTime: battle.runTime, infos: infos, balls: ballsInfo }; if (users.length > 0) { m_pusher(users, JSON.stringify(resp)); }
})(battle); } } };
|
原文篇幅太长,这里就只保留重要代码,不重要的就全换成注释了,完整代码可以在文末找到。
碰撞计算
由于逻辑全都放在了服务器端,我们就不能用现成的物理引擎了,那就自己写一个简单的碰撞逻辑吧,反正node本身也不适合精确计算,能用就行233
碰撞分为以下几种
- 角色x角色
- 角色x墙壁
- 角色x火球
- 火球x墙壁
- 火球x火球
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83
|
const onUserEnterUser = function (user1, user2, dt) { };
const onUserEnterWall = function (user, wall) { const N = wall.normal; const IX = user.speed; const INDot = utils.vector2Dot(I, N); const R = I - INDot * N; user.speed = R; const threshold = wall.width / 2 + user.size; const P = user.pos - wall.pos; const PNDot = utils.vector2Dot(P, N); const PP = PNDot * N; if (utils.vector2Dot(PP, N) > 0 && utils.vector2Length(PP) < threshold) { const OA = threshold * N; const OB = P + OA - PP; user.pos = wall.pos + OB; } };
const onBallEnterWall = function (ball, wall, dt) { if (ball.life < 0) { ball.explode = true; return; } const N = wall.normal; const I = ball.speed; ball.speed = I - 2 * utils.vector2Dot(I, N) * N; ball.acc = 0; ball.pos += ball.speed * dt; };
const onBallEnterUser = function (ball, user) { ball.explode = true; user.life -= ball.power; const N = utils.vector2Normalize(user.pos - ball.pos); user.speed = N * ball.power / user.mass; };
const onBallEnterBall = function (ball1, ball2) { ball1.explode = true; ball2.explode = true; };
|
服务端搭建
客户端相关准备
客户端的搭建非常简单,只要把代码clone下来用unity打开直接运行就行了,当然直接这样的话会提示服务器连接失败www
怎么搭建服务器呢,我们就假设服务器在本地(如果想把服务器部署在其他机器上的话,找到客户端Assets/Scripts/Conf/SFCommonConf.cs
文件,把相关IP地址改掉就行了。
安装node.js
当然Windows是可以安装node的,不过还是建议在Linux或者macOS下使用,口味更佳~
具体教程就没有必要贴出来了,大家可以到官网上自行下载。
部署服务器
Clone游戏代码,进入server/app
目录,运行npm install
命令即可一键安装所有依赖(其实就俩,colors和redis)
然后在相同目录下运行命令node ./
,即可启动服务器,CTRL+C
中断,因为进程运行在前台,所以如果是通过ssh操作服务器的话,可能需要screen之类的配套工具。
服务器端使用端口19621
,请确保该端口没有被占用,如果已经占用了的话会给出提示
然后就可以启动多个客户端进行联机游玩了。虽然还是有不少的bug,不过主游戏流程应该是OK…的吧
后记
一开始也没想着有人看,纯粹是自己的学习笔记,不过好像到现在还是有帮到一些人233,顺便感谢催更(误
这个项目算是我转Unity以来第一个练手的东西,用的框架现在在我们新项目中也在不断完善,之后心情好的话也许会记录新游戏的开发,或者是这个框架的完善也说不定~
最后打个广告,最近申请了个个人微信公众号,之后博客有更新的话会在公众号上推送消息,有兴趣的话可以关注一下~
完整代码
上面贴出的代码片段由于篇幅限制只保留了关键部分,完整的代码可在我的github上找到