使用socket.io制作帧同步游戏(思路)
前言
一直想做一个联机的游戏,之前也用socket.io做了几个demo,不过那个时候不知道帧同步这回事,所以那时我就是通过将所有玩家的数据(位置啊,血量啊),还有子弹的所有数据转发给所有的玩家(除了自己),然后其他的玩家通过判断是否有这个数据,如果没有就生成一个,有的话就将覆盖掉。
不过上面的这种做法超级卡,无比的卡,异常的卡,迫不得已,百度了一下怎么做联机游戏。
网络上,有两种做联机游戏的方式,一种是状态同步,一种是帧同步。
下面就简单的介绍一下两种方法的区别,不过就不过多的说了。(因为我还是一知半解2333333)
联机游戏最重要的就是所有客户端显示统一。
状态同步
这个做法是服务器为主,服务器将所有的计算处理好,然后返回统一的数据给所有的玩家,玩家通过这个数据渲染出游戏的界面。(这个方法和我之前做的有点类似= = )
这种做法很安全,因为所有的数据在服务器中,客户端不管怎么改数据,最后执行的依旧是服务器中的数据。
帧同步
这个就是本文重点讲的,在百度上怎么搜都没有看到js
/node
/socket.io
的联机游戏教程,所以只能自己硬着头皮学(瞎写)。
目前我实现的帧同步做法是,客户端发出的操作并不会在本地处理,而是会上传到服务器,让服务器保存所有玩家的操作,然后在固定时间发送给所有的客户端。然后客户端就会在固定的频率上处理这些操作,以到达一个同步的效果。
并且帧同步会保存玩家的操作,所以很容易做回放&观战很简单。
下面我简单说明一下做的两个demo。(代码过于烂,可以学习思路,但是不能抄,会死。)
demo1 - 小球画图
这个demo主要实现了中途加入游戏的玩家可以看到已经在游戏中的玩家、游戏回放、所有玩家操作统一。
demo2 - 球球大作战
这个主要是实现了一整个的房间系统,创建私密房间、加入房间、房间列表、踢出房间。游戏就写了一个移动(后面不想写了…)
还做了一个简单的聊天室,两个频道,一个世界频道(所有人都看到),一个房间频道(房间内看到)。
思路
我这里的做法是这样的, 通过前后端两个action.js
来分别解析发送给双方的动作:
服务器:
const actions = {
// 玩家加入游戏的操作
'player.add': (player, package) => {
},
// 创建房间
'room.create': (player, package) => {
},
// 加入房间
'room.add': (player, package) => {
},
// 离开房间
'room.leave': (player, package) => {
},
// 踢出房间
'room.shit': (player, package) => {
},
// 房间列表
'room.list': (player, package) => {
},
// 房间中准备游戏
'room.ready': (player, package) => {
},
// 创建一个游戏
'game.create': (player, package) => {
},
// 游戏的操作
'game.action': (player, package) => {
},
// 系统信息分发
'message': (player, package) => {
}
}
// 处理数据包
module.exports = function (data) {
if (!data.action) {
console.warn('无法处理的数据包', data);
return;
}
let action = actions[data.action];
if (!action) {
console.warn('无法处理的动作: ' + data.action);
return;
}
actions[data.action](this, data);
}
// 然后如果有玩家链接上了服务,所以的数据都是通过使用on('all'), emit('all')这个方法来接受和发送的
this.socket.on('all', data => {
action.call(this, data)
});
然后前端发送数据包是这样发送的:
on(name, fn) {
if (!this.io) {
console.error('not connect socket server.');
return;
}
this.io.on(name, fn);
},
emit(name, data) {
if (!this.io) {
console.error('not connect socket server.');
return;
}
this.io.emit(name, data);
},
// 在这里发送数据包
action(name, data) {
this.emit('all', {
data,
action: name,
time: new Date().getTime()
});
},
客户端也有一个action
来解析后端发送的数据包,这里我就不粘贴代码了(因为都一样)
接下来讲一讲,游戏中玩家的操作
目前我的做法是,服务器固定一个频率将接受到的所有玩家操作发送给所有的客户端。
class G{
constructor(room){
this.room = room;
// 这个是保存整个游戏所有的操作
this.frames = [];
// 这个保存每帧的客户端操作
this.actions = {};
// 频率,也就是帧,在每一帧发送保存客户端的所有操作
this.packageNum = global.option.gameFrame;
this.interval = null;
this.start();
}
start(){
this.interval = setInterval(() => {
// 将房间中所有玩家的动作统一发送给房间内所有人
global.Core.socket.emit('game.action', this.actions, null, this.room.key);
// 将这一帧的操作保存起来,后面就可以通过这个来制作游戏回放了。
this.frames.push(this.actions);
// 将操作清空
this.room.playerList.forEach(item => {
this.actions[item.id] = [];
});
}, 1000 / this.packageNum);
}
}
然后客户端就可以通过解析game.action
:
// action.js
'game.action': package => {
game.complite(package.data);
},
// game.js
complite(action) {
// 将动作拿出来给玩家的实例处理
Object.keys(action).forEach(key => {
action[key].forEach(ac => {
this.playerList[key].action(ac);
})
});
// 接收到后端发过来的动作帧才会让每个玩家实例移动,这样就可以达到所有客户端显示统一
Object.keys(this.playerList).forEach(key => {
this.playerList[key].move();
})
}
客户端发送游戏操作是这样的:
// 这些代码很简单= = ,我就懒得写注释了2333.
let prev = null;
function action(key, flag) {
let action = key + (flag ? '_up' : '');
if (action == prev) return;
app.action('game.action', {
action
});
prev = action;
}
let event = {
'0'(flag) {
action('left', flag);
},
'1'(flag) {
action('top', flag);
},
'2'(flag) {
action('right', flag);
},
'3'(flag) {
action('bottom', flag);
},
'-5'(flag){
action('speed', flag);
}
}
document.body.onkeydown = ev => {
if (!this.playStatus) return;
let code = ev.keyCode - 37;
event[code] && event[code]();
}
document.body.onkeyup = ev => {
if (!this.playStatus) return;
let code = ev.keyCode - 37;
event[code] && event[code](true);
}
游戏回放
在之前game.js
的注释我就写了,通过保存每一动作帧(我瞎编的名词,就是服务器每帧保存玩家的动作)玩家的操作,如果某个玩家需要看回放,就可以接受这个集合,然后遍历执行完所有的动作 = =(是不是很简单)
this.io.on('getGameLog', data => {
this.logs = data;
this.play();
})
play() {
// 如果看完了所有动作就退出
if (this.playLog >= this.logs.length) {
this.playStatus = false;
return;
}
this.playStatus = true;
let item = this.logs[this.playLog];
game.complete(item);
// 解析每一帧
this.playLog++;
// 这里是播放的倍率
setTimeout(this.play.bind(this), 1000 / (20 * this.playX));
}
代码地址
最后
因为本人文笔有限,可能会有错别字、思路没讲清的内容,还请多多担待= =(可能会看不懂我写的是什么个鬼东西)
还有,就是这个思路可能不是很行…,因为这是我自己想的,会有很多的问题,这里就当做抛砖引玉了。
下一篇: 实训—day04