SLGBaseで戦術級シミュレーションゲームを作ってみる

6.移動関係データの作成

最終更新:

slgbase

- view
管理者のみ編集可

6-1.移動関係処理の準備と解説


SLGBaseの柱の一つである、移動処理の解説を行います。

まず、ユニット種別ごとに一度にどのくらい移動できるかというパラメータとして、「移動力」を割り当てることとします。
これは、前の節で作成したUnitBaseクラスのTotalMoveCost()関数で返される値で表現されます。
そこで、UnitBaseを継承したUnitクラスにおいて、ユニット種別と、それに応じた移動力を設定するコードを書いてみましょう。

Unit.cs
namespace SLGTest
{
 
    //add 6-1
    public enum UnitEnum
    {
        Infantry,       //歩兵
        Tank,           //戦車
        Chopper,        //ヘリ
        Fighter,        //戦闘機
        Battleship,     //戦艦
        Submarine,      //潜水艦
        Num                 //←種別数
    }
 
    //add 4-1
    public class Unit : UnitBase
    {
 
        //add 6-1
        public UnitEnum UnitKind { get; set; }
        public string Name { get; set; }
        private int _totalMoveCost = 1;
 
        //add 6-1
        public Unit(UnitEnum unitKind)
        {
            UnitKind = unitKind;
            switch (unitKind)
            {
                case UnitEnum.Infantry: 
                    Name = "歩兵"; _totalMoveCost = 2; break;
                case UnitEnum.Tank:
                    Name = "戦車"; _totalMoveCost = 4; break;
                case UnitEnum.Chopper:
                    Name = "ヘリ"; _totalMoveCost = 6; break;
                case UnitEnum.Fighter:
                    Name = "戦闘機"; _totalMoveCost = 8; break;
                case UnitEnum.Battleship:
                    Name = "戦艦"; _totalMoveCost = 4; break;
                case UnitEnum.Submarine:
                    Name = "潜水艦"; _totalMoveCost = 3; break;
                default:
                    Name = "歩兵"; _totalMoveCost = 2; break;
            }
        }
        public Unit() : this(UnitEnum.Infantry) { }
 
        public override int TotalMoveCost
        {
            //change 6-1
            //get { return (1); }
            get { return (_totalMoveCost); }
 
        }
 
    }
}
 


次に、セルの側にも種別を作成します。

Cell.cs
namespace SLGTest
{
 
    //add 6-1
    public enum CellEnum
    {
        Sea,            //海や川
        Plain,          //平原
        Road,           //道路
        Forest,         //森
        Mountain,       //山
        Num,                //←種別数
    }
 
    //add 2-1
    public class Cell:CellBase
    {
        //add 5-3
        public static int Width = 64;
        public static int Height = 64;
 
        //add 6-1
        public CellEnum CellKind { get; set; }
        public string Name { get; set; }
        public Cell(CellEnum cellKind)
        {
            CellKind = cellKind;
            switch (cellKind)
            {
                case CellEnum.Sea:
                    Name = "海"; break;
                case CellEnum.Plain:
                    Name = "平原"; break;
                case CellEnum.Road:
                    Name = "道路"; break;
                case CellEnum.Forest:
                    Name = "森"; break;
                case CellEnum.Mountain:
                    Name = "山"; break;
                default:
                    Name = "海"; break;
            }
        }
        public Cell() : this(CellEnum.Sea) { }
 
    }
}
 

ところで、一般的にはセルの種別ごとにユニットの移動力の消費量(移動コスト)が異なるのが普通だと思います。
あるユニットが道路を移動するのと、森の中を移動するのでは当然移動コストが異なることでしょう。
その際、どのようなユニットに対しても一律で同じ移動力を消費して良いものでしょうか。
もちろんゲームデザインとしてそれで良い場合も多々あることでしょうが、たとえば歩兵ユニットが森に侵入する際に多くの移動力を消費するのに対し、飛行機ユニットはほとんど移動力を消費しないことにするのが自然かと思います。
このような移動処理を実現するには、ユニットとセルの関係から移動コストを決定する必要があります。

この処理を行うため、MapBaseクラスにはオーバーライド可能なGetMoveCost()関数が用意されています。
getMoveCost()関数は、ユニット、移動元セル、移動先セルを引数として受け取り、その状況で消費されるべき移動コストを返す必要があります。
オーバーライドしない場合は、この関数は常に1を返すようになっており、どのユニットがどのセルに移動しても常に移動コストが1になっています。
試しに適当な値を返すようなコードを書いてみましょう。

Map.cs(一部)
public class Map: MapBase
    {
 
        // - 略 - 
 
        //add 6-1
        public override double GetMoveCost(UnitBase unit, CellBase currentCell, CellBase nextCell)
        {
            return (_moveCost[(int)(nextCell as Cell).CellKind, (int)(unit as Unit).UnitKind]);
        }
        private double[,] _moveCost = new double[,]{
            //  歩兵,   戦車,   ヘリ,   戦闘機, 戦艦,   潜水艦
                {-1,    -1,     1,      1,      1,      1},          //海や川
                {2,     2,      1,      1,      -1,     -1},         //平原
                {1,     0.5,    1,      1,      -1,     -1},         //道路
                {2,     -1,     1,      1,      -1,     -1},         //森
                {-1,    -1,     2,      1,      -1,     -1}          //山
        };
    }
 


何度も繰り返しますが、これらのコードはサンプルです。
SLGBaseの各機能を使うのに最低限必要なのは、情報が適切に返されるよう各基本クラスの
メンバがオーバーライドされていることだけで、その処理をどのように行うかについては
あなたに任されます。
あなたが実際にゲームを作る際には、上記のユニットやセル情報、移動コスト計算等は、
ハードコーディングするよりもスクリプトファイルなどから読み込むようにするのが良い
方法でしょう。

