これは scouty Advent Calendar 2018 の11日目の記事です。
2018/12/18追記: 続きができました。
Font AwesomeとJavaScriptでシューティングゲーム(その2)
はじめに
Font Awesomeのサイトを眺めてたらゲームで使えそうなキャラクターがいっぱいあるなあと思ったので作ることにしました。
今回は、シューティングゲームを作ります。まずベタ書きで作ってみて、それをクラス化するところまでやります。
面倒だったので最初は慣れているjQueryを使っていますが、後ほどバニラなJavaScriptに書き換えます。
GitHubリポジトリ: https://github.com/naga3/font-awesome-shooting
アジェンダ
- ベタ書きでとりあえず作る
- クラスを使って整理する
- jQueryの使用をやめる
ベタ書きで作ってみる
以下にソースコードを示します。ブラウザでHTMLを開くとゲームができます。
<!DOCTYPE html>
<html lang="ja">
<head>
<style>
body {
margin: 0;
padding: 0;
}
#app {
width: 800px;
height: 400px;
background: black;
position: relative;
overflow: hidden;
left: 0;
top: 0;
cursor: none;
}
#player {
position: absolute;
color: lightskyblue;
}
#bullet {
position: absolute;
color: gold;
left: 800px;
}
#enemy {
position: absolute;
color: coral;
left: -100px;
}
</style>
<title>Font Awesome shooting game!</title>
</head>
<body>
<div id="app">
<i id="bullet" class="fas fa-toggle-on"></i>
<i id="player" class="fas fa-fighter-jet fa-3x"></i>
<i id="enemy" class="fas fa-helicopter fa-flip-horizontal fa-3x"></i>
</div>
<script src="https://use.fontawesome.com/releases/v5.5.0/js/all.js"></script>
<script src="https://code.jquery.com/jquery-3.3.1.min.js"></script>
<script>
let showBullet = false
setInterval(() => {
$('#enemy').css('left', $('#enemy').offset().left - 6)
if ($('#enemy').offset().left < -100) {
$('#enemy').offset({
left: 800,
top: Math.random() * (400 - $('#enemy').height())
})
}
if (showBullet) {
$('#bullet').css('left', $('#bullet').offset().left + 12)
if ($('#bullet').offset().left > 800) showBullet = false
if (
$('#bullet').offset().left + $('#bullet').width() >= $('#enemy').offset().left &&
$('#bullet').offset().left < $('#enemy').offset().left + $('#enemy').width() &&
$('#bullet').offset().top + $('#bullet').height() >= $('#enemy').offset().top &&
$('#bullet').offset().top < $('#enemy').offset().top + $('#enemy').height()
) {
showBullet = false
$('#bullet').css('left', 800)
$('#enemy').css('left', -100)
}
}
}, 16)
$('#app').mousemove(e => {
$('#player').offset({
left: e.clientX - $('#player').width() / 2,
top: e.clientY - $('#player').height() / 2
})
})
$('#app').click(e => {
if (showBullet) return
showBullet = true
$('#bullet').offset({
left: e.clientX - $('#bullet').width() / 2,
top: e.clientY - $('#bullet').height() / 2
})
})
</script>
</body>
</html>
ソースの解説
HTML内ののid:player
に自機、id:enemy
に敵、id:bullet
に弾の要素が入っています。
Font Awesomeではclass名にfa-3x
を指定すると、フォントの大きさが通常の三倍になります。便利ですね。
id:app
の要素がゲームフィールドで、横800px × 縦400pxの大きさです。
setInterval
を使って、16msに一回ゲームが更新されるようにしています。16msというはだいたい1/60秒です。歴史的な事情でこのタイミングで更新されるゲームが多いので、意味はありませんがなんとなくこの値にしています。
あとは、当たり判定が若干分かりにくいと思います。
$('#bullet').offset().left + $('#bullet').width() >= $('#enemy').offset().left &&
$('#bullet').offset().left < $('#enemy').offset().left + $('#enemy').width() &&
$('#bullet').offset().top + $('#bullet').height() >= $('#enemy').offset().top &&
$('#bullet').offset().top < $('#enemy').offset().top + $('#enemy').height()
この部分です。弾を囲む矩形と敵を囲む矩形の座標をチェックして、矩形同士が少しでも重なっていたら当たっていると見なしています。
クラスを使ってみる
shoot1.html
のソースコードは短いのですが、いくつか問題点があります。
- jQueryの機能にベッタリなので、違うライブラリを使うのが大変になる。
- キャラクターをDOM要素のidで管理しているので、キャラクターが増えると管理が大変。
- 当たり判定が増える(自機と敵など)と都度当たり判定のコードを書く必要がある。
などなど。
そこで最近のJavaScript(いわゆるモダンJSというやつですね)で追加されたクラスを使って、shoot1.html
を書き換えてみましょう。
<!DOCTYPE html>
<html lang="ja">
<head>
<style>
body {
margin: 0;
padding: 0;
}
#app {
width: 800px;
height: 400px;
background: black;
position: relative;
overflow: hidden;
left: 0;
top: 0;
cursor: none;
}
</style>
<title>Font Awesome shooting game! - with class</title>
</head>
<body>
<div id="app"></div>
<script src="https://use.fontawesome.com/releases/v5.5.0/js/all.js"></script>
<script src="https://code.jquery.com/jquery-3.3.1.min.js"></script>
<script>
// マウスイベントクラス
class Mouse {
constructor() {
this.click = false
$('#app').mousemove(e => {
this.x = e.clientX
this.y = e.clientY
})
$('#app').mousedown(() => this.click = true)
$('#app').mouseup(() => this.click = false)
}
}
// キャラクタークラス
let character_no = 1
class Character {
constructor(class_name, color) {
this.id_name = `character${character_no}`
this.is_show = true
character_no++
$('#app').append(`<i id="${this.id_name}" class="fas ${class_name}"></i>`)
this.$.css({
position: 'absolute',
color: color
})
}
get $() { return $('#' + this.id_name) }
get x() { return this.$.offset().left }
get y() { return this.$.offset().top }
get width() { return this.$.width() }
get height() { return this.$.height() }
set x(x) {
this.$.offset({ left: x, top: this.y })
}
set y(y) {
this.$.offset({ left: this.x, top: y })
}
show() {
this.$.css({visibility: 'visible'})
this.is_show = true
}
hide() {
this.$.css({visibility: 'hidden'})
this.is_show = false
}
hit(target) {
if (this.x + this.width >= target.x && this.x < target.x + target.width
&& this.y + this.height >= target.y && this.y < target.y + target.height)
return true
return false
}
}
// 弾クラス
class Bullet extends Character {
move() {
if (this.is_show) {
this.x += 12
if (this.x > 800) this.hide()
}
}
}
// 自機クラス
class Player extends Character {
constructor(class_name, color) {
super(class_name, color)
this.mouse = new Mouse()
this.bullet = new Bullet('fa-toggle-on', 'gold')
this.bullet.hide()
}
move() {
this.x = this.mouse.x - this.width / 2
this.y = this.mouse.y - this.height / 2
if (this.mouse.click && !this.bullet.is_show) {
this.bullet.x = this.mouse.x - this.bullet.width / 2
this.bullet.y = this.mouse.y - this.bullet.height / 2
this.bullet.show()
this.mouse.click = false
}
this.bullet.move()
}
}
// 敵クラス
class Enemy extends Character {
move() {
this.x -= 6
if (this.x < -100) {
this.x = 800
this.y = Math.random() * (400 - this.height)
}
}
}
// メインループ
$(() => {
const player = new Player('fa-fighter-jet fa-3x', 'lightskyblue')
const enemy = new Enemy('fa-helicopter fa-flip-horizontal fa-3x', 'coral')
enemy.x = -100
setInterval(() => {
player.move()
enemy.move()
if (player.bullet.hit(enemy)) {
player.bullet.hide()
enemy.x = -100
}
}, 16)
})
</script>
</body>
</html>
ソースの解説
Character
クラスは自機や敵や弾の共通の性質をまとめたクラスです。DOM要素の生成、CSSの設定、キャラクターの表示、消去、座標の取得と設定、大きさの取得、当たり判定などの機能があります。
Character
クラスを派生してPlayer
, Bullet
, Enemy
クラスを定義し、それぞれ独自の性質を追加しています。
Mouse
クラスにはマウスイベントを集約しています。
shoot1.html
に比べて、大変すっきりしましたね。また、jQueryに依存している部分がMouse
クラスとCharacter
クラスしかないので、他のライブラリに簡単に置き換えることができるようになりました。
jQueryの使用をやめる
jQueryを見ると蕁麻疹が出る方のために、バニラなJavaScriptで書き換えたソースコードを示します。shoot2.html
でjQuery部分を分離したので楽です。
<!DOCTYPE html>
<html lang="ja">
<head>
<style>
body {
margin: 0;
padding: 0;
}
#app {
width: 800px;
height: 400px;
background: black;
position: relative;
overflow: hidden;
left: 0;
top: 0;
cursor: none;
}
</style>
<title>Font Awesome shooting game! - vanilla JavaScript</title>
</head>
<body>
<div id="app"></div>
<script src="https://use.fontawesome.com/releases/v5.5.0/js/all.js"></script>
<script>
// マウスイベントクラス
class Mouse {
constructor() {
this.click = false
const app = document.getElementById('app')
app.addEventListener('mousemove', e => {
this.x = e.clientX
this.y = e.clientY
})
app.addEventListener('mousedown', () => this.click = true)
app.addEventListener('mouseup', () => this.click = false)
}
}
// キャラクタークラス
let character_no = 1
class Character {
constructor(class_name, color) {
this.id_name = `character${character_no}`
this.is_show = true
this._x = 0
this._y = 0
character_no++
const app = document.getElementById('app')
app.insertAdjacentHTML('beforeend', `<i id="${this.id_name}" class="fas ${class_name}"></i>`)
this.$.style.position = 'absolute'
this.$.style.color = color
}
get $() { return document.getElementById(this.id_name) }
get x() { return this._x }
get y() { return this._y }
get width() { return this.$.clientWidth }
get height() { return this.$.clientHeight }
set x(x) {
this._x = x
this.$.style.left = x + 'px'
}
set y(y) {
this._y = y
this.$.style.top = y + 'px'
}
show() {
this.$.style.visibility = 'visible'
this.is_show = true
}
hide() {
this.$.style.visibility = 'hidden'
this.is_show = false
}
hit(target) {
if (this.x + this.width >= target.x && this.x < target.x + target.width
&& this.y + this.height >= target.y && this.y < target.y + target.height)
return true
return false
}
}
// 弾クラス
class Bullet extends Character {
move() {
if (this.is_show) {
this.x += 12
if (this.x > 800) this.hide()
}
}
}
// 自機クラス
class Player extends Character {
constructor(class_name, color) {
super(class_name, color)
this.mouse = new Mouse()
this.bullet = new Bullet('fa-toggle-on', 'gold')
this.bullet.hide()
}
move() {
this.x = this.mouse.x - this.width / 2
this.y = this.mouse.y - this.height / 2
if (this.mouse.click && !this.bullet.is_show) {
this.bullet.x = this.mouse.x - this.bullet.width / 2
this.bullet.y = this.mouse.y - this.bullet.height / 2
this.bullet.show()
this.mouse.click = false
}
this.bullet.move()
}
}
// 敵クラス
class Enemy extends Character {
move() {
this.x -= 6
if (this.x < -100) {
this.x = 800
this.y = Math.random() * (400 - this.height)
}
}
}
// メインループ
window.onload = () => {
const player = new Player('fa-fighter-jet fa-3x', 'lightskyblue')
const enemy = new Enemy('fa-helicopter fa-flip-horizontal fa-3x', 'coral')
enemy.x = -100
setInterval(() => {
player.move()
enemy.move()
if (player.bullet.hit(enemy)) {
player.bullet.hide()
enemy.x = -100
}
}, 16)
}
</script>
</body>
</html>
https://github.com/nefe/You-Dont-Need-jQuery 当たりを参照すれば、簡単に書き換えられるのではないかと思います。
Mouse
クラスとCharacter
クラスと、$.ready
のみ書き換えています。$.ready
とwindow.load
は厳密に言えば若干違いますが、お気になさらず。
次回は?
今回はまだまだゲームになっていませんが、次回に、自機のやられ判定、スコア、弾の連射などを追加する予定です。お楽しみに