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

日々の奮闘を綴る日記です。

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

自作ゲームのマップデータ(Luaスクリプト)を
テキストエディタで編集するのがしんどくなってきたので
マップエディタを作ることにしました。

C#GUIツールを作成するのも始めてなので
作成過程で学んだことを書いていきたいと思います。
間違いがあったらツッコミをお願いします。

C#Luaを使う

自分のゲーム(C++)はリソースのファイル名、サイズ等をLuaで記述していますので
C#からそのリソースファイルを読むためにC#でもLuaが扱えるようにする必要があります。

C#Luaを使うには、LuaInterfaceというライブラリを使うのが一般的です。
http://luaforge.net/projects/luainterface/
ダウンロードはこちらから。
http://files.luaforge.net/releases/luainterface/luainterface

最新の2.0.3は7zで圧縮がしてあります。
7z解凍ツールは各自ご用意いただいて、展開してください。

ファイルを展開すると、以下の2つのdllが解凍されます。

  • Luainterface.dll
  • Lua51.dll

それぞれLuaInterface(C#Luaを繋ぐ、橋渡し部分)と、Luaスクリプトインタプリタ部分になります。

それぞれ、[参照の追加]からdllを指定してC#のプロジェクトに取り込んでください。

[参照の追加]はこのあたりのサイトを見ればわかるでしょう。
http://www.godpatterns.com/2006/05/scripting-with-lua-in-c.html

取り込めたかどうかは、以下のコードをC#のプロジェクト上で
コンパイルしてエラーになるかどうかで判断してください。

using LuaInterface;
LuaInterface.Lua lua = new LuaInterface.Lua();

次に、C#からLuaスクリプトがコールできるか試してみましょう。
Lua内にTestという関数をDoStringメソッドで定義します。
この関数は渡された文字列をそのまま返します。

lua.DoString("function Test(msg) return msg end");

Test関数をコールします。strに"Sample"が入っていれば、
正常にLuaスクリプトが実行されていることがわかります。

object[] objs = this.lua.GetFunction("Test").Call("Sample");
string str = (string)objs[0];

これができたら次は関数登録をして、Luaから登録したC#の関数をコールしてみましょう。
関数登録には実際のそのメソッドを持つクラスインスタンスが必要ですので、ここではthisを渡しています。
たとえば、Class AのメソッドtestMethodをAのメソッドregistLua内で登録するには、以下のようにします。

public class A
{
  class 

  LuaInterface.Lua lua = new LuaInterface.Lua();  // 初期化済とする
  string msgFromLua = "";

  // Luaからコールされるメソッド
  public void testMethod( string msg )
  {
    msgFromLua = msg;
  }

  // Luaを登録するメソッド
  public void registLua()
  {
    lua.RegisterFunction("testMethod", this,
                         this.GetType().GetMethod("testMethod"));
  }
}

Lua側ではtestMethodに引数をそのまま渡すものとします。

function sample( msg )
  testMethod( msg )
end

C#からsampleをコールして、msgFromLuaに"hello"が入れば正常に
LuaからC#の関数がコールできていることになります。


また、C++のときはいちいちメンバ関数を登録する必要ありましたが、
C#のLuaInterfaceでは、その必要がないようです。

ですので、クラスインスタンスさえLuaに渡せば、
未登録のメソッドであってもコールすることができます。
以下のサンプルコードのようなことができるということですね。

public class Test
{
  private int nValue = 0;

  public void setValue( int nValue )  // Luaに未登録
  {
    this.nValue = nValue;
  }
}

public static void Main()
{
  Lua lua = new Lua();
  Test test = new Test();

  lua.GetFunction("LoadValue").Call(test);  // Lua側でtestを渡して値を入れてもらう
}
function LoadValue( p )
  p:setValue( 100 );  // Luaに未登録でも、setValueメソッドがコールできる!!
end

C#DirectXを使う

C#DirectXを使用する方法については、以下のサイトが非常〜〜〜に詳しいので、
ここでは解説しません。(自分もここで勉強しました)
http://www.clks.jp/csg/dx001.html

今回はDirectGraphicsのみを扱うものとし、以下のコードを書いています。
DirectXも参照の追加からdllを追加する必要がありますが、それは↑のサイトを参考にしてください。

public class DirectGraphics
{
  /// <summary>
  /// ピクチャボックス用のDirectGraphicsデバイス
  /// </summary>
  private Microsoft.DirectX.Direct3D.Device mapMainDevice = null;
  public Device D3DDevice
  {
   get { return this.mapMainDevice; }
  }

  //新しく頂点と頂点バッファを宣言
  private CustomVertex.TransformedColoredTextured[] verts = null;
  private VertexBuffer vertexBuffer                       = null;

  public DirectGraphics()
  {
  }

  /// <summary>
  /// DirectGraphics デバイスを作成し、基本的な設定を行う。
  /// </summary>
  /// <param name="form"></param>
  public int init( System.Windows.Forms.Control form )
  {
   Microsoft.DirectX.Direct3D.PresentParameters param = new PresentParameters();
   param.Windowed              = true;
   param.SwapEffect            = SwapEffect.Discard;
   param.PresentationInterval  = PresentInterval.Immediate;

   this.mapMainDevice = new Microsoft.DirectX.Direct3D.Device(0, 
                    Microsoft.DirectX.Direct3D.DeviceType.Hardware,
                    form,
                    CreateFlags.SoftwareVertexProcessing,
                    param);
   if ( this.mapMainDevice == null )
   {
    return 1;
   }

   this.mapMainDevice.RenderState.CullMode = Cull.None;
   this.mapMainDevice.RenderState.Lighting = false;

   // アルファ合成の設定
   // テクスチャカラーと背景のブレンドを行う
   this.mapMainDevice.RenderState.AlphaBlendEnable = true;
   this.mapMainDevice.SetTextureStageState(0, 
                                           TextureStageStates.AlphaOperation, 
                                           (int)Microsoft.DirectX.Direct3D.TextureOperation.Modulate);
   this.mapMainDevice.SetTextureStageState(0, 
                                           TextureStageStates.AlphaArgument1,
                                           (int)Microsoft.DirectX.Direct3D.TextureArgument.TextureColor);
   this.mapMainDevice.SetTextureStageState(0, 
                                           TextureStageStates.AlphaArgument2,
                                           (int)Microsoft.DirectX.Direct3D.TextureArgument.Current);

   this.mapMainDevice.RenderState.SourceBlend      = Blend.SourceAlpha;
   this.mapMainDevice.RenderState.DestinationBlend = Blend.InvSourceAlpha;

   //頂点用オブジェクトの生成
   verts = new CustomVertex.TransformedColoredTextured[4];
   vertexBuffer = new VertexBuffer(typeof(CustomVertex.TransformedColoredTextured), 
                                          4, mapMainDevice, 0, 
                                          CustomVertex.TransformedColoredTextured.Format, 
                                          Pool.Managed);
   return 0;
  }

  /// <summary>
  /// 描画領域をクリアする
  /// </summary>
  /// <param name="color"></param>
  public void clear( System.Drawing.Color color )
  {
   this.mapMainDevice.Clear( ClearFlags.Target, color, 1.0f, 0 );
  }

  /// <summary>
  /// 描画を開始する
  /// </summary>
  public void beginScene()
  {
   this.mapMainDevice.BeginScene();
  }

  /// <summary>
  /// 描画を終了する
  /// </summary>
  public void endScene()
  {
   this.mapMainDevice.EndScene();
  }

  /// <summary>
  /// ウィンドウにシーンを描画する
  /// </summary>
  public void present()
  {
   if (this.mapMainDevice == null) return;
   try
   {
    this.mapMainDevice.Present();
   }
   catch
   {
    // TODO:
   }
  }
}

さて、今回作るフォームのメッセージポンプのスレッドと描画スレッドを分けたいと思います。
スレッドを分けない場合、例えばメインフォームのスクロールバーをドラッグアンドドロップすると、
フォームがメッセージを処理している間描画が止まってしまうことになります。

まずはMapEditorFormという名前でフォームアプリケーションを作成し、エントリポイント(Main())を定義します。
また、描画コードはMapEditorFormが持つものとし、onDraw関数で描画するものとします。
描画するかどうかのフラグもMapEditorFormで持ちます。

描画は、MapEditorFormのピクチャボックスとします。
VisualStudioのフォームエディタでピクチャボックスを作成します。
そして、getMapMainPb()メソッドでピクチャボックスを取得できるようにしておいてください。
DirectGraphics初期化時に、描画先として使用します。

public partial class MapEditorForm : Form
 {
  // メインFormクラス内をアプリケーションのエントリポイントとする
  [STAThread]
  static void Main()
  {
    // TODO:後述します
  }

  // 描画フラグ
 private volatile bool bDrawFlg = true;

  // 描画スレッド
  private DrawThread drawTh = null;

  // DirectGraphicsオブジェクト
  private DirectGraphics graphics = null;

  public bool DrawFlg
  { 
    get{ return this.bDrawFlg; }
  }

  public Thread DrawThread
  {
    get { return this.drawTh; } 
   set{ this.bDrawFlg = value; }
  }

  public void onDraw()
  {
    lock(this)
    {
      // TODO:描画コードを書く
    }
  }
}

次に、描画を行うスレッドクラスを作成します。
描画可否フラグ、描画コード自体は上でも言ったとおり、MapEditorForm側で持ちます。

 /// <summary>
 /// 描画用のスレッドクラス
 /// </summary>
 public class DrawThread
 {
  private MapEditorForm mainForm;

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

  public void mainLoop()
  {
   // フォームの描画フラグが有効な限り描画を続ける
   while (this.mainForm.DrawFlg)
   {
    // TODO:タイマウェイトをかける

    // 描画する
    this.mainForm.onDraw();
   }
  }
 }

Main関数内を書いていきましょう。以下はMain関数内のコードです。

まずはメインフォームを作成します。
次に、上でコードを紹介したDirectGraphicsクラスの初期化を行います。

Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);

