本ワークショップにはPlayCanvasアカウントが必要です!

PlayCanvasアカウントをお持ちでない方はこちら↓

PlayCanvasアカウント
作成のお願い

  • 本ハンズオンにはPlayCanvasアカウントが必要です!
  • 円滑なハンズオン進行のため作成をお願いいたします。
  1. 以下リンクよりPlayCanvasサインアップページへアクセス
    PlayCanvas Singup
  2. Eメールとパスワードを設定
    Googleアカウント連携も可
  3. Fullname, UserName,
    Jobを設定
  4. 右の画面になったら完了
  5. 本ワークショップではスマホも使います! ログイン →

1時間で複数人参加型の

WebVRアプリ開発ハンズオン!

PlayCanvas × Photon

ソーシャルVRアプリを作ろう!

2017.06.14 PlayCanvas運営事務局 Photon運営事務局

Agenda

  • 自己紹介
  • オリエンテーション(10min)
  • ハンズオン(60min)
     
  • アンケートにご協力ください

※卓上のVRゴーグルはワークショップで使用します!

組み立ててお待ちください!

自己紹介

津田良太郎  portal

GMOクラウド 

PlayCanvas運営事務局,Photon運営事務局

デベロッパーエバンジェリスト

 

Webはほぼ未経験でしたが

最近はPlayCanvasでWebコンテンツを作ってます

オリエンテーション

  • PlayCanvasとは
    • PlayCanvasで作るWebVR
  • Photonとは
    • PlayCanvas × Photon
  • ソーシャルVRについて

What is PlayCanvas

エンジン + エディタ + コミュニティ 一体型の

クラウド型ゲーム開発プラットフォーム

オープンソース JavaScript
3Dゲームエンジン エクストリームプログラミング
HTML5 + WebGL リアルタイムマルチプレイ
クロスプラットフォーム ブラウザゲーム
リッチメディア広告 WebVR

PlayCanvas Engine

https://github.com/playcanvas/engine

・MIT License

・エンジン単体で使用も可能

・JavaScript 96%

・サイズ 

 

 

 

 

コード全文 8MB
ランタイムコード 2.43MB
gZip圧縮後 147KB

2016.12

PlayCanvas Editor

WebVRも搭載!

コンパイル・リロードなしで反映!

3Dビジュアルエディター/コードエディター

チーム開発

  • すべてWeb上で動作
  • デプロイもシームレスに
     
  • チーム内の開発が
    リアルタイムに反映!

    開発がライブ感を持って
    進みます

PlayCanvas Community

エクスプローラー

コミュニティ

PlayCanvas 国内ご採用事例

NEW!

Ponta Dozer

PlayCanvasで作るWebVR

What is Photon

ネットワークゲームを作るための機能を提供した

サービス&ミドルウェア

photon cloud(クラウド型) ロビー,ルーム,RPC
photon server(ミドルウェア型) WebRPC/Webhooks
Free Start マルチプラットフォーム対応
クライアント/サーバ通信 全世界対応

国内ご採用事例

Event・Session

協力・対戦ゲームを支えるPhotonの新リリース情報、最新事例の詳解 : GTMF2015

 

マルチプレイを実現するネットワークエンジンの決定版『Photon』の本音を語るディベロッパー座談会&最新情報 : CEDEC2015

 

Cocos2d-x/Marmalade&Photon最新事例 : GTMF2016

 

ネットワークエンジンの現在・過去・未来 ~Photonもあるよ!~ :CEDEC2016

Photonのしくみ

  • 全てのクライアント(=プレイヤー)はサーバーへ接続する
  • サーバーはクライアント間の通信をリレーする役割
  • サーバー自身は基本的に処理を行わないが、WebhooksやPluginを利用してカスタマイズも可能

Photon

SDK

SDK

SDK

Client

Client

Client

  • クラウド型サービス
  • サーバーはサービス提供元で運用
  • クライアントにSDKを組み込むだけで利用可能
  • ミドルウェア型サービス
  • ソフトウェアとしてご提供、お客様で運用
  • サーバーサイドのカスタマイズが自由に

Photon × HTML5

  • Photonは多くのプラットフォームに対応しています!


     
  • HTML5でPhotonを利用する場合はWebSocket通信(ws,wss)で各クライアント間をリレーします。
     
  • Webでの通信手段は他にもありますが(WebRTC等)、
    手軽に実装できるPhotonはPlayCanvasにもお勧めです!

PlayCanvas × Photon

ソーシャルVR

Facebook Spaces

  • VR(Virtual Reality)の方向性として
    人と人をつなぐソーシャルVRが今話題
     
  • second life, Altspace VR, Cluster Inc
    Facebook Spaces等
     
  • 今後のVRでのひとつの柱となるか

PlayCanvas × Photon
ソーシャルVRアプリを1時間で開発します

ということで

ハンズオン

複数人参加型のソーシャルVRアプリを開発します

  • VRビュー,ロコモーション(移動)
  • 座標・ヘッドトラッキングを複数人で同期
  • VR Starter Kitの基礎知識
  • モデルインポート
  • Photon for PlayCanvas
  • Photonでの同期
  • タッチインタフェースの実装
  • PlayCanvasでのデプロイ

