使用React與WebSocket來作一個多人對戰的五子棋吧 系列之二

系列之一前端

建立房間

完善以前建立房間的代碼。 房間處理器建立時我傳一個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));
    }
}

開啓對戰

如今須要把前端的遊戲界面整出來了,我簡單的畫了幾個框框。服務器

battlefield.png

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差很少算是完成了。

game.png

最終代碼戳這裏

總結:週末兩天花在這上面的時間應該超過8個小時了,這大大超出我原來的預期,再簡單的東西若是加上實時交互複雜度就倍增,這仍是在這個小遊戲沒有須要處理同步的需求。

React Hooks用起來仍是力不從心,個人理解是若是用來作大一點的應用,狀態管理仍是跑不掉,我依然會引入ReMatch之類的東西。

相關文章
相關標籤/搜索