// マップエディタのメインフォームを生成します
MapEditorForm mainForm = new MapEditorForm();
if (mainForm == null)
{
   MessageBox.Show("フォームの生成に失敗");
   Application.Exit();
}
mainForm.Show();

オブジェクトを生成したら、initメソッドをコールします。
initメソッドには、描画先となるコントロールを渡します。
ここではMapEditorFormのピクチャボックスを渡します。getMapMainPb()を渡しましょう。
また、MapEditorFormにもDirectGraphicsオブジェクトを設定しておきます。

// DirectGraphicsオブジェクトを生成し、フォームにセットする
DirectGraphics d3dMainObj = new Draw.DirectGraphics();
if (d3dMainObj == null)
{
   MessageBox.Show("DirectGraphicsの生成に失敗");
   Application.Exit();
}

d3dMainObj.init(mainForm.getMapMainPb());
mainForm.LOZD3DGObj = d3dMainObj;

ここで描画用スレッドのオブジェクトを作成します。
DrawThreadクラスのmainLoopを指定してスレッドをスタートさせます。
mainLoop内では、描画フラグが立っている間、MapEditorFormのonDrawメソッドを延々とコールします。
スレッドクラスはメインフォームが所持しておきましょう。

// 描画スレッドの作成
DrawThread drawThread = new DrawThread(mainForm);
if (drawThread == null)
{
 MessageBox.Show("描画用スレッドの生成に失敗");
 Application.Exit();
}
Thread th = new Thread(new ThreadStart(drawThread.mainLoop));
if (th == null)
{
 MessageBox.Show("描画用スレッドの生成に失敗");
 Application.Exit();
}
th.Start();

// 描画スレッドは所持しておく
mainForm.DrawThread = th;

最後に、フォームをApplication.Runで開始させます。
これでフォームの終了と同時にアプリケーションが終了するようになりました。

また、アプリケーション終了時には描画スレッドを終了しなければなりません。
メインフォームのClosingイベントハンドラに次のコードを記載しましょう。

 lock (this)
 {
  this.bDrawFlg = false;
 }

 // 描画スレッドの終了を持つ
 this.drawThread.Join();

これでメインスレッド終了時には描画スレッドの終了を待ってから終了するようになりました。

次回から、実際にピクチャボックスへ描画を行いたいと思います。

プライバシーポリシー お問い合わせ