学べる内容

Section.1

VR Starter Kitの基礎知識

1.新規プロジェクト作成

  • playcanvas.comにアクセスします
  • [PROJECT]を選択し、右上の[NEW +]をクリックします

2.新規プロジェクト作成

  • VR Starter Kitを選択
  • 名前を[socialVR]にして、[CREATE]を選択します

①VR Starter Kitを選択

③CREATE

②名前を

[socialVR]にする

3.新規プロジェクト作成

  • Projectができると以下のような画面になります。
  • [EDITOR]を選択してエディターを開きます。

4.PlayCanvasエディター

  • エディターを起動すると以下のような画面になります。
  • エディターの構成は以下のとおりになっています

1. シーン(SCENE) シーンビューには製作中のゲーム世界(シーン)が表示され、自由な位置・角度から眺めることができます。

4. アセット(ASSETS) 製作中のプロジェクト(ゲーム全体)に含まれるモデル、スクリプト、グラフィックやサウンドなどのデータ、その他のリソースがファイル単位で表示されます。

2. インスペクター(INSPECTOR) シーンの中で選択肢中のオブジェクトが持つ属性を表示・編集するためのビューです。 属性には座標やメッシュといった見た目上のものから、衝突判定や物理制御に関するパラメーターなどもあり、その他ユーザー定義のものもここに表示されます。

3. ヒエラルキー(HIERARCHY) シーン内に存在するオブジェクトの一覧が表示されます。 編集中のシーン内でオブジェクトをコピー/ペーストしたり、適切な名前をつけて整理することもできます。

5. メニュー(MENU) シーンのビューモードやプロジェクトセッティング等の作業が行えます。

5.とりあえず実行してみる

  • VR Starter Kitのデフォルトでの実装を確認してみましょう
  • シーンの右上[ ]を押して実行してみます

6.PCで実行

  • カメラのヨー(水平方向首振り)・ピッチ(垂直方向首振り)
  • 焦点オブジェクトの取得ができていることが確認できます

7.スマホで実行

  • URLをスマホに渡して実行します。
  • iOS
    • 実行すると①のような画面になります。
    • ②の手順で回転を許可して、横持ちで③の画面になります
  • Androidも回転を許可→横持ちで同じ挙動になります。
     
  • ヨー,ピッチに加えロール(回転)ができるようになります
  • カメラが動きに追従することが確認できたらOKです

Section.2

モデルインポート,
外部ライブラリ導入

8.ワークスペースの作成

  • アセットビュー /の下に 新規フォルダ を作成します。
  • ASSETS [+] -> Add Asset -> Folder
  • 名前を[workspace]に変更してください。

①ASSETS [+]

> Folder を新規作成

②名前をworkspaceに変更

9.ワークスペースの作成

  • workspaceの下に、フォルダを3つ作ります
  • 名前を[model],[photon],[src]に変更してください。
  • 以後ファイル類はこのフォルダに入れていきます。

10.モデル作成

  • 他のプレイヤーをあらわすゴーグルのモデルを作成します。
  • 以下のボクセルモデルをダウンロードしてください
  • ある程度ならオリジナリティをいれてもかまいません!

11.モデルインポート

  • ダウンロードしたobjファイルをPlayCanvasにアップロードします
  • /workspace/model/を選択し、ファイルをD&Dします

①/workspace/model/を選択

②objをドラック&ドロップ

12.モデルインポート

  • PlayCanvasにアップロードされたモデルはマテリアルが分割された後、jsonにコンバートされます。
  • PlayCanavsではモデルアセットをjsonで扱います

13.マテリアルの設定

  • マテリアルがデフォルトではグレーの設定になっているので、各マテリアルの色を正しく設定します。
  • インスペクターからEMISSIVE > Colorの値を名前の値と同様に設定します
  • 例)[255,128,64,1Mat] は EMISSIVE > Color(r:255,g:128,b:64)

①マテリアルを選択

②EMMISIVEを選択

③Colorを設定

14.モデルの確認

  • インポートしたモデルを確認します。
  • モデルアセットをシーンにドラック&ドロップします。
  • 初期値が見えづらいので、ポジションとスケールを調整します。
  • 正しく表示されたらモデルのインポート完了です

15.Photonの導入

16.Photonの導入

17.Photonの導入

  • ダウンロードしたzipファイルを展開し、
    /workspace/photon/に全てアップロードします

18.Photonの導入

  • app.jsを選択し、インスペクターから、[parse]を実行します。
  • メニューから[Settings]を選択し、インスペクターに表示された
    [SCRIPTS LOADING ORDER]から、スクリプトのロード順を変更します。
  • Photon-Javascript_SDK.js > demoloadbalancing.js > app.jsの順で並べます

①app.jsを選択

②インスペクターから

PARSEを実行

③Settingを選択

④インスペクターから

SCRIPT LOADING ORDERを選択、並び順を変更

Section.3

ソーシャルVRの実装

19.不必要なものの削除

  • シーンに存在する不必要なオブジェクトを削除して行きます。
  • ヒエラルキーから、[Item One],[Item Two],[Item Three],
    Camera Offset > Camera > Reticule を削除します。
  • 先ほど確認用で追加したモデルも削除しておきましょう。

