使用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));
}
複製代碼

代碼地址

碼雲

最後

由於本人文筆有限,可能會有錯別字、思路沒講清的內容,還請多多擔待= =(可能會看不懂我寫的是什麼個鬼東西)

還有,就是這個思路可能不是很行...,由於這是我本身想的,會有不少的問題,這裏就當作拋磚引玉了。

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

相關文章
相關標籤/搜索