本チュートリアルの動画をYouTubeから試聴しながらの学習をお奨めしております。
目次
- 概要
- Chapter01 - 新規プロジェクトから作成する
- Chapter02 - クリックしたらPlayerが移動するスクリプトを作成
- Chapter03 - オブジェクトに触れたら処理するスクリプトを作成
- Chapter04 - オブジェクトに触れて処理後にボタンを表示
- Chapter05 - ボタンをクリックし、モーダルを表示
- Chapter06 - オブジェクトごとにモーダルのテキストを設定する
- おまけ - Playerの移動速度を等速にしたい
概要
このチュートリアルでは、PlayCanvasを使って箱庭(ミニスケープ)のコンテンツを作っていきます。
以下のようなコンテンツが出来ます。
Project: https://playcanvas.com/project/1000148/
マウスクリックで移動し、特定の場所でアクションを起こすコンテンツです。
モーダルを表示したり、動画を流したりと工夫次第でさまざまなことができます!
さっそくコンテンツを作成いきましょう!
PlayCanvasアカウントをまだ作成していない場合は、3分でアカウント作成から3Dモデルビュワーまで体験ができるチュートリアルがございますので、まずはこちらから参照ください。
→ https://support.playcanvas.jp/hc/ja/articles/4404199480089
Chapter01 - 新規プロジェクトから作成する
PlayCanvasの自身のアカウントページから新規プロジェクトを作成します。
テンプレートは「Blank Project」にします。
作成後、プロジェクトページに飛ばされます。
プロジェクトのEDITORボタンをクリックしてエディター画面へ進みます。
Blank Project なので、最低限の構成になっています。
ヒエラルキーに「Camera」「Light」、Primitiveな「Box」と「Plane」があります。
Chapter02 - クリックしたらPlayerが移動するスクリプトを作成
本チュートリアルでは、Planeのステージをクリックした場所に3DモデルのPlayerが移動するというものです。
早速、PlaneをクリックしたらPlayerが移動するというスクリプトから作ってみましょう。
スクリプトの前準備に、「Plane」や「Player」の設定をしていきます。
Planeを選択し、「Collision」と「RigidBody」を設定します。
Ammo.jsも忘れずにインポートしてください。
以下のように、「Plane」を設定してください。
Scale
x : 20 , y : 1 , z : 20
Collision
Type : Box
Half Extents : x: 10 , y : 0.01 , z : 10
Scale はお好みのサイズに変更し、そのサイズに合わせて Collision のサイズも変更してください。
RigidBody のTypeは「Static」のまま。
Material はご自身で用意したテクスチャがあれば、お好みで設定してください。
次はPlayerを設定します。
「Add Entity」から Entity を追加し、名前を Player に変更します。
このPlayerにヒエラルキーにある「Camera」と「Box」のエンティティを入れ子にします。
PlayerにCameraとBoxを入れ子にすることで、Playerが移動すると合わせてBoxもCameraも付いてくるように移動します。
次からPlayerを移動させるスクリプトを作っていきます。
移動させるために、「Tween ライブラリ」を使用します。
以下はTween ライブラリのでもサンプルになります。
エディター下部のASSETSの「ASSET STORE」から「Tween Animation Library」選択しプロジェクトにインポートしましょう。
Tweenライブラリについて: https://developer.playcanvas.com/ja/tutorials/tweening/
Tween.jsをインポートしたら「playerMove.js」を作成し、エンティティのPlayerに設定します。
設定ができたら、以下のスクリプトを「playerMove.js」にコピペします。
let PlayerMove = pc.createScript('playerMove');
// 属性の追加
PlayerMove.attributes.add("targetCamera", {type:"entity"});
PlayerMove.prototype.initialize = function() {
// ターゲットの位置の初期化
this.targetPosition = new pc.Vec3();
// マウスのイベント登録
this.app.mouse.on(pc.EVENT_MOUSEUP, this.mouseUp, this);
// 移動可能な状態へのイベントリスナー登録
this.app.on("move:able", () => {
this.app.mouse.on(pc.EVENT_MOUSEUP, this.mouseUp, this);
}, this);
// 移動不能な状態へのイベントリスナー登録
this.app.on("move:unable", () => {
this.app.mouse.off(pc.EVENT_MOUSEUP, this.mouseUp, this);
}, this);
};
// マウスクリック時の処理
PlayerMove.prototype.mouseUp = function (e) {
// レイキャスト処理の実行
this.doRaycast(e.x, e.y);
// Tweenアニメーションの開始
this.entity.tween(this.entity.getLocalPosition()).to(this.targetPosition, 0.5, pc.Linear).start();
};
// レイキャスト処理
PlayerMove.prototype.doRaycast = function(screenX, screenY) {
// レイキャストに使う開始点と終了点の座標を計算
let from = new pc.Vec3();
let to = new pc.Vec3();
if(this.targetCamera.camera.projection !== pc.PROJECTION_ORTHOGRAPHIC) {
from = this.targetCamera.getPosition();
to = this.targetCamera.camera.screenToWorld(screenX, screenY, this.targetCamera.camera.farClip);
} else {
var farClip = this.targetCamera.camera.farClip;
var nearClip = this.targetCamera.camera.nearClip;
this.targetCamera.camera.screenToWorld(screenX, screenY, nearClip, from);
this.targetCamera.camera.screenToWorld(screenX, screenY, farClip, to);
}
// レイキャスト処理の実行
const result = this.app.systems.rigidbody.raycastFirst(from, to);
// レイキャストの結果がない場合は、終了
if (!result) return;
// レイキャストの結果からターゲット位置を設定
this.targetPosition = result.point;
};
マウスのPositionを3D空間に置き換える場合は、screenToWorld()を使用していきます。
これは2Dスクリーン空間の点を3Dワールド空間に変換する処理を実行します。
詳しくはAPIリファレンスから: https://developer.playcanvas.com/api/pc.CameraComponent.html#screenToWorld
マウスでクリックした2D空間の座標を、コンテンツである3D空間の座標にクリックした箇所を変換してくれます。
このクリックした箇所が当たり判定を持っているか、を判定するのが、raycastFirst()です。
レイキャストとは、スタートからエンドまで、光線を放射してコリジョンコンポーネントを持つエンティティに衝突するとその結果を返す処理です。衝突がなければnullを返します。
詳しくはAPIリファレンスから: https://developer.playcanvas.com/api/pc.RigidBodyComponentSystem.html#raycastFirst
RigidBodyとCollisionを設定しているエンティティが引数であるfrom, toの直線上に存在していれば、その座標ポイントと直線上に存在したエンティティを返してくれます。
ここで返した座標ポイントを使って、Tween.jsでPlayerを移動させるスクリプトになります。
スクリプトをコピペし保存したら、Parseすることでスクリプトを一度読み込み、スクリプト属性の「targetCamera」を表示させます。
スクリプト属性のtargetCameraを表示できたら、エンティティの「Camera」をtargetCameraに設定します。
ここまでできたらLaunchして、Planeをクリックして移動するか確認しましょう。
---
エンティティのCameraのProjectionを変更して見栄えも確認してみましょう。
Perspective が透視投影、 Orthographic が平行投影です。
最初に見たデモでは、Orthographicを設定しています。
カメラの位置や角度もお好みで設定してみましょう。
最初に見たデモでは、以下のような設定をしています。
また、ライティングもお好みで変更してみましょう。これだけでも雰囲気が変わってきます。
CubeMapもAsset Storeなどから適用するとより見栄えが良くなると思います。
以下の画像では、Asset Storeから「Tropical Beach」をインポートし、Mipを5に設定しています
カメラやライトなどを調整することで、画像のような見栄えに変わります。
ここまでできたら、次はオブジェクトに触れたらテキスト情報の載っているモーダルを表示させる処理まで作ってみましょう。
Chapter03 - オブジェクトに触れたら処理するスクリプトを作成
ここまでで、クリックしてPlayerを移動させることができました。
次は移動させた先に、当たり判定を持ったオブジェクトがあるとイベントを処理させて、テキスト情報を載せたモーダルが表示されるものを作ります。
まずは当たり判定を持ったオブジェクトを作ります。
空のエンティティ「TriggerObject」を作成し、入れ子にPrimitiveなBoxを追加します。
Boxには当たり判定のため、CollisionとRigidBodyを追加します。
TriggerObjectは動く必要がないので、RigidBodyは Type: Static にします。
Materialも設定すると見分けがつきやすくなるのでお好みで設定します。
次はスクリプトを設定します。
「triggerCollision.js」を作成して、エンティティの triggerObject 配下の Box にスクリプトを追加します。
設定ができたら、以下のスクリプトを「triggerCollision.js」にコピペします。
var TriggerCollision = pc.createScript('triggerCollision');
TriggerCollision.prototype.initialize = function() {
// Collisionが発生したときに呼び出されるイベント
this.entity.collision.on('collisionstart', this.onTriggerEnter, this);
// Collisionが終了したときに呼び出されるイベント
this.entity.collision.on('collisionend', this.onTriggerLeave, this);
};
TriggerCollision.prototype.onTriggerEnter = function(result) {
// Collisionが発生したらログを出力
console.log("Collision Enter");
};
TriggerCollision.prototype.onTriggerLeave = function(result) {
// Collisionが終了したらログを出力
console.log("Collision Leave");
};
Playerにも当たり判定が必要になるので、エンティティのPlayerのBoxにも Collision と RigidBody を設定します。
Playerは移動するので、RigidBodyのTypeは Kinematic に設定します。
Launchで確認します。
ぶつかるとコンソールにログを確認することができます。
しかし、TriggerObjectであるBox自体もクリックできてしまい、上画像のように浮いてしまいます。
これを避けるために、クリックしたオブジェクトが Plane のみになるように Tag を設定します。
PlaneにTags「field」を追加します。
スクリプト「playerMove.js」を書き換えます。
「スクリプト属性」を追加します。
PlayerMove.attributes.add("targetTag", {type:"string", default:"field"});
スクリプト属性からクリック時の参照するTagsを設定ができます。
今回はデフォルトで「field」を設定しています。
「doRaycast()」の49行目の「result」の変数定義箇所を書き加えます。
// const result = this.app.systems.rigidbody.raycastFirst(from, to);
const result = this.app.systems.rigidbody.raycastAll(from, to).find(result => result.entity.tags.has(this.targetTag));
raycastFirst() はカメラから見てクリックした直線上にある最初のエンティティだけしか参照しません。そのため、エンティティをクリックしても後ろの Plane は参照してくれません。
それを raycastAll() なら直線上のエンティティを全て参照してくれます。
その中から、field の Tags を持っているか参照するために「find()」を使い result を返しています。
変更ができたらLaunchで確認。
オブジェクトをクリックしてもTagsにfieldを持つPlaneだけを参照しているので、浮くことがなくなります。
上記で一部変更を加えていた「playerMove.js」の最終的なコードが以下になります。
let PlayerMove = pc.createScript('playerMove');
PlayerMove.attributes.add("targetCamera", {type:"entity"});
// 以下のスクリプト属性を追加
PlayerMove.attributes.add("targetTag", {type:"string", default:"field"});
PlayerMove.prototype.initialize = function() {
this.targetPosition = new pc.Vec3();
this.app.mouse.on(pc.EVENT_MOUSEUP, this.mouseUp, this);
this.app.on("move:able", () => {
this.app.mouse.on(pc.EVENT_MOUSEUP, this.mouseUp, this);
}, this);
this.app.on("move:unable", () => {
this.app.mouse.off(pc.EVENT_MOUSEUP, this.mouseUp, this);
}, this);
};
PlayerMove.prototype.update = function(dt) {};
PlayerMove.prototype.mouseUp = function (e) {
this.doRaycast(e.x, e.y);
this.entity.tween(this.entity.getLocalPosition()).to(this.targetPosition, 0.5, pc.Linear).start();
};
PlayerMove.prototype.doRaycast = function(screenX, screenY) {
let from = new pc.Vec3();
let to = new pc.Vec3();
if(this.targetCamera.camera.projection !== pc.PROJECTION_ORTHOGRAPHIC) {
from = this.targetCamera.getPosition();
to = this.targetCamera.camera.screenToWorld(screenX, screenY, this.targetCamera.camera.farClip);
} else {
var farClip = this.targetCamera.camera.farClip;
var nearClip = this.targetCamera.camera.nearClip;
this.targetCamera.camera.screenToWorld(screenX, screenY, nearClip, from);
this.targetCamera.camera.screenToWorld(screenX, screenY, farClip, to);
}
// 以下の変数 result を書き換え
const result = this.app.systems.rigidbody.raycastAll(from, to).find(result => result.entity.tags.has(this.targetTag));
if (!result) return;
this.targetPosition = result.point;
};
オブジェクトに触れたら処理するスクリプトを作成ができました。
次は処理した際に、オブジェクトの上にボタンを表示してみましょう。
Chapter04 - オブジェクトに触れて処理後にボタンを表示
以下の画像のようにオブジェクトの上にボタンを表示します。
例えば、「triggerCollision.js」の各イベントの処理でElementのEnabledを切り替えれば簡単に実装ができます。
下のコードは例えなので本チュートリアルでは使わずの紹介までになります。
var TriggerCollision = pc.createScript('triggerCollision');
// targetElement属性を追加する
TriggerCollision.attributes.add("targetElement", {type: "entity" });
// 初期化関数を定義する
TriggerCollision.prototype.initialize = function() {
// Collisionの範囲に他のエンティティのCollisionが入ったらonTriggerEnter関数を呼び出す
this.entity.collision.on('collisionstart', this.onTriggerEnter, this);
// Collisionの範囲から他のエンティティのCollisionが出たらonTriggerLeave関数を呼び出す
this.entity.collision.on('collisionend', this.onTriggerLeave, this);
};
// 接触開始時に呼ばれる関数
TriggerCollision.prototype.onTriggerEnter = function(result) {
console.log("Collision Enter");
// targetElementを有効にする
this.targetElement.enabled = true;
};
// 接触終了時に呼ばれる関数
TriggerCollision.prototype.onTriggerLeave = function(result) {
console.log("Collision Leave");
// targetElementを無効にする
this.targetElement.enabled = false;
};
今回は「Template」機能を使ってボタンを実装します。
ヒエラルキーに3D Screen、Button、Textと入れ子で追加します。
Button の Tags に cursor を追加します
必要に応じてFontやボタンの画像のアセットも設定します。
Fontデータはデフォルトで設定されていないので、今回は以下のM+を使用します。
https://mplusfonts.github.io/
設定ができたら3D Screenを右クリックからNew Templateでテンプレートを作成します。
その後、ヒエラルキーの「3D Screen」のエンティティは削除してください。
次はスクリプト「showCursor.js」を作成し、以下をコピペします。
var ShowCursor = pc.createScript('showCursor');
// スクリプト属性の追加
ShowCursor.attributes.add("playerCamera", {type:"entity"}); // PlayerのCameraを設定
ShowCursor.attributes.add("cursorTemp", {type:"asset", assetType:"template"}); // 先ほど作成したTemplateを設定
ShowCursor.prototype.initialize = function() {
// ボタンのテンプレートをインスタンス化
this.instance = this.cursorTemp.resource.instantiate();
// イベントリスナーの設定
this.app.on('cursor:set', this.setCursor, this); // setCursor()を 'cursor:set' というイベント名で設定
this.app.on('cursor:remove', this.removeCursor, this); // removeCursor()を 'cursor:remove' というイベント名で設定
// インスタンスをシーンに追加して非表示に設定
this.app.root.addChild(this.instance);
this.instance.enabled = false;
};
ShowCursor.prototype.update = function(dt) {
// ボタンをカメラの方向に向ける
this.instance.lookAt(this.playerCamera.getPosition());
this.instance.rotateLocal(0, 180, 0); // lookAt()が特定の方向を向くため、上記のままではボタンが反対方向を向いてしまうためY軸を180回転させます
};
ShowCursor.prototype.setCursor = function(targetEntity) {
// ボタンをターゲットの位置に表示
this.instance.enabled = true;
this.instance.setPosition(
targetEntity.getPosition().x,
3,
targetEntity.getPosition().z
);
};
ShowCursor.prototype.removeCursor = function() {
// ボタンを非表示にする
this.instance.enabled = false;
};
作成した showCursor.js は、エンティティ「Root」 に設定します。
Parseを押し、スクリプト属性を表示させましょう。
スクリプト属性に Player の Camera と先ほど作成した Template をそれぞれ登録します。
このTemplateはスクリプト内でインスタンス化して使用されます。
PlayerのCameraを設定したのは、ボタンの向きが常にカメラの方を向くために設定しています。
ボタンを表示するイベントを作成し準備ができたので、次はPlayerがオブジェクトにぶつかった時にボタンを表示するイベントを呼ぶように書き足します。
「triggerCollision.js」 に、Playerがぶつかった時にボタンを表示する処理を追加します。
10行目以降の onTriggerEnter() と onTriggerLeave() を以下のように書き換えます。
// 衝突が発生したときに実行される関数
TriggerCollision.prototype.onTriggerEnter = function(result) {
console.log("Collision Enter");
// イベントを発火し、カーソルの位置を設定する
this.app.fire('cursor:set', this.entity); // showCursor.js で作成したイベントを引数に当たり判定のBoxを設定して処理して、このオブジェクト上にボタンを表示。
};
// 衝突が終了したときに実行される関数
TriggerCollision.prototype.onTriggerLeave = function(result) {
console.log("Collision Leave");
// イベントを発火し、カーソルを削除する
this.app.fire('cursor:remove'); // showCursor.js で作成したイベントを処理してボタンが非表示に。
};
this.app.fire()は、this.app.on()で設定したイベントを呼ぶことができます。
ここでは、showCursor.js で this.app.on('cursor:set') を設定し、triggerCollision.js でthis.app.fire('cursor.set')でイベントを処理した、という処理が行われています。
スクリプトのコードが書き加えられたら、Launchで確認してみましょう。
当たり判定のオブジェクトの上にボタンが表示されていればOKです。
オブジェクトに当たったらボタンを表示までができました。
次は、ボタンをクリックしてDOM要素を使ってモーダルを表示してみましょう。
Chapter05 - ボタンをクリックし、モーダルを表示
モーダルを表示させるために、ボタンをクリック時に処理をするスクリプトを作成していきます。
エンティティ「Root」に「showModal.js」を作成し追加します。
追加後、showModal.jsをEditorで開き、以下のコードをコピペします。
var ShowModal = pc.createScript('showModal');
ShowModal.prototype.initialize = function() {
this.modalFlag = false;
document.body.style.fontSize = "16px"; // 基準となるフォントサイズを指定
this.app.on('modal:set', this.setModal, this); // setModal() を呼ぶイベントを作成
this.app.on('modal:remove', this.removeModal, this); // removeModal() を呼ぶイベントを作成
};
ShowModal.prototype.setModal = function() {
if(this.modalFlag) return false;
// モーダルのdiv要素を作成
let targetModal = document.createElement("div");
targetModal.id = "showModal"; // 表示非表示など処理をするために、idを設定
// 以下はstyle設定のため説明割愛
targetModal.style.display = "flex";
targetModal.style.flexWrap = "nowrap";
targetModal.style.flexDirection = "column";
targetModal.style.position = "fixed";
targetModal.style.top = "50%";
targetModal.style.left = "50%";
targetModal.style.transform = "translate(-50%, -50%)";
targetModal.style.width = "75%";
targetModal.style.height = "75%";
targetModal.style.maxWidth = "720px";
targetModal.style.maxheight = "540px";
targetModal.style.padding = "40px 20px";
targetModal.style.backgroundColor = "rgba(75, 75, 75, 1)";
targetModal.style.border = "3px solid #cccccc";
targetModal.style.borderRadius = "5px";
targetModal.style.boxSizing = "border-box";
targetModal.style.overflow = "auto";
// モーダル内のheadテキストを作成
let head = document.createElement("h2");
// 以下はstyle設定のため説明割愛
head.style.color = "rgb(255, 255, 255)";
head.style.fontSize = "1.6rem";
head.style.fontWeight = "bold";
head.style.lineHeight = 1.2;
head.style.letterSpacing = "0.01em";
head.innerHTML = "ヘッダー"; // テキスト内容を入力
targetModal.appendChild(head); // モーダル内に追加する
// 本文となる要素を追加
let inner = document.createElement("div");
// 以下はstyle設定のため説明割愛
inner.style.color = "rgb(255, 255, 255)";
inner.style.fontSize = "1rem";
inner.style.lineHeight = 1.5;
inner.style.letterSpacing = "0.05em";
inner.innerHTML = "<p>ここか本文のテキストになる</p>"; // 入るテキストはHTMlの要素で入れる想定
targetModal.appendChild(inner); // モーダル内に追加する
// モーダルを閉じる要素を作成
let closeButton = document.createElement("div");
// 以下はstyle設定のため説明割愛
closeButton.style.position = "absolute";
closeButton.style.top = 0;
closeButton.style.right = 0;
closeButton.style.padding = ".5rem 1rem";
closeButton.style.color = "#ffffff";
closeButton.style.fontSize = "2rem";
closeButton.style.fontWeight = "bold";
closeButton.style.lineHegiht = 1;
closeButton.style.letterSpacing = 0;
closeButton.style.cursor = "pointer";
closeButton.innerHTML = "×"; // 閉じるボタンのバッテンをかける記号で設定
targetModal.appendChild(closeButton); // モーダル内に追加する
closeButton.addEventListener("click", this.removeModal.bind(this)); // 閉じるボタンにクリックイベントの処理を設定
document.body.appendChild(targetModal); // モーダルをbody要素に追加
this.modalFlag = true; // モーダルが表示されている時はTrueのフラグ
this.app.fire("move:unable"); // playerMove.jsで設定したイベント。これでPlaneをクリックしても移動しなくなるようにイベントをoffしている
};
ShowModal.prototype.removeModal = function() {
if(!this.modalFlag) return false; // モーダルが表示されている時のみスルー
document.getElementById("showModal").remove(); // モーダルに設定したidを参照してモーダルを削除
this.modalFlag = false // モーダルが非表示の時はFalseのフラグ
this.app.fire("move:able"); // playerMove.jsで設定したイベント。これでPlaneをクリックすると移動するようになるようにイベントを再度onしている
};
コードのおおよそがDOM要素を作成するコードになっています。
ここで作成したイベント、「modal:set」をボタンをクリックしたときに呼び出します。
テンプレート化したボタンのButtonエンティティには、Tags「cursor」を設定しています。
このTagsを参照するようにしてイベントを呼び出す処理を「showCursor.js」の、
26行目の「setCursor()」と36行目の「removeCursor()」を以下に書き換え、追記していきます。
ShowCursor.prototype.setCursor = function(targetEntity) {
this.instance.enabled = true;
this.instance.setPosition(
targetEntity.getPosition().x,
3,
targetEntity.getPosition().z
);
// 以下を追加
for(let i=0; i < this.instance.findByTag("cursor").length; i++) { // クリックしたボタンのEntityの中にcursorのTagsを持つEntityを探す
let funcModalSet = () => {
this.app.fire('modal:set'); // ここでmodal:setのイベント
};
this.instance.findByTag("cursor")[i].element.on(pc.EVENT_MOUSEUP, funcModalSet.bind(this)); // cursorのTagsを持つEntityにイベントを設定
}
};
ShowCursor.prototype.removeCursor = function() {
this.instance.enabled = false;
// 以下を追加
for(let i=0; i < this.instance.findByTag("cursor").length; i++) { // クリックしたボタンのEntityの中にcursorのTagsを持つEntityを探す
this.instance.findByTag("cursor")[i].element.off(pc.EVENT_MOUSEUP); // cursorのTagsを持つEntityに設定したイベントを削除
}
};
上記コードを追記できたら、Launchで確認してみましょう。
オブジェクトに触れて表示されたボタンをクリックすると、画像のようなモーダルが表示されます。
その後、右上の閉じるボタンの × をクリックするとモーダルが閉じます。
これで一通りの処理ができました。
次はモーダルのテキストをオブジェクトごとに設定できるようにしていきます。
Chapter06 - オブジェクトごとにモーダルのテキストを設定する
まず、オブジェクトに設定していた「triggerCollision.js」に、以下のスクリプト属性を追記します。
TriggerCollision.attributes.add("headText",{ type:"string", default:"これがHEADです"}); // ヘッダーのテキストを入力
TriggerCollision.attributes.add("innerHtml",{ type:"asset", assetType: "html"}); // 本文のHTMLをアセットからhtmlファイルを設定する
TriggerCollision.attributes.add("textColor",{ type:"rgb", default: [1,1,1]}); // テキストのカラーを指定
TriggerCollision.attributes.add("backgroundColor",{ type:"rgba", default: [0,0,0,1]}); // モーダルの背景色を指定
これでオブジェクトごとにテキストや色を設定する用意ができました。
ここで使用する HTML を New Asset から作成しましょう。
アセット名やHTMLの内容はお好みで問題ありません。
例として以下のHTMLを用意しましたので、HTMLのアセットを作成して以下をご活用ください
<p>
このオブジェクトに当たると<br>
ボタンが表示されて<br>
ボタンをクリックすると<br>
このモーダルが表示されるよ!
</p>
<p>モーダルを閉じる場合は右上の閉じるボタンをクリックしてね</p>
<p>
<a style="color: #fff;" href="https://playcanvas.jp/" target="_blank" rel="noopener">PlayCanvas日本公式サイトへ</a>
</p>
先にスクリプト属性を画像のように設定しましょう。
設定したスクリプト属性を使ってモーダルのstyleに割り当てていきます。
まずは、「showModal.js」の10行目の「setModal()」に引数としてスクリプト属性を扱うために、「setModal()」を以下に書き換えていきます。
// モーダル表示処理を定義
ShowModal.prototype.setModal = function(headText, innerHtml, textColor, backgroundColor) { // 引数にスクリプト属性を入れて処理していく
// モーダルが表示中であれば何もしない
if(this.modalFlag) return false;
// モーダル要素を生成し、スタイルを設定
let targetModal = document.createElement("div");
targetModal.id = "showModal";
targetModal.style.display = "flex";
targetModal.style.flexWrap = "nowrap";
targetModal.style.flexDirection = "column";
targetModal.style.position = "fixed";
targetModal.style.top = "50%";
targetModal.style.left = "50%";
targetModal.style.transform = "translate(-50%, -50%)";
targetModal.style.width = "75%";
targetModal.style.height = "75%";
targetModal.style.maxWidth = "720px";
targetModal.style.maxheight = "540px";
targetModal.style.padding = "40px 20px";
// 背景色はこのbackgroundで指定する。引数のbackgroundColorは0~1までを扱うため、色指定のため255で積を求めます。
targetModal.style.backgroundColor = "rgba(" + backgroundColor.r*255 + "," + backgroundColor.g*255 + "," + backgroundColor.b*255 + "," + backgroundColor.a + ")";
targetModal.style.border = "3px solid #cccccc";
targetModal.style.borderRadius = "5px";
targetModal.style.boxSizing = "border-box";
targetModal.style.overflow = "auto";
// モーダルの見出しを生成し、スタイルを設定
let head = document.createElement("h2");
// テキスト色はこのcolorで指定する。引数のtextColorは0~1までを扱うため、色指定のため255で積を求めます。
head.style.color = "rgb(" + textColor.r*255 + "," + textColor.g*255 + "," + textColor.b*255 + ")";
head.style.fontSize = "1.6rem";
head.style.fontWeight = "bold";
head.style.lineHeight = 1.2;
head.style.letterSpacing = "0.01em";
head.innerHTML = headText; // HEADのテキストはここで代入します
targetModal.appendChild(head);
// モーダルの内容を生成し、スタイルを設定
let inner = document.createElement("div");
// テキスト色はこのcolorで指定する。引数のtextColorは0~1までを扱うため、色指定のため255で積を求めます。
inner.style.color = "rgb(" + textColor.r*255 + "," + textColor.g*255 + "," + textColor.b*255 + ")";
inner.style.fontSize = "1rem";
inner.style.lineHeight = 1.5;
inner.style.letterSpacing = "0.05em";
inner.innerHTML = innerHtml.resource; // HTMLのAssetはここでresourceで中身をそのまま代入します
targetModal.appendChild(inner);
// モーダルの閉じるボタンを生成し、スタイルを設定
let closeButton = document.createElement("div");
closeButton.style.position = "absolute";
closeButton.style.top = 0;
closeButton.style.right = 0;
closeButton.style.padding = ".5rem 1rem";
closeButton.style.color = "#ffffff";
closeButton.style.fontSize = "2rem";
closeButton.style.fontWeight = "bold";
closeButton.style.lineHegiht = 1;
closeButton.style.letterSpacing = 0;
closeButton.style.cursor = "pointer";
closeButton.innerHTML = "×";
targetModal.appendChild(closeButton);
// 閉じるボタンがクリックされたときの処理を設定
closeButton.addEventListener("click", this.removeModal.bind(this));
// モーダルを body 要素に追加し、フラグを true に設定
document.body.appendChild(targetModal);
this.modalFlag = true;
// move:unable イベントを発火
this.app.fire("move:unable");
};
この「setModal()」は「modal:set」でイベントを作成して使用しています。
「modal:set」のイベントを使用しているのは「showCursor.js」の「setCursor()」です。
この26行目の「setCursor()」を以下のように書き換えていきます。
ShowCursor.prototype.setCursor = function(targetEntity) {
this.instance.enabled = true;
this.instance.setPosition(
targetEntity.getPosition().x,
3,
targetEntity.getPosition().z
);
for(let i=0; i < this.instance.findByTag("cursor").length; i++) {
let funcModalSet = () => {
this.app.fire('modal:set',
targetEntity.script.scripts[0].headText, // targetEntityが各オブジェクトなので、
targetEntity.script.scripts[0].innerHtml, // オブジェクトの持つScriptを参照しスクリプト属性を参照します。
targetEntity.script.scripts[0].textColor, // 左からの引数の順番を間違えないように、headText, innerHtml,
targetEntity.script.scripts[0].backgroundColor // textColor, backgroundColorを設定していきましょう。
);
};
this.instance.findByTag("cursor")[i].element.on(pc.EVENT_MOUSEUP, funcModalSet.bind(this));
}
};
ここまで書き換えができたら、Launchで確認してみましょう。
設定したスクリプト属性の通りにモーダルが切り替わっていれば、本チュートリアルは完成です!
あとはお好みでオブジェクトを増やしたり、オブジェクトの形を変えたり…
キャラクターの3Dモデルをインポートしてアニメーションなど…
参考: 3DキャラクターをPlayCanvasのState Graphで操作する
自由に追加・変更をして、自分だけのコンテンツにしましょう!
---
おまけ:Playerの移動速度を等速にしたい
クリック後にPlayerが移動する際に、Tweenでは目標地点へ指定した時間で移動する設定になっているため、距離に応じて移動速度が変わってしまう…という問題があります。
これが気になる場合は時間で指定ではなく、等速な移動速度で目標地点へ移動するように設定をします。
これを解決するために、 playerMove.js を以下のコードに書き換えます。
let PlayerMove = pc.createScript('playerMove');
PlayerMove.attributes.add("targetCamera", {type:"entity"});
PlayerMove.attributes.add("targetTag", {type:"string", default:"field"});
// 追加
PlayerMove.attributes.add("moveType", {type:"boolean", enum:[{'Tween': true},{'Linear': false}]}); // TweenとLinear移動を属性から選択可能に
PlayerMove.attributes.add("speed", {type:"number", default:5, description: "Tweenなら移動時間、Linearなら移動速度を指定する" });
PlayerMove.prototype.initialize = function() {
this.time = 0;
this.duration = 1;
this.direction = new pc.Vec3();
this.targetPosition = new pc.Vec3();
this.lerpRotate = new pc.Vec4();
this.app.mouse.on(pc.EVENT_MOUSEUP, this.mouseUp, this);
this.on('destroy', function() {
this.app.mouse.off(pc.EVENT_MOUSEUP, this.mouseUp, this);
}, this);
this.app.on("move:able", () => {
this.app.mouse.on(pc.EVENT_MOUSEUP, this.mouseUp, this);
}, this);
this.app.on("move:unable", () => {
this.app.mouse.off(pc.EVENT_MOUSEUP, this.mouseUp, this);
}, this);
if(this.app.touch) {
this.app.mouse.on(pc.EVENT_TOUCHEND, this.mouseUp, this);
this.on('destroy', function() {
this.app.mouse.off(pc.EVENT_TOUCHEND, this.mouseUp, this);
}, this);
this.app.on("move:able", () => {
this.app.mouse.on(pc.EVENT_TOUCHEND, this.mouseUp, this);
}, this);
this.app.on("move:unable", () => {
this.app.mouse.off(pc.EVENT_TOUCHEND, this.mouseUp, this);
}, this);
}
};
// 追加
PlayerMove.prototype.update = function(dt) {
if(!this.moveType) { // Linearの場合
if (this.direction.lengthSq() > 0) { // lengthSq()は選択した3次元ベクトルの大きさの2乗を返す
let d = this.speed * dt; // 移動スピードを設定
let newPosition = new pc.Vec3();
newPosition.copy(this.direction).scale(d); // 座標を処理する変数にコピーし、移動速度の積を求める
newPosition.add(this.entity.getPosition()); // 移動する座標にEntityの座標を追加
this.entity.setPosition(newPosition); // 新しい座標をEntityに代入
this.distanceToTravel -= d; // 移動処理したベクトル分をマイナス
if (this.distanceToTravel <= 0) { // 移動するベクトル量がない場合
this.entity.setPosition(this.targetPosition); // Entityの座標にクリックした座標を設定
this.direction.set(0, 0, 0); // 移動するベクトルをリセット
}
}
}
};
PlayerMove.prototype.mouseUp = function (e) {
this.doRaycast(e.x, e.y);
// 追加
if(this.moveType) { // Tweenの場合
this.entity.tween(this.entity.getLocalPosition()).to(this.targetPosition, this.speed, pc.Linear).start();
}
};
PlayerMove.prototype.doRaycast = function(screenX, screenY) {
let from = new pc.Vec3();
let to = new pc.Vec3();
if(this.targetCamera.camera.projection !== pc.PROJECTION_ORTHOGRAPHIC) {
from = this.targetCamera.getPosition();
to = this.targetCamera.camera.screenToWorld(screenX, screenY, this.targetCamera.camera.farClip);
} else {
let farClip = this.targetCamera.camera.farClip;
let nearClip = this.targetCamera.camera.nearClip;
this.targetCamera.camera.screenToWorld(screenX, screenY, nearClip, from);
this.targetCamera.camera.screenToWorld(screenX, screenY, farClip, to);
}
// const result = this.app.systems.rigidbody.raycastFirst(from, to);
const result = this.app.systems.rigidbody.raycastAll(from, to).find(result => result.entity.tags.has(this.targetTag));
if (!result) return;
this.targetPosition = result.point;
this.entity.findByTag("player")[0].lookAt(
this.targetPosition.x,
this.entity.findByTag("player")[0].getPosition().y,
this.targetPosition.z
);
// 追加
this.movePlayerTo(this.targetPosition); // Linear移動用
};
// 追加
PlayerMove.prototype.movePlayerTo = function () { // Linear移動用
this.targetPosition.y = this.entity.getPosition().y; // y軸は同じなのでそのまま代入
this.direction.sub2(this.targetPosition, this.entity.getPosition()); // 現在の地点と目標点を互いに減算
this.distanceToTravel = this.direction.length(); // 移動するベクトル量を代入
if (this.distanceToTravel > 0) {
this.direction.normalize(); // ベクトルに変換
} else {
this.direction.set(0, 0, 0); // 移動する方向がなければリセット
}
};
Tweenと等速移動(コード上ではLinearの意)をスクリプト属性で切り替えができる等していますので、Launchでそれぞれ比較してみてください。
またTweenの移動時間、等速移動の速度も同様にスクリプト属性で変更ができるようにしています。
合わせて、Player内に設定したModel (チュートリアルのままならBox) にTags「player」を設定してください。
ここで追加した等速移動の処理は、 Point and click movement というチュートリアルのコードから引用しています。
コメント
0件のコメント
サインインしてコメントを残してください。