「C#」カテゴリーアーカイブ

【北海道大戦】シーンを追加する

最新ソースはこちら(gitHub)。

https://github.com/takishita2nd/HokkaidoWar

バトルシーンを追加するに当たりまして、

今まではエンジンに直接オブジェクトを配置していましたが、

シーンを使用するとなると、このやり方では上手くいけないっぽい。

なので、メインシーンを追加して、このシーンのレイヤーにオブジェクトを配置する必要があります。

なので、今回はその修正を実行。

    class HokkaidoWar
    {

        public HokkaidoWar()
        {
        }

        public void Run()
        {
            asd.Engine.Initialize("北海道大戦", 1200, 1000, new asd.EngineOption());

            // シーンの登録
            var scene = new MainScene();
            asd.Engine.ChangeScene(scene);

            while (asd.Engine.DoEvents())
            {
                asd.Engine.Update();
            }
            asd.Engine.Terminate();
        }

    }

いままでエンジンに追加していたオブジェクトは全て無くなり、シーンを作成して、そのシーンに移行、という処理に変わります。

    class MainScene : asd.Scene
    {
中略
        protected override void OnRegistered()
        {
            var layer = Singleton.GetMainSceneLayer();
            AddLayer(layer);

            // 下地
            var background = new asd.GeometryObject2D();
            layer.AddObject(background);
            var bgRect = new asd.RectangleShape();
            bgRect.DrawingArea = new asd.RectF(0, 0, 1800, 1000);
            background.Shape = bgRect;

            cities = new List<City>();
            var r = new Random();
            foreach (var map in mapData.list)
            {
                City city = new City(map.name, map.point, map.population);
                cities.Add(city);
            }

            _battle = new Battle(cities);
            aliveCities = _battle.GetAliveCityList();
        }

        protected override void OnUpdated()
        {
            asd.Vector2DF pos = asd.Engine.Mouse.Position;

            switch (gameStatus)
            {
                case GameStatus.SelectCity:
                    cycleProcessSelectCity(pos);
                    break;
                case GameStatus.ActionEnemy:
                    cycleProcessActionEnemy(pos);
                    break;
                case GameStatus.ActionPlayer:
                    cycleProcessActionPlayer(pos);
                    break;
                case GameStatus.ShowResult:
                    break;
                case GameStatus.GameEnd:
                    cycleProcessGameEnd();
                    break;
                case GameStatus.GameOver:
                    cycleProcessGameOver(pos);
                    break;
            }

            if (asd.Engine.Mouse.LeftButton.ButtonState == asd.ButtonState.Push)
            {
                switch (gameStatus)
                {
                    case GameStatus.SelectCity:
                        onClickMouseSelectCity(pos);
                        break;
                    case GameStatus.ActionEnemy:
                        break;
                    case GameStatus.ActionPlayer:
                        onClickMouseActionPlayer(pos);
                        break;
                    case GameStatus.ShowResult:
                        onClickMouseShowResult();
                        break;
                    case GameStatus.GameEnd:
                        break;
                    case GameStatus.GameOver:
                        break;
                }
            }
        }

後略

OnRegistered()はシーン登録時に実行されます。

オブジェクトの配置はここで行います。

レイヤーはシングルトンで取り出すようにしました。(いろんなところでオブジェクトの配置やっているので)

OnUpdated()には、while (asd.Engine.DoEvents()){ }内で実行していた処理を行います。

これは、asd.Engine.Update();実行時に実行されるイメージです。

細かい所は結構修正しているのですが、概要はこんな感じです。

とりあえず、シーンを使用して、今までと同じ動きが出来ることを確認しました。

次はバトルシーンの追加をしていきましょうか。

【openTK】STLファイルを表示させたい。

以前取り上げたopenTKのやつ。

これを使ってSTLファイルを表示する、というのをやってみたいと思います。

使用するSTLファイルは、こちらからダウンロードしました。

https://www.3dagogo.com/creativetools/designs/3DBenchy

ただしく処理できれば、こんな風に表示されるはずです。

STLファイルの読み込みはこちらのサイトを参考にしました。

https://codingsquare.net/cs/stlfile/#toc11

STLのデータは法線ベクトルと、三角形を構成する頂点の座標を示す3つのベクトルで構成されています。

法線ベクトルは三角形の面の表側を向いている方向を示すベクトルです。

		public bool ReadBinary(string filePath)
		{
			// filePath が null か、ファイルが存在しない場合はエラーとする
			if (filePath == null || File.Exists(filePath) == false)
				return false;

			try
			{
				// バイナリファイルの読み込み
				using (var reader = new BinaryReader(new FileStream(filePath, FileMode.Open, FileAccess.Read)))
				{
					// ヘッダ読み込み
					Header = reader.ReadBytes(HeaderLength);

					// ファセットの枚数読み込み
					uint size = reader.ReadUInt32();

					// ファイルの残りのバイト数
					long rest = reader.BaseStream.Length - reader.BaseStream.Position;

					// ファセット1枚分のバイト数
					const int FacetLength = 50;

					// ファイルの残りのバイト数が、求められるファセットの枚数分のバイト数より少なければエラー
					if (rest < FacetLength * size)
						return false;

					// 全ファセット読み込み
					Facets = new Facet[size];
					for (int i = 0; i < size; ++i)
					{
						// ファセット1個分のバイト配列読み込み
						byte[] bytes = reader.ReadBytes(FacetLength);

						// ファセットデータ生成と配列への格納
						int index = 0;
						const int offset = sizeof(float);
						Facets[i] = new Facet(
							new Vertex(
								BitConverter.ToSingle(bytes, index),
								BitConverter.ToSingle(bytes, index += offset),
								BitConverter.ToSingle(bytes, index += offset)),
							new Vertex(
								BitConverter.ToSingle(bytes, index += offset),
								BitConverter.ToSingle(bytes, index += offset),
								BitConverter.ToSingle(bytes, index += offset)),
							new Vertex(
								BitConverter.ToSingle(bytes, index += offset),
								BitConverter.ToSingle(bytes, index += offset),
								BitConverter.ToSingle(bytes, index += offset)),
							new Vertex(
								BitConverter.ToSingle(bytes, index += offset),
								BitConverter.ToSingle(bytes, index += offset),
								BitConverter.ToSingle(bytes, index += offset))
						);
					}
				}
			}
			catch (Exception)
			{
				return false;
			}
			return true;
		}

これでバイナリのSTLデータを読み込み、描画させます。

        STLFile stlFile = new STLFile();
        public Game() : base(800, 600, GraphicsMode.Default, "0-3:GameWindow")
        {
            stlFile.ReadBinary("3DBenchy.stl");
        }

        //画面描画で実行される。
        protected override void OnRenderFrame(FrameEventArgs e)
        {
            base.OnRenderFrame(e);

            GL.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit);

            GL.MatrixMode(MatrixMode.Modelview);
            Matrix4 modelview = Matrix4.LookAt(Vector3.Zero, Vector3.UnitZ, Vector3.UnitY);
            GL.LoadMatrix(ref modelview);

            int count = 0;
            foreach(var f in stlFile.Facets)
            {
                count++;
                GL.Begin(BeginMode.Triangles);

                GL.Color4(Color4.White);
                GL.Normal3(f.Normal.X, f.Normal.Y, f.Normal.Z);
                GL.Vertex3(f.Vertex1.X / 30, f.Vertex1.Y / 30, f.Vertex1.Z / 30);
                GL.Vertex3(f.Vertex2.X / 30, f.Vertex2.Y / 30, f.Vertex2.Z / 30);
                GL.Vertex3(f.Vertex3.X / 30, f.Vertex3.Y / 30, f.Vertex3.Z / 30);

                GL.End();
            }

            SwapBuffers();
        }

