[遊戲開發] 24點數學遊戲 - 網站製作篇(二)

閱讀時間約 18 分鐘
raw-image


前言

前一篇介紹網站基本架構,這篇主要介紹遊戲最核心的部分,以單人遊戲為出發點,從發牌、抽牌、出牌、檢查算式結果是否正確以及計算分數到遊戲結算,一環扣著一環把遊戲邏輯完成,那就讓我們開始吧!


單人遊玩頁面

這個頁面我把它切成三等份,由上到下分別為,功能按鈕區塊,在多人連線中會多塞對手玩家的畫面,中間區塊顯示組合中的算式以及算式結果,最下面的區塊顯示手牌、玩家資訊、操作按鈕,如下圖。

三大區塊

三大區塊

Websocket

雖然是單人遊玩,但整體架構還是建立在 socket 上,也就是說單人就自己一個遊戲房間,這樣的好處是未來在開發多人遊玩時,就可以共用遊戲事件與邏輯方法,當載入這個頁面的時候,需要 new 一個 socket 變數,開啟與伺服器的連線。

File path: hooks\useSinglePlay.ts

const socket = io();

我使用了 hook 來封裝整個前端事件以及遊戲狀態

const useSinglePlay = () => {
  const [roomInfo, setRoomInfo] = useState<Room>();

  useEffect(() => {
    const roomId = uuidv4();
    socket.emit(SocketEvent.JoinRoom, {
      roomId,
      maxPlayers: 1,
      playerName: 'single',
      mode: GameMode.Single,
    });

    socket.on(SocketEvent.JoinRoomSuccess, () => {
      // 遊戲開始
      socket.emit(SocketEvent.StartGame, { roomId });
    });

    socket.on(SocketEvent.ErrorMessage, message => {
      toast.error(message);
    });

    // 房間更新
    socket.on(
      SocketEvent.RoomUpdate,
      ({
        room,
        extra,
      }: {
        room: Room;
        extra?: { event: SocketEvent; data: any };
      }) => {
        setRoomInfo(room);
        if (extra?.event === SocketEvent.PlayCardResponse) {
          if (extra.data) {
            toast.success('答案正確');
          } else {
            toast.error('答案不等於 24');
          }
          setCheckAnswerCorrect(extra.data as boolean);
        }
      },
    );

    return () => {
      socket.disconnect();
    };
  }, []);

  return {
    roomInfo,
  };
};

export default useSinglePlay;

在 useEffect 中會打一個 JoinRoom 事件到後端,會隨機給一個 roomId,這邊用 uuid 套件產生一個,最大玩家數就設定一個,模式設定單人,這些參數後端會拿來判斷要準備什麼類型的房間,單人有單人的牌庫與設定,接著開啟 socket.on 來監聽後端的事件,事件 JoinRoomSuccess 的作用是後端把玩家成功加入房間後觸發,通知玩家可以開始遊戲了,接著由前端再打 StartGame 事件並打 roomId 過去到後端,觸發 RoomUpdate 事件把房間資訊回傳給前端並渲染畫面。

整個畫面的資料都放在 roomInfo 裡面

const [roomInfo, setRoomInfo] = useState<Room>();

以下是 Room 的型別

export type Room = {
  roomId: string;
  roomName?: string;
  password?: string; // 房間密碼
  maxPlayers: number; // 最大玩家數
  currentOrder: number; // 當前輪到的玩家
  deck: NumberCard[]; // 牌庫
  players: Player[]; // 玩家資訊
  isGameOver: boolean; // 是否遊戲結束
  selectedCards: SelectedCard[]; // 當前所選的牌
  status: GameStatus; // 房間狀態
  settings: RoomSettings; // 房間設定
  countdownTime?: number; // 回合倒數計時
};

最主要的房間資訊是 roomId、maxPlayers、currentOrder、deck、isGameOver、selectedCards。

接下來是後端的部分,由於程式碼太長我舉幾個比較重要的事件來介紹。

const httpServer = createServer(handler);
 const io = new Server(httpServer, {
   pingInterval: 24 * 60 * 60 * 1000,
   pingTimeout: 3 * 24 * 60 * 60 * 1000,
});

