質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

ただいまの
回答率

87.48%

穴掘り法迷路生成のLinqについて。

解決済

回答 1

投稿 編集

  • 評価
  • クリップ 0
  • VIEW 1,265

score 373

前提・実現したいこと

こちらのサイトで穴掘り法の迷路生成の勉強をしているのですが、Linqの処理が何をしているのかわかりません。
ご教示お願いします。

該当のソースコード

ソースのGetPositionメソッドの中の下記のLinqがわかりません。

Dictionary<int, int[]> positions = position.Where(p => !isOutOfRange(p[0], p[1]) && walls[p[0], p[1]] == 0)
                                                   .Select((p, i) => new { p, i })
                                                   .ToDictionary(p => p.i, p => p.p);

試したこと

理解したことは、下記です。

・Whereの中のpは、positionの要素を示している。
 つまり、
            new int[] {x, y + 2},
            new int[] {x, y - 2},
            new int[] {x + 2, y},
            new int[] {x - 2, y}
 のどれかの要素となる。

・Whereでやっていることは、
 pが、!isOutOfRange(p[0], p[1]) && walls[p[0], p[1]] == 0 の条件を満たしているものでフィルタリングしている。

わからないことは下記です。

・.Select((p, i) => new { p, i }) の処理がわかりません。
 (p, i)のpとiは何を示しているのか。
 new { p, i }は何を生成しているのか。
  { }をnewしていますが、この{ }は何を意味しているのか。

・.ToDictionary(p => p.i, p => p.p);
 どんなDictionaryを生成しているのかわかりません。
 ToDictionaryの処理そのものはわかります。
 p.iをKeyに、p.pをValueにしたDictionaryを生成していることはわかります。
 しかし、p.iとp.pが何を示しているのかわかりません。


GetPositionで移動可能なリストを取得するということですが、
GetPositionで取得されるDictionary<int, int[]>は、どういうDictionaryになっているのか、
ご教示お願いします。

あと、所々、

walls[tmpPos[0], tmpPos[1]] = 1;
walls[xPos, yPos] = 1;


という記述がありますが、
サイトの図の説明のように、Y座標が下方向、X座標が右方向とするならば、
2次元配列の性質から考えると、

walls[tmpPos[1], tmpPos[0]] = 1;
walls[yPos,xPos] = 1;


のほうが合っているような気がしますが、これに関してご教示お願いします。

  • 気になる質問をクリップする

    クリップした質問は、後からいつでもマイページで確認できます。

    またクリップした質問に回答があった際、通知やメールを受け取ることができます。

    クリップを取り消します

  • 良い質問の評価を上げる

    以下のような質問は評価を上げましょう

    • 質問内容が明確
    • 自分も答えを知りたい
    • 質問者以外のユーザにも役立つ

    評価が高い質問は、TOPページの「注目」タブのフィードに表示されやすくなります。

    質問の評価を上げたことを取り消します

  • 評価を下げられる数の上限に達しました

    評価を下げることができません

    • 1日5回まで評価を下げられます
    • 1日に1ユーザに対して2回まで評価を下げられます

    質問の評価を下げる

    teratailでは下記のような質問を「具体的に困っていることがない質問」、「サイトポリシーに違反する質問」と定義し、推奨していません。

    • プログラミングに関係のない質問
    • やってほしいことだけを記載した丸投げの質問
    • 問題・課題が含まれていない質問
    • 意図的に内容が抹消された質問
    • 過去に投稿した質問と同じ内容の質問
    • 広告と受け取られるような投稿

    評価が下がると、TOPページの「アクティブ」「注目」タブのフィードに表示されにくくなります。

    質問の評価を下げたことを取り消します

    この機能は開放されていません

    評価を下げる条件を満たしてません

    評価を下げる理由を選択してください

    詳細な説明はこちら

    上記に当てはまらず、質問内容が明確になっていない質問には「情報の追加・修正依頼」機能からコメントをしてください。

    質問の評価を下げる機能の利用条件

    この機能を利用するためには、以下の事項を行う必要があります。

回答 1

checkベストアンサー

+1

LINQの Select 句では、前の行で出力された値 p とint型 i をもつ匿名型に変換しています。
ToDictionary 句では、i を Key に、p を Value とするDictionaryに変換しています。 

シンプルな例に置き換えると以下のとおりです。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.Linq;

public class LinqTest : MonoBehaviour
{
    void Start ()
    {
        var list = new List<string>();
        list.Add ( "AAA" );
        list.Add ( "BBB" );
        list.Add ( "CCC" );

        // (1) で、元のリストの値 x と、int型 i を持つ匿名型(のIEnumerable)に変換
        // (2) で、i を Key、x を Value とする Dictionary に変換
        var dictionary = list.Select((x, i) => new { x, i} )        // (1)
                             .ToDictionary(d => d.i, d => d.x);     // (2)

        // 確認用
        foreach ( var item in dictionary )
        {
            Debug.Log ( $"{item.Key}, {item.Value}" );
        }
    }
}