ルームとカメラのみ!

空の状態になっていればOK!

20.Photon Controllerの追加

  • Photonを扱うためのオブジェクトを追加します。
  • ヒエラルキー [+] Add Entity > Entity を選択し、空のEntityを追加します
  • 名前を[PhotonController]に設定し、[+ADD COMPONENT]からScriptを追加します。その後[ADD SCRIPT]よりappを選択してアタッチします。

①Entityを新規作成

②名前を変更

[PhotonController]

③ADD COMPONENT

Scriptを追加

④ADD SCRIPT

appを追加

21.Photonの設定

  • インスペクターからappを編集し、Photonの設定を行います。
  • AppId : 9e85f5fe-b1d4-4d4d-b4d8-f74727e51847
  • Region : Japan, Tokyo
  • 開発中の混乱を防ぐため、Appversion : [ご自身のATND名]を入力します

①AppIDを設定

②AppVersionに

自分のATND名を入力

③Regionを
Japan, Tokyoに

22.Photonの動作確認

  • 実行するとPhotonのインタフェースが表示されます
  • PCとスマホでそれぞれ実行し、片側でNewGame, もう片方でJoinします
  • 画面左側に表示されているログからマッチングできていることを確認します

23.プレイヤーの実装

  • プレイヤーの制御をするスクリプトを作成します。
  • /workspace/srcを選択して開きます。
  • ヒエラルキーから Camera Offset > Cameraを選択し、
    インスペクターのSCRIPTS > ADD SCRIPTから、"player.js"を新規作成します

①/workspace/srcを選択

②Camera offset
> Cameraを選択

③ADD SCRIPT > New Scriptから
"player.js"を新規作成・追加

24.プレイヤーの実装

  • player.jsの右にある[edit       ]を選択してコードエディターを起動します
  • player.jsを以下のように書き換え、保存します。
  • selectorCamera, touchControllerは使用しないのでdisableにします
var Player = pc.createScript('player');
var istouch = false; //タッチしているかどうか

// initialize code called once per entity
Player.prototype.initialize = function() {
    this.entity.parent.setLocalPosition((Math.random() - 0.5) * 10,1.5,(Math.random() - 0.5) * 10); //初期化時に位置をランダムに設定
    if(this.app.touch){//もしタッチデバイスなら
        this.app.touch.on("touchstart",this._touchstart,this);//タッチした瞬間のイベントハンドラを設定
        this.app.touch.on("touchend",this._touchend,this);//タッチが離れた瞬間のイベントハンドラを設定
    }
};

// update code called every frame
Player.prototype.update = function(dt) {
    if(istouch){//タッチしていたら
        this.locomotion();//前方向に移動
    }
};

/*プレイヤー移動の関数*/
Player.prototype.locomotion = function(){
    var rot = playangleToRealangle(this.entity.getEulerAngles());//プレイヤーの向いている角度をY軸オイラー角のみ抽出
    if(rot){
        this.entity.parent.translate(-0.1 * Math.sin(rot / 180 * Math.PI),0,-0.1 * Math.cos(rot / 180 * Math.PI));//向いている方向に移動
    }
};

Player.prototype._touchstart = function(ob){//タッチした瞬間に呼ばれる
    istouch = true;
};

Player.prototype._touchend = function(ob){//タッチが離れた瞬間に呼ばれる
    istouch = false;
};

var playangleToRealangle = function(playangle){//PlayCanvasで得られるオイラー角のうちYが取りうる範囲を0-360に変換して返す関数
    //playangle is eulerAngles Vec3
    //vec3を度数法に変換する
    var ans;
    if(playangle.x < 100 && playangle.x > -100){
        //angle 0 - 90 , 270 - 360
        if(playangle.y >= 0 ){
            //angle 0 - 90
            ans = playangle.y;
        }else{
            //angle 270 - 360
            ans = 360 + playangle.y;
        }
    }
    if(playangle.x <= -100 && playangle.x >= -170){
       ans = 90 + (90 - playangle.y); 
    }
    
    if(playangle.x >= 100){
        //angle 90 -180
        ans = 180 - playangle.y;
    }
    if(playangle.x < -170){
        //angle 180 - 270
        ans = 180 - playangle.y;
    }
    
    return ans;
};

// swap method called for script hot-reloading
// inherit your script state here
// Player.prototype.swap = function(old) { };

// to learn more about script anatomy, please read:
// http://developer.playcanvas.com/en/user-manual/scripting/
  • スマホで実行すると、ランダムな位置に出現し、タッチで向いている方向に進むことが確認できると思います。

25.他プレイヤーの実装

  • 他プレイヤーが入室してきた際の処理を実装します。
  • まず、他プレイヤーを追加していく入れ物を用意します。
  • ヒエラルキーからAdd Entity > Entityで空のオブジェクトを作り、
    名前を[otherPlayerPool]にし、"playerpool.js"を新規作成・アタッチします。

①Entity作成

②名前を

[otherPlayerPool]に

③ADD COMPONENT

-> Script

④ADD SCRIPT

-> new [playerpool.js]

