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

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

ただいまの
回答率

87.79%

【Unity,C#】SortedListのKeyの値が同一の物がある場合の上書きを防止する方法について

解決済

回答 2

投稿

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

score 611

前提・実現したいこと

投稿記事:【C#,Unity】Dictionary<GameObject,float>をfloat基準で昇順にSortしようとしたら謎処理を挟んでいた話

もともとQiitaの方でDictionaryを使ってSortをする際に苦戦したという話を投稿した際に、
コメントで読み取りの頻度が高いのであればSortedListの方が向いていますよ。というアドバイスをいただき、

変更したのですが、後日、SortedListは同じKeyの値が登録された場合、追加ではなく上書きされるので防止したほうがいいですよ、
(Unityで試しに同じ値を追加しようとしたらArgumentエラーが発生しました)
とアドバイスと頂き、コードを提示までした頂いたのですが、まるで理解が及ばず困惑しています。

実際に現在使用しているスクリプトが以下のスクリプトになります。

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

public class LockOnTargetDetector : MonoBehaviour
{
    public GameObject player;
    public List<GameObject> hitsOb {get; set;}
    private SortedList<float,GameObject> SortList;

    void Start()
    {
        if(player == null)
        {
            player = GameObject.FindGameObjectWithTag("Player");
        }
        SortList = new SortedList<float,GameObject>();
        hitsOb = new List<GameObject>();
    }

    public void GetTargetClosestScreenCenter()
    {
        float search_radius = 10f;

        var hits = Physics.SphereCastAll( //playerを中心に半径10fの半球内にあるTargetを取得
            player.transform.position,
            search_radius,
            player.transform.forward,
            0.01f,
            LayerMask.GetMask("Target")
        ).Select(h => h.transform.gameObject).ToList();

        hits = FilterTargetObject(hits);

        if (0 < hits.Count()) {

            foreach (var hit in hits) {

                Vector3 targetScreenPoint = hit.transform.position; //各Targetの位置を取得
                float target_distance = Vector2.Distance( //playerの位置から各Targetまでの距離を取得
                    player.transform.position, targetScreenPoint );

                SortList.Add(target_distance,hit); //各Targetと距離を格納
            }

            foreach(var item in SortList) //Sortしなくとも自動で昇順に並び替えて出力してくれる
            {
                hitsOb.Add(item.Value);
            }
            SortList = new SortedList<float,GameObject>();

        } else {
            SortList = new SortedList<float,GameObject>(); //何もなかった場合リストを初期化
            hitsOb = null; //何もなかったらNullを返す
        }
    }  
    protected List<GameObject> FilterTargetObject(List<GameObject> hits) //Physics.SphereCastAllでゲットしたTargetが画面内に入っているか判定
    {
        return hits
            .Where(h => {
                Vector3 screenPoint = Camera.main.WorldToViewportPoint(h.transform.position);
                return screenPoint.x > 0 && screenPoint.x < 1 && screenPoint.y > 0 && screenPoint.y < 1;
            })
            .ToList();
    }
}

処理内容的には、画面内のTargetオブジェクトをプレイヤーに近い順に取得する。となっています。
ここに提示していただいたコードを追加しようとしたのですが、記事の方にも書いてあるように
各値に何を渡してあげればいいのか、どこに記述して処理を実行すればいいのか、まるでわかりません。

そこで教えていただきたいのは、提示していただいたコードの各値に上記のスクリプトのどの値を渡せばいいのか、
提示していただいたコード1と2の関係性はどうなっているのか?
そもそもこの中に組み込むことが出来ないのか?等の知識を教えてください。

理解が及ばないため、どうか皆様のお力をお借りできれば幸いです。

提示していただいたコード

class MultiSortedList : SortedList<float, List<GameObject>>
{
  //Addメソッドのオーバーロード
  public void Add(GameObject obj) {
    float distance = obj.距離計算メソッド();
    if (this.TryGetValue(distance, out insertTarget)) {
      //すでに同じ距離の登録済みオブジェクトあり
      insertTarget.Add(obj);
    } else {
      //同じ距離のオブジェクトなし
      this.Add(distance, new List<GameObject> { obj };
    }
  }

  //ソート済みリストを一次元リストに展開して取り出す
  IEnumerable<GameObject> GetSotedObjects() {
    return this.Values.SelectMany(elems => elems);
  }
}


//本当は「距離でソートしたリストを作成」と「リスト順に処理を実行」は別メソッドに分けた方がいいと思う
public void Sort() {
  var list = new MultiSortedList();
  //ソートしたいオブジェクトを全部登録
  foreach(var obj in itemTable.Where(ここにフィルタ条件を記述)) {
    float distance = objから距離を算出;
    list.Add(dicstance, obj);//距離もパラメータとして渡すことにします。メソッドの実装はわざわざコードを書かなくてもわかるんじゃないかと思います。
  }

  //距離順に個別処理を実行
  foreach(var obj in list.Values) {//個別処理には距離情報が不要な場合
    //個別処理
  }
}

試したこと

分からないなりに無理やり突っ込んでみましたが、結局どの値をどこに渡せばいいかわからず実装できませんでした。

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

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

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

    クリップを取り消します

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

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

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

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

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

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

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

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

    質問の評価を下げる

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

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

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

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

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

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

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

    詳細な説明はこちら

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

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

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

質問への追記・修正、ベストアンサー選択の依頼

  • Zuishin

    2019/11/19 15:57

    なぜこの段階でこっちで聞くんでしょうか?
    もう少しやり取りして、向こうで解決できないことがはっきりしてからでも遅くないように思います。