結果は下図のとおりです。
イメージ説明

参考
型推論と匿名型 - C# によるプログラミング入門 | ++C++; // 未確認飛行 C
https://ufcpp.net/study/csharp/sp3_inference.html

なお元のコードにおいては、わざわざ持たせた Key を後工程で特に使用していないようですね。

【追記1】
変数がどのような型なのかは、Visual Studioであればその箇所にマウスカーソルを乗せれば表示されますよ。
イメージ説明
LINQのなかでどのように変換されているか、ご自身でたしかめてみるのが早いと思います。

【追記2】
参考として、元のコードをこちらであれこれ書き換えて動かしてみたものを載せておきます。
変数名・メソッド名を含めて弄り回したので読み取りにくいかと思いますが、Dictionaryへの変換を行っていないことだけ確認してください。Listで処理しています。これでも同じように動くと思います。

using UnityEngine;
using System.Collections.Generic;
using System.Linq;

public class MakeDungeon : MonoBehaviour
{
    // 設定する値
    public int max = 5;        //縦横のサイズ ※必ず奇数にすること
    public GameObject wall;    //壁用オブジェクト
    public GameObject floor;   //床用オブジェクト
    public GameObject start;   //スタート地点に配置するオブジェクト
    public GameObject goal;    //ゴール地点に配置するオブジェクト

    // 内部パラメータ
    private enum CellType { Wall, Path };   //セルの種類
    private CellType[,] cells;

    private Vector2Int startPos;    //スタートの座標
    private Vector2Int goalPos;     //ゴールの座標


    private void Start ()
    {
        //マップ状態初期化
        cells = new CellType[max, max];

        //スタート地点の取得
        startPos = GetStartPosition ();

        //通路の生成
        //初回はゴール地点を設定する
        goalPos = MakeMapInfo ( startPos );

        //通路生成を繰り返して袋小路を減らす
        var tmpStart = goalPos;
        for ( int i = 0 ; i < max * 5 ; i++ )
        {
            MakeMapInfo ( tmpStart );
            tmpStart = GetStartPosition ();
        }

        //マップの状態に応じて壁と通路を生成する
        BuildDungeon ();

        //スタート地点とゴール地点にオブジェクトを配置する
        //初回で取得したスタート地点とゴール地点は必ずつながっているので破綻しない
        var startObj = Instantiate(start, new Vector3(startPos.x, 1, startPos.y), Quaternion.identity);
        var goalObj = Instantiate(goal, new Vector3(goalPos.x, 1, goalPos.y), Quaternion.identity);

        startObj.transform.parent = this.transform;
        goalObj.transform.parent = this.transform;
    }


    // スタート地点の取得
    private Vector2Int GetStartPosition ()
    {
        //ランダムでx,yを設定
        int randomX = Random.Range(0, max);
        int randomY = Random.Range(0, max);

        //x、yが両方共偶数になるまで繰り返す
        while ( !( randomX % 2 == 0 && randomY % 2 == 0 ) )
        {
            randomX = Mathf.RoundToInt ( Random.Range ( 0, max ) );
            randomY = Mathf.RoundToInt ( Random.Range ( 0, max ) );
        }

        return new Vector2Int ( randomX, randomY );
    }


    // マップ生成
    private Vector2Int MakeMapInfo ( Vector2Int _startPos )
    {
        //スタート位置配列を複製
        var tmpStartPos = _startPos;

        //移動可能な座標のリストを取得
        var movablePositions = GetMovablePositions(tmpStartPos);

        //移動可能な座標がなくなるまで探索を繰り返す
        while ( movablePositions != null )
        {
            //移動可能な座標からランダムで1つ取得し通路にする
            var tmpPos = movablePositions[Random.Range(0, movablePositions.Count)];
            cells[tmpPos.x, tmpPos.y] = CellType.Path;

            //元の地点と通路にした座標の間を通路にする
            var xPos = tmpPos.x + (tmpStartPos.x - tmpPos.x) / 2;
            var yPos = tmpPos.y + (tmpStartPos.y - tmpPos.y) / 2;
            cells[xPos, yPos] = CellType.Path;

            //移動後の座標を一時変数に格納し、再度移動可能な座標を探索する
            tmpStartPos = tmpPos;
            movablePositions = GetMovablePositions ( tmpStartPos );
        }

        //探索終了時の座標を返す
        return tmpStartPos;
    }


