cannon.jsで駄菓子屋10円ゲームをつくる(1)の続きです。今回はボール、コースなどを描画、動作させるためのクラスを定義します。
Ballクラスの定義
Ballクラスを定義します。コンストラクタを示します。
ボールの重さ、反発係数、摩擦係数を指定して最初は動かないように固定します。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
class Ball { constructor(x, y){ this.InitX = x; this.InitY = y; this.Body = new CANNON.Body({ mass: 0.01, // 単位は kg shape: new CANNON.Sphere(BALL_RADIUS), position: new CANNON.Vec3(this.InitX, 1000, 0), material: new CANNON.Material({ restitution: 0.4, // 反発係数 friction: 1.0, // 摩擦係数 }), }); this.Body.sleep(); } } |
worldにボールを追加する関数を定義します。またボールを動かないように固定したり隠す(見えない位置に移動させる)関数も定義しておきます。
|
1 2 3 4 5 |
class Ball { AddToWorld = (world) => world.addBody(this.Body); Sleep = () => this.Body.sleep(); Hide = () => this.Body.position = new CANNON.Vec3(this.InitX, 1000, 0); } |
ゲーム開始時にボールを初期化する処理を示します。速度は0にして初期位置にセットします。そのあとwakeUp関数を実行して固定状態を解除して動くようにします。
|
1 2 3 4 5 6 7 |
class Ball { Init(){ this.Body.velocity.set(0, 0, 0); this.Body.position.set(this.InitX, this.InitY, 0); this.Body.wakeUp(); } } |
ボールを打ち出す処理を示します。
同じ速度で打ち出しても動作が異なる場合があるのですが、壁に当たったボールが動いていたり変な位置でsleep関数を実行すると壁にめり込んだ状態で止まってしまうのが原因のようです。
そこで打ち出すときは壁と地面から少し離した位置で速度を与えることにします。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
class Ball { ShotToLeft(value){ if(this.Body.position.x > RIGHT_X - BALL_RADIUS - 4){ this.Body.velocity.set(0, 0, 0); this.Body.position.x = RIGHT_X - BALL_RADIUS - 4; this.Body.position.y += 4; this.Body.velocity.x = -value; return true; } else return false; } ShotToRight(value){ if(this.Body.position.x < LEFT_X + BALL_RADIUS + 4){ this.Body.velocity.set(0, 0, 0); this.Body.position.x = LEFT_X + BALL_RADIUS + 4; this.Body.position.y += 4; this.Body.velocity.x = value; return true; } else return false; } } |
ゴールに辿り着いたと判定されたときはGoal関数を呼び出します。ボールを動かないように停止させ、操作用のボタンを非表示、効果音の再生、スタートボタンを再表示などの処理をおこないます。
|
1 2 3 4 5 6 7 8 9 10 11 12 |
class Ball { Goal(){ this.Sleep(); $control_buttons.style.display = 'none'; goalSound.play(); setTimeout(() => { $start_buttons.style.display = 'block'; }, 1000); } } |
穴に落ちたと判定されたときはDead関数を呼び出します。ボールを隠して動かないように停止させ、操作用のボタンを非表示、効果音の再生、スタートボタンを再表示などの処理をおこないます。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
class Ball { Dead(){ this.Sleep(); $control_buttons.style.display = 'none'; deadSound.play(); setTimeout(() => this.Hide(), 300); // 穴に落ちたように見せるためにしばらくして非表示にする setTimeout(() => { gameoverSound.play(); $start_buttons.style.display = 'block'; }, 1000); } } |
描画の処理を示します。
worldにおける座標からcanvasに描画するための中心になる座標を求め、その位置に描画します。10円玉が転がっているようにイメージを回転させていますが、回転はcannon.jsで得られる回転角ではなく、10円玉の移動距離で算出しています(転がらずにまっすぐ射出されたり坂を転がらずに滑ってしまうため)。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
class Ball { Draw(){ this.Body.position.z = 0; this.Body.velocity.z = 0; // 描画するときの中心座標を求める const cx = this.Body.position.x; const cy = TOP_Y - this.Body.position.y; // TOP_Y - Y座標 を描画するときのY座標とする ctx.save(); ctx.translate(cx, cy); ctx.rotate(Math.PI * cx * 4 / 180); ctx.translate(-cx, -cy); ctx.drawImage(ballImage, cx - BALL_RADIUS, cy - BALL_RADIUS, 32, 32); ctx.restore(); ctx.beginPath(); ctx.arc(cx, cy, BALL_RADIUS - 2, 0, Math.PI * 2); ctx.strokeStyle = '#fff'; ctx.stroke(); } } |
Courseクラスの定義
薄い板を生成してこれを適度に回転させた状態でworldに追加してコースを作ります。そのためのCourseクラスを定義します。
コンストラクタの引数は坂の両端の座標です。ここから中心点の座標を求め、板のサイズやここを中心にどれだけ回転させればよいかを算出します。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
class Course { constructor(position1, position2){ this.X1 = position1.x; this.Y1 = position1.y; this.X2 = position2.x; this.Y2 = position2.y; const material = new CANNON.Material({ restitution: 0.5, }); const cx = (this.X1 + this.X2) / 2; const cy = (this.Y1 + this.Y2) / 2; const half_width = Math.sqrt((this.X1 - cx) * (this.X1 - cx) + (this.Y1 - cy) * (this.Y1 - cy)); const rad = Math.atan2(this.Y1 - cy, this.X1 - cx); this.Body = new CANNON.Body({ mass: 0, // 重力の影響を受けないように 0 を指定する shape: new CANNON.Box(new CANNON.Vec3(half_width, 2, 1000)), position: new CANNON.Vec3(cx, cy, 0), material: material, }); this.Body.quaternion.setFromEuler(0, 0, rad); } AddToWorld = (world) => world.addBody(this.Body); Draw(){ ctx.beginPath(); ctx.moveTo(this.X1, TOP_Y - this.Y1); ctx.lineTo(this.X2, TOP_Y - this.Y2); ctx.strokeStyle = '#fff'; ctx.lineWidth = 8; ctx.stroke(); ctx.lineWidth = 1; } } |
Holeクラスの定義
穴に落ちた時の処理をするためのHoleクラスを定義します。
穴に落ちたかどうかの判定ですが、穴がある位置よりも穴の半径ぶん下がった位置に平面を追加し、ボールがこれに衝突したかどうかでおこないます。描画は平面にはしないで穴のような半円とします。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
class Hole { constructor(position){ this.CX = position.x; this.CY = position.y; const half_width = 20; const material = new CANNON.Material({ restitution: 0.5, }); this.Body = new CANNON.Body({ mass: 0, shape: new CANNON.Box(new CANNON.Vec3(half_width, 2, 1000)), position: new CANNON.Vec3(this.CX, this.CY - 20, 0), material: material, }); // 穴の底にボールが衝突したらミス判定 this.Body.addEventListener('collide', () => ball.Dead()); } AddToWorld = (world) => world.addBody(this.Body); Draw(){ ctx.beginPath(); ctx.arc(this.CX, TOP_Y - this.CY, 20, 0, Math.PI); ctx.strokeStyle = '#fff'; ctx.lineWidth = 8; ctx.stroke(); ctx.lineWidth = 1; ctx.drawImage(outImage, this.CX - outImage.width / 2, TOP_Y - this.CY - 28); } } |
Wallクラスの定義
左右の壁と下側(下の見えない部分にある)の壁が必要なので Wallクラスを定義します。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 |
class Wall { constructor(){ const material = new CANNON.Material({ restitution: 0, friction: 1.0, }); // 左右の壁 const box1 = new CANNON.Box(new CANNON.Vec3(2, 1000, 1000)); const positions = []; positions.push(new CANNON.Vec3(LEFT_X - 1, 500, 0)); positions.push(new CANNON.Vec3(RIGHT_X + 1, 500, 0)); this.Bodies = []; for(let i = 0; i < positions.length; i++){ this.Bodies.push( new CANNON.Body({ mass: 0, // static shape: box1, position: positions[i], material: material, }) ); } // 下の壁(ここに当たったら当たりではなくハズレに落ちた) const box2 = new CANNON.Box(new CANNON.Vec3(1000, 2, 1000)); const bottomBody = new CANNON.Body({ mass: 0, shape: box2, position: new CANNON.Vec3((RIGHT_X - LEFT_X) / 2, -30, 0), material: material, }); bottomBody.addEventListener('collide', () => ball.Dead()) this.Bodies.push(bottomBody); } AddToWorld(world){ for(let i=0; i<this.Bodies.length; i++) world.addBody(this.Bodies[i]); } Draw(){ // 左右の壁のみ描画する for(let i=0; i<16; i++){ ctx.drawImage(wallImage, LEFT_X - 32 - 2, i * 32, 32, 32); ctx.drawImage(wallImage, RIGHT_X + 2, i * 32, 32, 32); } } } |
Goalクラスの定義
ゴールの描画とゴール時の処理をするためにGoalクラスを定義します。
上記の底にある壁の少し上側に、ゴールとなる部分をカバーするように板を設置します。ボールがこれと衝突したらゴールしたことになります。
またcanvas下部のゴールとハズレ部分に相当する位置に文字を描画することにします。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
class Goal { constructor(position1, position2){ this.X1 = position1.x; this.Y1 = position1.y; this.X2 = position2.x; this.Y2 = position2.y; const material = new CANNON.Material({ restitution: 0.5, }); const cx = (this.X1 + this.X2) / 2; const cy = (this.Y1 + this.Y2) / 2; const half_width = Math.sqrt((this.X1 - cx) * (this.X1 - cx) + (this.Y1 - cy) * (this.Y1 - cy)); this.Body = new CANNON.Body({ mass: 0, shape: new CANNON.Box(new CANNON.Vec3(half_width, 2, 1000)), position: new CANNON.Vec3(cx, cy, 0), material: material, }); // これにボールが衝突したらゲームクリア this.Body.addEventListener('collide', () => ball.Goal()) } AddToWorld = (world) => world.addBody(this.Body); Draw(){ const textCX = (this.X1 + this.X2) / 2; ctx.drawImage(goalImage, textCX - goalImage.width / 2, TOP_Y - 100); ctx.drawImage(outImage, textCX - outImage.width / 2 - 50, TOP_Y - 70); ctx.drawImage(outImage, textCX - outImage.width / 2 + 60, TOP_Y - 70); } } |
次回はこれらのクラスを用いてゲームを完成させます。
