使用socket.io制做帧同步游戏(思路)

前言

一直想作一个联机的游戏,以前也用socket.io作了几个demo,不过那个时候不知道帧同步这回事,因此那时我就是经过将全部玩家的数据(位置啊,血量啊),还有子弹的全部数据转发给全部的玩家(除了本身),而后其余的玩家经过判断是否有这个数据,若是没有就生成一个,有的话就将覆盖掉。javascript

不过上面的这种作法超级卡,无比的卡,异常的卡,无可奈何,百度了一下怎么作联机游戏。前端

网络上,有两种作联机游戏的方式,一种是状态同步,一种是帧同步。java

下面就简单的介绍一下两种方法的区别,不过就不过多的说了。(由于我仍是只知其一;不知其二2333333)node

联机游戏最重要的就是全部客户端显示统一。git

状态同步

这个作法是服务器为主,服务器将全部的计算处理好,而后返回统一的数据给全部的玩家,玩家经过这个数据渲染出游戏的界面。(这个方法和我以前作的有点相似= = )后端

这种作法很安全,由于全部的数据在服务器中,客户端无论怎么改数据,最后执行的依旧是服务器中的数据。安全

帧同步

这个就是本文重点讲的,在百度上怎么搜都没有看到js/node/socket.io的联机游戏教程,因此只能本身硬着头皮学(瞎写)。服务器

目前我实现的帧同步作法是,客户端发出的操做并不会在本地处理,而是会上传到服务器,让服务器保存全部玩家的操做,而后在固定时间发送给全部的客户端。而后客户端就会在固定的频率上处理这些操做,以到达一个同步的效果。网络

而且帧同步会保存玩家的操做,因此很容易作回放&观战很简单。app

下面我简单说明一下作的两个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));
}
复制代码

代码地址

码云

最后

由于本人文笔有限,可能会有错别字、思路没讲清的内容,还请多多担待= =(可能会看不懂我写的是什么个鬼东西)

还有,就是这个思路可能不是很行...,由于这是我本身想的,会有不少的问题,这里就当作抛砖引玉了。

撒花,在掘金第一篇文章~

相关文章
相关标签/搜索