はじめに
VRでの衝突判定を作成後、以前からやりたかったNPCとぶつかった際に、ポップアップメッセージを表示させる実装をしてみました。
市販ゲームは、画面の下にメッセージ用のダイアログを表示させる形式が多いですが、 その場合、HTML・CSSで比較的簡単にできるので、今回はキャラクターの頭上付近に吹き出しメッセージを実装を試してます。
今回もVRモードで動画を撮ってみました。
1. なぜ吹き出しにしたか
Three.js でキャラクターを配置すると、次にやりたくなるのは
「このキャラが何か反応している感じを出したい」
よくある方法としては、
- 画面下にメッセージウィンドウを出す
- UIとしてテキストを固定表示する
といったやり方があります。
ただ今回は、あえてそれを選びませんでした。
理由はシンプルで、
- キャラクター自身の存在感を強めたかった
- 「空間にいるキャラ」が喋っているように見せたかった
- VRでは画面固定UIが視界の邪魔になりやすい
という点があったからです。
特に VR では、 画面下や画面中央にUIを出すと没入感を壊しやすくなります。
一方で、 キャラクターの頭上に短いメッセージが出るだけなら、
- 視線移動が自然
- 今何が起きているか直感的
- キャラとの距離感が分かりやすい
というメリットがあります。
「吹き出し」というよりは、 キャラの上にセリフが浮かぶ、そのくらいの軽い表現を目指しました。
2. DOMではなくSpriteを使った理由
最初に考えたのは、 HTML(DOM)で吹き出しを作り、CSSで位置を制御する方法です。
ただ、この方法には問題がありました。
- 3D座標と2D座標の変換が必要
- カメラ移動やVR時のズレ対策が面倒
- VRとPCで分岐が増える
そこで選んだのが THREE.Sprite です。
Sprite は、
- 常にカメラ正面を向く
- 3D空間の座標にそのまま置ける
- VR / non-VR 共通で使える
という特徴があります。
「キャラの頭上にテキストを浮かせたい」という用途には、 Sprite がかなり相性が良いです。
3. CanvasTextureでセリフを描画する
Sprite に直接テキストを書くことはできないので、 Canvas に文字を描いて、それをテクスチャとして使う構成にしました。
基本的な流れは以下です。
- Canvas を生成
- 背景を描画
- テキストを描画
- CanvasTexture に変換
- Sprite に貼る
実際のコードはこのようになります。
createTextSprite(text) {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
canvas.width = 700;
canvas.height = 140;
// 背景
ctx.clearRect(0, 0, canvas.width, canvas.height);
// 角丸背景
this.roundRect(ctx, 10, 10, canvas.width - 20, canvas.height - 20, 20);
ctx.fillStyle = 'rgba(0,0,0,0.6)';
ctx.fill();
// テキスト設定
ctx.fillStyle = '#fff';
ctx.font = '27px sans-serif';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
// 改行対応
const lines = text.split('\n');
const lineHeight = 32;
const totalHeight = lines.length * lineHeight;
const startY = canvas.height / 2 - totalHeight / 2 + lineHeight / 2;
lines.forEach((line, i) => {
ctx.fillText(line, canvas.width / 2, startY + i * lineHeight);
});
const texture = new THREE.CanvasTexture(canvas);
const material = new THREE.SpriteMaterial({ map: texture, transparent: true });
const sprite = new THREE.Sprite(material);
sprite.scale.set(1.5, 0.4, 1);
return sprite;
}
4. 角丸背景を描くための関数
Canvas には標準で角丸矩形を描くAPIがないため、 簡単なヘルパー関数を用意しています。
roundRect(ctx, x, y, w, h, r) {
ctx.beginPath();
ctx.moveTo(x + r, y);
ctx.lineTo(x + w - r, y);
ctx.quadraticCurveTo(x + w, y, x + w, y + r);
ctx.lineTo(x + w, y + h - r);
ctx.quadraticCurveTo(x + w, y + h, x + w - r, y + h);
ctx.lineTo(x + r, y + h);
ctx.quadraticCurveTo(x, y + h, x, y + h - r);
ctx.lineTo(x, y + r);
ctx.quadraticCurveTo(x, y, x + r, y);
ctx.closePath();
}
全面を fillRect で塗ってしまうと角丸が分からなくなるため、
背景はこの roundRect のみで描画しています。
5. キャラクターの頭上に表示する
作成した Sprite は、 キャラクターのモデルにそのまま追加します。
say(text) {
if (this.talkSprite) {
this.model.remove(this.talkSprite);
}
const sprite = this.createTextSprite(text);
sprite.position.set(0, 2.2, 0); // 頭の上
this.model.add(sprite);
this.talkSprite = sprite;
setTimeout(() => {
this.model.remove(sprite);
this.talkSprite = null;
}, 3000);
}
Sprite は常にカメラを向くため、 回転や向きの調整は不要です。
6. セリフはデータとして持たせる
セリフをコードに直書きすると管理が面倒になるため、 キャラクターデータ側に持たせています。
"dialogue": {
"random": [
"うぇるかむかもーん!",
"チノちゃん、見なかった?",
"Rabbit Houseに遊びに来てね!",
"みんな!パン作りをなめちゃいけないよ。\nすこしのミスが完成度を左右する戦いなんだからね!"
]
}
改行は \n のまま使い、
Canvas 側で分割して描画しています。
7. セリフ表示に効果音を足す
仕上げとして、 セリフ表示と同時に軽い効果音を鳴らしています。
const se = new Audio('./assets/sound/ペタッ.mp3');
se.volume = 0.4;
se.play();
BGMのような重い実装は行わず、 反応音として最低限の演出に留めています。
8. NPCをプレイヤーの方へ向かせる
動画を見ると分かりますが、普通に接触してメッセージを出すだけだとNPCが違う方向を向いて違和感があるので、プレイヤーの方へ方向転換する処理も追加しています。
基本コード
rotateTowardsPlayer(deltaTime) {
const basePos = this.isVR
? config.playerRig.position
: config.player.box.position;
const targetPos = basePos.clone();
targetPos.y = this.model.position.y;
const m = new THREE.Matrix4();
m.lookAt(
targetPos, // ※モデル都合で逆を使用
this.model.position,
this.model.up
);
const targetQuat = new THREE.Quaternion()
.setFromRotationMatrix(m);
const rotateSpeed = 2.0; // 回転速度
this.model.quaternion.slerp(
targetQuat,
Math.min(1, rotateSpeed * deltaTime)
);
}
※実装上の事情として モデルの forward 軸の都合で lookAt を逆にしています
呼び出しタイミング
「常に向く」はやらない。
if (playerBox.intersectsBox(this.box)) {
this.rotateTowardsPlayer(deltaTime);
}
- 近づいた時だけ向く
- 離れたらそのまま
なぜ「完全に向かせない」のが良いか
- 急にクルッと回らない
- 視線が合った感じになる
- 威圧感が出ない
特に VR では 真正面を向かせすぎると怖いため。
NPCがこちらを向くだけで、 セリフが「空間の装飾」ではなく 「プレイヤーへの反応」に変わります。
💬 コメント