この記事は Ebiten Advent Calendar 2018 7日目の記事です。
作戦は大体いつも「ガンガンいこうぜ」な @kemokemo ですʕ◔ϖ◔ʔ
前回、前々回と解説してきたVirtualGamePad
を使ってシューティングのショットを撃とうとしたときの話をします。
ボタンベタ押しで普通のショットを撃ちたい
ボタンがトリガーされている分だけショットを出し続けた結果がこれです。
シューティングゲームでたまに見かける、短射程で高火力な火炎放射系のショット
を実現するには良さそうです。
今回は、自機のノーマルショットなどに多いボタンをベタ押ししている間ずっと適度な時間間隔で出るショット
を実現したいので工夫します。また、自機のモードを変えてショットの特性(画像、連射速度など)の切り替えもしたいので、その辺も工夫します。
では見ていきましょうʕ◔ϖ◔ʔ
準備
銃と弾にいろんな種類があってね
ショットを実現する仕組みとして、以下のように考えました。
- ショットを撃つ銃
Gun
構造体を作り、どんな時間間隔でどの方向にショットを出すか制御する- 後述の
Bullet
を複数生成しておいて管理する
- 後述の
- ショット自体の
Bullet
構造体を作り、発射された後の移動処理や自分自身の描画処理を行う
複数方向に撃つショットや連射速度重視のショットなどなど、いろんなバリエーションのショットを作る際に特に便利なのではないかと思います。今回の試作では1-way
ショットを2種類作りました。
さらに種類を増やしたいあなたに
さらに、今回はそこまで実施していませんが、Gun
をinterface
にして以下の3つのメソッドを定義しておけば、どのバリエーションのショットもゲーム制御コードから同じように扱えて楽できそうです。
- Update
- Draw
- Fire
実装内容のご紹介
ショットの時間間隔を調整する
Gun
の生成とBullet
の扱い
弾をいい感じの間隔で発射する
、これは弾を発射する銃Gun
の役割です。
弾の画像や発射時間間隔などを引数で渡しながら、Gun
のインスタンスを作りましょう。
// 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
を使う
ゲーム制御コードからは、以下のUpdate
とDraw
メソッドを呼び出して、内部ステータスの更新や描画処理を実施します。
// 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 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
経由で以下のUpdate
やDraw
メソッドが呼び出されます。
// 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
フラグで以下のように制御します。
- 発射前または一旦使い終わった弾:
visible
がfalse
で位置情報の更新も描画もしない - 発射後の弾:
visible
がtrue
で位置情報の更新も描画もする
Gun
のFire
メソッドから、個々の弾であるBullet
のFire
メソッドが呼び出されます。
// 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))
}
引数で発射位置の情報をもらいます。この処理によりBullet
のvisible
フラグがtrue
になり、以降画面外に出るまで移動し描画されます。
こんな感じになります
ここまで見てきた仕組みにより、連射速度も画像も異なるショットが実現できました。やったね!ʕ◔ϖ◔ʔ
まとめ
なんだかこの勢いでシューティングゲームを作り込みたくなりましたが、私には「黒菜んダッシュ」完成という使命があるので気持ちを切り替えて行きたいと思います。
そもそもVirtualGamePad
も「黒菜んダッシュ」のための試作目的がメインだったのですが、思っていた以上に面白い題材でガッツリのめり込んでしまいました(;´∀`)
皆様も、ぜひ ebiten でゲーム作りしましょう!ʕ◔ϖ◔ʔ