26.他プレイヤーの実装

  • 他プレイヤーのオブジェクトを作ります。
  • インポートしたモデルは原点が中心ではないので空のEntityの中にモデルを配置し調整します。
  • ADD Entity > Entity から空のEntityを追加し、名前を[otherPlayer]にします。
  • [otherPlayer]を選択した状態で、モデルをシーンに配置し、[otherPlayer]の子にします。

①Entity作成

②名前を

[otherPlayer]に

③[otherplayer]を選択

④モデルをシーンに
ドラッグ&ドロップ

27.他プレイヤーの実装

  • 他プレイヤーのオブジェクトを微調整します。
  • [otherPlayer]を選択し、y座標を変更して見える位置まで持ってきます。
  • モデルのscaleを[5, 5, 5]に変更します。また、[otherPlayer]の中心がモデルの中心に来るように調整します。

[otherPlayer > モデル]

Position[0.071, 0.124, -0.105]

Rotation[0, 180, 0]

Scale[5, 5, 5]

28.他プレイヤーの実装

  • 他プレイヤーの参加,退出を制御するスクリプトを編集します。
  • ヒエラルキーから、[otherPlayerPool]にアタッチされた"playerpool.js"をコードエディターで開き、以下のコードをコピー&ペーストします。
  • 保存したあと、"playerpool"横のParse  をクリックします。
var Playerpool = pc.createScript('playerpool');
Playerpool.attributes.add("ptemplate",{type:"entity"}); //他プレイヤーのオブジェクト
var photonObject;//photonオブジェクト
var _self;//自分自身

// initialize code called once per entity
Playerpool.prototype.initialize = function() {
    photonObject = this.app.root.findByName("PhotonController");//PhotonControllerを格納
    _self = this;//自分自身を格納(スコープが外れるため)
};

// update code called every frame
Playerpool.prototype.update = function(dt) {
    
    //in update method...
    photonObject.photon.onActorJoin = function(actor){//アクターがジョインしたときに呼ばれるコールバック
        if(actor.actorNr == this.myActor().actorNr)
        {//もし入ったアクターが自分自身なら
            //if other player joined room before you join 
            for(var i = 1;i < this.myActor().actorNr;i++){//アクターナンバーの数だけまわす
                if(this.myRoomActors()[i]){//もしそのアクターナンバーのアクターがまだ存在したら
                    if(!this.myRoomActors()[i].isLocal){//そのアクターがlocal(自分自身)でなければ
                        //loop num of players in the room
                        _self.addplayer(i);//自分が入った以前に入ったアクターを生成
                    }
                }
            }
        }
        else
        {//もし他のアクターが入ったら
            _self.addplayer(actor.actorNr);//アクターを生成
        }
    };
    
    //in update method...
    photonObject.photon.onActorLeave = function(actor,cleanup){//アクターが退室したときに呼ばれるコールバック
        if(actor.actorNr == this.myActor().actorNr)
        {//自分自身
            //if player leave room  
        }
        else
        {//他プレイヤー
            //other player leave room
            _self.removePlayer(actor.actorNr);//他プレイヤーを削除
        }
    };
};

Playerpool.prototype.addplayer = function(name){//プレイヤーを追加する関数 引数は名前
    var object = this.ptemplate.clone();
    object.setName(name);
    this.entity.addChild(object);
    object.enabled = true;
};

Playerpool.prototype.removePlayer = function(name){//引数で指定したプレイヤーを削除するメソッド
    var object = this.app.root.findByName(name);
    if(object){
        object.destroy();
    }
};
// swap method called for script hot-reloading
// inherit your script state here
// Playerpool.prototype.swap = function(old) { };

// to learn more about script anatomy, please read:
// http://developer.playcanvas.com/en/user-manual/scripting/

29.他プレイヤーの実装

  • "playerpool"に拡張されたptemplateにヒエラルキーから[otherPlayer]をアタッチします。
  • otherPlayerPoolは他プレイヤーが入ってきた際に、otherPlayerを生成します。
  • ここまでできたらオリジナルのotherPlayerは必要ないのでdisableにします

①playerpool > ptemplateに

otherPlayerをアタッチ

②otherPlayerをdisableに

30.他プレイヤーの実装

  • スマホとPCで実行してみましょう。
    同一ルームに入ると、それぞれ原点付近に他プレイヤー入室時にオブジェクトが追加されることが確認できると思います。

31.座標・角度の同期 送信

  • 最後に各プレイヤーの座標・角度情報を同期するようにします。
  • まずplayer.jsを以下のように書き換え保存します。
  • 自身の座標と角度を取得してシリアライズしたあと送信するようにするコードです
var Player = pc.createScript('player');
var istouch = false; //タッチしているかどうか
var serialstring = "";//判別用シリアル文字列
var photonObject;//Photonオブジェクト

// initialize code called once per entity
Player.prototype.initialize = function() {
    photonObject = this.app.root.findByName("PhotonController");//Photonオブジェクトを格納
    this.entity.parent.setLocalPosition((Math.random() - 0.5) * 10,1.5,(Math.random() - 0.5) * 10); //初期化時に位置をランダムに設定
    if(this.app.touch){//もしタッチデバイスなら
        this.app.touch.on("touchstart",this._touchstart,this);//タッチした瞬間のイベントハンドラを設定
        this.app.touch.on("touchend",this._touchend,this);//タッチが離れた瞬間のイベントハンドラを設定
    }
};

