結果だけでなく過程も見てください

たい焼きさんの日々の奮闘を綴る日記です。

C#でゲームのマップエディタを作る (2)

前回記事の続きです。
http://d.hatena.ne.jp/taiyakisun/20120331#1333204146

今回はピクチャボックスの描画を行います。
まずは、C#で変換行列を作成し、描画するコードを書くための肩慣らしとして、
エディタのグリッド線を描いてみましょう。

頂点のフォーマットは座標と色のみとします。
座標変換は自分で行います。

グリッド線を描画するクラス

まずは、以下のようにグリッドを描画するクラスを作成します。

using System;
using System.Collections.Generic;
using System.Text;
using System.Drawing;
using Microsoft.DirectX;
using Microsoft.DirectX.Direct3D;

class DrawGrid
{
  private Device device = null;
  private CustomVertex.PositionColored[] verts = null;
  private VertexBuffer vertexBuffer = null;

  private Size mapSize; // マップサイズ
  private Size gridSize; // グリッドサイズ
  private Point gridNum = new Point(); // グリッド数(マップサイズ/グリッドサイズで算出される)
  private Point scrollOffset = new Point(); // スクロールバーによりずれるオフセット

  public DrawGrid(Device device){ this.device = device; }
  public void createGrid(Size mapSize, Size gridSize, Color color){ ... }
  public void onDraw(){ ... }
}

グリッド描画に必要な情報を持ち、以下の機能を所持するクラスです。

  • 描画に必要な情報の作成
  • グリッド線の描画

描画に必要な情報の作成はcreateGrid関数で行います。

