無職が倉庫番をコンソールアプリで作ってみた C#

どうも！無職の進藤京介です！
正確には年明けから:confetti_ball:無職:confetti_ball:になります

さて、現在はC#の勉強中ということで倉庫番ゲームを作ってみました
Visual Studio for Mac v7.0.1 で作成しました。
機能としましては

<ul>
<li>テキストファイルからステージデータを読み込みます</li>
<li>荷物とゴールは複数に対応</li>
</ul>

くらいです（少ない）


<h3>クラスの紹介</h3>

作ったクラスは


<P>
Map<br>
オブジェクトを格納する配列を持っています。<br>
ほとんどの処理をこのクラスが行います<br>
オブジェクトの作成　キー入力　プレイヤーの移動と判定等　クリアチェック
</p>

<P>
GameScene<br>
正直要らないクラスですが作っちゃいました
</p>

<P>MapObject<br>
オブジェクトの種類を持っています。
プレイヤー、ゴール、荷物、壁、床、などをenumで持っています。<br>
こちらのクラスのインスタンスがMapクラスの配列に入ります。
</P>
<P>Program<br>
最初に呼ばれるmainメソッドを持っています</P>

ゲーム出力例
■■■■■■■■■■■■■■
■□□□□□□□□□□□□■
■□□□□□■N□□G□□■
■□□□P□□□□□□□□■
■□□□□D■□□□□□□■
■■■■■■■■■■■■■■

P：プレイヤー
N：荷物
G：ゴール
□：床
■：壁
D：ゴールの上にのった荷物

ルール
プレイヤーは荷物を押してゴールに運びます。
全てのゴールに荷物を乗せればゲームクリアです。


<h3>プログラム</h3>

```csharp:GameScene.cs
using System;
namespace soukoban3
{
    public class GameScene
    {
        public GameScene()
        {
            var filepath = @"/Users/kyosuke/Projects/sokoban/sokoban/StageData.txt";
            Map.InitializeMap(filepath);
        }

        public void Update(){
            Map.RunSystem();
        }
    }
}

```



```csharp:Program.cs
using System;

namespace soukoban3
{
    class MainClass
    {
        public static void Main(string[] args)
        {
            GameScene scene = new GameScene();
            scene.Update();
        }
    }
}

```


GameSceneでやることを、Programでやっても変わらないですね



```csharp:MapObject.cs
using System;
namespace soukoban3
{
    public class MapObject
    {

        public enum OBJ_TYPE{
            PLAYER,
            NIMOTU,
            GOAL,
            WALL,
            FLOOR,
            GOAL_ON_NIMOTU,
        }

        public OBJ_TYPE objType{
            get;set;
        }

        public MapObject(OBJ_TYPE type)
        {
            this.objType = type;
        }
    }
}

```

MapObjectクラスは配列に入れるもので、オブジェクトのタイプをenumで管理します。
コンストラクタでどのOBJ_TYPEにするかを決定させています。


