ミナミバショウの雑記置き場

フリーゲームを作っています。制作ゲーム一覧 : https://sites.google.com/view/sbzer7123

ゲーム作りでImageクラスを活かす

本記事は Siv3D Advent Calendar 2017 24日の記事です。

SIv3Dで画像を描画するにはTextureクラスを使用しますが、これとは別に画像を扱うImageクラスがあります。Imageクラスはそのまま画面に描画することはできませんが、画像を編集することができます。本記事では、ゲーム制作でImageクラスを使用したことについて書いていきます。

マップチップを用いたマップ描画

私が今年の大学祭で展示したゲームでは、マップチップを敷き詰めてマップを表示しました。
f:id:sbzer7123:20171224060225p:plain
これの最も単純な実装は、マップチップ一つ一つテクスチャで描画する方法です。

# include <Siv3D.hpp>

void Main()
{
  Window::Resize(1280, 720);

  const int MAPCHIP_SIZE = 32;	// マップチップのサイズ

  Array<Texture> mapchipTextures = {  Texture(L"ground.png"),	Texture(L"wall.png")};	

  Grid<int> map;  

  CSVReader reader(L"map.csv");  // 床を表示する箇所に0, 壁を表示にする箇所に1が書かれたCSV

  map.resize(reader.columns(0), reader.rows);

  for (unsigned y = 0; y < reader.rows; y++)
  {
    for (unsigned x = 0; x < reader.columns(0); ++x)
    {
      map.at(y,x) = reader.get<int>(y, x);			
    }
  }

  reader.close();

  while (System::Update())
  {
    for (unsigned y = 0; y < map.height; y++)
    {
      for (unsigned x = 0; x < map.width; ++x)
      {
        mapchipTextures[map.at(y, x)].draw(Point( x * MAPCHIP_SIZE, y * MAPCHIP_SIZE ) );
      }
    }
  }
}


しかし、この方法では一度に大量のテクスチャを描画することになってしまいます。私のゲームでは、1280*720の画面に32*32のサイズのマップチップでマップを描画したので、マップの表示に880つのテクスチャを使用することになります。さらに、16*16のサイズのマップチップを拡大して32*32のマップチップとして使ったり、アイテムやキャラクターを合計200個以上出したりしたので、この方法では処理が重くなってしまいました。

この問題は、プログラム上で画像を編集し、マップを1つのテクスチャで表示することで解決しました。これにImageクラスを使用しました。

以下のようにImageクラスのoverwrite関数を使うと fuga という画像の{ 10,10 }の位置に hoge という画像を描き込むことができます。

  Image hoge, fuga;
  hoge.overwrite(fuga, { 10,10 });

この要領で、マップを1枚の画像に書き込み、その画像からテクスチャを生成することでマップの描画回数を880回から1回まで削減することができました。

# include <Siv3D.hpp>

void Main() 
{
  Window::Resize(1280, 720);

  const int MAPCHIP_SIZE = 32;	

  Array<Image> mapchipImages = {  Image(L"ground.png"), Image(L"wall.png") };	

  CSVReader reader(L"Map.csv");  // 床を表示する箇所に0, 壁を表示にする箇所に1が書かれたCSV

  // このImageにマップ画像を描き込む	
  Image mapImage(reader.columns(0) * MAPCHIP_SIZE, reader.rows * MAPCHIP_SIZE);	

  for (unsigned y = 0; y < reader.rows; y++)
  {
    for (unsigned x = 0; x < reader.columns(0); ++x) 
    {
      mapchipImages[reader.get<int>(y, x)]
          .overwrite(mapImage, { x * MAPCHIP_SIZE, y * MAPCHIP_SIZE });
    }
  }
  reader.close();
	
  Texture mapTexture(mapImage);	// Imageからテクスチャを生成する

  while (System::Update())
  {
    mapTexture.draw();	
  }
}

ゲーム作成で大量の画像を表示にする際は、Imageクラスの存在を思い出すとゲームのパフォーマンスを向上させることができるかもしれません。

ちなみに、私が大学祭で展示したゲームはこちらです。4人対戦用ゲームで、ゲームパッド4つの接続を前提としております。
ux.getuploader.com

 

背景が透過されていない画像を扱う

Siv3Dでは、背景の透過色を指定して透過させて表示することはできません。なので、特定の色をプログラム側で透過することを想定して作られている、背景が透過されていない画像をそのまま使うことができません。
f:id:sbzer7123:20171222012707p:plain
しかし、Siv3Dは画像自体を容易に編集することができます。画像にピクセル単位でアクセスできるので、

  1. 画像の左上のピクセルの色を背景色とする。
  2. 背景色と同じ色のピクセルの不透明度を0にする

という操作をすることで簡単に背景を透過させることができます。
 
ダイアログで開いた画像の背景を、上記の手法で透過させるプログラムが次になります。

# include <Siv3D.hpp>
void Main() 
{
  // 画像を開く
  Image image = Dialog::OpenImage();
  if ( image.isEmpty ) { return; }

  // 画像の左上の点を背景色とする
  const Color background_color = image[0][0];

  // 背景色と同じ色のピクセルのアルファ値(不透明度)を0にする
  for (Color& pixel : image) 
  {
    if (pixel == background_color) 
    {
      pixel.a = 0;
    }
  }

  // ダイアログで保存するファイル名を決定
  const Optional<String> save = Dialog::GetSaveImage();
  if ( !save.has_value() ) { return; }
  const FilePath filepath = save.value();
  image.save(filepath);
}

しかし、一つ一つ保存するのは大変なので、ドロップされた画像を変換するように改造したのが次のプログラムです。拡張子はPNG, 保存場所はConvertedという名前のフォルダ内としています。

void Main()
{
  Window::SetTitle(L"ドロップされた画像を透過PNGに変換します");
  while (System::Update())  
  {
    // 何かがドロップされたら
    if (Dragdrop::HasItems())
    {
      // ドロップされたすべてのアイテムを取得
      const Array<FilePath> items = Dragdrop::GetFilePaths();

      for (const auto& item : items) {
        Image image(item);
        const Color background_color = image[0][0];        // 画像の左上の点を背景色とする
        for (Color& pixel : image)
        {
          if (pixel == background_color) 
          {
            pixel.a = 0;          // 背景色と同じ色のピクセルのアルファ値(不透明度)を0にする
          }
        }
        // 保存ファイル名を決定
        String pictureName = item.substr(item.lastIndexOf(L"/"));
        Println(pictureName);
        const String saveFilePath(Format(L"Converted", 
            pictureName.substr(0, pictureName.lastIndexOf(L".")), L".png"));
        image.encodePNG().save(saveFilePath);
      }
    }
  }
}

本記事では、背景を透過させた後に画像として保存する例を載せていますが、当然背景を透過させてからテクスチャを生成することもできます。
この手法の問題点は、画像の左上の点が背景でない場合に対応できないことです。私が使用した画像ではこのようなことが起こりませんでしたが、もし左上の点が背景じゃない画像を処理するならば、背景色の取得方法に工夫が必要になります。

この背景透過処理プログラムは、このゲームの敵キャラクターのために作成しました。
ux.getuploader.com