欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

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

程序员文章站 2024-03-25 22:59:04
...

前言

一直想做一个联机的游戏,之前也用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));
}

代码地址

码云

最后

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

还有,就是这个思路可能不是很行…,因为这是我自己想的,会有很多的问题,这里就当做抛砖引玉了。

相关标签: javascript js