0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

前回は自機を出すところまでやりました。今回は敵が弾を撃ってくるようにして、完成とします。

敵弾クラスを作る

例によって、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

左上に「つ」という文字が出ています。良さそうです。

スクリーンショット 2018-12-22 21.17.47.png

弾をたくさん出す

弾が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フラグが立ったものをまとめて削除できます。

実行すると、こんな感じになりました。

beam1.gif

いくつか問題がありそうなので、順に解決していきます。

弾が敵の肩から出ている

まず、弾が敵の肩のあたりから出ているように見えます。これは、弾を作るときに

      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

ここまでの修正を入れると、こんな感じになりました。

beam2.gif

弾をばらけさせる

だいぶマシになりましたが、ちょっと攻撃が単調ですよね。今は弾の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

実行するとこうなります。

beam3.gif

どうでしょう?ぐっとゲームっぽくなりましたね。

当たり判定を付ける

最後にひと頑張りして、敵弾と自機の当たり判定を実装しましょう。当たり判定に使えるのは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ライセンスとしますので、このページへのリンクを貼ってもらえれば自由に改造・再利用などして構いません。動くものができたら、ぜひ公開にもチャレンジしてください:-)

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?