新しい記事がこちらになります
【JavaScript】Photon + PlayCanvasを使ってモバイル・デスクトップで動く一人称視点のマルチプレイができる空間を作る【WebGL】
https://qiita.com/yushimatenjin/items/74b69d682fcaeb124308
PlayCanvasアドベントカレンダー11日目!
投稿日が遅れてしまいましたが、この記事はPlayCanvasアドベントカレンダー 12月11日の記事となります。
PhotonのJS SDKとゲームエンジンを組み合わせて色々できそうだなと思っていたのでアドベントカレンダーを機にもう一度触ってみます。
HTML5 VTuber LIVEシステムとか作りたい
JavaScriptでリアルタイム通信するゲームを作る
JavaScriptはよく触ってるけど、そういえばリアルタイムの通信/対戦ゲームってどうやって作るのってなった時に参考になるかもしれないです。
PlayCanvasでマルチ対戦のゲームを作るために今回はPhotonというネットワークエンジンを使用して作成します。役割としては描画の部分はPlayCanvas
、リアルタイム通信の部分についてはPhoton
を使用して作っていきます。
今回デモとして作成したこちらのゲームは[WASD]で移動ができるシンプルなゲームとなっております。
PhotonとPlayCanvas
— はが (@Mxcn3) December 13, 2019
[W A S D]で移動(スマホはタッチで飛ぶだけ)#playcanvas #2窓https://t.co/mPDM21Lfme
PlayCanvasについて
PlayCanvasはJavaScriptで記述されたWebGLのエンジンで、最近GitHubのスター数が5000を超えました。
OSSのEngineと、SaaS型のエディターを兼ね備えているゲームエンジンです。engineについては、Three.js, Babylon.jsなどに似た使い方を使用することができます。エディターはビジュアルエディターとコードエディターがあり、エディターはクラウド上でプロジェクトの作成から公開までが出来るもので、FBX
やOBJ
といった3Dモデルの素材については、ブラウザへドラッグアンドドロップをすることでGUIから操作・配置ができるエディターとなっております。
Photonについて
Photonについては、Unity多く使われているようですが、JavaScriptのSDKもありますので、Photon JavaScript SDKを使用してマルチ対戦のゲームを作っていきます。
Photonは20CCU(同時接続)までであれば、無料で使用できます。
https://www.photonengine.com/ja/photon
PlayCanvasとPhotonを組み合わせる
PlayCanvasとPhotonを組み合わせるにはこちらのリポジトリを参考になります。
https://github.com/utautattaro/Photon-for-PlayCanvas
Photon x PlayCanvasでできること
3Dや2Dのリアルタイムゲームを作ることができます。こちらのタンクのゲームは4人がリアルタイム1つのルームに入り対戦できるゲームです。
プロジェクトをフォークして作成する
上記のリポジトリを参考に、自分でプロジェクトを使用して作ることもできますが、PlayCanvasには便利なForkという機能があるのでそちらを利用して作ってみます。
1. Forkする
こちらのデモ用のPlayCanvasのプロジェクトはPublicなプロジェクトとなっておりますので、PlayCanvasにログインしアクセスしていただくと、Forkできます。
2. PlayCanvas EditorのAppIdを設定する
今回簡単なプロジェクトを作る上では、AppIdを設定することができればマルチプレイ対戦ができるようになっています。AppIdの設定にはPhotonのIDを作る必要があるので、設定します。
a. Photonに登録する
- Photon 公式サイトにアクセスして登録をします。
b. PhotonからAppId取得する
- Photonのダッシュボードから新規アプリを作成し、AppIdを取得します。
c. AppIdをPlayCanvas上に設定する
- Forkしたプロジェクトに入る
- コピーしたAppIdを貼り付ける
PhotonのJavaScriptSDKをダウンロード
PlayCanvasでPhotonを使用するためにJavaScriptのSDKをダウンロードします。
- PhotonのSDKの最新版をダウンロード
https://www.photonengine.com/ja/sdks
① SDKを解凍し\photon-javascript-sdk_v4-1-0-1\libの中にあるPhoton-Javascript_SDK
をPlayCanvasにアップロード
② SETTINGSをクリック
③ Photon-Javascript_SDKのSCRIPT LOADIGN ORDERを一番上に!
しかしこのままだとPlayCanvasで使用する際にPhotonのSDKを読み込む前にPhotonのAPIを使用するプログラムが書いてあるため、SETTIGNSからSCRIPTのLOADING ORDERを変更します。
ゲームを起動する
PlayCanvasではエディター上には開発時に使用するLaunch
とリリースする際に使用するPublish
の2つの機能があります。
a. Launchで確認する
開発時にはこちらのLaunchボタンを押して確認します。
スクリプトについて
init.js
起動Photonの起動を行っています。index.html
, style.css
を読み込みUIのセットアップも行います。
app.js
app.jsにてPhotonとPlayCanvasの同期をしています。Photonには様々なライフサイクルで発行されるイベントがありますが、今回使用したものについて説明します。
/*jshint esversion: 6, asi: true, laxbreak: true*/
const App = pc.createScript('app');
App.prototype.initialize = function() {
this.photon = this.app.root.children[0].photon;
this.photon.setLogLevel(999);
this.photon.onJoinRoom = () => {
Object.values(this.photon.actors).map(event => {
const { isLocal, actorNr } = event;
if (isLocal) {
} else {
const entity = new pc.Entity();
entity.addComponent("model", {
type: "box"
});
entity.setPosition(0, 1, 0);
entity.tags.add(`${actorNr}`);
this.app.root.addChild(entity);
}
});
};
this.photon.onActorJoin = event => {
const { isLocal, actorNr } = event;
if (isLocal) {
} else {
const entity = new pc.Entity();
entity.addComponent("model", {
type: "box"
});
entity.setPosition(0, 1, 0);
entity.tags.add(`${actorNr}`);
this.app.root.addChild(entity);
}
};
this.photon.onEvent = (code, content, actorNr) => {
const entities = this.app.root.findByTag(`${actorNr}`);
const [entity] = entities;
switch (code) {
case 1: {
const { x, y, z } = content;
entity.setLocalPosition(x, y, z);
break;
}
case 2: {
const { x, y, z, w } = content;
entity.setLocalRotation(x, y, z, w);
break;
}
default: {
break;
}
}
};
};
photon.onJoinRoom
onJoinRoomはルームに入った際の処理になります。今回のような作りでは呼ばれるタイミング
- ルームを新しく作ったとき
- 既存のルームに入る時
- ルームに入った時
- 他のプレイヤーを取得
- 新しいエンティティ("Model:Box")としてゲーム画面に配置
// 1.ルームに入った時
this.photon.onJoinRoom = () => {
//2. 他のプレイヤーを取得
Object.values(this.photon.actors).map(event => {
const { isLocal, actorNr } = event;
if (isLocal) {
} else {
// 新しいエンティティを作成
const entity = new pc.Entity();
entity.addComponent("model", {
type: "box"
});
// 新しく生成したエンティティのポジションの設定とタグを付与している
entity.setPosition(0, 1, 0); // x, y, z
entity.tags.add(`${actorNr}`);
// PlayCanvasの画面上に配置する
//3. 新しいエンティティ("Model:Box")としてゲーム画面に配置
this.app.root.addChild(entity);
}
});
};
photon.onActorJoin
onActorJoinは自分の入っているルームに他のプレイヤーが参加してきたときの処理になります。
- 他のプレイヤーが入ってきた時
- 新しいエンティティを作成
- 新しいエンティティ("Model:Box")としてゲーム画面に配置
// 1.他のプレイヤーが入ってきた時
this.photon.onActorJoin = event => {
// isLocal: 自分であるかの判定
// actorNr: 入った順に1から振られる、ユニークな番号
const { isLocal, actorNr } = event;
// 自分自身であるかの判定
if (isLocal) {
} else {
// 2. 新しいエンティティを作成
const entity = new pc.Entity();
entity.addComponent("model", {
type: "box"
});
// ポジションとタグを設定
entity.setPosition(0, 1, 0);
entity.tags.add(`${actorNr}`);
// 3. 新しいエンティティ("Model:Box")としてゲーム画面に配置
this.app.root.addChild(entity);
}
};
photon.onEvent
onEventは、今回はplayer.js
により、データが送信された場合に呼び出されます。
-
actorNr
のタグを元にエンティティ
を検索 -
Code
によってPosition
かRotation
かの判定を行う
this.photon.onEvent = (code, content, actorNr) => {
// 1. `actorNr`のタグを元に`エンティティ`を検索
const entities = this.app.root.findByTag(`${actorNr}`);
const [entity] = entities;
//2. `Code`によって`Position`か`Rotation`かの判定を行う
switch (code) {
case 1: {
const { x, y, z } = content;
entity.setLocalPosition(x, y, z);
break;
}
case 2: {
const { x, y, z, w } = content;
entity.setLocalRotation(x, y, z, w);
break;
}
default: {
break;
}
}
};
player.js
Player.jsはキーボードの操作が押された際にPhotonサーバーにデータを送るものになります。
/*jshint esversion: 6, asi: true, laxbreak: true*/
const Player = pc.createScript("player");
// PlayCanvas Editor上で使用するためにAttributesを作成
// Attributesの説明
// https://developer.playcanvas.com/ja/user-manual/scripting/script-attributes/
Player.attributes.add("moveSpeed", { type: "number", default: 0.1 });
Player.attributes.add("rotateSpeed", { type: "number", default: 2 });
const move = (direction, entity, self) => {
const { photon } = self;
switch (direction) {
case "up": {
entity.translateLocal(0, 0, -self.moveSpeed);
break;
}
case "down": {
entity.translateLocal(0, 0, self.moveSpeed);
break;
}
default: {
break;
}
}
// send position
// コード番号 1で現在のEntityのポジションを送信
photon.raiseEvent(1, entity.getLocalPosition());
};
const rotate = (direction, entity, self) => {
const { photon } = self;
switch (direction) {
case "left": {
entity.rotate(0, -self.rotateSpeed, 0);
break;
}
case "right": {
entity.rotate(0, self.rotateSpeed, 0);
break;
}
default: {
break;
}
}
// send rotation
// コード番号 2で現在のEntityのポジションを送信
photon.raiseEvent(2, entity.getLocalRotation());
};
Player.prototype.initialize = function () {
this.photon = this.app.root.children[0].photon;
if (this.app.touch) {
this.app.touch.on(
pc.EVENT_TOUCHSTART,
() => {
const { photon } = this
const { x, y, z } = this.entity.getPosition();
this.entity.rigidbody.teleport(x, y + 0.5, z);
photon.raiseEvent(1, this.entity.getLocalPosition());
photon.raiseEvent(2, this.entity.getLocalRotation());
},
null
);
}
};
// update code called every frame
Player.prototype.update = function (dt) {
const { keyboard } = this.app;
// 移動のキーが押されていたら
if (keyboard.isPressed(pc.KEY_W) || keyboard.isPressed(pc.KEY_UP)) {
move("up", this.entity, this);
} else if (keyboard.isPressed(pc.KEY_S) || keyboard.isPressed(pc.KEY_DOWN)) {
move("down", this.entity, this);
}
// 回転のキーが押されたら
if (keyboard.isPressed(pc.KEY_A) || keyboard.isPressed(pc.KEY_LEFT)) {
rotate("right", this.entity, this);
} else if (keyboard.isPressed(pc.KEY_D) || keyboard.isPressed(pc.KEY_RIGHT)) {
rotate("left", this.entity, this);
}
};
photon.raiseEvent
raiseEventを使用して任意のタイミングでデータの送信を行います。今回は移動が行われた時と回転が行われた際に現在の場所を送信しています。
raiseEvent(eventCode, data, options)
型はそれぞれ
eventCode:number
object: object
options: object
移動時
....
// send position
// コード番号 1で現在のEntityのポジションを送信
photon.raiseEvent(1, entity.getLocalPosition());
};
回転時
...
// send rotation
// コード番号 2で現在のEntityのポジションを送信
photon.raiseEvent(2, entity.getLocalRotation());
};
参考
initialize
PlayCanvasのスクリプトのライフサイクルには、スクリプト一度だけ呼び出されるinitialize
、毎フレーム呼び出されるUpdate
があります。
- Photonのプロパティを代入
- タッチされた際に発火するイベントの定義
Player.prototype.initialize = function () {
// Rootエンティティからphotonプロパティを取得
// 1. Photonのプロパティを代入
this.photon = this.app.root.children[0].photon;
// タッチ操作が可能だったら
if (this.app.touch) {
// タッチ操作が行われたら
// PlayCanvas: タッチについて
// https://support.playcanvas.jp/hc/ja/articles/227190908
// 2. タッチされた際に発火するイベントの定義
// PlayCanvas: イベント種類について
// https://developer.playcanvas.com/ja/user-manual/scripting/communication/
this.app.touch.on(
pc.EVENT_TOUCHSTART,
() => {
const { photon } = this
const { x, y, z } = this.entity.getPosition();
this.entity.rigidbody.teleport(x, y + 0.5, z);
// コード1で現在のポジションを送信
photon.raiseEvent(1, this.entity.getLocalPosition());
// コード2で現在の回転を送信
photon.raiseEvent(2, this.entity.getLocalRotation());
},
null
);
}
};
update
update
はフレームごとに呼ばれます
- 移動のキーが押されていたら
- 回転のキーが押されたら
Player.prototype.update = function (dt) {
const { keyboard } = this.app;
// 1. 移動のキーが押されていたら
if (keyboard.isPressed(pc.KEY_W) || keyboard.isPressed(pc.KEY_UP)) {
// 移動
move("up", this.entity, this);
} else if (keyboard.isPressed(pc.KEY_S) || keyboard.isPressed(pc.KEY_DOWN)) {
// 移動
move("down", this.entity, this);
}
// 2. 回転のキーが押されたら
if (keyboard.isPressed(pc.KEY_A) || keyboard.isPressed(pc.KEY_LEFT)) {
// 回転
rotate("right", this.entity, this);
} else if (keyboard.isPressed(pc.KEY_D) || keyboard.isPressed(pc.KEY_RIGHT)) {
// 回転
rotate("left", this.entity, this);
}
};
起動
ゲーム起動するとindex.html
とstyle.css
に定義されているUIと、ゲームが表示されます。
b. PublishをしてURLを共有する
Launch
はログインしているユーザーしか閲覧できないのでこちらから公開用のURLを発行することで、インターネット上で共有できます。
おわりに
リアルタイムの通信をするゲームもかなり簡単に作れるそうですね。
もう少し興味がある方は、PhotonSDKのAPIはplayer.js
で記述されているのでそちらをいじると色々できます。
→ player.jsをいじった記事を書きました
そういえば、JavaScriptでリアルタイム通信のゲームってどうやって作るのってなったとき。その2
https://qiita.com/yushimatenjin/items/eb4311c3640a20bbdfb9
以下PlayCanvas開発で参考になりそうな記事の一覧です。
入門
- PlayCanvas入門- モデルの作成~ゲームに入れ込むまで
- JavaScriptでスロットを実装する。【PlayCanvas】
- 3Dモデルのビューワーを3分で作る【初めてのPlayCanvas】
- PlayCanvasのコードエディターでes6に対応する
- Gulpのプラグインを書いたらPlayCanvasでの開発がめちゃくちゃ便利になった
- PlayCanvas Editorに外部スクリプトを読み込む新機能が追加されたので開発方法を考える。- Reduxを組み込む
その他の記事はこちらになります。
- AR年賀状を作る
- React Native + PlayCanvasを使ってスマートフォンゲームを爆速で生み出す
- PlayCanvasのエディター上でHTML, CSSを組み込む方法
- 【iOS13】新しくなったWebVRの使い方
PlayCanvasのユーザー会のSlackを作りました!
少しでも興味がありましたら、ユーザー同士で解決・PlayCanvasを推進するためのSlackを作りましたので、もしよろしければご参加ください!