系列之一前端
完善以前建立房間的代碼。 房間處理器建立時我傳一個Room參數進去,不用之後再次查找了。git
RoomProcessor.tsgithub
constructor( public conn: Connection, public lobby: Lobby, public player: Player, public room: Room ) { ... }
在有人退出房間後,須要通知大廳其餘人更新房間數據canvas
export interface UPDATE_ROOM { action: Types.UPDATE_ROOM, id: number, members: number[] } hungup() { ... const roomData = this.room.serialize(); this.lobby.boardcast(CREATE_UPDATE_ROOM(roomData.id, roomData.members)) }
lobby.boardcast
增長條件判斷,不通知在房間內的玩家。segmentfault
boardcast(message: Message, ignoreId?: number) { this.players.forEach(player => { - if (player.id !== ignoreId) { + if (player.roomId === 0 && player.id !== ignoreId) { player.connection.send(message); } }); }
玩家處理器一開始給玩家發送進入房間的消息。 這裏我把規則簡化,房間的Host自動爲黑方,先手,因此,在玩家不是房主的時候給他發送房主的信息。後端
export interface ENTER_ROOM { action: Types.ENTER_ROOM, roomId: number, isHost: boolean, } export interface CHALLENGER_COMING { action: Types.CHALLENGER_COMING, id: number, name: string, isHost: boolean, ready: boolean, } enter() { const host = this.room?.host as Player; const isHost = host === this.player; this.conn.send(CREATE_ENTER_ROOM(this.room.id, isHost)); if (!isHost) { this.conn.send(CREATE_CHALLENGER_COMING(host.id, host.name, true, host.ready)); } }
如今須要把前端的遊戲界面整出來了,我簡單的畫了幾個框框。服務器
export default function BattleField() { return ( <div className="battleField"> <div className="players"> <Chair {...player1} /> <Chair {...player2} /> <div className="controls"> <button type="button" disabled={ready} onClick={() => { setReady(true); Game.ready() }}>Start</button> <button type="button" disabled={!playing}>Surrender</button> </div> </div> <canvas ref={ref} className="board" width="450" height="450" /> <Dialog list={notifications} /> </div> ) }
Dialog
組件顯示收到的消息。ide
Chair
顯示對戰雙方的信息。this
export default function Chair(props: ChairProps) { const { name, ready, side } = props; const cls = side === 0 ? 'chair isBlack' : 'chair'; return ( <div className={cls}> <div className="username">{name}</div> <div className="status">{ready ? 'READY' : ''}</div> </div> ) }
棋盤我用Canvas畫出來。按道理來講,棋類這種不須要用Canvas來畫,確實要用,應該實現一套相似桌面UI同樣的髒矩形渲染,我這裏管不了這麼多了,一切爲了省事。 須要注意的是爲了把鼠標座標轉化爲棋子位置,我使用了MouseEvent對象的offsetX屬性,這會有兼容問題,誰在意呢,除了Chrome都是異端😄。spa
mousePosToCoordinate(x: number, y: number) { const rx = x - this.startX; const ry = y - this.startY; const ix = Math.max(0, Math.floor(rx / CELLSIZE)); const iy = Math.max(0, Math.floor(ry / CELLSIZE)); const offsetX = (rx % CELLSIZE) > (CELLSIZE / 2) ? 1 : 0; const offsetY = (ry % CELLSIZE) > (CELLSIZE / 2) ? 1 : 0; return { x: ix + offsetX, y: iy + offsetY } }
另外我把Connection
更名爲Game
了,由於它除了鏈接的功能以外還須要作一些遊戲的工做。
點擊Start
以後告訴服務器我準備就緒了。
Game.ts
export interface READY_TO_RACE { action: Types.READY_TO_RACE, id: number } ready() { this.send(CREATE_READY_TO_RACE(-1)); }
READY_TO_RACE
的消息是雙向的,Front end <==> Server。 Server通知的時候迴帶上對應玩家的id, 前端發送則不須要了,能夠自動獲取。
後端的房間處理器收到READY消息以後。通知房間內的全部玩家該玩家已準備好了。
當兩我的都準備完畢以後遊戲自動開始。
case Types.READY_TO_RACE: { this.player.ready = true; let readyCount = 0; this.room.members.forEach(player => { player.ready && readyCount++; player.connection.send(CREATE_READY_TO_RACE(this.player.id)); }); if (readyCount === 2) { this.lobby.startGame(this.room); } break; }
開始遊戲後把房間的轉態設爲Playing
, 只有狀態是Playing
的時候,房間處理器纔會響應玩家的下棋動做。
給房間內玩家發送GAME_START
消息,給當前回合的玩家發送GAME_ROUND
消息。
export interface GAME_START { action: Types.GAME_START roomId: number } export interface GAME_ROUND { action: Types.GAME_ROUND } startGame(room: Room) { room.playing = true; room.members.forEach(player => { player.connection.send(CREATE_GAME_START(room.id)); }); room.host?.connection.send(CREATE_GAME_ROUND()); room.roundId = room.host?.id as number; }
前端收到GAME_START
消息以後開始一局遊戲。把棋盤清空,而後顯示一條遊戲開始的文字信息。
收到GAME_ROUND
消息以後,board
對象能夠進行鼠標操做,點擊以後若是是有效的位置則向服務器發送OCCUPATION
消息,帶上當前棋子的座標。
// BattleField.ts board.on('occupation', (index: number) => { Game.occupation(index); }); Game.on('game_start', () => { board.start(); setPlaying(true); }); Game.on('game_round', () => { board.yourTurn = true; }); // Game.ts occupation(index: number) { this.send(CREATE_OCCUPATION(index, -1)); this.emit('notice', 'Waiting...'); }
服務器對下棋操做的處理:
round(index: number, playerId: number) { if (!this.playing) return; if (this.roundId !== playerId) return; if (this.chessboard[index]) { return; } this.chessboard[index] = playerId; this.members.forEach(p => { p.connection.send(CREATE_OCCUPATION(index, (playerId === this.host?.id) ? 0 : 1)); }) if (this.check(index)) { this.gameOver(); } else { let next = this.members.find(p => p.id !== this.roundId) as Player; this.roundId = next.id; next.connection.send(CREATE_GAME_ROUND()); } }
首先判斷一下合法性,一切沒問題以後向前端發送落子。以後檢查5子連線, 成功則結束這局遊戲,發送結局消息,不然交換手。
輸贏條件判斷我擼了一段樸素的代碼,甚至沒有檢查是否能在全部條件下正常工做。
checkedId: number = 0; check(index: number) { const x = index % 15; const y = Math.floor(index / 15); let startX = Math.max(0, x - 5); let endX = Math.min(14, x + 5); let startY = Math.max(0, y - 5); let endY = Math.min(14, y + 5); let id = this.chessboard[index]; this.checkedId = id; const checkLines = () => { let count = 0; for (let i = 0; i < lines.length; i++) { const loc = lines[i]; if (this.chessboard[loc] === id) { count++; } else { count = 0; } if (count >= 5) break; } lines = []; return count >= 5; } let lines: number[] = []; for (let px = startX; px < endX; px++) { const loc = px + y * 15; lines.push(loc); } if (checkLines()) return true; for (let py = startY; py < endY; py++) { const loc = x + py * 15; lines.push(loc); } if (checkLines()) return true; for (let i = 1; i < 5; i++) { if (x - i > -1 && y - i > -1) lines.push(x - i + (y - i) * 15); } lines.push(x + y * 15); for (let i = 1; i < 5; i++) { if (x + i < 15 && y + i < 15) lines.push(x + i + (y + i) * 15); } if (checkLines()) return true; for (let i = 1; i < 5; i++) { if (x - i > -1 && y + i < 15) lines.push(x - i + (y + i) * 15); } lines.push(x + y * 15); for (let i = 1; i < 5; i++) { if (x - i < 15 && y - i > -1) lines.push(x + i + (y - i) * 15); } if (checkLines()) return true; }
那麼,加上一些必要的玩家中途退出的處理,這個小DEMO差很少算是完成了。
總結:週末兩天花在這上面的時間應該超過8個小時了,這大大超出我原來的預期,再簡單的東西若是加上實時交互複雜度就倍增,這仍是在這個小遊戲沒有須要處理同步的需求。
React Hooks
用起來仍是力不從心,個人理解是若是用來作大一點的應用,狀態管理仍是跑不掉,我依然會引入ReMatch
之類的東西。