首先先 new 一個 socket server。

io.on('connection', socket => {
const playerId = socket.id;

// 以下省略...
});

開啟連線並監聽從 client 來的事件,使用 socket.id 來做為玩家 id。

socket.on(
  SocketEvent.JoinRoom,
  ({ roomId, maxPlayers, playerName, roomName, password, mode }) => {
    const canJoin = checkCanJoinRoom(roomId, playerId, mode);
    if (canJoin) {
      socket.join(roomId);
      const { room, msg, needPassword } = joinRoom(
        { roomId, maxPlayers, roomName, password },
        playerId,
        playerName,
        mode,
      );

      if (room) {
        io.sockets.to(roomId).emit(SocketEvent.JoinRoomSuccess, room);
        socket.emit(SocketEvent.GetPlayerId, playerId);
      } else if (needPassword) {
        socket.emit(SocketEvent.NeedRoomPassword);
      } else {
        socket.emit(SocketEvent.ErrorMessage, msg);
      }
    } else {
      socket.emit(SocketEvent.ErrorMessage, '房間人數已滿或不存在');
    }
  },
);

JoinRoom 為玩家加入房間時觸發,會先檢查是否可以加入房間 checkCanJoinRoom 這個方法。

export function checkCanJoinRoom(
  roomId: string,
  playerId: string,
  mode: GameMode,
) {
  if (mode === GameMode.Single) {
    return true;
  }

  return false;
}

單人比較簡單因為只有一個人一律回傳 true,接下來就呼叫 joinRoom 方法把玩家加入房間。

let _rooms: Room[] = [];

所有的房間都存在內存中,在專案的初始階段這個方法簡單又快速,但是當玩家人數多的時候可能會有記憶體不足的問題,或是重新上版時重啟伺服器會把所有已存在的房間清除,所以只能挑合適的時間上版。

// 加入房間
export function joinRoom(
  payload: Pick<Room, 'roomId' | 'maxPlayers' | 'roomName' | 'password'>,
  playerId: string,
  playerName: string,
  mode: GameMode,
): Response & { needPassword?: boolean } {
  try {
    // 創建新房間
    const newRoom: Room = {
      roomId: payload.roomId,
      maxPlayers: payload.maxPlayers,
      deck: [],
    currentOrder: -1,
      isGameOver: false,
      selectedCards: [],
      roomName: payload.roomName,
     password: payload.password,
      status: GameStatus.Idle,
      settings: {
        deckType: DeckType.Standard,
        remainSeconds: 60,
      },
      players: [
        {
          id: playerId,
          isMaster: true,
          name: playerName,
          handCard: [],
          score: 0,
          isLastRoundPlayer: false,
          isReady: true,
        },
      ],
    };

    _rooms.push(newRoom);

    return {
      room: newRoom,
    };
  }
} catch (e) {
  return { msg: '發生錯誤,請稍後再試 (join room)' };
}
}

初始化房間資訊,建立一個新的房間並回傳當前的房間資訊到前端。

io.sockets.to(roomId).emit(SocketEvent.JoinRoomSuccess, room);

還記得前端會在收到 JoinRoomSuccess 事件後打 StartGame 事件嗎,當後端收到 StartGame 後就會準備寫入遊戲設置到房間資訊。

socket.on(SocketEvent.StartGame, ({ roomId }) => {
  const { room, msg } = startGame(roomId);
  if (room) {
    io.sockets.to(roomId).emit(SocketEvent.RoomUpdate, { room });
  } else {
    socket.emit(SocketEvent.ErrorMessage, msg);
  }
});

接下來呼叫 startGame 方法。

