Help us understand the problem. What is going on with this article?

【JavaScript】Photon + PlayCanvasを使ってモバイル・デスクトップで動く一人称視点のマルチプレイができる空間を作る【WebGL】

PhotonのJavaScript SDKとPlayCanvasを組み合わせて利用するためのリポジトリです。
大体30分ほどで、ブラウザー上で動く、リアルタイムで位置と回転が同期されているゲームを作ることができます。PlayCanvasとネットワークエンジン PhotonのJavaScript SDKを使用してリアルタイム通信のできる空間を作っていきます。

今回作るもの

前回の記事【JavaScript】 PlayCanvasの公式サンプルを使ってモバイル・デスクトップで動く一人称視点の空間を作る【WebGL】
の続きとして、リアルタイムで動ける3Dのゲームを作ります。

※ こちらは一つのURLを3画面で開いたものを録画したものです。
今回の完成品となるプロジェクトはこちらです
https://playcanv.as/p/f6yr7YSW/

それでは早速作っていきます

準備するもの

準備 アカウント作成 (PlayCanvasとPhotonアカウントの作成)

PlayCanvasについて

PlayCanvas Editorを使用して開発をしていきますので、
今回のプロジェクトを実装する上で必要なPlayCanvasアカウントについてはこちらから作成ください。
https://playcanvas.com/

Photonについて

マルチプレイを簡単に実現!世界No.1の独立系ネットワーキングエンジンかつマルチプレイヤープラットフォーム— 迅速で信頼性が高く、拡張可能です。インディーかプロかを問わず、あらゆる用途に対応します。
https://www.photonengine.com/ja/photon

Photonは、Unity多くで使われているようですが、PhotonのREALTIMEにはJavaScriptのSDKも存在しており、20CCU(同時接続)までのプロジェクトでは無料で使用できます。

準備 プロジェクトの準備

こちらの手順は前回のプロジェクトを作成してる方はスキップして頂いても構いません。

前回の記事
【JavaScript】 PlayCanvasの公式サンプルを使ってモバイル・デスクトップで動く一人称視点の空間を作る【WebGL】
の続きですが、こちらを作成されている方は、そのまま利用できます。
この記事で初めてPlayCanvasを触る方は、フォーク機能を使って前回までのプロジェクトをコピーしましょう。

プロジェクトをフォークする


こちらは前回作成したプロジェクトをのASSETSを一部フォルダを整理したプロジェクトです。
このプロジェクトをフォークして使います。

1. フォークするプロジェクトへアクセス

フォークするためのプロジェクトにアクセスをします。

https://playcanvas.com/project/696633/

2. Forkをクリックしてプロジェクトを複製する

3. フォークしたプロジェクトからEDITORへアクセス

4. シーンを選択

PlayCanvas エディタ

PlayCanvas エディタはゲームを構成するシーンやエンティティを作成および編集するために使用する視覚的な編集ツールです。
ブラウザー内で実行できるのでどこでも利用可能。
PlayCanvas エンジンを使ってシーンをレンダリングします。WYSIWYG。
ボタン一つでゲームを新規タブでプレイ。
ライブ編集によりゲーム実行時にもイテレート可能。
https://developer.playcanvas.com/ja/user-manual/designer/

起動

プロジェクトの作成はこれで完了しました。
起動すると、一人称視点で動き回れる事がわかります。
このプロジェクトからPhotonとPlayCanvasを組み合わせて作っていきましょう。

Photonの準備 プロジェクト作成 ~SDKの準備

1.管理画面から「新しくアプリを作成する」をクリック

Photonの管理画面にアクセスをします。

https://dashboard.photonengine.com/ja-JP/PublicCloud

2.Photon Realtimeを選択してプロジェクトを作成

3. アプリケーションIDのコピー

プロジェクトを作成することができました、こちらのアプリケーションIDを使用しますので保存をしておきましょう。

4. SDKをダウンロード