また余談ですが、GetMoveCost()の引数として移動元セルと移動先セルが指定できるように
なっている理由は、たとえば敵ユニットに囲まれていないセルから囲まれているセルへの
移動などをチェックすることで、いわゆるZOC(敵に囲まれたセルでは移動に制限を受けるルール)
を実現できるからです。
ZOCに限らず、なにか面白いルールができないか、考えてみてください。
例えば、セルに高さ情報を持たせておくと、現在のセルと移動先セルの高さの差が1以内なら
移動できるルールを作成することで、階段等の段差を上り下りできるようになります。



6-2.描画処理の修正

次の節で実際の移動処理を確認できるよう、描画処理を修正します。
具体的には、セル種別、ユニット種別を画面上に表示する他、移動範囲セルを強調表示できるようにします。
(画像素材の準備が面倒なので、ここでは文字での表示のみとします)
また、テスト用のマップデータをテキストファイルから読み込めるようにします。


Map.cs(一部)
namespace SLGTest
{
    //add 3-1
    public class Map: MapBase
    {
        private Cell[,] _cells = null;
        private int _cols = 0, _rows = 0;
 
        //add 5-3
        /// <summary>
        /// マップ描画中心セル
        /// </summary>
        public Position ViewCenter { get; set; }
 
        //add 6-2
        private Font _font = new Font("MS ゴシック", 12);
        private Random _rnd = new Random();
        public CellList MoveRangeCells { get; set; }
 
        //renew 6-2
        public void Init(string filename)
        {
            using (StreamReader r = new StreamReader(filename, Encoding.Default))
            {
                string line;
                string[] item;
                Unit u;
                int row = 0;
                while ((line = r.ReadLine()) != null)
                {
                    item = line.Split("=,".ToCharArray());
                    if (item.Length > 0)
                    {
                        switch (item[0].ToUpper())
                        {
                            case "SIZE":
                                _cols = int.Parse(item[1]);
                                _rows = int.Parse(item[2]);
                                _cells = new Cell[_cols,_rows];
                                break;
 
                            case "MAP":
                                for (int i = 1; i < item.Length; i++)
                                {
                                    _cells[i - 1, row] = new Cell((CellEnum)int.Parse(item[i])) 
                                           { X = i - 1, Y = row };
                                }
                                row++;
                                break;
 
                            case "UNIT":
                                u = new Unit((UnitEnum)int.Parse(item[1]));
                                u.SetPos(int.Parse(item[2]), int.Parse(item[3]));
                                this.Units.Add(u);
                                break;
                        }
                    }
                }
            }
 
            ViewCenter = new Position();
            MoveRangeCells = new CellList();
        }
 
        //  - 略 -
 
        //add 5-3
        public void Draw(PictureBox pic, Graphics g)
        {
            g.Clear(Color.White);
 
            //セルの表示サイズを基準に表示範囲を計算
 
            int h = pic.Height / Cell.Height;       //縦方向表示可能セル数
            int w = pic.Width / Cell.Width;         //横方向表示可能セル数
 
            for (int x = ViewCenter.X - w / 2 - 1; x <= ViewCenter.X + w / 2 + 1; x++)
                for (int y = ViewCenter.Y - h / 2 - 1; y <= ViewCenter.Y + h / 2 + 1; y++)
                {
                    if (x >= 0 && x < Cols && y >= 0 && y < Rows)
                    {
                        Position p = Cell2View(pic, GetCell(x, y));
                        g.DrawRectangle(Pens.Green, p.X, p.Y, Cell.Width, Cell.Height);
 
                        //add 6-2
                        g.DrawString((GetCell(x, y) as Cell).Name, _font, Brushes.Green, p.X, p.Y);
                    }
                }
 
            //add 6-2
            foreach (Unit item in Units)
            {
                Position p = Cell2View(pic, item);
                g.DrawString(item.Name, _font, Brushes.Blue, p.X, p.Y + 16);
            }
 
            //add 6-2
            if (MoveRangeCells != null && MoveRangeCells.Count > 0)
            {
                using(Brush b  = new SolidBrush(Color.FromArgb(64, Color.Cyan)))
                {
                    foreach (Cell item in MoveRangeCells)
                    {
                        Position p = Cell2View(pic, item);
                        g.FillRectangle(b, p.X, p.Y, Cell.Width, Cell.Height);
                    }
                }
            }
        }
    }
}
 

フォームの初期化時に、テストマップデータを読み込む処理を追加します。
また、フォームのサイズ変更時の地図再描画を忘れていたので、イベント処理を追加しておきます。

Form1.cs(一部)
//add 5-2
        private void Form1_Load(object sender, EventArgs e)
        {
            _map = new Map();
 
            //change 6-2
            //_map.Init(50, 50);
            _map.Init(@"../../TestMap.txt");
 
            //remove 6-2
            //Unit u = new Unit();
            //u.SetPos(5, 5);
            //_map.Units.Add(u);
 
            hScrollBar1.Maximum = _map.Cols;
            vScrollBar1.Maximum = _map.Rows;
 
            pictureBox1.Invalidate();
        }
 
        //add 6-2
        private void pictureBox1_SizeChanged(object sender, EventArgs e)
        {
            pictureBox1.Invalidate();
        }
 

6-3.実行結果とここまでのプロジェクト


移動範囲などの表示は、次の節以降で行います。
#ref error :ご指定のファイルが見つかりません。ファイル名を確認して、再度指定してください。 (画面.jpg)



※テスト用のマップデータであるTestMap.txtは、上記のプロジェクトに含まれています。
ウィキ募集バナー