そしてライトの設定も加えます。

こちらのサイトを参考にしました。

https://ameblo.jp/nishi-u6fa4/entry-10864018960.html

        //ウィンドウのサイズが変更された場合に実行される。
        protected override void OnResize(EventArgs e)
        {
            base.OnResize(e);
            GL.Viewport(ClientRectangle.X, ClientRectangle.Y, ClientRectangle.Width, ClientRectangle.Height);
            GL.MatrixMode(MatrixMode.Projection);
            Matrix4 projection = Matrix4.CreatePerspectiveFieldOfView((float)Math.PI / 4, (float)Width / (float)Height, 1.0f, 64.0f);
            GL.LoadMatrix(ref projection);
            GL.MatrixMode(MatrixMode.Modelview);

            Matrix4 look = Matrix4.LookAt(3.0f * Vector3.One, Vector3.Zero, Vector3.UnitY);
            GL.LoadMatrix(ref look);
            GL.Enable(EnableCap.Lighting);
            float[] position = new float[] { 1.0f, 2.0f, 3.0f, 0.0f };
            GL.Light(LightName.Light0, LightParameter.Position, position);
            GL.Enable(EnableCap.Light0);
        }

実行結果はこちら。

なんだこりゃ。

たぶん、データは間違ってないと思うんですよ。

