C#とYaneuraoGameSDKでゲームを作る(1)

だらだら読んでたC#ゲームプログラミングの本ついに読み終わったー
んなわけで中に書かれてるテトリスのサンプルコードを元にC#の練習も兼ねてYaneuraoGameSDKで書き直してみた。



目標は出来る限り元ソース(本)の実行結果と同じくなるように作る!
そんなこんなで出来た実行ファイルは これ (.netFramework2.0必須)

30fpsで動作*1。元ソースに準拠してゲーム状態遷移無し。
開発環境はVS2005でした。


以下解説。

構成

最終的なソースファイルは以下の5つでした。

  • Field.cs
  • Piece.cs
  • PieceDirection.cs
  • Program.cs
  • form1.cs

上の三つは元のソースと全く同じのゲームオブジェクトクラス。
さすがプロ・・・・ちゃんとデータが描画から分離してます。
Program.csはVS2005が勝手に作ったFormのエントリポイントです。放置。
んで僕がやったことと言えばform1.csに
フレーム制御と描画と入力部分をYanesdkで書き直しただけという・・・うああ。
今思うとエントリにするほどの内容ではなかった・・・・。

変更したソースのおおざっぱな解説など

using Yanesdk.Draw;
using Yanesdk.Timer;
using Yanesdk.Input;

参照した先はこんな感じ。

        private void Init()
        {
            //初期化コードはここに書く
            //fps関連
            fpsTimer.Fps = 30;
            fpsLayer.FpsTimer = fpsTimer;
            
            //リソース読み込み
            win.InitByHWnd(pictureBox1.Handle);
            Yanesdk.Draw.Screen2DGl scr = win.Screen;
            {
                scr.Select();
                //スクリーンに対する処理はここに書く

                //font設定               
                fl.LoadDefFile("mem:msmincho.ttc , 16 , 0 , 0\n");
                fr.SetLoader(fl, 0);

                sikaku = fr.GetTexture("■", 0);
                nuki = fr.GetTexture("□", 0);

                scr.Unselect();
            }

            //データ初期化
            field = new Field(12, 24);
            Next();

            //ここでTimerを起動!
            timer1.Enabled = true;
        }

初期化はこんなん。
なるべく元ソースのテトリスの実行画面に近づけようと思ったんで、画像じゃなくfontテクスチャ利用してみました。
フォントリポジトリの恩恵で恐らく元ソースのテトリスより負荷が無くなったと思う。

        private void timer1_Tick(object sender, EventArgs e)
        {
            if (fpsTimer.ToBeRendered)
            {
                //ここに入力コードを書く
                //?低fps時に反応が悪い?
                key.Update();
                onKeyDown(key);

                //ここに移動コードを書く
                if (!MovePiece(PieceDirection.Down))
                {
                    field.PieceToField(currentPiece);
                    int line = field.AdjustLine();

                    Next();

                    //終了
                    if (field.IntersectsWidth(currentPiece))
                    {
                        timer1.Stop();
                        MessageBox.Show(this, "GameOver");
                    }
                }                

                //ここに描画コードを書く
                //描画計算を外部に?
                Yanesdk.Draw.Screen2DGl scr = win.Screen;
                {
                    scr.Select();

                    //スクリーンに対する処理はここで書く
                    scr.SetClearColor(255, 255, 255);
                    scr.Clear();

                    scr.Blend = true;
                    scr.BlendSrcAlpha();
                    scr.SetColor(0, 0, 0);
          

                    for (int y = 0; y < field.Height; y++)
                    {
                        int dx = 1;
                        int dy = (int)sikaku.Height * y;
                                               
                        int py = y - currentPiece.Y;
                    
                        for (int x = 0; x < field.Width; x++)
                        {
                            if (field[x, y])
                                scr.Blt(sikaku, dx, dy);
                            else
                            {
                                int px = x - currentPiece.X;
                                if (px >= 0 && px < Piece.WIDTH && py >= 0 &&
                                    py < Piece.HEIGHT && currentPiece[px, py])
                                    scr.Blt(sikaku, dx, dy);
                                else scr.Blt(nuki, dx, dy);
                            }
                            dx += (int)sikaku.Height;
                        }
                    }
                    fpsLayer.OnDraw(scr, 0, 10);
                   
                    scr.ResetColor();
                    scr.Update();
                }
                fpsTimer.WaitFrame();
            }
        }

fps制御と描画。Piece内の描画計算を独立させた方が良かったかも。
つーかメインループのフローは「キー入力、オブジェクト移動、描画」の順で良かったっけか・・・?

        protected void onKeyDown(Key1 key)
        {
            if (key.IsPress(3)) MovePiece(PieceDirection.Left);
            else if (key.IsPress(4)) MovePiece(PieceDirection.Right);
            else if (key.IsPress(2)) MovePiece(PieceDirection.Down);
            else if (key.IsPress(5)) TurnPiece();
        }

最後は入力部分。Yanesdkのおかげでさくっとゲームパッドまで対応!


おしまい。

雑感

書き出してみるとやったことこんだけ・・・・・。すくなー
プログラミングってレベルじゃねぇぞ


とは言っても初心者C#だったんで、落ち着くまで時間はかなりかかってしまった。
元ソースがあったから良いようなものの一から作ったらどんだけかかるんだ!