public void createGrid(Size  mapSize,
          Size  gridSize,
          Color color)
{

マップサイズ(=ピクチャボックスのサイズ)、グリッドのサイズ、グリッド線の色は
コール元が決めて、このメソッドに渡します。

this.mapSize = mapSize;
this.gridSize = gridSize;
this.gridNum.X = mapSize.Width / gridSize.Width;
this.gridNum.Y = mapSize.Height / gridSize.Height;

this.verts = new CustomVertex.PositionColored[(this.gridNum.X * this.gridNum.Y)*2 + 2];
this.vertexBuffer = new VertexBuffer(typeof(CustomVertex.PositionColored),
             (this.gridNum.X * this.gridNum.Y)*2 + 2,
             this.device, 0, CustomVertex.PositionColored.Format, Pool.Managed);

マップサイズ、グリッドサイズをセットします。
グリッド数はこの2つの情報から算出します。

次に頂点情報(PositionColored)と、バーテックスバッファ(VertexBuffer)のメモリを確保します。

これは、"頂点の"数なので、線1本に対して2つの頂点データが必要になります。
そのため2倍の数を確保しています。

// 横線
int cnt = 0;
int index = 0;
for(cnt=0, index=0; cnt < (this.gridNum.Y+1); ++cnt, index+=2)
{
 this.verts[index].X = 0;                      
 this.verts[index + 1].X = mapSize.Width;

 this.verts[index].Y = cnt * gridSize.Height;
 this.verts[index+1].Y = cnt * gridSize.Height;

 this.verts[index].Z = 0;
 this.verts[index+1].Z = 0;

 this.verts[index].Color = color.ToArgb();
 this.verts[index+1].Color = color.ToArgb();
}

// 縦線
for (cnt=0; cnt < (this.gridNum.X+1); ++cnt, index += 2)
{
 this.verts[index].X = cnt * gridSize.Width;
 this.verts[index + 1].X = cnt * gridSize.Width;

 this.verts[index].Y = 0;
 this.verts[index + 1].Y = mapSize.Height;

 this.verts[index].Z = 0;
 this.verts[index+1].Z = 0;

 this.verts[index].Color = color.ToArgb();
 this.verts[index + 1].Color = color.ToArgb();
}

横と縦に線を引きます。
ここは特に説明はいらないと思いますが、それぞれの線の始点と終点の座標を設定します。
始点と終点は、横線の場合はマップの幅、縦線の場合はマップの高さがそのまま長さになります。
あとはそれをグリッド数分設定するだけです。
Zはとりあえず0に。色は引数で指定されたものをそのまま渡します。

これで、グリッド線を構成する情報は揃いました。

public void onDraw()
{
   // 平行移動は原点に戻す
   Matrix matWorld = Matrix.Translation(0, 0, 0);
   this.device.Transform.World = matWorld;

   // テクスチャは無効に
   this.device.SetTexture(0, null);

   Microsoft.DirectX.GraphicsStream stm = this.vertexBuffer.Lock(0, 0, 0);
   stm.Write(this.verts);
   this.vertexBuffer.Unlock();

   this.device.SetStreamSource(0, this.vertexBuffer, 0);
   this.device.VertexFormat = CustomVertex.PositionColored.Format;
   this.device.DrawPrimitives(PrimitiveType.LineList, 0, (this.gridNum.X * this.gridNum.Y) + 1);
}

グリッド線を描画します。
グリッド線は決まった場所(モデル座標そのまま)に描画しますので、ワールド変換行列は単位行列でよいです。

テクスチャは念のため無効にしておきます。

頂点バッファをロックして、書き込み先のストリームを取得したら、頂点情報を書き込んでロックを解除します。

バイスにSetStreamSourceで描画情報と、フォーマット(PositionColored)を指定したら、
DrawPrimitivesで描画します。描画はLineList(線の集合)です。
また、第三引数は描画するプリミティブ(ここでは線)の"数"なので、ここは2倍しない点に注意です。

DrawGridクラスを利用する側が行うこと

おそらくDrawGridクラスを利用するのはフォームクラスになると思いますが、
以下のようにDrawGridクラスインスタンスを生成し、マップサイズ、グリッドサイズ、色を渡せばよいでしょう。

volatileは、複数スレッドが正しい値を取得できるようにするキーワードです。
最適化を抑止し、複数スレッドが同じタイミングで
同じ値を取得できるように、とりあえずつけておきます。

volatile DrawGrid dgrid = new DrawGrid();

:

lock(thisLock)
{
  grid.createGrid(new Size(640,480),
                     new Size(16,16),
                     new Color(Color.Goldenrod));
}

メインフォームのonDraw()内でグリッドを描画します。
メインスレッドと描画スレッド両方からアクセスされるため、メインフォームのロックオブジェクトで
lockしてアクセスします。

public void onDraw()
{
  lock(thisLock)
  {
    this.device.BeginScene();

    this.device.Clear( ClearFlags.Target | ClearFlags.ZBuffer, Color.SlateGray, 1.0f, 0 );

    this.dgrid.onDraw();

    this.device.EndScene();

    this.device.Present();
  }
}

描画スレッドは、メインフォームのonDraw()をコールして描画します。

// mainLoop関数が描画スレッドとして起動される

private MapEditorForm mainForm;

public DrawThread(MapEditorForm mainForm)
{
 this.mainForm = mainForm;
}

public void mainLoop()
{
 while (this.mainForm.DrawFlg)
 {
  // とりあえず60FPS
  System.Threading.Thread.Sleep((int)(1000f / 60f));

  this.mainForm.onDraw();
 }
}

正しく動作すれば、以下のようにピクチャボックスにグリッド線が描画されます。

追記(5/13)

すいません。上記コードだけでは、カメラが考えられていません。
以下のように、xy座標はスクリーンの中心を、z座標は-1.0fから、z座標0を
見るように設定してください。

Matrix matView
    = Matrix.LookAtLH(new Vector3(SCREENSIZE.Width / 2, SCREENSIZE.Height / 2, -1.0f), 
                      new Vector3(SCREENSIZE.Width / 2, SCREENSIZE.Height / 2, 0),
                      new Vector3(0, 1, 0));
this.device.Transform.View = matView;