// update code called every frame
Player.prototype.update = function(dt) {
    
    var sendstring = "";//送信用シリアル文字列
    var position = this.entity.getPosition();//自身の座標を取得
    var rotation = this.entity.getEulerAngles();//自身のオイラー角を取得
    sendstring += position.x + ";" + position.y + ";" + position.z + ";" + rotation.x + ";" + rotation.y + ";" + rotation.z;//座標とオイラー角をシリアライズしsendstringに格納
    
    if(serialstring != sendstring){//座標・角度の更新があれば
        sendMyPosition();//送信
        serialstring = sendstring;//判別用文字列に格納
    }
    
    if(istouch){//タッチしていたら
        this.locomotion();//前方向に移動
    }
};

/*プレイヤー移動の関数*/
Player.prototype.locomotion = function(){
    var rot = playangleToRealangle(this.entity.getEulerAngles());//プレイヤーの向いている角度をY軸オイラー角のみ抽出
    if(rot){
        this.entity.parent.translate(-0.1 * Math.sin(rot / 180 * Math.PI),0,-0.1 * Math.cos(rot / 180 * Math.PI));//向いている方向に移動
    }
};

Player.prototype._touchstart = function(ob){//タッチした瞬間に呼ばれる
    istouch = true;
};

Player.prototype._touchend = function(ob){//タッチが離れた瞬間に呼ばれる
    istouch = false;
};

function sendMyPosition(){//Photonを用いて座標・角度の送信を行う関数
    photonObject.photon.raiseEvent(1,serialstring);//イベントコード1でserialstringを送信
}

var playangleToRealangle = function(playangle){//PlayCanvasで得られるオイラー角のうちYが取りうる範囲を0-360に変換して返す関数
    //playangle is eulerAngles Vec3
    //vec3を度数法に変換する
    var ans;
    if(playangle.x < 100 && playangle.x > -100){
        //angle 0 - 90 , 270 - 360
        if(playangle.y >= 0 ){
            //angle 0 - 90
            ans = playangle.y;
        }else{
            //angle 270 - 360
            ans = 360 + playangle.y;
        }
    }
    if(playangle.x > 170){
        //angle 90 -180
        ans = 180 - playangle.y;
    }
    if(playangle.x < -170){
        //angle 180 - 270
        ans = 180 - playangle.y;
    }
    
    return ans;
};

// swap method called for script hot-reloading
// inherit your script state here
// Player.prototype.swap = function(old) { };

// to learn more about script anatomy, please read:
// http://developer.playcanvas.com/en/user-manual/scripting/

32.座標・角度の同期 受信

  • 受信した座標・角度をデシリアライズし、各プレイヤーに反映させます。
  • playerpool.jsを以下のように書き換えます。
var Playerpool = pc.createScript('playerpool');
Playerpool.attributes.add("ptemplate",{type:"entity"}); //他プレイヤーのオブジェクト
var photonObject;//photonオブジェクト
var _self;//自分自身

// initialize code called once per entity
Playerpool.prototype.initialize = function() {
    photonObject = this.app.root.findByName("PhotonController");//PhotonControllerを格納
    _self = this;//自分自身を格納(スコープが外れるため)
};

// update code called every frame
Playerpool.prototype.update = function(dt) {
    photonObject.photon.onEvent = function(code, content, actorNr){//メッセージを受信したときに呼ばれるコールバック
        if(code == 1){//もしイベントコードが1なら
            for(var l = 0;l<_self.entity.children.length;l++){//自身の子の数(他プレイヤーの数)だけまわす
                if(_self.entity.children[l].name == actorNr){//送信元のプレイヤーと他プレイヤーオブジェクトの関連付け
                    var position = new pc.Vec3(parseFloat(content.split(";")[0],10),parseFloat(content.split(";")[1],10),parseFloat(content.split(";")[2],10));//座標情報のデシリアライズ
                    var rotation = new pc.Vec3(parseFloat(content.split(";")[3],10),parseFloat(content.split(";")[4],10),parseFloat(content.split(";")[5],10));//角度情報のデシリアライズ
                    _self.entity.children[l].setPosition(position);//座標を指定
                    _self.entity.children[l].setEulerAngles(rotation);//角度を指定
                }
            }
        }
    };
    
    //in update method...
    photonObject.photon.onActorJoin = function(actor){//アクターがジョインしたときに呼ばれるコールバック
        sendMyPosition();//座標を送信
        if(actor.actorNr == this.myActor().actorNr)
        {//もし入ったアクターが自分自身なら
            //if other player joined room before you join 
            for(var i = 1;i < this.myActor().actorNr;i++){//アクターナンバーの数だけまわす
                if(this.myRoomActors()[i]){//もしそのアクターナンバーのアクターがまだ存在したら
                    if(!this.myRoomActors()[i].isLocal){//そのアクターがlocal(自分自身)でなければ
                        //loop num of players in the room
                        _self.addplayer(i);//自分が入った以前に入ったアクターを生成
                    }
                }
            }
        }
        else
        {//もし他のアクターが入ったら
            _self.addplayer(actor.actorNr);//アクターを生成
        }
    };
    
    //in update method...
    photonObject.photon.onActorLeave = function(actor,cleanup){//アクターが退室したときに呼ばれるコールバック
        if(actor.actorNr == this.myActor().actorNr)
        {//自分自身
            //if player leave room  
        }
        else
        {//他プレイヤー
            //other player leave room
            _self.removePlayer(actor.actorNr);//他プレイヤーを削除
        }
    };
};

