用Unity做个游戏(十) - 完结篇,内容补全

前言

这个项目差不多5月份就已经没有再更新了,6月初正式从公司离职开始专心做独立游戏了。差不多到现在已经一个月了,工作也慢慢进入了正轨,这两天手头暂时闲下来了,也差不多该把这个系列完结掉了,了却我一桩心愿233

服务端主要游戏逻辑

上次说到主要逻辑是由各个具体的Controller来实现的,这个游戏分为两个Controller:UserControllerBattleController

前者主要负责用户的登陆登出等等,逻辑比较简单,我们主要来看BattleController的逻辑

这个Controller只处理一个协议,就是SFRequestMsgUnitSync同步状态协议,里面包含4个参数,移动方向,鼠标朝向和是否释放了技能。

处理协议的逻辑如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* 用户同步操作信息
* @param req
*/
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 () {
// dt = 40ms
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));
}

// 清理状态数据
// 1. 清理技能释放状态
// 2. 清理爆炸了的火球
// 3. 清理被击败的角色
})(battle);
}
}
};

原文篇幅太长,这里就只保留重要代码,不重要的就全换成注释了,完整代码可以在文末找到。

碰撞计算

由于逻辑全都放在了服务器端,我们就不能用现成的物理引擎了,那就自己写一个简单的碰撞逻辑吧,反正node本身也不适合精确计算,能用就行233

碰撞分为以下几种

  1. 角色x角色
  2. 角色x墙壁
  3. 角色x火球
  4. 火球x墙壁
  5. 火球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
/**
* 当角色碰撞到角色
* @param user1
* @param user2
* @param dt
*/
const onUserEnterUser = function (user1, user2, dt) {
// 碰撞到其他角色 - 交换速度,根据动量和能量守恒
// v1'=((m1-m2)v1+2m2v2)/(m1+m2) v2'=((m2-m1)v2+2m1v1)/(m1+m2)
};

/**
* 当角色碰撞到墙
* @param user
* @param wall
*/
const onUserEnterWall = function (user, wall) {
// 墙壁法线方向的速度归零 R'=I-(IN)N
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);
// pp是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;
}
};

/**
* 当球碰撞到墙
* @param ball
* @param wall
* @param dt
*/
const onBallEnterWall = function (ball, wall, dt) {
if (ball.life < 0) {
// 寿命结束,撞墙爆炸
ball.explode = true;
return;
}
const N = wall.normal;
const I = ball.speed;
// 反射向量R = I - 2(I*N)N
ball.speed = I - 2 * utils.vector2Dot(I, N) * N;
// 撞墙去掉加速度
ball.acc = 0;
ball.pos += ball.speed * dt;
};

/**
* 当球碰撞到角色
* @param ball
* @param user
*/
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;
};

/**
* 当球碰撞到球
* @param ball1
* @param ball2
*/
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,请确保该端口没有被占用,如果已经占用了的话会给出提示

1001

然后就可以启动多个客户端进行联机游玩了。虽然还是有不少的bug,不过主游戏流程应该是OK…的吧

后记

一开始也没想着有人看,纯粹是自己的学习笔记,不过好像到现在还是有帮到一些人233,顺便感谢催更(误

这个项目算是我转Unity以来第一个练手的东西,用的框架现在在我们新项目中也在不断完善,之后心情好的话也许会记录新游戏的开发,或者是这个框架的完善也说不定~

最后打个广告,最近申请了个个人微信公众号,之后博客有更新的话会在公众号上推送消息,有兴趣的话可以关注一下~

公众号二维码

完整代码

上面贴出的代码片段由于篇幅限制只保留了关键部分,完整的代码可在我的github上找到