こちらからPhotonのJavaScript SDKをダウンロードします。

使用するのは、v4.1.0.1のバージョンのSDKを使用します。

SDKはZIP形式でダウンロードされますので解凍します。

5. SDKをPlayCanvasへドラッグアンドドロップ

解凍したPhotonのSDKをドラッグアンドドロップドロップで配置します

ファイルはphoton-javascript-sdk_v4-1-0-1/libディレクトリにある。Photon-Javascript_Emscripten_SDK.min.jsを使用します。

6. scriptsフォルダーへ移動

ディレクトリの構造によって動作は変わりませんが、管理しやすくするために、scriptsフォルダーにPhotonのSDKをドラッグアンドドロップで移動させましょう。

実装

PlayCanvasでPhotonのSDKを利用するためのスクリプトを書きます。
今回は、3つのファイルを作成します。

スクリプトファイルを3つ作成する

3つのファイルをエディタ上から保存します。

  1. photon-playcanvas.js
  2. photon-loadbalancing.js
  3. photon-playcanvas-connect.js

1. photon-playcanvas.jsを作成

a. ASSETSで右クリック→ New Asset → Script

b. ファイル名photon-playcanvas.jsを入力

2. 同じ手順でphoton-loadbalancing.js,photon-playcanvas-connect.jsを作成

先程と同様の手順でphoton-playcanvas.jsphoton-playcanvas-connect.jsを作成します。

この手順を終えたときにこの3つのファイルが新規に追加されていれば大丈夫です。

スクリプトファイルをコードエディターで編集する

作成した3つのスクリプトファイルをコードエディタから編集をします。

それぞれのコードについては、GitHub こちらのリポジトリにも公開しております。

コードエディタで編集する方法は、ASSETSの中にあるスクリプトファイルをダブルクリックすることでコードエディタを開くことができます。コードエディタを開くことができたらそれぞれのファイルにソースコードをコピーしましょう。

コードエディタについて

コードエディタについて、ショートカットキー詳しく知りたい方は公式ドキュメントが参考になります。

スクリプトアセットはPlayCanvasのコードエディタを使用して編集されます。エディタでファイルを開くにはスクリプトアセットをダブルクリックします。コード編集の権限を持つ全てのユーザはエディタでリアルタイムに共同編集することができます。コードエディタ画面の右下に他のユーザーのアバターが表示されます。
https://developer.playcanvas.com/ja/user-manual/scripting/code-editor/

それぞれのスクリプトをファイルにコピーしていきます。

こちらのコードになります。

1. photon-playcanvas.js

PlayCanvasのゲームが起動されたときにPhotonを利用するためのスクリプトになります。

photon-playcanvas.js
/*jshint esversion: 6, asi: true, laxbreak: true*/

class PhotonPlayCanvas extends pc.ScriptType {
    initialize () {
      const options = {
        app: this.app,
        appId: this.appId,
        appVersion: this.appVersion,
        wss: this.wss ? 1 : 0
      }

      // Photonのセットアップ
      this.app.photon = new LoadBalancing(options)

      // Connect to Photon Server
      this.app.photon.connectToRegionMaster(this.region)

      this.app.on('lobby:join', () => {
        if (this.app.photon.roomInfos.length === 0) {
          try {
            this.createRoom(this.roomName)
          } catch (e) {
            this.joinRoom(this.roomName)
          }
        } else {
          this.joinRoom(this.roomName)
        }
      })
    }

    createRoom (roomName) {
      this.app.photon.createRoom(roomName)
    }