Playerpool.prototype.addplayer = function(name){//プレイヤーを追加する関数 引数は名前
    var object = this.ptemplate.clone();
    object.setName(name);
    this.entity.addChild(object);
    object.enabled = true;
};

Playerpool.prototype.removePlayer = function(name){//引数で指定したプレイヤーを削除するメソッド
    var object = this.app.root.findByName(name);
    if(object){
        object.destroy();
    }
};
// swap method called for script hot-reloading
// inherit your script state here
// Playerpool.prototype.swap = function(old) { };

// to learn more about script anatomy, please read:
// http://developer.playcanvas.com/en/user-manual/scripting/

33.実行!

  • スマホプレイヤーの傾きが同期されることが確認できたら成功です!

34.インタフェースを消す

  • スマホでプレイする際に、インタフェースが邪魔なので消します。
  • ただ消すと、入室できなくなってしまうため、オートジョインの機能も追加します。
  • workspace/photon/内のapp.js, style.css, demoloadbalancing.jsを以下のように編集します。
var App = pc.createScript('app');
App.attributes.add("PHOTON CLOUD",{type:"title"});
App.attributes.add("appid",{type:"string",default:"",title:"AppId",description:"Please input your AppId"});
App.attributes.add("appversion",{type:"string",default:"1.0",title:"Appversion",description:"Application version. You can not be matching if you set diferense version."});
App.attributes.add("Region",{
    type: 'string',
    default: 'default',
    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'},
        { "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'}
    ]
});
App.attributes.add("PHOTON SERVER",{type:"title"});
App.attributes.add("Masterserver",{type:"string",title:"Server Address",description:"use Photon Server if you input server address."});
App.attributes.add("GENERAL",{type:"title"});
App.attributes.add("ConnectOnStart",{type:"boolean",default:true,title:"Auto-Join Lobby",description:"Define if PhotonNetwork should join the 'lobby' when connected to the Master server"});

var DemoWss;
var DemoMasterServer;
var connectRegion;
var ConnectOnStart;

// initialize code called once per entity
App.prototype.initialize = function() {
    //////////////////////////////////////////////////
    //Photon Cloud
    //////////////////////////////////////////////////
    if(this.appid){
        if(document.domain == "playcanvas.com"){
            localStorage.setItem("appid",this.appid);
        }        
        DemoAppId = this.appid;
    }else
    {
        if(localStorage.getItem("appid")){
            DemoAppId = localStorage.getItem("appid");
        }
        else if(!this.Masterserver)
        {
            if(window.confirm("Thank you for your forking! \n Please get and enter your AppId if you started development \n  Do you want to acquire AppId now?") === true){
                 window.location.href = 'https://www.photonengine.com/en/Account/Signup';
            }else{
                 window.open('about:blank','_self').close();
            }
        }
        
    }
    DemoAppVersion = this.appversion;
    connectRegion = this.Region;
    
    //////////////////////////////////////////////////
    //Photon Server
    //////////////////////////////////////////////////
    if(this.Masterserver){
        DemoMasterServer = this.Masterserver;
        DemoAppId = "using photon server";
    }
    
    //////////////////////////////////////////////////
    //General
    //////////////////////////////////////////////////
    ConnectOnStart = this.ConnectOnStart;
    
    //////////////////////////////////////////////////
    //load and seting UI 
    //////////////////////////////////////////////////
    //html load and initialize
    var htmlAsset = this.app.assets.find('index.html');
    var div = document.createElement('div');
    div.innerHTML = htmlAsset.resource;
    document.body.appendChild(div);

    htmlAsset.on('load', function () {
        div.innerHTML = htmlAsset.resource;
    });
    
    // css load and initialize
    var cssAsset = this.app.assets.find('style.css');
    var stylecss = document.createElement('style');
    stylecss.innerHTML = cssAsset.resource;
    document.head.appendChild(stylecss);
        
    cssAsset.on('load', function() {
        style.innerHTML = cssAsset.resource;
    });
    
    //////////////////////////////////////////////////
    //Photon start
    //////////////////////////////////////////////////
    this.entity.photon = new DemoLoadBalancing();
    this.entity.photon.start();
};


//////////////////追加 オートジョインルーム//////////////////////
App.prototype.update = function(dt){
    this.autojoinroom();
};

