iT邦幫忙

2022 iThome 鐵人賽

DAY 18
0
Modern Web

派對動物嗨起來!系列 第 18

D18 - 玩家一起粗乃玩!

  • 分享至 

  • xImage
  •  

本系列文已改編成書「甚麼?網頁也可以做派對遊戲?使用 Vue 和 babylon.js 打造 3D 派對遊戲吧!」

書中不只重構了程式架構、改善了介面設計,還新增了 2 個新遊戲呦!ˋ( ° ▽、° )

新遊戲分別使用了陀螺儀與震動回饋,趕快買書來研究研究吧!ლ(╹∀╹ლ)

在此感謝深智數位的協助,歡迎大家前往購書,鱈魚感謝大家 (。・∀・)。

助教:「所以到底差在哪啊?沒圖沒真相,被你坑了都不知道。(´。_。`)」

鱈魚:「你對我是不是有甚麼很深的偏見啊 (っ °Д °;)っ,來人啊,上連結!」

Yes


現在我們可以讓玩家加入房間,也可以傳輸玩家的搖桿資料了,現在我們把遊戲大廳內的玩家頭像對應實際上的玩家吧!( •̀ ω •́ )✧

必須在玩家數量發生變更時,發送玩家數量更新事件,前往伺服器專案,追加「玩家加入房間」與「玩家斷線」對應功能。

首先新增事件定義。

types\socket.type.ts

...
export interface EmitEvents {
  ...
  'game-console:player-update': (data: Player[]) => void;
}
...

並在 room.service 新增 emitPlayerUpdate 功能,用於發送發送指定房間之玩家數量。

...
@Injectable()
export class RoomService {
  ...
  /** 發送指定房間之玩家數量*/
  async emitPlayerUpdate(
    founderId: string,
    server: Server<OnEvents, EmitEvents>,
  ) {
    const room = this.getRoom({
      founderId,
    });
    if (!room) return;

    // 取得 game-console Client 資料
    const founderClient = this.wsClientService.getClient({
      clientId: founderId,
    });
    if (!founderClient) return;

    const players = room.playerIds.map((playerId) => ({
      clientId: playerId,
    }));

    // 對特定 socket 發送資料
    const gameConsoleSocket = server.sockets.sockets.get(
      founderClient.socketId,
    );
    gameConsoleSocket?.emit('game-console:player-update', players);
  }
}

並在 room.gateway 處理事件。

src\room\room.gateway.ts

...
@WebSocketGateway()
export class RoomGateway {
  ...
  @WebSocketServer()
  private server!: Server<OnEvents, EmitEvents>;
  ...
  handleDisconnect(socket: ClientSocket) {
    ...
    if (client.type === 'player') {
      // 取得此玩家所處房間
      const room = this.roomService.getRoom({
        playerId: client.id,
      });

      // 刪除玩家
      this.roomService.deletePlayer(client.id);

      // 若房間存在則發送玩家資料更新
      if (room) {
        this.roomService.emitPlayerUpdate(room.founderId, this.server);
      }
      return;
    }
  }

  @SubscribeMessage<keyof OnEvents>('player:join-room')
  async handlePlayerJoinRoom(socket: ClientSocket, roomId: string) {
    ...

    // 發送玩家資料更新
    this.roomService.emitPlayerUpdate(room.founderId, this.server);

    const result: SocketResponse<Room> = {
      status: 'suc',
      message: '成功加入房間',
      data: room,
    };
    return result;
  }
}

現在伺服器會在房間人數發生變更時,自動推送訊息至遊戲機網頁。

差點忘記還需要加入轉送搖桿資料發送的功能惹。(´,,•ω•,,)

將網頁專案的 player.type 檔案移過來伺服器專案。

types\player.type.ts

import { Socket } from 'socket.io';
import { Room } from 'src/room/room.service';
import {
  GameConsoleState,
  Player,
  UpdateGameConsoleState,
} from 'src/game-console/game-console.type';
import { GamepadData } from './player.type';

export interface OnEvents {
  'player:join-room': (data: Room) => void;
  'player:request-game-console-state': () => void;
  'player:gamepad-data': (data: GamepadData) => void;

  'game-console:state-update': (data: UpdateGameConsoleState) => void;
}

export interface EmitEvents {
  'game-console:room-created': (data: Room) => void;
  'game-console:state-update': (data: GameConsoleState) => void;
  'game-console:player-update': (data: Player[]) => void;

  'player:gamepad-data': (data: GamepadData) => void;
}

export type ClientSocket = Socket<OnEvents, EmitEvents>;

export interface SocketResponse<T = undefined> {
  status: 'err' | 'suc';
  message: string;
  data?: T;
  error?: any;
}

並在 socket.type 新增事件。

types\socket.type.ts

...
export interface OnEvents {
  ...
  'player:gamepad-data': (data: GamepadData) => void;
  ...
}

export interface EmitEvents {
  ...
  'player:gamepad-data': (data: GamepadData) => void;
}
...

最後在 game-console.gateway 處理事件。

src\game-console\game-console.gateway.ts

...
@WebSocketGateway()
export class GameConsoleGateway {
  ...
  @SubscribeMessage<keyof OnEvents>('player:gamepad-data')
  async handlePlayerGamepadData(socket: ClientSocket, data: GamepadData) {
    const client = this.wsClientService.getClient({
      socketId: socket.id,
    });
    if (!client) {
      const result: SocketResponse = {
        status: 'err',
        message: '此 socket 不存在 client',
      };
      return result;
    }

    const room = this.roomService.getRoom({
      playerId: client.id,
    });
    if (!room) {
      const result: SocketResponse = {
        status: 'err',
        message: 'client 未加入任何房間',
      };
      return result;
    }

    const founderClient = this.wsClientService.getClient({
      clientId: room.founderId,
    });
    if (!founderClient) {
      const result: SocketResponse = {
        status: 'err',
        message: '此 socket 不存在 client',
      };
      return result;
    }

    const targetSocket = this.server.sockets.sockets.get(
      founderClient.socketId,
    );
    if (!targetSocket) {
      const result: SocketResponse = {
        status: 'err',
        message: '不存在 room founder 對應之 Client',
      };
      return result;
    }

    targetSocket.emit('player:gamepad-data', data);

    const result: SocketResponse = {
      status: 'suc',
      message: '傳輸搖桿資料成功',
    };
    return result;
  }
}

現在伺服器具備了「主動通知玩家人數變化」與「轉傳搖桿控制訊號」的功能了!

監聽玩家變化事件

讓我們回到網頁專案,並在 use-client-game-console 加入玩家數量變化事件的 hook 吧!( •̀ ω •́ )✧

src\composables\use-client-game-console.ts

...
export function useClientGameConsole() {
  ...
  const playerUpdateHook = createEventHook<Player[]>();
  client?.value?.on('game-console:player-update', playerUpdateHook.trigger);
  onBeforeUnmount(() => {
    client?.value?.removeListener('game-console:player-update', playerUpdateHook.trigger);
  });

  return {
    ...
    /** 玩家人數發生變更,例如玩家加入或斷線等等 */
    onPlayerUpdate: playerUpdateHook.on,
    ...
  }
}

接著在 game-console 中使用 hook,更新玩家清單狀態。

src\views\game-console.vue

...
<script setup lang="ts">
...
function init() {
  ...
  gameConsole.onPlayerUpdate((players) => {
    gameConsoleStore.updateState({
      players,
    });
  });

  // 跳轉至遊戲大通
  ...
}
init();
</script>

這樣遊戲機網頁就可以在玩家加入或斷線時,變更玩家數量了。

產生玩家頭像

現在讓我們調整 game-console-lobby 中的 player-avatar 內容。

我們讓 player-avatar 改成由 v-for 動態產生,新增 playersInfo 負責產生玩家資料。

src\views\game-console-lobby.vue

<template>
  ...
  <div class="absolute inset-0 flex">
    <div class="flex w-full h-full">
      <!-- 選單 -->
      <div class="w-1/3 flex flex-col p-12">
        ...

        <!-- 玩家清單 -->
        <transition-group
          name="list"
          tag="div"
          class="flex justify-center items-center gap-4 h-32"
        >
          <player-avatar
            v-for="player in playersInfo"
            :key="player.id"
            :player-id="player.id"
            :code-name="player.codeName"
          />
        </transition-group>
      </div>
      ...
    </div>
  </div>
</template>

<script setup lang="ts">
...
const gameConsoleStore = useGameConsoleStore();

const playersInfo = computed(() => {
  const result = gameConsoleStore.players.map((player, i) => ({
    id: player.clientId,
    codeName: `${i + 1}P`,
  }));

  return result;
});
...
</script>
...

並新增 transition-group 用的動畫 class

src\style\animate.sass

...

.list-move, .list-enter-active, .list-leave-active 
  transition: all 0.5s ease
.list-enter-from, .list-leave-to 
  opacity: 0
.list-leave-active 
  position: absolute

現在讓我們試試看加入遊戲並讓搖桿網頁斷線看看。

ezgif-3-fc176dd9ff.gif

玩家出現了!✧*。٩(ˊᗜˋ*)و✧*。

發送搖桿訊號

現在讓搖桿發出訊號並於遊戲大廳監聽搖桿資料看看。

首先是搖桿發出訊號,概念很簡單,就是接收控制組件 @trigger 事件即可。

src\views\player-gamepad-lobby.vue

<template>
  <div .. >
    <gamepad-d-pad
      ...
      @trigger="({ keyName, status }) => handleBtnTrigger(keyName, status)"
    />
    <gamepad-btn
      ...
      @trigger="(status) => handleBtnTrigger('confirm', status)"
    />
    ...
  </div>
</template>

<script setup lang="ts">
import { KeyName } from '../types/player.type';
...
function handleBtnTrigger(keyName: `${KeyName}`, status: boolean) {
  console.log(`[ handleBtnTrigger ] : `, { keyName, status });

  player.emitGamepadData([{
    name: keyName,
    value: status,
  }]);
}
...
</script>
...

最後在 game-console-lobby 中接收訊號吧。

src\views\game-console-lobby.vue

...
<script setup lang="ts">
...
function init() {
  ...
  gameConsole.onGamepadData((data) => {
    console.log(`[ onGamepadData ] data : `, data);
  });
}
init();
</script>
...

現在來按按看搖桿上的按鍵。

ezgif-3-ab80968d60.gif

可以看到遊戲機網頁成功接收到玩家搖桿網頁的控制訊號了!♪( ◜ω◝و(و

總結

  • 完成伺服器自動推送玩家數量變更資料功能
  • 完成伺服器轉傳搖桿網頁控制資料功能
  • 完成搖桿網頁傳輸控制資料至遊戲機網頁功能

以上程式碼已同步至 GitLab,大家可以前往下載:

Web

GitLab - D18

Server

GitLab - D18


上一篇
D17 - 所以我說那個搖桿呢?
下一篇
D19 - 是誰偷按確定?
系列文
派對動物嗨起來!30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言