    joinRoom (roomName) {
      this.app.photon.joinRoom(roomName)
    }
  }

  pc.registerScript(PhotonPlayCanvas)
  // LoadBalancing options
  PhotonPlayCanvas.attributes.add('appId', { type: 'string' })
  PhotonPlayCanvas.attributes.add('appVersion', {
    type: 'string',
    default: '1.0'
  })
  PhotonPlayCanvas.attributes.add('wss', { type: 'boolean', default: true })

  // Photon realtime options
  PhotonPlayCanvas.attributes.add('region', {
    type: 'string',
    default: 'jp',
    description:
      'Photon Cloud has servers in several regions, distributed across multiple hosting centers over the world.You can choose optimized region for you.',
    enum: [
      { 'Select Region': 'default' },
      { 'Asia, Singapore': 'asia' },
      { 'Australia, Melbourne': 'au' },
      { 'Chinese Mainland (See Instructions)    Shanghai': 'cn' },
      { 'Canada, East Montreal': 'cae' },
      { 'Europe, Amsterdam': 'eu' },
      { 'India, Chennai': 'in' },
      { 'Japan, Tokyo': 'jp' },
      { 'South America, Sao Paulo': 'sa' },
      { 'South Korea, Seoul': 'kr' },
      { 'USA, East Washington': 'us' },
      { 'USA, West San José': 'usw' }
    ]
  })

  // Room options
  PhotonPlayCanvas.attributes.add('roomName', { type: 'string', default: 'room' })

2. photon-loadbalancing.jsを作成

Photon SDK内のLoadBalancingClientを利用して、Photonを使用するためのスクリプトになります。

photonloadbalancing.js
class LoadBalancing extends Photon.LoadBalancing.LoadBalancingClient {
  constructor (props) {
    // Photon Settings
    const wss = props.wss
    const appId = props.appId
    const appVersion = props.appVersion
    super(wss, appId, appVersion)
    // pc.Application
    this.app = props.app
    this.setLogLevel(4)
  }

  onRoomList (e) {
    this.app.fire('lobby:join')
  }

  onJoinRoom (e) {
    this.app.fire('room:join')
  }

  onEvent (code, content, actorNr) {
    const payload = { code, content, actorNr }
    if (code === 0) {
      this.app.fire('sync:position', payload)
      return
    }
    if (code === 1) {
      this.app.fire('sync:rotation', payload)
    }
  }

  onActorJoin (e) {
    const payload = Object.assign({}, this.myRoomActors())
    this.app.fire('sync:players', payload)
  }

  onActorLeave (e) {
    const payload = Object.assign({}, this.myRoomActors())
    this.app.fire('sync:players', payload)
  }
}

3. photon-playcanvas-connect.jsを作成

Photonからイベントが発火された際にPlayCanvasに変更を加えるスクリプトになります。

