2022年08月26日

新人がリアルタイムオセロゲームを実装してみた 前編

はじめに

V-CUBEのAgora技術チームの若手メンバーで作った、リアルタイムに盛り上がることのできるオセロゲームの制作過程を大公開します!

AgoraとReact.jsでWebアプリ(オセロゲーム)を実装しました。

今回はAgora Video SDKをオセロゲームに実装するところまでの記事になります。

資料ダウンロード  【すぐ読める!ガイドブック】ビデオ通話・ライブ配信SDK「Agora」  ビデオ通話やライブ配信の機能をあらゆるアプリケーションに簡単に実装できるSDKの特徴から活用例まで徹底解説! 無料ダウンロード

アプリの概要

対局者映像と盤面の状況を見ながら、勝敗予想がリアルタイムで出来たら面白そう!

ということで

 

対局者:二人で対局

視聴者:盤面をリアルタイムで見ながら勝敗予想できる

 

こんな仕様のWebアプリを作っていきます。

具体的な機能一覧はこんな感じ!

 

対局者機能:
・オセロ対戦
・映像/音声の送受信
・映像/音声ミュート
・チャット(視聴者とも交流可能)
・集中モード(自分の画面にはオセロ盤面のみ表示)

 

視聴者機能:
・オセロ観戦
・オセロ勝敗予想
・チャット(視聴者とも交流可能)

ゲームイメージ概要

まず開発前にどんなゲームを作りたいか、イメージ図を作成しました!

ゲームフローイメージについては以下のようになっています!

出場者と観客それぞれが単一のページでアプリケーションを構成するSPA(シングルページアプリケーション)を作成していこうと思います!

Othello-1_01

入室画面で出場するか、観戦するかを選ぶことができます。
出場者になる場合、プレイヤー名を入力して対戦相手と観客に表示できるようにします。

 

Othello-1_02

出場者で入るとお互いの映像が両側に表示され、対局スタートです。
ミュート機能・チャット機能・集中モード等が使える画面のイメージです。

 

Othello-1_03

観戦者になった場合、対局者映像と盤面がリアルタイムで見られるようになっています。観戦しながらチャット機能、勝敗予想機能を使えるようなイメージです。

 

Othello-1_04

ゲームイメージはひとまず上のように決まったので、開発環境を用意していざ実装!!

開発環境

Windows10 pro 64bit

Google Chrome

Visual Studio Code

GitHub

Agora Video SDK for Web 4.12.1
Agora RTM SDK for Web 1.4.5

実装

GitHub

実装概要

今回のwebアプリは、React(8.5.5)を用いたSPAとなっています。

SPAの実装にはreact-router-domを使用しました。

各要素を配置

まずやった事として、我々がまだReactにあまり慣れていないこともあった為、「スタート画面」「タイトル部分」「プレイヤーの映像枠」「映像出力用(後述)」「チャット欄」「オセロ盤面」「フッター」等の各要素をコンポーネントに分け、必要なファイルを作成した後に各要素の配置を決めました。

 

↑ほぼイメージ概要のイラストと同じ配置で作成
↑ほぼイメージ概要のイラストと同じ配置で作成

AgoraRTCの導入

ここから、いよいよ本題です。

 

プレイヤー同士の映像、音声の送受信の為にAgoraRTCをinstallします。

npm install agora-rtc-sdk-ng

また、Agoraの記述にはtypeScriptを用いるので、tsが使えるようにします。

tsconfig.json

{
  "compilerOptions": {
    "target": "es2016",
    "jsx": "react-jsx",
    "module": "esnext",
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true,
    "strict": true,
    "skipLibCheck": true,
    "lib": [
      "dom",
      "dom.iterable",
      "esnext"
    ],
    "allowJs": true,
    "allowSyntheticDefaultImports": true,
    "noFallthroughCasesInSwitch": true,
    "moduleResolution": "node",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "noEmit": true
  },
  "include": [
    "src"
  ]
}

Agoraの記述はuseAgora.tsxにまとめます。
また、映像を出力する為のコンポーネントはMediaPlayer.tsxです。

参考にしたAgoraサンプル

useAgora.tsx

import React, { useState, useEffect } from 'react';
import AgoraRTC, {
  IAgoraRTCClient, IAgoraRTCRemoteUser, MicrophoneAudioTrackInitConfig, CameraVideoTrackInitConfig, IMicrophoneAudioTrack, ICameraVideoTrack, ILocalVideoTrack, ILocalAudioTrack } from 'agora-rtc-sdk-ng';

const appId = "your appId";
const channel = "ChannelName";

