- DXOpalでUndertaleっぽい画面を作る - Qiita
- DXOpalでUndertaleっぽい画面を作る(続) - Qiita
- DXOpalでUndertaleっぽい画面を作る(続2) - Qiita
前回は自機を出すところまでやりました。今回は敵が弾を撃ってくるようにして、完成とします。
敵弾クラスを作る
例によって、Spriteを継承して敵弾クラスを作ります。画像は、適当に丸を描いて敵弾ということにする手もありますが、せっかく「タイピングが速い人のイラスト」を使っているので、文字が弾として飛んでくるようにします。
といってもいろんな文字の画像を用意するのは大変なので、Image.new
を使って自動で生成しましょう。Image#draw_font
を使うと、Windowではなく画像オブジェクトに文字を描画できます。
class EnemyBullet < Sprite
FONT_BULLET = Font.new(20)
def initialize(x, y)
img = Image.new(20, 20)
# 「あ」から「ん」までの文字をランダムに1つ選ぶ
str = ("あ".."ん").to_a.sample
img.draw_font(0, 0, str, FONT_BULLET, C_WHITE)
super(x, y, img)
end
end
うまく生成できているか、描画して確かめてみましょう。
Window.load_resources do
Window.bgcolor = C_BLACK
enemy = Enemy.new
# 敵弾を一つ生成する
enemy_bullet = EnemyBullet.new(0, 0)
player = Player.new
# ...
enemy.update
enemy.draw
# 敵弾1つを描画する
enemy_bullet.draw
左上に「つ」という文字が出ています。良さそうです。
弾をたくさん出す
弾が1個だとたぶん簡単すぎるので、敵弾は複数出したいですよね。DXOpalでは複数のスプライト(Spriteを継承したオブジェクト)をまとめて扱うメソッドがあるので、これらを使うと便利です。
- Sprite.update: まとめてupdateする
- Sprite.draw: まとめてdrawする
- Sprite.clean: vanishフラグが立っているものを削除する
使い方は、以下のようになります。
class EnemyBullet < Sprite
# ...
def update
self.y += 8
# 画面外に出たらvanishフラグを立てる
self.vanish if self.y > Window.height
end
end
# ...
Window.load_resources do
Window.bgcolor = C_BLACK
enemy = Enemy.new
# 敵弾の配列
enemy_bullets = []
player = Player.new
# ...
player.draw
# 敵弾が減ったら補充する
if enemy_bullets.length < 8
enemy_bullets.push(EnemyBullet.new(enemy.x, enemy.y))
end
Sprite.clean(enemy_bullets) # 画面外に出たものを配列から削除する
Sprite.update(enemy_bullets) # 敵弾のupdateメソッドを順に呼ぶ
Sprite.draw(enemy_bullets) # 敵弾のdrawメソッドを順に呼ぶ
敵弾が複数になるので、enemy_bullet =
としていた部分をenemy_bullets = []
と配列にしています。メインループでは、敵弾が8個未満のときに新しい弾を作っています。
敵弾はEnemyBullet#update
を呼ぶごとに下に移動します。画面外に出たら、Spriteクラスのvanishメソッドを呼んで、「vanishフラグ」を立てるようにします。こうすると、Sprite.clean
でvanishフラグが立ったものをまとめて削除できます。
実行すると、こんな感じになりました。
いくつか問題がありそうなので、順に解決していきます。
弾が敵の肩から出ている
まず、弾が敵の肩のあたりから出ているように見えます。これは、弾を作るときに
enemy_bullets.push(EnemyBullet.new(enemy.x, enemy.y))
としているのが原因です。enemy.x
, enemy.y
は敵の画像の左上を指すので、enemy.hand_pos
で手の間くらいの位置を取得できるようにしましょう。
class Enemy < Sprite
# ...
def hand_pos
[self.x + self.image.width / 2,
self.y + self.image.height * 0.8]
end
# ...
enemy_bullets.push(EnemyBullet.new(*enemy.hand_pos))
これでいい感じの位置から弾が出るようになりました。
弾がダンゴになる
弾を生成している箇所は以下です。DXOpalのメインループは1秒におよそ60回実行されるので、このコードだと弾があっという間に8個作られてしまいます。
if enemy_bullets.length < 8
enemy_bullets.push(EnemyBullet.new(*enemy.hand_pos))
end
そこで、弾を作る条件を以下のようにします。randは0.0から1.0未満の数を返すので、この例だと約1/10の確率で弾が生まれることになります。
if enemy_bullets.length < 8 && rand < 0.1
enemy_bullets.push(EnemyBullet.new(*enemy.hand_pos))
end
弾が速すぎる
EnemyBullet#update
ではy座標を8ずつ増やしていましたが、これだとちょっと避けきれない気がします。実際の速度を見ながら、値を適当に調整します。2くらいだとちょうどいいでしょうか。
def update
self.y += 2
self.vanish if self.y > Window.height
end
ここまでの修正を入れると、こんな感じになりました。
弾をばらけさせる
だいぶマシになりましたが、ちょっと攻撃が単調ですよね。今は弾のy座標だけ変化させていますが、x座標も変化するようにしてみましょう。
class EnemyBullet < Sprite
FONT_BULLET = Font.new(20)
def initialize(x, y)
img = Image.new(20, 20)
img.draw_font(0, 0, ("あ".."ん").to_a.sample, FONT_BULLET, C_WHITE)
super(x, y, img)
# x方向の動きを決める(-2,-1,0,+1,+2のいずれかになる)
@dx = rand(-2..2)
end
def update
# x方向にも動かす
self.x += @dx
self.y += 2
self.vanish if self.y > Window.height
end
end
実行するとこうなります。
どうでしょう?ぐっとゲームっぽくなりましたね。
当たり判定を付ける
最後にひと頑張りして、敵弾と自機の当たり判定を実装しましょう。当たり判定に使えるのはSprite.checkです。
Sprite.checkには配列を2つ渡します。1つ目は「当たる側のスプライト」、2つ目は「当たられる側のスプライト」です。今回は「弾がプレイヤーに当たったとき」に処理をしたいので、1つ目をenemy_bullets
、2つ目を[player]
(playerだけが入った配列)にします。
if enemy_bullets.length < 8 && rand < 0.1
enemy_bullets.push(EnemyBullet.new(*enemy.hand_pos))
end
# 当たり判定をチェック
Sprite.check(enemy_bullets, [player])
Sprite.clean(enemy_bullets)
Sprite.update(enemy_bullets)
Sprite.draw(enemy_bullets)
こうすると、敵弾がplayerと接触したときに以下が起こります。
- 敵弾の
shot
メソッドが呼ばれる - プレイヤーの
hit
メソッドが呼ばれる
当たったときの処理を書く
ということでEnemyBullet#shot
と、Player#hit
を実装しましょう。敵弾については、ヒットしたらvanishを呼んで消えることにしましょう。
def shot
self.vanish
end
プレイヤーの方はHPが減ることにしましょう。そういえば体力をまだ変数にしていなかったので、プレイヤーのインスタンス変数として持つことにしましょう。また、メインループからplayer.hp
で取得できるようにattr_reader
を書いておきます。
class Player < Sprite
def initialize
# ...
# 初期体力
@hp = 20
end
attr_reader :hp
# ...
def hit
@hp -= 1
end
end
メインループにHPを描画する箇所があるので、以下のようにしてplayer.hp
が反映されるようにします。
Window.draw_font(270, 370, "HP #{player.hp}/20", FONT_HP, color: C_WHITE)
ためしに弾に当たってみて、HPが減ることを確認してください。
まとめ
今回は敵弾を実装して、当たるとHPが減るようにしました。本シリーズはこれで終わりですが、「ゲーム」として考えるとやれることはまだまだあるでしょう。HPが0になったらゲームオーバーにするとか、当たり判定を改善するとか。(デフォルトでは画像全体が判定エリアになりますが、collision=
で画像の一部だけを設定することもできます。画像より一回り小さい円に設定すると自然な感じになります。) このあたりの話は、Rubyist Magazineの以下の記事が参考になると思います。ぜひいろいろ試してみてください。
完成したものは以下にあります。
ソースはMITライセンスとしますので、このページへのリンクを貼ってもらえれば自由に改造・再利用などして構いません。動くものができたら、ぜひ公開にもチャレンジしてください:-)