    // 移動可能な座標のリストを取得する
    // private Dictionary<int, Vector2Int> GetPosition ( Vector2Int _startPos )
    private List<Vector2Int> GetMovablePositions ( Vector2Int _startPos )
    {
        //可読性のため座標を変数に格納
        var x = _startPos.x;
        var y = _startPos.y;

        //移動方向毎に2つ先のx,y座標を仮計算
        var positions = new List<Vector2Int> {
            new Vector2Int(x, y + 2),
            new Vector2Int(x, y - 2),
            new Vector2Int(x + 2, y),
            new Vector2Int(x - 2, y)
        };

        //移動方向毎に移動先の座標が範囲内かつ壁であるかを判定する
        //真であれば、返却用リストに追加する
        var movablePositions = positions.Where(p => !IsOutOfBounds(p.x, p.y) && cells[p.x, p.y] == CellType.Wall);

        return movablePositions.Count () != 0 ? movablePositions.ToList () : null;
    }


    //与えられたx、y座標が範囲外の場合真を返す
    private bool IsOutOfBounds ( int x, int y ) => ( x < 0 || y < 0 || x >= max || y >= max );


    //パラメータに応じてオブジェクトを生成する
    private void BuildDungeon ()
    {
        //縦横1マスずつ大きくループを回し、外壁とする
        for ( int i = -1 ; i <= max ; i++ )
        {
            for ( int j = -1 ; j <= max ; j++ )
            {
                //範囲外、または壁の場合に壁オブジェクトを生成する
                if ( IsOutOfBounds ( i, j ) || cells[i, j] == CellType.Wall )
                {
                    var wallObj = Instantiate(wall, new Vector3(i, 0, j), Quaternion.identity);
                    wallObj.transform.parent = this.transform;
                }

                //全ての場所に床オブジェクトを生成
                var floorObj = Instantiate(floor, new Vector3(i, -1, j), Quaternion.identity);
                floorObj.transform.parent = this.transform;
            }
        }
    }
}


上の(書き換えたほうの)コードで動かしてみたGIFです。
イメージ説明

投稿

編集

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2019/01/15 00:16

    ご回答ありがとうございます。

    念の為、確認ですが、
    list.Select((x, i) => new {x, i} )
    とあったら、new {x, i}は、new{IEnumerable型の要素(元のリストの値), IEnumerable型の要素番号}
    になるということですか?

    >なお元のコードにおいては、わざわざ持たせた Key を後工程で特に使用していないようですね。
    int[] tmpPos = movePos[Random.Range(0, movePos.Count)];
    で使用していませんか?
    movePosの[ ]内がKeyではないですか?

    今回、
    list.Select((x, i) => new { x, i})
    という書き方を初めて知ったのですが、
    これを書いたら、この後に続くのは、
    .ToDictionary(d => d.i, d => d.x);
    ぐらいしかないように思えたのですが、後に続くものとして他にもあったりしますか?

    キャンセル

  • 2019/01/15 00:51

    回答のほうに参考画像を追加しましたが、LINQのなかの変数の型については、Visual Studio(またはその他のコードエディタ)で実際にどのような型になっているのか確認されるのが早いと思います。私はそれ以上踏み込んで言語仕様を説明できません。

    後工程の件は言葉足らずでしたね。たしかに後工程でDictionaryを受け取っているのですが、Valueのint[]のほうしか処理に使用していないという意味です。movePosの[]内でも使用していませんね。数をCountしているだけなので、Dictionaryである必要はありません。

    「他にもあるか」の件は、よく分かりません。

    キャンセル

  • 2019/01/15 01:05

    参考まで回答のほうに、さきほどこちらで個人的に弄り回していたコードを載せてみました。Unityで動かしてみて、確認いただければと思います。
    途中でDictionaryには変換していませんが、(私の勘違いでなければ)元のコードと同じように動作するはずです。

    キャンセル

  • 2019/01/16 02:05

    ご回答ありがとうございます。

    エディタはVisual Studio Codeを使っていたので、Visual Studioのようにオンマウスで変数の型までは表示されなかったです。エディタ選びも重要ですね。

    コードのご提示ありがとうございます。
    negitama様のListのコードでも動作するのを確認致しました。
    Listも要素番号で要素を取得できるので、Dictionaryである必要はないということですね。

    まだ今回の迷路生成のコードを読み進めている途中なので、考えて分からないことがあったら、また他でトピックを立てて質問させていただくことがあるかもしれません。

    ありがとうございました。

    キャンセル

15分調べてもわからないことは、teratailで質問しよう!

  • ただいまの回答率 87.48%
  • 質問をまとめることで、思考を整理して素早く解決
  • テンプレート機能で、簡単に質問をまとめられる

関連した質問

同じタグがついた質問を見る