// 開始遊戲
export function startGame(roomId: string): Response {
const roomIndex = _getCurrentRoomIndex(roomId);
  if (roomIndex === -1) return { msg: '房間不存在' };
  try {
    let tempDeck: number[] = [];
    switch (room.players.length) {
      case 1:
        tempDeck = createDeckByRandomMode(40, 10);
        break;
      default:
        return {
          msg: '開始遊戲失敗',
        };
    }

    // 洗牌
    const shuffledDeck: NumberCard[] = shuffleArray(tempDeck).map(d => ({
      id: uuidv4(),
      value: d,
    }));

   _rooms[roomIndex].players[0].playerOrder = order;
    _rooms[roomIndex].players[0].score = 0;
   _rooms[roomIndex].players[0].isLastRoundPlayer = false;
   if (shuffledDeck.length) {
    // 抽牌並改變牌庫牌數
    _rooms[roomIndex].players[0].handCard = draw(
      shuffledDeck,
      HAND_CARD_COUNT,
      );
    }

    // 寫入牌庫
    _rooms[roomIndex].deck = shuffledDeck;

    // 從玩家1開始
    _rooms[roomIndex].currentOrder = 1;

    // 初始化
    _rooms[roomIndex].status = GameStatus.Playing;
    _rooms[roomIndex].isGameOver = false;
    _rooms[roomIndex].selectedCards = [];

    return {
      room: _rooms[roomIndex],
    };
  } catch (e) {
    return { msg: '發生錯誤,請稍後再試 (start game)' };
  }
}

我們先呼叫 _getCurrentRoomIndex 方法找到當前房間,沒找到則回傳錯誤訊息。

const roomIndex = _rooms.findIndex(room => room.roomId === roomId);

在第 9 行,我們呼叫 createDeckByRandomMode 方法產生隨機的牌庫。

/** 產生牌庫 n 為共幾張,maxValue 為最大值 */
export function createDeckByRandomMode(n: number, maxValue: number) {
  const array = [];

  for (let i = 0; i < n; i++) {
    array.push(Math.floor(Math.random() * maxValue) + 1);
  }

  return array;
}

接著呼叫 shuffleArray 洗牌。

/* shuffle */
export function shuffleArray<T>(array: T[]) {
  // 複製原始陣列以避免修改
  let shuffledArray = array.slice();

  // Fisher-Yates 洗牌算法
  for (let i = shuffledArray.length - 1; i > 0; i--) {
    // 生成 0 到 i 之間的隨機整數
    const j = Math.floor(Math.random() * (i + 1));
    // 交換元素 shuffledArray[i] 和 shuffledArray[j]
    [shuffledArray[i], shuffledArray[j]] = [shuffledArray[j], shuffledArray[i]];
  }

  return shuffledArray;
}

我們透過強大的 ChatGPT 幫我們寫洗牌邏輯,洗完牌後把牌寫進牌庫中,並切換遊玩狀態到遊玩中,這樣我們就完成單人遊戲的初始化了。

結語

感謝大家看到這邊,寫到加入房間與開始遊戲,就已經這麼落落長了,事件的部分我想把它分成幾篇文章來介紹,完整的程式碼可以到我的 github 看喔,不過有包含多人遊戲的部分,我們下篇文章見 ^^。