C#については、まぁJava?ってかんじで書けるのが良い=ドット入れた瞬間メソッドとか出てくるのがハッピー!
でもdelegateとインデクサの意味が理解し切れてない・・・・、先にこの本以外でC#の作法学んだ方が良いと思った。
あとはYanesdkでめちゃ楽にコード書けるのは良いけど、ネット上の参考文献がそんなに無いのがちとツライ。
fontテクスチャ使うのがよく分からんかったので最後はsdkのソース見ながらうろうろ書いた。
おまえらもっとネット上のエントロピー増やせよ!


あとは
空のプロジェクトでビルドして実行したときに出る謎のコンソール画面を消すには
「プロジェクトのプロパティ」にいって
「アプリケーションタブ」の「出力の種類」を
windowsプロジェクト」
に変更する、ってとこが一番難しかった。
途中までどうしてもコンソール消えなくてやめようかと思った。
次回は本のシューティングサンプルをYaneSDKで書いてみるつもり。taskSystemいくどー!おー


どっかのプログラマの格言に「初心者ほどソースを見せない」とかあったんで
一応Form1.csのフルソースを以下に。

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Text;
using System.Windows.Forms;

using Yanesdk.Draw;
using Yanesdk.Timer;
using Yanesdk.Input;

namespace sampleTettoris
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();

            Init();            
        }
        
        private void Init()
        {
            //初期化コードはここに書く
            //fps関連
            fpsTimer.Fps = 30;
            fpsLayer.FpsTimer = fpsTimer;
            
            //リソース読み込み
            win.InitByHWnd(this.Handle);
            Yanesdk.Draw.Screen2DGl scr = win.Screen;
            {
                scr.Select();
                //スクリーンに対する処理はここに書く

                //font設定               
                fl.LoadDefFile("mem:msmincho.ttc , 16 , 0 , 0\n");
                fr.SetLoader(fl, 0);

                sikaku = fr.GetTexture("■", 0);
                nuki = fr.GetTexture("□", 0);

                scr.Unselect();
            }

            //データ初期化
            field = new Field(12, 24);
            Next();

            //ここでTimerを起動!
            timer1.Enabled = true;
        }

        private FpsTimer fpsTimer = new FpsTimer();
        private FpsLayer fpsLayer = new FpsLayer();
        private FontRepository fr = new FontRepository(delegate { return new GlTexture(); });
        private FontLoader fl = new FontLoader();
        private Textures sikaku, nuki;
        private Field field;
        private Piece currentPiece;
        private Win32Window2DGl win = new Win32Window2DGl();

        private Yanesdk.Input.Key1 key = new Yanesdk.Input.Key1();        
        
        private void timer1_Tick(object sender, EventArgs e)
        {
            if (fpsTimer.ToBeRendered)
            {
                //ここに入力コードを書く
                //?低fps時に反応が悪い?
                key.Update();
                onKeyDown(key);

                //ここに移動コードを書く
                if (!MovePiece(PieceDirection.Down))
                {
                    field.PieceToField(currentPiece);
                    int line = field.AdjustLine();

                    Next();

                    //終了
                    if (field.IntersectsWidth(currentPiece))
                    {
                        timer1.Stop();
                        MessageBox.Show(this, "GameOver");
                    }
                }                

                //ここに描画コードを書く
                //描画計算を外部に?
                Yanesdk.Draw.Screen2DGl scr = win.Screen;
                {
                    scr.Select();

                    //スクリーンに対する処理はここで書く
                    scr.SetClearColor(255, 255, 255);
                    scr.Clear();

                    scr.Blend = true;
                    scr.BlendSrcAlpha();
                    scr.SetColor(0, 0, 0);
          

                    for (int y = 0; y < field.Height; y++)
                    {
                        int dx = 1;
                        int dy = (int)sikaku.Height * y;
                                               
                        int py = y - currentPiece.Y;
                    
                        for (int x = 0; x < field.Width; x++)
                        {
                            if (field[x, y])
                                scr.Blt(sikaku, dx, dy);
                            else
                            {
                                int px = x - currentPiece.X;
                                if (px >= 0 && px < Piece.WIDTH && py >= 0 &&
                                    py < Piece.HEIGHT && currentPiece[px, py])
                                    scr.Blt(sikaku, dx, dy);
                                else scr.Blt(nuki, dx, dy);
                            }
                            dx += (int)sikaku.Height;
                        }
                    }
                    fpsLayer.OnDraw(scr, 0, 10);
                   
                    scr.ResetColor();
                    scr.Update();
                }
                fpsTimer.WaitFrame();
            }
        }

        protected void onKeyDown(Key1 key)
        {
            if (key.IsPress(3)) MovePiece(PieceDirection.Left);
            else if (key.IsPress(4)) MovePiece(PieceDirection.Right);
            else if (key.IsPress(2)) MovePiece(PieceDirection.Down);
            else if (key.IsPress(5)) TurnPiece();
        }

        public bool MovePiece(PieceDirection pd)
        {
            Piece piece = currentPiece.Copy();
            if (pd == PieceDirection.Down) piece.Y++;
            else if (pd == PieceDirection.Left) piece.X--;
            else if (pd == PieceDirection.Right) piece.X++;

            if (!field.Contains(piece)) return false;
            else if (field.IntersectsWidth(piece)) return false;

            currentPiece = piece;
            return true;
        }       
        
        public void Next()
        {
            currentPiece = Piece.Random();
            currentPiece.X = field.Width / 2 - Piece.WIDTH / 2;
            currentPiece.Y = -2;
        }

        public bool TurnPiece()
        {
            Piece piece = currentPiece.TurnPiece();
            if (!field.Contains(piece)) return false;
            else if (field.IntersectsWidth(piece)) return false;

            currentPiece = piece;
            return true;
        }
    }  
}

*1:早すぎた・・・・。