photon-playcanvas-connect.js
/*jshint esversion: 6, asi: true, laxbreak: true*/
const EVENT_LIST = {
    POSITION: 0,
    ROTATION: 1,
    COMMAND: 2
  }
  class PhotonPlaycanvasConnect extends pc.ScriptType {
    raiseEvent (event, data) {
      // Photonを使用
      this.app.photon.raiseEvent(event, data)
    }
    // スクリプト読み込まれた際に一度実行される
    initialize () {
      this.isSync = false
      this.app.on('room:join', () => {
        this.isSync = true
        if (pc.platform.mobile) {
          //モバイル端末だった場合の処理

          this.app.touch.on(pc.EVENT_TOUCHMOVE, () => {
            // get current Player position & rotation
            const position = this.Player.getLocalPosition()
            const rotation = this.Player.findByName('Camera').getLocalRotation()

            this.raiseEvent(EVENT_LIST.ROTATION, Object.assign({}, rotation))
            this.raiseEvent(EVENT_LIST.POSITION, Object.assign({}, position))
          })
        } else {
          // KEYDOWNイベントが発火されたら同期をする
          this.app.keyboard.on(pc.EVENT_KEYDOWN, () => {
            // get current Player position & rotation
            const position = this.Player.getLocalPosition()

            this.raiseEvent(EVENT_LIST.POSITION, Object.assign({}, position))
          })
          // MOUSEMOVEイベントが発火されたら同期をする
          this.app.mouse.on(pc.EVENT_MOUSEMOVE, () => {
            const rotation = this.Player.findByName('Camera').getLocalRotation()
            this.raiseEvent(EVENT_LIST.ROTATION, Object.assign({}, rotation))
          })
        }
      })

      this.app.on('sync:players', actors => {
        const prefix = 'player_'
        const tagName = 'player'
        const actorNums = Object.values(actors).map(actor => actor.actorNr)
        // Playerを同期する
        for (let actor of Object.values(actors)) {
          const { isLocal, actorNr, name } = actor
          if (!isLocal) {
            const otherPlayer = this.PlayerTemplate.clone()
            otherPlayer.setName(`${prefix}${actorNr}`)
            otherPlayer.tags.add(tagName)
            otherPlayer.enabled = true
            this.app.root.addChild(otherPlayer)
            otherPlayer.setLocalPosition(0, 0, 0)
          }
        }
        // Playerのタグタグから現在いないPlayerを消す
        {
          const players = this.app.root.findByTag(tagName)
          for (let player of players) {
            const actorNr = player.name.replace(prefix, '')
            if (!actorNums.includes(Number(actorNr))) {
              player.destroy()
            }
          }
        }
      })

      this.app.on('sync:rotation', ({ content, actorNr }) => {
        const { y, w } = content
        const entity = this.app.root.findByName(`player_${actorNr}`)
        if (!entity) return

        entity.setLocalRotation(0, y, 0, w)
      })

      this.app.on('sync:position', ({ content, actorNr }) => {
        const { x, y, z } = content
        const entity = this.app.root.findByName(`player_${actorNr}`)
        if (!entity) return

        entity.setPosition(x, y + this.yOffset, z)
      })
    }
  }

  pc.registerScript(PhotonPlaycanvasConnect)

  // Room Option
  PhotonPlaycanvasConnect.attributes.add('roomName', {
    type: 'string',
    default: 'room'
  })

  // Sync option
  PhotonPlaycanvasConnect.attributes.add('Player', { type: 'entity' })
  PhotonPlaycanvasConnect.attributes.add('PlayerTemplate', { type: 'entity' })
  PhotonPlaycanvasConnect.attributes.add('yOffset', {
    type: 'number',
    default: -0.7
  })

  PhotonPlaycanvasConnect.attributes.add('reverse', {
    title: "逆向き",
    type: 'boolean',
    default: true
  })


設置

スクリプトをPlayCanvas Editorを使って適用していきます。

スクリプトの読み込み順を変える

PlayCanvasではSCRIPTを読み込む順番をエディタから制御します。
今回PhotonのSDKを使用しているのですが、SDKが読み込まれる前に、PhotonのAPIを使用するとエラーが出てしまいますので、スクリプトの読み込み順序を変更していきます。

1. 設定ボタンをクリック

2. ASSET LOAD TASKを変更する

スクリプトの読み込み順序を、PhotonのSDK → photon-loadbalancing → その他の順に変更します。

変更前

変更後

スクリプトの追加

1. Rootにスクリプトコンポーネントを追加

2. ADD SCRIPTSからphoton-playcanvas, photon-playcanvas-connectを追加

a. パースボタンをクリック

追加ができたらパースボタンをクリックします。
スクリプトのパースが行われると、roomNamePlayerなどの属性を追加できる要素が追加されます。

!マークなどが出た際には、パースボタンを押してから一度、パースボタンの横からスクリプトを削除してから、もう一度、ADD SCRIPTを押してください。

3. PhotonのアプリケーションIDを追加する

a. アプリケーションIDを取得

こちらのページからPhotonのアプリケーションIDを取得します。

b. 属性にアプリケーションIDをペースト

https://dashboard.photonengine.com/ja-JP/PublicCloud

これでPhotonとPlayCanvasのつなぎこみの設定は終わりました。
次にプレイヤーを追加していきます。

その他の属性の設定

1. Playerを選択して追加

a. PhotonPlaycanvasConnectスクリプトのPlayer属性をクリック