問題は視点なのかな、と思います。

視点を変えるとか、変更できるようにすればちゃんと表示させることができるかもしれません。

もうちょっと勉強します。

【北海道大戦】今後の実装について

まぁ、いまのままでもそこそこ楽しめるのですが、

ゲーム性を高めるために、いろいろ実装していこうかと思います。

まずは、今のバトルは完全乱数で発生させた値でのバトルなので、

ゲーム性を高めるために、じゃんけんバトルのようなものにしようかと思います。

そうなると必要なのがAltseedのシーン切り替え機能ですかね。

このシーン切り替え機能を使用するのに、どれだけ回収が必要なのかも考えないと行けません。

その検証を次回やりましょう。

あと、今は防御側勝利時、特に何もメリットがないので、防御側勝利時に1ターン戦力ボーナス・ペナルティを付与しようかなと思っています。

たぶん、そうすることでゲーム性が向上すると思うんですよね。

よし、がんばります。

あ、あと、Altseed2というのがリリースされたみたいですね。

https://altseed.github.io/index.html

【北海道大戦】ターンを飛ばされる都市がある問題を修正する。

前回までの状況はこちら。

最新ソースはこちら(gitHub)

https://github.com/takishita2nd/HokkaidoWar

さて、現在のソースでは、ターンが飛ばされる都市がある事が分かりました。

https://github.com/takishita2nd/HokkaidoWar/blob/03569e0b8728860e73d11e38eeade24a0aafef9c/HokkaidoWar/Battle.cs#L15

原因はこの_citiesを一つで存在する都市と行動する都市を管理していたためです。

https://github.com/takishita2nd/HokkaidoWar/blob/03569e0b8728860e73d11e38eeade24a0aafef9c/HokkaidoWar/Battle.cs#L78

ここで行動済みの都市を削除すると(図の真ん中)、

図の右のように行動順が二つズレてしまうんですよね。

なので、次に行動するはずだった都市が飛ばされてしまいます。

なので、存在する都市と行動する都市を分けて管理する必要があります。

        List<City> cities = null;
        List<City> aliveCities = null;

必要になるのは、List<City>をコピーする処理。

このとき、Listの部分だけを複製して、中の都市オブジェクトは共有で管理します。

        private List<City> copyCity(List<City> cities)
        {
            List<City> ret = new List<City>();
            foreach(var c in cities)
            {
                ret.Add(c);
            }
            return ret;
        }

こんな感じで修正してみました。

これで、順番が飛ばされることはなくなるはず。

【北海道大戦】戦闘結果表示、ゲーム終了動処理を実装する。

前回までの状況はこちら。

最新ソースはこちら(gitHub)

https://github.com/takishita2nd/HokkaidoWar

以前作成したこれ。

status=4、5の処理を作成していきます。

とは言っても、特に難しいことはしていません。

なので、一気に作成します。

ついでにゲームオーバーの処理も作成します。

    class HokkaidoWar
    {
        enum GameStatus
        {
            SelectCity,
            ActionEnemy,
            ActionPlayer,
            ShowResult,
            GameEnd,
            GameOver
        }

        public void Run()
        {
中略
            while (asd.Engine.DoEvents())
            {
                asd.Vector2DF pos = asd.Engine.Mouse.Position;

                switch (gameStatus)
                {
                    case GameStatus.SelectCity:
                        cycleProcessSelectCity(pos);
                        break;
                    case GameStatus.ActionEnemy:
                        cycleProcessActionEnemy(pos);
                        break;
                    case GameStatus.ActionPlayer:
                        cycleProcessActionPlayer(pos);
                        break;
                    case GameStatus.ShowResult:
                        break;
                    case GameStatus.GameEnd:
                        cycleProcessGameEnd();
                        break;
                    case GameStatus.GameOver:
                        cycleProcessGameOver(pos);
                        break;
                }

                if (asd.Engine.Mouse.LeftButton.ButtonState == asd.ButtonState.Push)
                {
                    switch (gameStatus)
                    {
                        case GameStatus.SelectCity:
                            onClickMouseSelectCity(pos);
                            break;
                        case GameStatus.ActionEnemy:
                            break;
                        case GameStatus.ActionPlayer:
                            onClickMouseActionPlayer(pos);
                            break;
                        case GameStatus.ShowResult:
                            onClickMouseShowResult();
                            break;
                        case GameStatus.GameEnd:
                            break;
                        case GameStatus.GameOver:
                            break;
                    }
                }
                asd.Engine.Update();
            }
        private void cycleProcessGameEnd()
        {
            var gameinfo = Singleton.GetGameProcessInfomation();
            gameinfo.ShowText(_player.City.GetPosition(), string.Empty);
            var info = Singleton.GetInfomationWindow();
            info.ShowText(cities[0].GetPosition(), "ゲームが終了しました\r\n");
            info.ShowText(cities[0].GetPosition(), cities[0].Name + "の勝利です\r\n");
        }

        private void cycleProcessGameOver(asd.Vector2DF pos)
        {
            var gameinfo = Singleton.GetGameProcessInfomation();
            gameinfo.ShowText(_player.City.GetPosition(), string.Empty);
            var info = Singleton.GetInfomationWindow();
            info.ShowText(pos, "敗北しました\r\n");
        }

        private void onClickMouseShowResult()
        {
            _battle.MyTurnEnd();
            cities = _battle.GetCityList();
            if(cities.Count <= 1)
            {
                gameStatus = GameStatus.GameEnd;
            }
            else
            {
                gameStatus = GameStatus.ActionEnemy;
            }
        }
    class Battle
    {
        public void MyTurnEnd()
        {
            if (lastDeffece != null)
            {
                lastDeffece.ClearPaint();
                lastDeffece = null;
            }
            if (lastAttack != null)
            {
                lastAttack.ClearPaint();
                lastAttack = null;
            }
            cityCnt++;
            if (cityCnt >= _cities.Count)
            {
                _cities = cityRandomReplace(_cities);
                cityCnt = 0;
                turn++;
            }
        }

その他、微調整も加えています。

これで一応一通り完成しました。

次は、ちょっとした不具合とか、微調整を行って行きます。

【北海道大戦】プレイヤーの行動処理を実装する。

前回までの状況はこちら。

最新ソースはこちら(gitHub)

https://github.com/takishita2nd/HokkaidoWar

以前作成したこれ。

これの「自分の行動」の部分を作成していきます。

            while (asd.Engine.DoEvents())
            {
                asd.Vector2DF pos = asd.Engine.Mouse.Position;

                switch (gameStatus)
                {
                    case GameStatus.SelectCity:
                        cycleProcessSelectCity(pos);
                        break;
                    case GameStatus.ActionEnemy:
                        cycleProcessActionEnemy(pos);
                        break;
                    case GameStatus.ActionPlayer:
                        cycleProcessActionPlayer(pos);
                        break;
                }

                if (asd.Engine.Mouse.LeftButton.ButtonState == asd.ButtonState.Push)
                {
                    switch (gameStatus)
                    {
                        case GameStatus.SelectCity:
                            onClickMouseSelectCity(pos);
                            break;
                        case GameStatus.ActionEnemy:
                            break;
                        case GameStatus.ActionPlayer:
                            onClickMouseActionPlayer(pos);
                            break;
                    }
                }
                asd.Engine.Update();
            }

分かりやすいようにサブルーチン化した。

        private void cycleProcessActionPlayer(asd.Vector2DF pos)
        {
            _battle.MyTurn(_player.City);
            _player.City.PaintAttackColor();
            var info = Singleton.GetInfomationWindow();
            info.ShowText(pos, "都市を選択してください\r\n");
            var cities = _player.City.GetLinkedCities();
            foreach(var city in cities)
            {
                if (city.IsOnMouse(pos))
                {
                    city.OnMouse(pos);
                    city.PaintDeffenceColor();
                }
                else
                {
                    city.ClearPaint();
                }
            }
        }
        public void MyTurn(City player)
        {
            if (lastDeffece != null)
            {
                lastDeffece.ClearPaint();
            }
            if (lastAttack != null)
            {
                lastAttack.ClearPaint();
            }
            var info = Singleton.GetGameProcessInfomation();
            info.ShowText(player.GetPosition(), string.Format("{0} turn {1} / {2} {3}", turn, cityCnt + 1, _cities.Count, player.Name));
        }

周期処理です。

まず、自分の都市を赤くします。

攻撃対象を選択するのですが、自分の都市の上にマウスカーソルがある場合のみ、その都市を青く表示します。

        private City _target = null;
        private void onClickMouseActionPlayer(asd.Vector2DF pos)
        {
            var info = Singleton.GetInfomationWindow();
            info.ShowText(pos, String.Empty);
            var cities = _player.City.GetLinkedCities();
            foreach (var city in cities)
            {
                if (city.IsOnMouse(pos))
                {
                    _target = city;
                    _battle.MyTrunAttack(_player.City, _target);
                    gameStatus = GameStatus.ShowResult;
                }
            }
        }
        public void MyTrunAttack(City player, City target)
        {
            var info = Singleton.GetGameProcessInfomation();
            var r = Singleton.GetRandom();
            lastAttack = player;
            lastDeffece = target;
            double attack = lastAttack.Population * (double)(r.Next(minRate, maxRate) / 10.0);
            double deffence = lastDeffece.Population * (double)(r.Next(minRate, maxRate) / 10.0);
            if (attack > deffence)
            {
                info.ShowText(lastAttack.GetPosition(), string.Format("{0} turn {1} / {2} {3}\r\ntarget {4} \r\n{5} vs {6}\r\nwin",
                    turn, cityCnt + 1, _cities.Count, lastAttack.Name, lastDeffece.Name, (int)attack, (int)deffence));
                lastAttack.CombinationCity(lastDeffece);
                _cities.Remove(lastDeffece);
                lastDeffece = null;
            }
            else
            {
                info.ShowText(lastAttack.GetPosition(), string.Format("{0} turn {1} / {2} {3}\r\ntarget {4} \r\n{5} vs {6}\r\nlose",
                    turn, cityCnt + 1, _cities.Count, lastAttack.Name, lastDeffece.Name, (int)attack, (int)deffence));
            }
        }

マウスクリック時の処理です。

マウスカーソルにある都市に対して攻撃処理を行います。

攻撃対象は記憶しておきます。

【北海道大戦】敵の行動処理を実装する。

最新ソースはこちら(gitHub)

https://github.com/takishita2nd/HokkaidoWar

以前作成したこれ。

これの「敵の行動」の部分を作成していきます。

敵を行動させるのであれば、すでに作成済みである、

    class Battle
    {
        public void NextTurn()

を実行すればいいでしょう。

では、次の状態遷移の条件である、「自分の都市の順番が来た場合」を考えます。

それを行うには、自分の都市と、次に実行する都市をBattleクラスから取得する処理があればいいでしょう。

    class Battle
    {
        public City GetActionCity()
        {
            return _cities[cityCnt];
        }
    class Player
    {
        private City _city;

        public City City { get { return _city; } }

        public Player(City city)
        {
            _city = city;
        }
    }
    class HokkaidoWar
    {
        public void Run()
        {
// 中略
            while (asd.Engine.DoEvents())
            {
                asd.Vector2DF pos = asd.Engine.Mouse.Position;

                switch (gameStatus)
                {
// 中略
                    case GameStatus.ActionEnemy:
                        if (_player.City.Equals(_battle.GetActionCity()))
                        {
                            gameStatus = GameStatus.ActionPlayer;
                        }
                        else
                        {
                            Thread.Sleep(100);
                            _battle.NextTurn();
                        }
                        break;
                }

                if (asd.Engine.Mouse.LeftButton.ButtonState == asd.ButtonState.Push)
                {
// 中略
                    switch (gameStatus)
                    {
                        case GameStatus.ActionEnemy:
                            break;
                    }
                }
                asd.Engine.Update();

マウスクリック処理は特にやることがないので、何もやる必要はありません。

【北海道大戦】パラメータを調整して札幌以外でも勝てるようにする

前回までの状況はこちら。

最新ソースはこちら(gitHub)

https://github.com/takishita2nd/HokkaidoWar

パラメータを調整して、ランダムの値の範囲をもう少し大きくしました。

        private const int maxRate = 50;
        private const int minRate = 1;

                double attack = lastAttack.Population * (double)(r.Next(minRate, maxRate) / 10.0);
                double deffence = lastDeffece.Population * (double)(r.Next(minRate, maxRate) / 10.0);

戦力値0.1倍~5倍に広げました。

清水の勝利でした。

札幌の勝利100%では無くなったので、パラメータはこれで行きましょう。

当然のことながら、このままではゲームではありません。

ただ成り行きを眺めているだけなので。

次回以降、さらにゲームっぽく改造していこうと思います。

【北海道大戦】バトル処理を実装する。

前回までの状況はこちら。

最新ソースはこちら(gitHub)

https://github.com/takishita2nd/HokkaidoWar

前回は対戦相手を選択する処理を作成したので、

今回は実際にバトルを行う処理を作成します。

    class Battle
    {
        public void NextTurn()
        {
            if (lastDeffece != null)
            {
                lastDeffece.ClearPaint();
            }
            if (lastAttack != null)
            {
                lastAttack.ClearPaint();
            }

            var targets = _cities[cityCnt].GetLinkedCities();
            var r = Singleton.GetRandom();
            int targetIdx = r.Next(0, targets.Count + 1);
            lastAttack = _cities[cityCnt];
            lastAttack.PaintAttackColor();

            var info = Singleton.GetGameProcessInfomation();
            if(targetIdx >= targets.Count)
            {
                info.ShowText(lastAttack.GetPosition(), string.Format("{0} turn {1} / {2} {3}",
                    turn, cityCnt + 1, _cities.Count, lastAttack.Name));
            }
            else
            {
                lastDeffece = targets[targetIdx];
                lastDeffece.PaintDeffenceColor();
                float attack = lastAttack.Population * (float)(r.Next(5, 30) / 10.0);
                float deffence = lastDeffece.Population * (float)(r.Next(5, 30) / 10.0);
                if(attack > deffence)
                {
                    info.ShowText(lastAttack.GetPosition(), string.Format("{0} turn {1} / {2} {3}\r\ntarget {4} \r\n{5} vs {6}\r\nwin",
                        turn, cityCnt + 1, _cities.Count, lastAttack.Name, lastDeffece.Name, (int)attack, (int)deffence));
                    lastAttack.CombinationCity(lastDeffece);
                    _cities.Remove(lastDeffece);
                    lastDeffece = null;
                }
                else
                {
                    info.ShowText(lastAttack.GetPosition(), string.Format("{0} turn {1} / {2} {3}\r\ntarget {4} \r\n{5} vs {6}\r\nlose",
                        turn, cityCnt + 1, _cities.Count, lastAttack.Name, lastDeffece.Name, (int)attack, (int)deffence));
                }
            }

            cityCnt++;
            if(cityCnt >= _cities.Count)
            {
                _cities = cityRandomReplace(_cities);
                cityCnt = 0;
                turn++;
            }
        }
    class City
    {
        public List<Map> GetMaps()
        {
            return _maps;
        }

        public void CombinationCity(City lose)
        {
            addMaps(lose.GetMaps());
            _population += lose.Population;
        }

        private void addMaps(List<Map> maps)
        {
            foreach(var m in maps)
            {
                m.SetCity(this);
            }
            _maps.AddRange(maps);
        }

とりあえず、0.5倍~3倍の乱数で戦力値に補正をかけて勝敗を決めます。

そして、攻撃側が勝利した場合は防御側を吸収合併します。

これで一通り実装は完了したので、実際に大戦を実行しましょう。

札幌の一人勝ちでした。

もうちょっとパラメータを調整しないといけないですね。

【北海道大戦】ランダムで隣接する都市を選択する

前回までの状況はこちら。

最新ソースはこちら(gitHub)

https://github.com/takishita2nd/HokkaidoWar

今回はバトルを仕掛ける市町村を選択する処理を実装します。

自分のターンが回ってきた市町村から、隣接する市町村をランダムで選択し、攻撃を仕掛ける、という感じにしたいと思います。

ただし、何もしない(攻撃を仕掛けない)ケースもあるものとします。

さらに、攻撃側の市町村と防御側の市町村が分かるように色分けしましょうか。

    class City
    {

        public void PaintAttackColor()
        {
            var color = new asd.Color(255, 0, 0);
            foreach(var m in _maps)
            {
                m.SetColor(color);
            }
        }

        public void PaintDeffenceColor()
        {
            var color = new asd.Color(0, 0, 255);
            foreach (var m in _maps)
            {
                m.SetColor(color);
            }
        }

        public void ClearPaint()
        {
            foreach (var m in _maps)
            {
                m.SetColor(_color);
            }
        }
    class Battle
    {
        public void NextTurn()
        {
            if(prevAttack != null)
            {
                prevAttack.ClearPaint();
            }
            if(prevDeffece != null)
            {
                prevDeffece.ClearPaint();
            }

            var targets = _cities[cityCnt].GetLinkedCities();
            var r = Singleton.GetRandom();
            int targetIdx = r.Next(0, targets.Count + 1);
            prevAttack = _cities[cityCnt];
            prevAttack.PaintAttackColor();

            var info = Singleton.GetGameProcessInfomation();
            if(targetIdx >= targets.Count)
            {
                info.ShowText(prevAttack.GetPosition(), string.Format("{0} turn {1} / {2} {3}",
                    turn, cityCnt + 1, _cities.Count, prevAttack.Name));
            }
            else
            {
                prevDeffece = targets[targetIdx];
                prevDeffece.PaintDeffenceColor();
                info.ShowText(prevAttack.GetPosition(), string.Format("{0} turn {1} / {2} {3}\r\ntarget {4}",
                    turn, cityCnt + 1, _cities.Count, prevAttack.Name, prevDeffece.Name));
            }

            cityCnt++;
            if(cityCnt >= _cities.Count)
            {
                _cities = cityRandomReplace(_cities);
                cityCnt = 0;
                turn++;
            }
        }

隣接する都市を取得する処理は今までに作成しているので、メソッド1つで取り出せます。

その中から乱数で1つ都市を選択します。

ただし、何もしないターンもあるので、乱数の範囲を大きめにとって、配列より大きければ、何もしない、と扱います。