```csharp:MapObject.cs
using System;
using System.IO;
using System.Text;
using System.Collections.Generic;
using System.Linq;

namespace soukoban3
{
    public static class Map
    {
        public static MapObject[] map;
		public static int mapWidth;
		public static int mapHeight;
        public static List<MapObject> objectList = new List<MapObject>();

         //初期化するメソッド
         public static void InitializeMap(string filePath){
	         int index = 0;
	          string[] lines = File.ReadAllLines(filePath);
              //mapの高さは行数で設定します
	          mapHeight = lines.Length;
              //mapの幅は一行の文字数にします
	          foreach (var val in lines[0])
	              mapWidth++;
             //高さと幅を使って配列の要素数を決定します
            map = new MapObject[mapWidth * mapHeight];


            MapObject mapobject;
	            using (var reader = new StreamReader(filePath, Encoding.UTF8))
	            {
		              while (reader.Peek() > -1)
		             {
		                  char c = Convert.ToChar(reader.Read());
		                    //改行コードを無視して配列の要素数を超えないようにする
		                   if (c != Convert.ToChar(Environment.NewLine))
		                  {
                              //c の内容によって生成するオブジェクトのTYPEを決定する
                              mapobject = getObject(c);
                              map[index] = mapobject;
		                      index += 1;
		                  }   
		             }
	             }
                    DrawMap();
            }


        //オブジェクトのOBJ_TYPEを見てコンソールに出力する
        public static void DrawMap()
        {
            for (int i = 0; i < map.Length; i++)
            {
                switch (map[i].objType)
                {
                    case MapObject.OBJ_TYPE.PLAYER: Console.Write('P'); break;
                    case MapObject.OBJ_TYPE.NIMOTU: Console.Write('N'); break;
                    case MapObject.OBJ_TYPE.WALL: Console.Write('■'); break;
                    case MapObject.OBJ_TYPE.GOAL: Console.Write('G'); break;
                    case MapObject.OBJ_TYPE.FLOOR: Console.Write('□'); break;
                    case MapObject.OBJ_TYPE.GOAL_ON_NIMOTU: Console.Write('D'); break;
                }
                //幅の数だけ表示したら改行させます
                if ((i + 1) % mapWidth == 0)
                {
                    Console.Write(Environment.NewLine);
                }
            }
        }

        //文字を元に生成するタイプを指定して返却する
        private static MapObject getObject(char type){
            if (type == 'P')
                return new MapObject(MapObject.OBJ_TYPE.PLAYER);
            else if (type == 'N')
                return new MapObject(MapObject.OBJ_TYPE.NIMOTU);
            else if (type == 'G')
                return new MapObject(MapObject.OBJ_TYPE.GOAL);
            else if (type == '■')
                return new MapObject(MapObject.OBJ_TYPE.WALL);
            else
                return new MapObject(MapObject.OBJ_TYPE.FLOOR);
        }


        public static void RunSystem(){
            
            while(true){              
                string key = Console.ReadLine();
				int dirction = 0;
                //入力したキーに応じてプレイヤーの移動先を決定します。
				switch (key)
				{
					case "a":
						dirction = -1;
						break;
					case "d":
						dirction = 1;
						break;
					case "w":
						dirction = Map.mapWidth * (-1);
						break;
					case "s":
						dirction = Map.mapWidth;
						break;
					default:
						Console.Write("ゲームを終了します");
						return;
				}

                //ここでプレイヤーの移動先を調べて、移動できるかどうかを判断して実際に移動させます。
				for (int i = 0; i < map.Length; i++)
				{
                    //プレイヤーの特定をする
                    if (map[i].objType == MapObject.OBJ_TYPE.PLAYER){
                        //プレイヤーの移動先の特定
                        var nextObject = map[i + dirction].objType;
                        //移動先が壁だった場合の処理
                        if (nextObject == MapObject.OBJ_TYPE.WALL){
                            break;
                        //荷物だった場合の処理（ここが特に汚い）
                        }else if(nextObject == MapObject.OBJ_TYPE.NIMOTU){
                            //荷物の移動先のオブジェクト特定する
                            var NimotuNextObject = map[(i + dirction) + dirction].objType;
                            //荷物の移動先が壁と荷物と荷物が乗ったゴール以外だったら
                            if (NimotuNextObject != MapObject.OBJ_TYPE.WALL && 
                                NimotuNextObject != MapObject.OBJ_TYPE.NIMOTU &&
                                NimotuNextObject != MapObject.OBJ_TYPE.GOAL_ON_NIMOTU)
                            {
                                //荷物の移動先がゴールだった場合
                                if(NimotuNextObject == MapObject.OBJ_TYPE.GOAL) {
                                    map[(i + dirction) + dirction].objType = MapObject.OBJ_TYPE.GOAL_ON_NIMOTU;
                                    map[i + dirction].objType = MapObject.OBJ_TYPE.PLAYER;
									map[i].objType = MapObject.OBJ_TYPE.FLOOR;
                                    CheckGoal();
									break;
                                }
                                //荷物の移動
                                map[(i + dirction) + dirction].objType = MapObject.OBJ_TYPE.NIMOTU;
                                //プレイヤーの移動
                                map[i + dirction].objType = MapObject.OBJ_TYPE.PLAYER;
                                //プレイヤー移動後お床にする
                                map[i].objType = MapObject.OBJ_TYPE.FLOOR;
                                break;
                            }
                        //移動先が床だった場合の処理
                        }else if (nextObject == MapObject.OBJ_TYPE.FLOOR){
                            map[i + dirction].objType = MapObject.OBJ_TYPE.PLAYER;
                            map[i].objType = MapObject.OBJ_TYPE.FLOOR;
                            break;
                        }
                     }
                  }
                Map.DrawMap();
				}//end while
			}


        private static void CheckGoal(){
            //まず描画します
            Map.DrawMap();
            //配列の内容をListに入ます。
            List<MapObject> mapObjects = new List<MapObject>();
            mapObjects.AddRange(map);
            //ゴールの数が0以下ならばゲームクリアです。
            var goalcount = mapObjects.Count(s => s.objType == MapObject.OBJ_TYPE.GOAL);
            if(goalcount <= 0){
                Console.WriteLine("ゲームクリア！¥nゲームを終了します");
                //コンソールの終了
                Environment.Exit(0);
            }
        }
    }
 }


```