App.prototype.autojoinroom = function(){
    var isFullhouse = true;//満室かどうか
    for(var l = 0;l < this.entity.photon.availableRooms().length;l++){
        var room = this.entity.photon.availableRooms()[l];
        if(room.maxPlayers > room.playerCount){
            isFullhouse = false;
            console.log(room);
        }
    }
    var canJoin = this.entity.photon.isInLobby() && !this.entity.photon.isJoinedToRoom() && this.entity.photon.availableRooms().length > 0 && !isFullhouse;//入れるかどうか
    if (this.entity.photon.isInLobby()) {
        if (canJoin) {//もし入れるなら
            this.entity.photon.output("Random Game...");
            this.entity.photon.joinRandomRoom();//適当なルームにジョイン
        }else{//入れないなら
            var name = document.getElementById("newgamename");
            this.entity.photon.output("New Game");
            this.entity.photon.createRoom(name.value.length > 0 ? name.value : undefined,{maxPlayers:5});//最大5名の部屋を作成し入室
        }
    }
};
body {
    font-family: "Courier New", Courier, monospace;
    text-align: center;
}

.container {
    position: absolute;
    background-color: rgba(255,255,255,0.6);
    border-radius: 30px;
    top: 200%;
    left: 200%;
    width: 320px;
}

.log {
    position: absolute;
    text-align:left;
    font-size:5px;
    color:#fff;
    border-radius: 30px;
    top: 0%;
    left: 0%;
    width: 320px;
}

input {
	background-color: transparent;
	border: 2px solid #666;
	color: #111;
	line-height: 20px;
    margin: 2px; padding: 2px;
}

select {
	background-color: transparent;
	border: 2px solid #666;
	color: #111;
	line-height: 20px;
    margin: 2px; padding: 2px;
}

button {
	background-color: transparent;
	border: 2px solid #666;
	color: #111;
	line-height: 20px;
    margin: 2px; padding: 2px;
}

button[disabled] {
	background-color: transparent;
	border: 2px solid #666;
	color: #888;
	line-height: 20px;
    margin: 2px; padding: 2px;
}

button:hover {
	background-color: rgba(255, 255, 255, .4);
}

input:hover {
	background-color: rgba(255, 255, 255, .4);
}

select:hover {
	background-color: rgba(255, 255, 255, .4);
}

button[disabled]:hover {
    background-color: transparent;
}

.white{
    color: #ffffff;
}

.black{
    color: #000000;
}

* {
  -webkit-tap-highlight-color: rgba(0, 0, 0, 0);
  -webkit-touch-callout: none;
  -webkit-user-select: none;
}

app.js

style.css