b. ヒエラルキーからPlayerエンティティを選択

スクリプト属性について

これで、スクリプトのPlayer属性にPlayerエンティティが追加されました。このような形でPlayCanvasでは、ソースコードを変更せず値を変更したい場合などについてはスクリプト属性を使用して値を設定します。

スクリプト属性
スクリプトのアトリビュート機能は、スクリプト内で使用する変数をPlayCanvasエディタ内で編集することができるようにする便利な機能です。この機能を使うことで、一度コードを書いた後にエンティティごと作られるインスタンスにそれぞれ違うパラメータを設定する調整ができるようになります。これにより、アーティスト、デザイナーやその他のプログラマーではないチームメンバーがコードを書かずに値を変更できるにプロパティを露出させることができます。
https://developer.playcanvas.com/ja/user-manual//script-attributes/

自分以外を表示するためのEntityを追加

今回はFPS視点なので、自分のキャラクターは追加をしないで、他の人が入ってきたときにきつねのアバターで参加をしてもらうことにします。今ヒエラルキーを上にあるFoxを使用して、他の人のアバターを設定しましょう。

1. ヒエラルキーからエンティティを追加する

ヒエラルキーの+ボタンをクリックし、Entityを追加します。

2. エンティティの名前をPlayerTemplateという名前に変更します。

3. ヒエラルキー上のFoxPlayerTemplateヒエラルキーの配下に配置

ヒエラルキー上から、FoxエンティティをPlayerTemplateの配下にドラッグアンドドロップをして配置します。

4. Rootのスクリプトコンポーネントに適用する

5. PlayerTemplateスクリプトを非表示にする

このPlayerTemplateとしたエンティティは他のプレイヤーが入ってきた際にスクリプトから制御をして表示を切り替えますので、ページを起動した際には、非表示にしておきます。

ここまでできれば終わりです。あとは起動するだけでマルチプレイのゲームができているかと思います。

起動

これで起動をしてみます。

これでPlayCanvasで動くマルチプレイのできるゲームを作成することができました。

ルームの機能がありますので、ルーム名を変えたりすることで参加するユーザーを区切ることもできます。
本日使用したPhotonのドキュメントと、PlayCanvasのドキュメントはそれぞれこちらになります。

Photon LoadBalancingClient
https://doc-api.photonengine.com/en/javascript/current/Photon.LoadBalancing.LoadBalancingClient.html

PlayCanvas ユーザーマニュアル
https://developer.playcanvas.com/ja/user-manual/

補足

向きを変更する

逆向きになっていますのでヒエラルキーをからFoxを選択してyrotate180度に設定してあげるといい感じになると思います。

ゲームを公開する

PlayCanvasでは作成したゲームをウェブ上ですぐに公開できます。

1. 左のMEBUからPUBLISH/DOWNLOADをクリック

publish.png

2. PUBLISH TO PLAYCANVASから公開

oc.png

3. BUILDSを確認

PUBLISHが成功するとBUILDSに共有できるURLが生成されます。
こちらを共有することで、第三者に完成したプロジェクトを公開できます。

URL.png

今回の完成品となるプロジェクトはこちらになります。 

https://playcanv.as/p/f6yr7YSW/

今回のプロジェクトで質問や意見がありましたら。GitHubのIssueか@mxcn3まで連絡をお願いします。


PlayCanvas開発で参考になりそうな記事の一覧です。

PlayCanvasのユーザー会のSlackを作りました!

少しでも興味がありましたら、ユーザー同士で解決・PlayCanvasを推進するためのSlackを作りましたので、もしよろしければご参加ください!

yushimatenjin
インターネットに無限の可能性を感じています。
https://twitter.com/Mxcn3
playcanvas
"PlayCanvasは、ブラウザ向けに作られたWebGL/HTML5ゲームエンジンです。PlayCanvas運営事務局は日本国内でのPlayCanvasの普及を目的に活動しています"
https://playcanvas.jp
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした