2
0

More than 1 year has passed since last update.

ラストのまほう 第10話「ゲームのステージを描画する」

Last updated at Posted at 2022-12-09

レベルを表現する

それでゲーム開発をするうえでレベルエディタ1をどうするかは結構悩むところです。プラットフォーマーを作る以上、何らかの方法でレベルデザイン、つまりゲームのステージをどうやって表現するのかを考えなければなりません。

画像をレベルデータとして使う方法も検討したのですが、編集がやや面倒なので、以下のような感じで文字列リテラルとしてレベルをソースコードに埋め込み、レベルデザイン自体もソースコード上でやっていく方法でとりあえず進めようかなと思います。コードとしてはこんな感じになります。

pub const WORLD_WIDTH: u32 = 32;
pub const WORLD_HEIGHT: u32 = 240;
pub const WORLD: &str = "\
################################\
#                              #\
#                              #\
#                              #\
#                              #\
#                              #\
#                              #\
#                              #\

#が壁(プレイヤーキャラクターなどが侵入できない領域)、空白文字はそのままなにもない空間になります。また@がプレイヤーのスタート地点、Rを置くとそこに右向きの案内看板を表示するなど、文字の種類でレベルを表現できます。

これだと細かい調整は難しいもののレベルエディタなどを別に作る必要がないので比較的簡単です。汎用のマップエディタのようなものは売られているのですが、WASM-4だとそのマップエディタで作られたファイルを読み込むこと自体が難しいので、そこに手間をかけたくないわけです。そもそもWASM-4の思想自体がそういうことに手間をかけないでまずはちゃんと完成させようね、というところにあるようなので、私も仔細にはこだわらずまずはゲームを完成させることを優先したいと思います。

なお、Rustではバックスラッシュを文字列リテラルの行末に置くことで、そのバックスラッシュ以降の空白文字を無視することができるようです。いろいろ試した中で、これが一番レベルをシンプルな方法かなと思いました。

ステージを表現する

各ブロックはそのまま文字列として扱ってもいいのですが、ここではもう少しお上品にenumを使ってブロックの種類を表現する手でいきます。enumでブロックの種類を列挙して定義します。

#[derive(PartialEq, Eq, PartialOrd, Ord)]
pub enum Block {
    Empty,
    Wall,
    RightArrow,
    LeftArrow,
    UpArrow,
    Ladder,
    Sting,
    Carrot,
}

それから、任意の位置 (x, y)のブロックを取得する関数をこんな感じで書きました。

pub fn get_cell(&self, x: i32, y: i32) -> Block {
    if 0 <= x && x <= self.width as i32 && 0 <= y && y < self.height as i32 {
        let i = (WORLD_WIDTH as i32 * y + x) as usize;
        let s = WORLD[i..(i + 1)].to_string();
        if s == "#" {
            return Block::Wall;
        } else if s == "R" {
            return Block::RightArrow;
        } else if s == "L" {
            return Block::LeftArrow;
        } else if s == "U" {
            return Block::UpArrow;
        } else if s == "=" {
            return Block::Ladder;
        } else if s == "^" {
            return Block::Sting;
        } else if s == "$" {
            return Block::Carrot;
        } else {
            return Block::Empty;
        }
    } else {
        return Block::Wall;
    }
}

ステージを描画する

あとはこんな感じで、画面内のすべてのブロックの種類を特定しながら、matchで場合分けしつつそのブロックに応じた画像をblitで描画するだけです。

for y in min_y..(max_y + 1) {
    for x in min_x..max_x {
        let cell = self.get_cell(x as i32, y as i32);
        match cell {
            Block::Empty => {}

            Block::Wall => {
                set_draw_color(0x4321);
                g.draw(
                    &TILE_IMAGE,
                    (CELL_SIZE * x) as i32,
                    (CELL_SIZE * y) as i32,
                    TILE_IMAGE.flags,
                );
            }

            ...
        }
    }
}

ちなみに、ここでちゃんと画面内のブロックの範囲に限ってループするのがポイントです。ステージ全体のブロックを描画しても正しく描画できるのですが、ブロックが多すぎて処理が重くなり、60FPSを維持することができません。私は次のような感じで画面内の範囲を計算しています。

let min_x = u32::max(
    0,
    // 看板のように基準位置より右に描くものがあるので、 - CELL_SIZE で少し広めにとる
    i32::max(0, -g.dx - CELL_SIZE as i32) as u32 / CELL_SIZE,
);
let max_x = u32::min(
    min_x + (wasm4::SCREEN_SIZE / CELL_SIZE) + 2,
    self.width as u32,
);
let min_y = u32::max(
    0,
    // 看板のように基準位置より右に描くものがあるので、 - CELL_SIZE で少し広めにとる
    i32::max(0, -g.dy - CELL_SIZE as i32) as u32 / CELL_SIZE,
);
let max_y = u32::min(
    min_y + (wasm4::SCREEN_SIZE / CELL_SIZE) + 2,
    self.height as u32,
);

次回予告

セーブとロードです。Rustの王道、安牌を取りに行ったつもりが、悲しい結末を迎えました。

  1. そういえば、日本語だとアクションゲームのこういうのは「ステージ」と呼びますが、英語だと level とか呼んだりするようです。RPGの経験値を積むと上がるレベルと紛らわしいので、この記事ではステージとかマップとかレベルとか呼び方が一定していなくてすみません。

2
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
2
0