var DemoLoadBalancing = (function (_super) {
    __extends(DemoLoadBalancing, _super);
    function DemoLoadBalancing() {
        _super.call(this, DemoWss ? Photon.ConnectionProtocol.Wss : Photon.ConnectionProtocol.Ws, DemoAppId, DemoAppVersion);
        this.logger = new Exitgames.Common.Logger("Demo:");
        this.output(this.logger.format("Init", this.getNameServerAddress(), DemoAppId, DemoAppVersion));
        this.logger.info("Init", this.getNameServerAddress(), DemoAppId, DemoAppVersion);
        this.setLogLevel(Exitgames.Common.Logger.Level.DEBUG);
    }
    
    DemoLoadBalancing.prototype.start = function () {
        this.setupUI();
        
        // connect if no fb auth required 
        if (ConnectOnStart) {
            if (DemoMasterServer) {
                this.setMasterServerAddress(DemoMasterServer);
                this.connect();
                
            }
            else {
                this.connectToRegionMaster(connectRegion);
            }
        }
    };
    DemoLoadBalancing.prototype.onError = function (errorCode, errorMsg) {
        this.output("Error " + errorCode + ": " + errorMsg);
    };
    DemoLoadBalancing.prototype.onStateChange = function (state) {
        // "namespace" import for static members shorter acceess
        var LBC = Photon.LoadBalancing.LoadBalancingClient;
        var stateText = document.getElementById("statetxt");
        stateText.textContent = LBC.StateToName(state);
        this.updateRoomButtons();
        this.updateRoomInfo();
    };
   
    
    DemoLoadBalancing.prototype.updateRoomInfo = function () {
        var stateText = document.getElementById("roominfo");
        var message;
        if(this.isJoinedToRoom()){
            message = "room: " + this.myRoom().name;
        }
        else
        {
            message = "not joined room";
        }
        stateText.innerHTML = message;
        stateText.innerHTML = stateText.innerHTML + "<br>";
        this.updateRoomButtons();
    };
    DemoLoadBalancing.prototype.onActorPropertiesChange = function (actor) {
        this.updateRoomInfo();
    };
    DemoLoadBalancing.prototype.onMyRoomPropertiesChange = function () {
        this.updateRoomInfo();
    };
    DemoLoadBalancing.prototype.onRoomListUpdate = function (rooms, roomsUpdated, roomsAdded, roomsRemoved) {
        this.logger.info("Demo: onRoomListUpdate", rooms, roomsUpdated, roomsAdded, roomsRemoved);
        this.output("Demo: Rooms update: " + roomsUpdated.length + " updated, " + roomsAdded.length + " added, " + roomsRemoved.length + " removed");
        this.onRoomList(rooms);
        this.updateRoomButtons(); // join btn state can be changed
    };
    DemoLoadBalancing.prototype.onRoomList = function (rooms) {
        var menu = document.getElementById("gamelist");
        while (menu.firstChild) {
            menu.removeChild(menu.firstChild);
        }
        var selectedIndex = 0;
        for (var i = 0; i < rooms.length; ++i) {
            var r = rooms[i];
            var item = document.createElement("option");
            item.textContent = r.name;
            menu.appendChild(item);
            if (this.myRoom().name == r.name) {
                selectedIndex = i;
            }
        }
        menu.selectedIndex = selectedIndex;
        this.output("Demo: Rooms total: " + rooms.length);
        this.updateRoomButtons();
    };
    DemoLoadBalancing.prototype.onJoinRoom = function () {
        this.output("Game " + this.myRoom().name + " joined");
        this.updateRoomInfo();
    };
    DemoLoadBalancing.prototype.onActorJoin = function (actor) {
        this.output("actor " + actor.actorNr + " joined");
        this.updateRoomInfo();
    };
    DemoLoadBalancing.prototype.onActorLeave = function (actor) {
        this.output("actor " + actor.actorNr + " left");
        this.updateRoomInfo();
    };
    DemoLoadBalancing.prototype.sendMessage = function (message) {
        try {
            this.raiseEvent(1, { message: message, senderName: "user" + this.myActor().actorNr });
           }
        catch (err) {
            this.output("error: " + err.message);
        }
    };
    DemoLoadBalancing.prototype.setupUI = function () {
        var _this = this;
        this.logger.info("Setting up UI.");
        var btnJoin = document.getElementById("joingamebtn");
        btnJoin.onclick = function (ev) {
            if (_this.isInLobby()) {
                var menu = document.getElementById("gamelist");
                var gameId = menu.children[menu.selectedIndex].textContent;
                _this.output(gameId);
                _this.joinRoom(gameId);
            }
            else {
                _this.output("Reload page to connect to Master");
            }
            return false;
        };
        var btnJoin2 = document.getElementById("joinrandomgamebtn");
        btnJoin2.onclick = function (ev) {
            if (_this.isInLobby()) {
                _this.output("Random Game...");
                _this.joinRandomRoom();
            }
            else {
                _this.output("Reload page to connect to Master");
            }
            return false;
        };
        var btnNew = document.getElementById("newgamebtn");
        btnNew.onclick = function (ev) {
            if (_this.isInLobby()) {
                var name = document.getElementById("newgamename");
                _this.output("New Game");
                _this.createRoom(name.value.length > 0 ? name.value : undefined);
            }
            else {
                _this.output("Reload page to connect to Master");
            }
            return false;
        };
        var btn = document.getElementById("leavebtn");
        btn.onclick = function (ev) {
            _this.leaveRoom();
            _this.updateRoomInfo();
            return false;
        };
        this.updateRoomButtons();
    };
    DemoLoadBalancing.prototype.output = function (str, color) {
        // var log = document.getElementById("theDialogue");
        // if(log.clientHeight / window.parent.screen.height > 0.9){
        //     log.innerHTML = "";
        // }
        // var escaped = str.replace(/&/, "&").replace(/</, "<").
        //     replace(/>/, ">").replace(/"/, """);
        // if (color) {
        //     escaped = "<FONT COLOR='" + color + "'>" + escaped + "</FONT>";
        // }
        // log.innerHTML = log.innerHTML + escaped + "<br>";
        // log.scrollTop = log.scrollHeight;
    };
    DemoLoadBalancing.prototype.updateRoomButtons = function () {
        var btn;
        btn = document.getElementById("newgamebtn");
        btn.disabled = !(this.isInLobby() && !this.isJoinedToRoom());
        var canJoin = this.isInLobby() && !this.isJoinedToRoom() && this.availableRooms().length > 0;
        btn = document.getElementById("joingamebtn");
        btn.disabled = !canJoin;
        btn = document.getElementById("joinrandomgamebtn");
        btn.disabled = !canJoin;
        btn = document.getElementById("leavebtn");
        btn.disabled = !(this.isJoinedToRoom());
    };
    return DemoLoadBalancing;
}(Photon.LoadBalancing.LoadBalancingClient));

demoloadbalancing.js

35.最後にみんなでプレイ!

  • 最初にATND名で指定したAppversionを[ 1.0 ]に変更します。
  • 同一AppId, AppVersionでマッチングするので、これで全てのプレイヤーがマッチングします
  • 左上のManage Scene > PUBLISHからパブリッシュしてビルドURLを配ることができます!

ソーシャルVR体験を是非共有してみてください!

おわりに

いかがでしたか?

ソーシャルVRがこんなに簡単に作れるPlayCanvasとPhotonのパワフルさを感じていただければ幸いです

大変駆け足になってしまいましたが

ご質問は随時募集しております。

今後の予定

日時 タイトル 場所
6/28 Photon TrueSync詳細
PlayCanvas超基礎編
大阪
6/30 GTMF・セッション/ブース 大阪
7/14 GTMF・セッション/ブース 東京・秋葉原
7/27 第2回! ソーシャルVRハンズオン ※同内容 東京・渋谷
8/30-9/1 CEDEC2017 セッション/ブース/ハンズオン 横浜

今後もイベント続々参加/開催予定!

最後に...

一緒に働ける仲間を

大募集中!!


詳細はこちら

ありがとうございました!

アンケートにお答えください