やっていることはコメントに書きました。
全てをこのクラスに突っ込んでしまいました。

なので汚いコードです:scream:


<h3>ポイント</h3>

ステージは四角形である必要があります。
テキストファイルを読み込むと改行コードまで配列に入れてしまうようで、高さと幅で配列の要素数を決定すると、テキストファイルの文字数よりも多くの要素が入ってしまいます。

```csharp:Map.cs
//改行コードを無視して配列の要素数を超えないようにする
if (c != Convert.ToChar(Environment.NewLine))
{
  //c の内容によって生成するオブジェクトのTYPEを決定する
  mapobject = getObject(c);
  map[index] = mapobject;
  index += 1;
} 
```
なのでここで改行文字じゃない場合のみ、配列に要素を格納しています。


配列には文字列を格納するのではなく、MapObjectを格納し、そのインスタンスが持っているOBJ_TYPEによってコンソールに出力する文字を変えています。



移動処理ですが一次元配列なので横長の箱に要素が並んだ状態です
[P][□][□][□][□][□][■][□][□][□][□][□]
(この例は適当です)
なので、コンソールで出力すれば1行に表示されてしまいます。

そこでMapの幅で改行文字を出力させることで対応しています。

```csharp:Map.cs

        //オブジェクトのOBJ_TYPEを見てコンソールに出力する
        public static void DrawMap()
        {
            for (int i = 0; i < map.Length; i++)
            {
                switch (map[i].objType)
                {
                    case MapObject.OBJ_TYPE.PLAYER: Console.Write('P'); break;
                    case MapObject.OBJ_TYPE.NIMOTU: Console.Write('N'); break;
                    case MapObject.OBJ_TYPE.WALL: Console.Write('■'); break;
                    case MapObject.OBJ_TYPE.GOAL: Console.Write('G'); break;
                    case MapObject.OBJ_TYPE.FLOOR: Console.Write('□'); break;
                    case MapObject.OBJ_TYPE.GOAL_ON_NIMOTU: Console.Write('D'); break;
                }
                //幅の数だけ表示したら改行させます
                if ((i + 1) % mapWidth == 0)
                {
                    Console.Write(Environment.NewLine);
                }
            }
        }

```


上記のようにすれば配列は
[ 0][ 1][ 2][ 3][ 4][ 5]
[ 6][ 7][ 8][ 9][10][11]
[12][13][14][15][16][17]

のように並んでいるのです。
そのため移動方向を決めるのは簡単です。

7から8に移動するならば1を足す
7から6に移動するならば1を引く
7から13に移動するならば、6を足す(マップの幅)
7から1に移動するならば、6を引く(マップの幅)

ということをしています。


<h3>感想</h3>

実は倉庫番作りに悩んでいて、全てをMapクラスに任せるのは心地悪い！と思ったわけです。
最初に作ったのはプレイヤークラスと荷物クラスの基底クラスにポジションプロパティを持たせていました。それはそれでうまくいったのですが

荷物を増やした時に、それだとオブジェクトの特定管理が大変だということでListと配列の両方で管理させました。
配列のindex番号とMapObjectのポジションプロパティを紐づけたような管理にしました。

結果的に移動処理で入れ替えたりするのが大変で断念。



まぁよくわからないことを書きましたが、namespace soukoban3ということで何回か作り直しているということです:sob:

ゲームを作るって難しい。。。けど楽しい


<h3>参考書籍</h3>
ゲームプログラマになる前に覚えておきたい技術
実戦で役立つ C#プログラミングのイディオム/定石&パターン

今回の倉庫番は
「ゲームプログラマになる前に覚えておきたい技術」のチャプター1-2を読んで参考に作って見ました。
本の部分を読んだだけですが、全体像が掴めたので大変勉強になりました。






以上になります。
ありがとうございました。