どうもです、タドスケです。
前回はマウス入力を扱ったので、今回はキーボード入力を使ってゲームに応用できそうなものを作ってみました。
完成品
- WASD キーで上下左右に移動できます(※斜め移動はできません)
コード
コードは以下です。
(ChatGPT を使用して生成したものを手直ししています)
<!DOCTYPE html>
<html>
<head>
<title>3D ステージ移動</title>
<style>
body { margin: 0; }
canvas { display: block; }
</style>
</head>
<body>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<script>
// シーンの初期化を行うクラス
class SceneInitializer {
constructor() {
this.scene = new THREE.Scene();
this.camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
this.camera.position.y += 6;
this.renderer = new THREE.WebGLRenderer();
this.renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(this.renderer.domElement);
}
getScene() {
return this.scene;
}
getCamera() {
return this.camera;
}
getRenderer() {
return this.renderer;
}
}
// ライトの設定を行うクラス
class LightManager {
constructor(scene) {
this.scene = scene;
}
setupLights() {
const ambientLight = new THREE.AmbientLight(0xffffff, 0.3);
this.scene.add(ambientLight);
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.7);
directionalLight.position.set(200, 500, 300);
this.scene.add(directionalLight);
}
}
// ステージと壁の生成を行うクラス
class StageCreator {
constructor(scene) {
this.scene = scene;
this.STAGE_SIZE = 20;
this.WALL_HEIGHT = 2;
this.WALL_THICKNESS = 1;
}
createStage() {
const geometry = new THREE.BoxGeometry(this.STAGE_SIZE, 1, this.STAGE_SIZE);
const material = new THREE.MeshLambertMaterial({ color: 0x808080 });
const stage = new THREE.Mesh(geometry, material);
stage.position.y = -1;
this.scene.add(stage);
}
createWalls() {
const wallMaterial = new THREE.MeshLambertMaterial({ color: 0x0000ff });
// 壁の位置を定義
const positions = [
{ x: 0, y: this.WALL_HEIGHT / 4, z: -this.STAGE_SIZE / 2 - this.WALL_THICKNESS / 2, width: this.STAGE_SIZE, height: this.WALL_HEIGHT, depth: this.WALL_THICKNESS }, // 前
{ x: 0, y: this.WALL_HEIGHT / 4, z: this.STAGE_SIZE / 2 + this.WALL_THICKNESS / 2, width: this.STAGE_SIZE, height: this.WALL_HEIGHT, depth: this.WALL_THICKNESS }, // 後
{ x: -this.STAGE_SIZE / 2 - this.WALL_THICKNESS / 2, y: this.WALL_HEIGHT / 4, z: 0, width: this.WALL_THICKNESS, height: this.WALL_HEIGHT, depth: this.STAGE_SIZE }, // 左
{ x: this.STAGE_SIZE / 2 + this.WALL_THICKNESS / 2, y: this.WALL_HEIGHT / 4, z: 0, width: this.WALL_THICKNESS, height: this.WALL_HEIGHT, depth: this.STAGE_SIZE } // 右
];
// 壁の生成
this.walls = positions.map(pos => {
const geometry = new THREE.BoxGeometry(pos.width, pos.height, pos.depth);
const wall = new THREE.Mesh(geometry, wallMaterial);
wall.position.set(pos.x, pos.y, pos.z);
this.scene.add(wall);
return wall;
});
return this.walls;
}
}
// プレイヤーの生成を行うクラス
class PlayerCreator {
constructor(scene) {
this.scene = scene;
}
createPlayer() {
const geometry = new THREE.BoxGeometry(1, 1, 1);
const material = new THREE.MeshLambertMaterial({ color: 0xff0000 });
const player = new THREE.Mesh(geometry, material);
this.scene.add(player);
return player;
}
}
// 障害物の生成を行うクラス
class ObstacleManager {
constructor(scene, numberOfObstacles) {
this.scene = scene;
this.numberOfObstacles = numberOfObstacles;
}
createObstacles() {
const obstacles = [];
const obstacleGeometry = new THREE.BoxGeometry(1, 1, 1); // 障害物のサイズ
const obstacleMaterial = new THREE.MeshLambertMaterial({ color: 0x00ff00 }); // 障害物の色
for (let i = 0; i < this.numberOfObstacles; i++) {
const obstacle = new THREE.Mesh(obstacleGeometry, obstacleMaterial);
let x, z;
do {
x = Math.floor(Math.random() * 20 - 10); // ステージサイズに基づくランダムな整数X座標
z = Math.floor(Math.random() * 20 - 10); // ステージサイズに基づくランダムな整数Z座標
} while (x === 0 && z === 0); // プレイヤーの初期位置を避ける
obstacle.position.set(x, 0, z);
this.scene.add(obstacle);
obstacles.push(obstacle);
}
return obstacles;
}
}
// キーボード入力を処理するクラス
class InputHandler {
constructor() {
this.keys = {};
document.addEventListener('keydown', (event) => {
this.keys[event.key] = true;
});
document.addEventListener('keyup', (event) => {
this.keys[event.key] = false;
});
}
// キーが押されているか?
isKeyPressed(key) {
return this.keys[key];
}
}
// ゲームの状態を更新するクラス
class GameUpdater {
constructor(player, camera, inputHandler, walls, obstacles) {
this.player = player;
this.camera = camera;
this.inputHandler = inputHandler;
this.walls = walls;
this.obstacles = obstacles;
this.playerSpeed = 0.1;
this.playerBox = new THREE.Box3().setFromObject(this.player); // プレイヤーの境界ボックス
}
// 更新
update() {
this.handlePlayerMovement();
this.updateCamera();
}
// プレイヤーの移動を処理する
handlePlayerMovement() {
let deltaPosition = new THREE.Vector3();
let moveHorizontal = false;
let moveVertical = false;
if (this.inputHandler.isKeyPressed('w')) {
deltaPosition.z -= this.playerSpeed;
moveVertical = true;
}
if (this.inputHandler.isKeyPressed('s')) {
deltaPosition.z += this.playerSpeed;
moveVertical = true;
}
if (this.inputHandler.isKeyPressed('a')) {
deltaPosition.x -= this.playerSpeed;
moveHorizontal = true;
}
if (this.inputHandler.isKeyPressed('d')) {
deltaPosition.x += this.playerSpeed;
moveHorizontal = true;
}
if (moveHorizontal && moveVertical) {
deltaPosition.x = 0;
deltaPosition.z = 0;
}
// 新しい位置での衝突を確認する
if (!this.checkCollisions(deltaPosition)) {
this.player.position.add(deltaPosition);
this.playerBox.copy(new THREE.Box3().setFromObject(this.player));
}
}
checkCollisions(deltaPosition) {
const newPlayerBox = this.playerBox.clone().translate(deltaPosition);
const collidedWithWall = this.walls.some(wall => newPlayerBox.intersectsBox(new THREE.Box3().setFromObject(wall)));
const collidedWithObstacle = this.obstacles.some(obstacle => newPlayerBox.intersectsBox(new THREE.Box3().setFromObject(obstacle)));
return collidedWithWall || collidedWithObstacle;
}
// カメラの位置を更新する
updateCamera() {
this.camera.position.x = this.player.position.x;
this.camera.position.z = this.player.position.z + 5;
this.camera.lookAt(this.player.position);
}
}
// ゲームの実行
const sceneInitializer = new SceneInitializer();
const lightManager = new LightManager(sceneInitializer.getScene());
lightManager.setupLights();
const playerCreator = new PlayerCreator(sceneInitializer.getScene());
const player = playerCreator.createPlayer();
const stageCreator = new StageCreator(sceneInitializer.getScene());
stageCreator.createStage();
const walls = stageCreator.createWalls(); // 壁のリストを取得
const obstacleManager = new ObstacleManager(sceneInitializer.getScene(), 10);
const obstacles = obstacleManager.createObstacles();
const inputHandler = new InputHandler();
const updater = new GameUpdater(player, sceneInitializer.getCamera(), inputHandler, walls, obstacles);
function animate() {
requestAnimationFrame(animate);
updater.update();
sceneInitializer.getRenderer().render(sceneInitializer.getScene(), sceneInitializer.getCamera());
}
animate();
</script>
</body>
</html>
ChatGPT プロンプト
ChatGPT へのプロンプトは以下のようにしました。
あなたはプロのゲームプログラマーです。
斜め見下ろし型で、プレイヤーがステージ上を上下左右に移動するプログラムを書いてください。
– Three.js を CDN で使うこと
– プレイヤーとステージはキューブを組み合わせて作ること
– キーボードの WASD キーで上下左右に移動できる
– 斜め移動はできない – ステージ外周には壁があり、壁を越えて移動はできない
– プレイヤーは常に画面の中央に表示し、プレイヤーの移動に合わせてカメラも平行移動させること
– コードは処理単位で関数分割を行い、日本語のコメントをつけること
初回からそれなりに動くものができますが、機能を加えてコード量が増えてくると、だんだんとレスポンスが遅くなったり、精度が落ちてきたりします。
レスポンスと精度を保つためには、こまめにリファクタリングを行ってもらい、クラス・関数を小さくしておくのがコツです。
初期化、入力処理、更新処理をそれぞれクラスに分割して
コード全体だと多すぎるときは、クラスを指定して分割してもらうのも手です。
GameInitializer クラスについて、処理単位ごとにクラスを細分化して。
コードには日本語のコメントをつけて。
うまく動かないときは、ブラウザの開発者ツール(Edge なら Ctrl+Shift+I)のコンソールを開き、エラーメッセージが出ていたら、それを ChatGPT に伝えます。
以下のエラーが出ます
Uncaught ReferenceError: numberOfObstacles is not defined at ObstacleManager.createObstacles (3d_move.html:121:37) at 3d_move.html:237:43
実装のポイント
ステージ内の障害物(緑色の四角)は、開始時にランダム配置されます。
配置はグリッド状になるように整数のみの乱数を使い、プレイヤーの初期位置と重ならないようにしています。
障害物を応用すれば、「触れたら消えるコインのようなオブジェクト」や、「触れたらクリアになるゴールポイント」なども追加できそうです。
コメント
コメント一覧 (1件)
[…] 【Three.js】3D ステージ内の移動 | しぬまでワクワクしていたい どうもです、タドスケです。 […]