Help us understand the problem. What is going on with this article?

ユーザーがボタン押しっぱなしの状況で、いかにそれっぽくショットを連射するか

More than 1 year has passed since last update.

この記事は Ebiten Advent Calendar 2018 7日目の記事です。
作戦は大体いつも「ガンガンいこうぜ」な @kemokemo ですʕ◔ϖ◔ʔ
前回、前々回と解説してきたVirtualGamePadを使ってシューティングのショットを撃とうとしたときの話をします。

ボタンベタ押しで普通のショットを撃ちたい

Screenshot_2018-12-02-17-57-37.jpeg

ボタンがトリガーされている分だけショットを出し続けた結果がこれです。
シューティングゲームでたまに見かける、短射程で高火力な火炎放射系のショットを実現するには良さそうです。

今回は、自機のノーマルショットなどに多いボタンをベタ押ししている間ずっと適度な時間間隔で出るショットを実現したいので工夫します。また、自機のモードを変えてショットの特性(画像、連射速度など)の切り替えもしたいので、その辺も工夫します。

では見ていきましょうʕ◔ϖ◔ʔ

準備

銃と弾にいろんな種類があってね

ショットを実現する仕組みとして、以下のように考えました。

  • ショットを撃つ銃Gun構造体を作り、どんな時間間隔でどの方向にショットを出すか制御する
    • 後述のBulletを複数生成しておいて管理する
  • ショット自体のBullet構造体を作り、発射された後の移動処理や自分自身の描画処理を行う

複数方向に撃つショットや連射速度重視のショットなどなど、いろんなバリエーションのショットを作る際に特に便利なのではないかと思います。今回の試作では1-wayショットを2種類作りました。

さらに種類を増やしたいあなたに

さらに、今回はそこまで実施していませんが、Guninterfaceにして以下の3つのメソッドを定義しておけば、どのバリエーションのショットもゲーム制御コードから同じように扱えて楽できそうです。

  • Update
  • Draw
  • Fire

実装内容のご紹介

ショットの時間間隔を調整する

Gunの生成とBulletの扱い

弾をいい感じの間隔で発射する、これは弾を発射する銃Gunの役割です。
弾の画像や発射時間間隔などを引数で渡しながら、Gunのインスタンスを作りましょう。

NewGun

gun.go
// NewGun returns a new gun.
// Please sets the area of movement for bullets.
func NewGun(img *ebiten.Image, area image.Rectangle, interval time.Duration) (*Gun, error) {
    g := &Gun{interval: interval}
    err := g.createBullets(img, area)
    if err != nil {
        return nil, err
    }
    return g, nil
}

Gunは、自分が発射可能な弾Bulletのインスタンスを十分な数だけ生成して保持しておく仕組みを採用しました。ショットはゲーム中たくさん撃つものなので、インスタンスの生成と破棄をずっと繰り返す処理は避けようと考えたからです。

Gunを使う

ゲーム制御コードからは、以下のUpdateDrawメソッドを呼び出して、内部ステータスの更新や描画処理を実施します。

Update and Draw

gun.go
// Update updates the bullets status.
func (g *Gun) Update() {
    for index := 0; index < bulletsCount; index++ {
        g.bullets[index].Update()
    }
}

// Draw draws bullets.
func (g *Gun) Draw(screen *ebiten.Image) error {
    var e error
    for index := 0; index < bulletsCount; index++ {
        e = g.bullets[index].Draw(screen)
        if e != nil {
            return e
        }
    }
    return nil
}

ユーザー入力などにより弾を発射する指示があれば、ゲーム制御コードからFireメソッドを呼び出します。

Fire

gun.go
// Fire fires a bullet.
// If the duration time is less than the interval from previous fire,
// this function do nothing.
func (g *Gun) Fire(point image.Point) {
    t := time.Now()
    if t.Sub(g.fired) < g.interval {
        return
    }
    g.fired = t

    if g.bulletsIndex < bulletsCount-1 {
        g.bulletsIndex++
    } else {
        g.bulletsIndex = 0
    }
    g.bullets[g.bulletsIndex].Fire(
        image.Point{
            point.X - g.bulletSize.X/2,
            point.Y - g.bulletSize.Y/2})
}

このメソッドのコメントにもあるように、前回弾が発射されてから経過した時間と予め設定された発射時間間隔とを比較して、前者の方が短ければ何もしない(=弾の追加発射しない)です。

発射されたら動くし描画する

次は弾Bulletを見ていきましょう。

Bulletを使う

Gun経由で以下のUpdateDrawメソッドが呼び出されます。

bullet.go
// Update update the internal state of this bullet.
func (b *Bullet) Update() {
    if !b.visible {
        return
    }
    b.move()
    b.checkArea()
}

// move moves this bullet.
func (b *Bullet) move() {
    b.point = b.point.Add(b.velocity)
    b.op.GeoM.Translate(float64(b.velocity.X), float64(b.velocity.Y))
}

// checkAreac checks whether this bullet is out of the
func (b *Bullet) checkArea() {
    if !b.point.In(b.area) {
        b.visible = false
    }
}

// Draw draws the image of this bullet.
func (b *Bullet) Draw(screen *ebiten.Image) error {
    if !b.visible {
        return nil
    }
    return screen.DrawImage(b.baseImg, b.op)
}

前述のようにBulletのインスタンスは使い回すことを考えていますので、visibleフラグで以下のように制御します。

  • 発射前または一旦使い終わった弾: visiblefalseで位置情報の更新も描画もしない
  • 発射後の弾: visibletrueで位置情報の更新も描画もする

GunFireメソッドから、個々の弾であるBulletFireメソッドが呼び出されます。

bullet.go
// Fire sets the initial position and make this bullet
func (b *Bullet) Fire(point image.Point) {
    b.point = point
    b.visible = true

    b.op.GeoM.Reset()
    b.op.GeoM.Translate(float64(b.point.X), float64(b.point.Y))
}

引数で発射位置の情報をもらいます。この処理によりBulletvisibleフラグがtrueになり、以降画面外に出るまで移動し描画されます。

こんな感じになります

ここまで見てきた仕組みにより、連射速度も画像も異なるショットが実現できました。やったね!ʕ◔ϖ◔ʔ

shot_type_01.jpeg

shot_type_02.jpeg

まとめ

なんだかこの勢いでシューティングゲームを作り込みたくなりましたが、私には「黒菜んダッシュ」完成という使命があるので気持ちを切り替えて行きたいと思います。

そもそもVirtualGamePadも「黒菜んダッシュ」のための試作目的がメインだったのですが、思っていた以上に面白い題材でガッツリのめり込んでしまいました(;´∀`)

皆様も、ぜひ ebiten でゲーム作りしましょう!ʕ◔ϖ◔ʔ

KemoKemo
ケモミミストなソフトウェア技術者。嫁言語はGo言語。 OpenAPI Generatorのgo-gin-serverジェネレーターの作者。 マイクロサービスをコンテナ化して、Kubernetesクラスタ上でうまく連携させるのに夢中。 フロントエンドはVue.jsとAngularが好き。
https://t2wonderland.blogspot.com/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away