function useAgora(client: IAgoraRTCClient | undefined)
  :
   {
      localAudioTrack: ILocalAudioTrack | undefined,
      localVideoTrack: ILocalVideoTrack | undefined,
      joinState: boolean,
      leave: Function,
      join: Function,
      remoteUsers: IAgoraRTCRemoteUser[],
    }
    {
  const [localVideoTrack, setLocalVideoTrack] = useState(undefined);
  const [localAudioTrack, setLocalAudioTrack] = useState(undefined);

  const [joinState, setJoinState] = useState(false);

  const [remoteUsers, setRemoteUsers] = useState<iagorartcremoteuser[]>([]);

  async function createLocalTracks(audioConfig?: MicrophoneAudioTrackInitConfig, videoConfig?: CameraVideoTrackInitConfig)
  : Promise<[IMicrophoneAudioTrack, ICameraVideoTrack]> {
    const [microphoneTrack, cameraTrack] = await AgoraRTC.createMicrophoneAndCameraTracks(audioConfig, videoConfig);
    setLocalAudioTrack(microphoneTrack);
    setLocalVideoTrack(cameraTrack);
    return [microphoneTrack, cameraTrack];
  }

  async function join(){
    if (!client) return;
    const [microphoneTrack, cameraTrack] = await createLocalTracks();
   
    await client.join(appId, channel, null);
    await client.publish([microphoneTrack, cameraTrack]);

    (window as any).client = client;
    (window as any).videoTrack = cameraTrack;

    setJoinState(true);
  }

  async function leave() {
    // window.confirm("投了して退出します");
    if (localAudioTrack) {
      localAudioTrack.stop();
      localAudioTrack.close();
    }
    if (localVideoTrack) {
      localVideoTrack.stop();
      localVideoTrack.close();
    }
    setRemoteUsers([]);
    setJoinState(false);
    await client?.leave();
  }

  useEffect(() => {
    if (!client) return;
    setRemoteUsers(client.remoteUsers);

    const handleUserPublished = async (user: IAgoraRTCRemoteUser, mediaType: 'audio' | 'video') => {
      await client.subscribe(user, mediaType);
      // toggle rerender while state of remoteUsers changed.
      setRemoteUsers(remoteUsers => Array.from(client.remoteUsers));
    }
    const handleUserUnpublished = (user: IAgoraRTCRemoteUser) => {
      setRemoteUsers(remoteUsers => Array.from(client.remoteUsers));
    }
    const handleUserJoined = (user: IAgoraRTCRemoteUser) => {
      setRemoteUsers(remoteUsers => Array.from(client.remoteUsers));
    }
    const handleUserLeft = (user: IAgoraRTCRemoteUser) => {
      setRemoteUsers(remoteUsers => Array.from(client.remoteUsers));
    }
    client.on('user-published', handleUserPublished);
    client.on('user-unpublished', handleUserUnpublished);
    client.on('user-joined', handleUserJoined);
    client.on('user-left', handleUserLeft);

    return () => {
      client.off('user-published', handleUserPublished);
      client.off('user-unpublished', handleUserUnpublished);
      client.off('user-joined', handleUserJoined);
      client.off('user-left', handleUserLeft);
    };
  }, [client]);

  return {
    localAudioTrack,
    localVideoTrack,
    joinState,
    leave,
    join,
    remoteUsers,
  };
}

export default useAgora;

MediaPlayer.tsx

import { ILocalVideoTrack, IRemoteVideoTrack, ILocalAudioTrack, IRemoteAudioTrack } from "agora-rtc-sdk-ng";
import React, { useRef, useEffect } from "react";

export interface VideoPlayerProps {
  videoTrack: ILocalVideoTrack | IRemoteVideoTrack | undefined;
  audioTrack: ILocalAudioTrack | IRemoteAudioTrack | undefined;
}

const MediaPlayer = (props: any) => {
  const container = useRef<HTMLDivElement>(null);
  useEffect(() => {
    if (!container.current) return;
    if(props.who == "localUser"){
      props.videoTrack?.play("left-video");
    }else if(props.who == "remoteUser"){
      props.videoTrack?.play("right-video");
      if(props.name) document.getElementById('right-name')!.innerHTML = props.name.substring(6);
    }
    return () => {
      props.videoTrack?.stop();
    };
  }, [container, props.videoTrack]);
  useEffect(() => {
    if(props.audioTrack){
      props.audioTrack?.play();
    }
    return () => {
      props.audioTrack?.stop();
    };
  }, [props.audioTrack]);

  // document.getElementById("left-video")?.innerHTML = '<div ref={container}  className="video-player"}></div>'
  return (
    <div ref={container}  className="video-player" style=></div>
  );
}

export default MediaPlayer;

後は、各コンポーネントから必要に応じてこれらをimportして使用します。

これで映像音声の送受信は完了です。

左上の枠に自分の映像、右上の枠に相手の映像を出力します。

↑ほぼイメージ概要のイラストと同じ配置で作成

Agoraの利用用途

Agora Video SDK for Web : 映像音声の送受信、映像音声のミュート・ミュート解除(今回はこちらの実装記事になります。)

Agora RTM (Real-time Messaging) SDK for Web : ゲーム参加者数の取得、ゲーム開始情報の送受信

参照

Agora.io Developer Center : Agora Web SDK API Reference

Agora.io Developer Center : RTM API Reference
Bootstrap

まとめ

今回はReactを用いたwebアプリにAgora Video SDK for Webを導入するまでを行いました。

後編では、ここからオセロとして二人で対戦できるようにしていきます。

また、チャット機能の実装に伴いAgora RTM (Real-time Messaging) SDK for Webも導入します。

 

 

※『オセロ』は株式会社オセロが保有する登録商標です。

資料ダウンロード  【一気に読める!】Agora 導入事例集  オンラインフィットネス・カウンセリング、音声サービスやファンサービスなど、バラエティに富んだ導入事例をご紹介! 無料ダウンロード
菊名 良一&礒部 洸生

執筆者菊名 良一&礒部 洸生

株式会社ブイキューブ 技術本部
Agoraの技術サポートを担当。

関連記事

先頭へ戻る