    キャンセル

  • Y0241-N

    2019/11/19 16:10

    一対一でのやりとりですと、情報の交換できる速度が遅く、またアドバイスをしていただけるのは
    ありがたいことなのですが、先方の表現では私には理解できなかったので、
    他の方からの解説や助言が欲しかったので、こちらで質問させていただきました。

    キャンセル

回答 2

checkベストアンサー

+2

色々と誤解をしているようです

DictonaryやSortedListというのは連想配列というもので、
数値以外も指定できる配列です。

data[0] = "Tokyo";
Console.WriteLine(data[0]);    // Tokyoが出力される

data["yamada"] = "Tarou";
Console.WriteLine(data["yamada"]); // Tarouが出力される

0やらyamadaやらがKeyの部分で、TokyoやTarouがValueとなります。

連想配列は、Keyが指定されると高速にValueを探し出すように作られています。
この辺が同じKeyがあると上書きされる、という理由になります。

ただ、「同じKeyで上書きされるのが困る」「順番に並べたい」となると、
DictonaryやSortedListを使わず、Listに入れたあとソートしたほうが処理的にも速いです。

[追記]
ただのペアとして管理したいのであれば、それ専用のクラスを作ってまとめるべきで、
使いもしない余計な機能がついているDictonaryを使うべきではありません

public class DistanceData{
  public float distance;
  public GameObject gobj;
}

List<DistanceData> dlist = new List<DistanceData>();

// 面倒ならこれでもあり、First, Secondという変数になるけれども
List<Pair<float, GameObject>> ddlist = new List<Pair<float, GameObject>>();
ddlist.Add(new Pair<float, GameObject>(1.0, gameObject));

Console.WriteLine($"distance : {ddlist[0].First} obj:{ddlist[0].Second.ToString()}");

投稿

編集

  • 回答の評価を上げる

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

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

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

  • 回答の評価を下げる

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

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

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

  • 2019/11/19 16:58

    C# List Addで検索して出てきた情報を試したところ、
    dlist.Add(new DistanceData() {distance = 代入する数値, gobj = 代入するオブジェクト});
    としたところ、要素を追加できました。すみません。

    キャンセル

  • 2019/11/19 17:05

    >Zuishinさん
    一応補足
    質問を立てろってのは、今の状況を言語化して整頓しろ、って意味です

    キャンセル

  • 2019/11/19 17:19

    お二方ともにありがとうございました、教えていただいた方法で問題は解決することが出来ました。
    また適切な表現ができていなかったようで、ご迷惑をお掛け致しました。

    こちらの回答をベストアンサーとし、自己回答で変更したコードを追記しておきます。

    キャンセル

0

教えていただいた方法に変更したスクリプトです。
大変勉強になりました、以後も精進してまいります。

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

public class LockOnTargetDetector : MonoBehaviour
{
    public GameObject player;
    public List<GameObject> hitsOb {get; set;}
    public class DistanceDate //追加
    {
        public float distance;
        public GameObject TargetOb;
    }
     List<DistanceDate> DisList; //追加

    void Start()
    {
        if(player == null)
        {
            player = GameObject.FindGameObjectWithTag("Player");
        }
        DisList = new List<DistanceDate>(); //追加
        hitsOb = new List<GameObject>();
    }
    void Update()
    {
        Debug.DrawRay(player.transform.position,player.transform.forward *10f,Color.yellow,float.PositiveInfinity);
        if(Input.GetKeyDown(KeyCode.Q))
        {
            GetTargetClosestScreenCenter();
        }
    }
    public void GetTargetClosestScreenCenter()
    {
        float search_radius = 10f;

        var hits = Physics.SphereCastAll( //playerを中心に半径10fの半球内にあるvalveTargetを取得
            player.transform.position,
            search_radius,
            player.transform.forward,
            0.01f,
            LayerMask.GetMask("valve")
        ).Select(h => h.transform.gameObject).ToList();

        hits = FilterTargetObject(hits);

        if (0 < hits.Count()) {

            foreach (var hit in hits) {

                Vector3 targetScreenPoint = hit.transform.position; //各Targetの位置を取得
                float target_distance = Vector2.Distance( //playerの位置から各Targetまでの距離を取得
                    player.transform.position, targetScreenPoint );

                DisList.Add(new DistanceDate() { distance = target_distance, TargetOb = hit}); //変更
            }
            DisList.Sort((a,b) =>Math.Sign( a.distance - b.distance )); //ソート

            foreach(var item in DisList)
            {
                Debug.Log("Distance : " + item.distance + " / Object : " + item.TargetOb);
                hitsOb.Add(item.TargetOb); //並び替えたオブジェクトのみ別のListに登録
            }
            DisList = new List<DistanceDate>(); //取得用のListは初期化

        } else {
            DisList = new List<DistanceDate>();
            hitsOb = null; //何もなかったらNullを返す
        }
    }  
    protected List<GameObject> FilterTargetObject(List<GameObject> hits) //Physics.SphereCastAllでゲットしたTargetが画面内に入っているか判定
    {
        return hits
            .Where(h => {
                Vector3 screenPoint = Camera.main.WorldToViewportPoint(h.transform.position);
                return screenPoint.x > 0 && screenPoint.x < 1 && screenPoint.y > 0 && screenPoint.y < 1;
            })
            .ToList();
    }
}

投稿

  • 回答の評価を上げる

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

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

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

  • 回答の評価を下げる

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

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

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

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

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

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