留言0
查看全部
avatar-img
發表第一個留言支持創作者!
Johnson Huang的沙龍 的其他內容
這篇文章介紹了網站的整體架構以及開發時所使用的工具和套件,包括 Next.js、Tailwind CSS 和 socket.io 等。文章回顧了程式碼的重構與優化,幫助開發者提高工作效率,適合希望深入瞭解前端開發和網站架構的讀者。
24點數學遊戲是一款適合小朋友與想動動腦的朋友們的小遊戲,遊戲規則簡單易懂,可訓練邏輯能力。遊戲分為單人與多人模式,可以讓玩家自行挑戰高分或是與其他玩家競爭。算式中不同的數學符號會對應不同的加分機制。遊戲網站連結與專案 repo 也都提供在文章中。
由於最近比較忙碌停更了好一陣子,關於 Flutter 系列目前難產中XD,在開發 Flutter 遊戲之前,我正在進行一個桌遊專案的開發,因為我很喜歡玩桌遊,如果在外面無聊的時候打開手機就可以與朋友一同連線遊玩那該有多好(雖然現在手遊很多),所以我就開始了這個專案,目標是上架各式各樣的桌遊.....
前一篇我們介紹了遊戲模板的基本架構,在這篇文章中會記錄目前的開發進度,主要先把遊戲核心邏輯寫完,再慢慢完成其他功能,在這系列的文章中,我不會太聚焦於寫程式的部分,如果想看程式內容的話,可以到我的 github 上看喔,讓我們開始吧!
前一篇說到遊戲開發的契機以及介紹了開發工具與官方提供的遊戲模板,在這一篇文章中,會開始進行遊戲的設計與製作,目標是上架到 Google Play 與 App Store 平台上,讓我們開始吧!
前言 當今時代,遊戲已經不再只是單純的娛樂,更成為人們生活中不可或缺的一部分。隨著技術的發展,遊戲也從單機遊戲轉變成網路遊戲、手機遊戲、虛擬實境(VR)遊戲等多種形式。Flutter 作為一種跨平台的開發框架,正逐漸成為開發遊戲的一個重要工具。本系列文章將以「輕鬆入門 Flutter 遊戲開發」為題
這篇文章介紹了網站的整體架構以及開發時所使用的工具和套件,包括 Next.js、Tailwind CSS 和 socket.io 等。文章回顧了程式碼的重構與優化,幫助開發者提高工作效率,適合希望深入瞭解前端開發和網站架構的讀者。
24點數學遊戲是一款適合小朋友與想動動腦的朋友們的小遊戲,遊戲規則簡單易懂,可訓練邏輯能力。遊戲分為單人與多人模式,可以讓玩家自行挑戰高分或是與其他玩家競爭。算式中不同的數學符號會對應不同的加分機制。遊戲網站連結與專案 repo 也都提供在文章中。
由於最近比較忙碌停更了好一陣子,關於 Flutter 系列目前難產中XD,在開發 Flutter 遊戲之前,我正在進行一個桌遊專案的開發,因為我很喜歡玩桌遊,如果在外面無聊的時候打開手機就可以與朋友一同連線遊玩那該有多好(雖然現在手遊很多),所以我就開始了這個專案,目標是上架各式各樣的桌遊.....
前一篇我們介紹了遊戲模板的基本架構,在這篇文章中會記錄目前的開發進度,主要先把遊戲核心邏輯寫完,再慢慢完成其他功能,在這系列的文章中,我不會太聚焦於寫程式的部分,如果想看程式內容的話,可以到我的 github 上看喔,讓我們開始吧!
前一篇說到遊戲開發的契機以及介紹了開發工具與官方提供的遊戲模板,在這一篇文章中,會開始進行遊戲的設計與製作,目標是上架到 Google Play 與 App Store 平台上,讓我們開始吧!
前言 當今時代,遊戲已經不再只是單純的娛樂,更成為人們生活中不可或缺的一部分。隨著技術的發展,遊戲也從單機遊戲轉變成網路遊戲、手機遊戲、虛擬實境(VR)遊戲等多種形式。Flutter 作為一種跨平台的開發框架,正逐漸成為開發遊戲的一個重要工具。本系列文章將以「輕鬆入門 Flutter 遊戲開發」為題
你可能也想看
Google News 追蹤
Thumbnail
Hi 我是 VK~ 在 8 月底寫完〈探索 AI 時代的知識革命:NotebookLM 如何顛覆學習和創作流程?〉後,有機會在 INSIDE POSSIBE 分享兩次「和 NotebookLM 協作如何改變我學習和創作」的主題,剛好最近也有在許多地方聊到關於 NotebookLM 等 AI 工具
Thumbnail
這是張老師的第三本書,我想前二本應該也有很多朋友們都有讀過,我想絕對是受益良多,而這次在書名上就直接點出,著重在從投資的角度來切入
Thumbnail
這篇文章介紹了網站的整體架構以及開發時所使用的工具和套件,包括 Next.js、Tailwind CSS 和 socket.io 等。文章回顧了程式碼的重構與優化,幫助開發者提高工作效率,適合希望深入瞭解前端開發和網站架構的讀者。
Thumbnail
這篇內容,將會講解什麼是函式,以及與函式相關的知識。包括函式的簡介、Runtime Function、自訂函式、Script Function 腳本函式、Method 方法。
Thumbnail
介紹一個優質的遊戲知識部落格:遊戲設計中藥鋪,其中「Game Design 資源分享表」十分推薦遊戲開發者閱讀。另外提到Gamker攻壳是一個專業的遊戲評鑑頻道,其深入的評論幫助作者入坑《健身環大冒險》。作者在後記也分享了自己在遊戲開發上的經歷和挑戰。
Thumbnail
這篇內容,將透過實戰教學,來講解「滑鼠點方塊」的程式碼。包括如何測試遊戲、座標系統、自訂參數和內建參數、if else、and、遊戲的邏輯設計、程式碼解析。
Thumbnail
這篇內容,將簡單介紹Asset Browser、Workspace、Inspector、Code Browser,作為入門的介面導覽。
Thumbnail
24點數學遊戲是一款適合小朋友與想動動腦的朋友們的小遊戲,遊戲規則簡單易懂,可訓練邏輯能力。遊戲分為單人與多人模式,可以讓玩家自行挑戰高分或是與其他玩家競爭。算式中不同的數學符號會對應不同的加分機制。遊戲網站連結與專案 repo 也都提供在文章中。
Thumbnail
一、了解思維 二、大量體驗 三、拆解架構 四、找出樂趣
Thumbnail
建立幾個變數如下,最上面兩個變數值為清單值 接下來分別設定球1位置到左上角落、設定球2位置到右上角落、設定球3位置到左下角落、設定球4位置到右下角落 當螢幕初始化的時候,設定玩家球的X、Y座標和大小,並將玩家球的初始顏色,設定成(變數_顏色清單)中.....
Thumbnail
Hi 我是 VK~ 在 8 月底寫完〈探索 AI 時代的知識革命:NotebookLM 如何顛覆學習和創作流程?〉後,有機會在 INSIDE POSSIBE 分享兩次「和 NotebookLM 協作如何改變我學習和創作」的主題,剛好最近也有在許多地方聊到關於 NotebookLM 等 AI 工具
Thumbnail
這是張老師的第三本書,我想前二本應該也有很多朋友們都有讀過,我想絕對是受益良多,而這次在書名上就直接點出,著重在從投資的角度來切入
Thumbnail
這篇文章介紹了網站的整體架構以及開發時所使用的工具和套件,包括 Next.js、Tailwind CSS 和 socket.io 等。文章回顧了程式碼的重構與優化,幫助開發者提高工作效率,適合希望深入瞭解前端開發和網站架構的讀者。
Thumbnail
這篇內容,將會講解什麼是函式,以及與函式相關的知識。包括函式的簡介、Runtime Function、自訂函式、Script Function 腳本函式、Method 方法。
Thumbnail
介紹一個優質的遊戲知識部落格:遊戲設計中藥鋪,其中「Game Design 資源分享表」十分推薦遊戲開發者閱讀。另外提到Gamker攻壳是一個專業的遊戲評鑑頻道,其深入的評論幫助作者入坑《健身環大冒險》。作者在後記也分享了自己在遊戲開發上的經歷和挑戰。
Thumbnail
這篇內容,將透過實戰教學,來講解「滑鼠點方塊」的程式碼。包括如何測試遊戲、座標系統、自訂參數和內建參數、if else、and、遊戲的邏輯設計、程式碼解析。
Thumbnail
這篇內容,將簡單介紹Asset Browser、Workspace、Inspector、Code Browser,作為入門的介面導覽。
Thumbnail
24點數學遊戲是一款適合小朋友與想動動腦的朋友們的小遊戲,遊戲規則簡單易懂,可訓練邏輯能力。遊戲分為單人與多人模式,可以讓玩家自行挑戰高分或是與其他玩家競爭。算式中不同的數學符號會對應不同的加分機制。遊戲網站連結與專案 repo 也都提供在文章中。
Thumbnail
一、了解思維 二、大量體驗 三、拆解架構 四、找出樂趣
Thumbnail
建立幾個變數如下,最上面兩個變數值為清單值 接下來分別設定球1位置到左上角落、設定球2位置到右上角落、設定球3位置到左下角落、設定球4位置到右下角落 當螢幕初始化的時候,設定玩家球的X、Y座標和大小,並將玩家球的初始顏色,設定成(變數_顏色清單)中.....