【Unity】Raycastを使ってオブジェクトを配置・入れ替えしてみる

## はじめに
* Unity 2ヶ月目くらいの初心者がゲーム開発をする上で詰まったポイントをまとめます。
* 自分と同じような初心者向けなので、上級者の方はブラウザバックを推奨します。

## やりたいこと
*  [~~Auto Chess~~](https://www.zizouqi.com/ja.htmlRTS) RTSっぽいサムシングを作りたい
    * 盤面に配置する駒をマスに配置・移動させたい

## 詰まったこと
最初はCanvasからWorldSpaceにドラッグ&ドロップで実現したかった・・・
が、ダメ・・・！
      ![20190624.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/328095/35608e02-df8d-2a6e-3574-c09b947512f1.gif)

CanvasはあくまでUIを配置するものであり、CanvasのUIそのものが直接的に3D空間に影響を与える目的で用意されたものでない（という予想）

では、どのようにしてオブジェクトの配置をするべきなのか？
そもそも、3d空間でクリックやドラッグ&ドロップしてオブジェクトを検知する方法ってどうするんだ・・・？

そんな悩みを解決してくれたのがRaycastでした

## Raycastとは
* 光線を放つ、的な意味
* ある始点から指定の方向に光線を放ってみて、ぶつかった物体があればその情報を取得できるというもの
* 参考
    * [Physics.Raycast](https://docs.unity3d.com/ja/current/ScriptReference/Physics.Raycast.html)
* 試しに以下のようなコードを適当なGameObjectにattachして動かしてみます。

  ```c#
  using UnityEngine;

  public class RayCastTest : MonoBehaviour
  {
      GameObject gameObject;

      void Update()
      {
          // Click確認
          if (Input.GetMouseButtonDown(0))
          {
              Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
              RaycastHit hit;
  
              if (Physics.Raycast(ray, out hit, 100))
              {
                  gameObject = hit.collider.gameObject;
                  Debug.DrawRay(ray.origin, ray.direction * 3, Color.green, 5, false);
                  Debug.Log(gameObject);
              }
          }
      }
  }
  ``` 

![20190706qita.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/328095/0a31d452-26e4-070b-3f6d-f527606ded29.gif)

Cameraからクリックした箇所に向けてRayが描画されてるのがわかります。

## Rayが当たらない？
よしやってみようということで、サンプルコードを使ってフリーのAssetに対してやってみると、当たるものと当たらないものがある・・・
当たるものも、見た目通りに当たらない？

そもそもRayが当たる条件はどう判定されているのか？

その条件に使われるのがColliderです

## coliderとは
* 物理衝突のためのオブジェクト形状を定義
* 必ずしもモデルと同じ形状をとる必要はない
    * 見えざる判定を加えることも可能
* 参考
    * [Collider](https://docs.unity3d.com/ja/current/Manual/CollidersOverview.html)

## AssetにColliderが設定されているとは限らない
今回Assetストアから適当なキャラクタモデルを使わせてもらいましたが、その中には必ずしもColliderが設定されているとは限りません。
当然自身で、Colliderを設定しないと目的は達成できませんが、キャラクタは各パーツが複数組み合わせってできていて、一つ一つ手でColliderを設定していくのは面倒です。

そこで今回は[SA collider builder](https://assetstore.unity.com/packages/tools/sacolliderbuilder-15058)というものを利用させていただきました。

ここでは紹介を省きますが、詳しくは[こちら](http://kan-kikuchi.hatenablog.com/entry/SAColliderBuilder)の記事が参考になると思いますので、ご参照ください

Add Componentでこちらを足せば、自動的にこんな感じ（緑枠）のそれっぽいColliderを作ってくれます。
Mesh Colliderよりも軽いし、ボタン一つで子オブジェクトにも自動で反映されてめっちゃ簡単です。

![20190706_footman_after 0.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/328095/a2f44c1d-6310-31b3-61fd-aea907e39ee7.png)



## colliderを設定したはいいが・・・

Rayが当たる箇所はキャラクターを生成するそれぞれの部品で、腕や足の部分など様々です。
やりたいことはそれをまとめる親オブジェクトを丸ごと動かすことですが、Assetを使っている以上、
どのような階層関係になっているかはasset次第です。

そのため、指定のcomponentをもったobjectを再帰的に探すことにしました。

## GameObjectの拡張メソッド実装

GameObjectやTransoformなどに対して拡張メソッドを定義することができます。
今回は以下のようなコードを用意して、GameObjectおよびTransformの親オブジェクトを再帰的に調べるコードを作りました。
ついでに子を持っているか、指定componentをもっているか、調べるメソッドも追加しました。

```C#
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public static partial class GameObjectExtensions
{

    public static bool HasChild(this GameObject gameObject)
    {
        return 0 < gameObject.transform.childCount;
    }

    public static bool HasComponent<T>(this GameObject self) where T : Component
    {
        return self.GetComponent<T>() != null;
    }

    public static Transform FindInParents<T>(this GameObject g) where T : Component
    {

        object comp;
        Transform t = g.transform;

        while (t != null)
        {
            comp = t.GetComponent<T>();
            if (comp == null)
            {
                t = t.transform.parent;
            }
            else
            {
                break;
            }
            
        }
        return t;
    }
}

public static partial class TransformExtensions
{
    public static bool HasChild(this Transform transform)
    {
        return 0 < transform.childCount;
    }

    public static bool HasComponent<T>(this Transform self) where T : Component
    {
        return self.GetComponent<T>() != null;
    }

    public static Transform FindInParents<T>(this Transform t) where T : Component
    {

        object comp;

        while (t != null)
        {
            comp = t.GetComponent<T>();
            if (comp == null)
            {
                t = t.transform.parent;
            }
            else
            {
                break;
            }
        }

        return t;
    }
}
```

## いいから実装だ！

駒の配置および入れ替えを行うソースコードです
前提条件として駒を置くタイルオブジェクトにはTileというタグを付与した状態で、
キャラクタのrootオブジェクトにはPieceAbstractという(クラスを継承した)componentを付与しています。

```C#
using UnityEngine;

public class PreparationManager : MonoBehaviour
{
    GameObject clickedGameObject;
    private Transform selectedPiece = null;
    private bool isSelected = false;

    void Update()
    {

        // Click確認
        if (Input.GetMouseButtonDown(0))
        {
            clickedGameObject = null;

            Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
            RaycastHit hit = new RaycastHit();

            Debug.DrawRay(ray.origin, ray.direction * 1, Color.green, 5, false);
            
            // Collider objectの存在確認（RayHit)
            if (Physics.Raycast(ray, out hit))
            {
                clickedGameObject = hit.collider.gameObject;

                //非選択モードか確認
                if (!isSelected)
                {
                    //  PieceAbstract objectか判定
                    selectedPiece = clickedGameObject.FindInParents<PieceAbstract>();
                    if (selectedPiece != null)
                    {
                        // 選択状態をTrueに
                        isSelected = true;                        
                    }
                }
                //選択モードの場合
                else
                {
                    //  Tile objecetの判定
                    if (clickedGameObject.gameObject.tag == "Tile")
                    {
                        // 既に駒が配置されていた場合は、その駒とswap
                        if (clickedGameObject.HasChild())
                        {
                            foreach(Transform child in clickedGameObject.transform)
                            {
                                //念のためPieceか確認
                                if (child.HasComponent<PieceAbstract>())
                                {
                                    swapPiece(child, selectedPiece);
                                    //Childは一つのはずだが、念のためBreak
                                    break;
                                }
                            }
                        }
                        //  tileにpieceObjectを配置
                        else
                        {
                            setPiece(selectedPiece, clickedGameObject.transform);
                        }
                        // effect解除
                    }
                    else if(clickedGameObject.FindInParents<PieceAbstract>()){
                        //swap処理
                        swapPiece(selectedPiece, clickedGameObject.FindInParents<PieceAbstract>().transform);
                    }
                    // 選択状態をFalseに
                    isSelected = false;
                    selectedPiece = null;
                }
            }
            // RayCastがhitしなかった場合
            else
            {
                isSelected = false;
                selectedPiece = null;
            }
        }
    }

    private void setPiece(Transform piece, Transform tile)
    {
        //Pieceの親オブジェクトにTileを設定
        piece.parent = tile.transform;
        //TileObjectを親としたローカルポジションを設定
        piece.localPosition = new Vector3(0, 0, 0);
    }

    private void swapPiece(Transform piece1, Transform piece2) 
    {
        //各Pieceの親となるTileオブジェクトを見つける
        Transform tile1 = piece1.parent;
        Transform tile2 = piece2.parent;

        //親となるTileをそれぞれ入れかえ
        piece1.parent = tile2;
        piece2.parent = tile1;

        //駒を新しい親Tileの場所に移動（Swap）
        piece2.localPosition = new Vector3(0, 0, 0);
        piece1.localPosition = new Vector3(0, 0, 0);
    }
};
```


動作は以下のようになります。
![20190706.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/328095/e0192fdf-978e-945d-762f-a59bdc747e73.gif)

参考になれば幸いです。
また、至らない点が多々あると思いますので、もっといい方法などぜひコメントお願いします。