ぐーるらいふ

しがないリーマンの記録。遊びのunityのメモ帳にしたい。

【Unity】第六回 unity1week「space」参加作品「スペースキューブ」の開発

ぐーるです。
ついに始まった、unity1week。
皆勤賞で今回も参加しました。

f:id:ghoul_life:20171121143440p:plain

今回の作品はこちら。
https://unityroom.com/games/spaceproj

unityちゃんが宇宙貨物をスペースシャトルに運ぶゲームです。
三次元的に登ったり降りたり動かしたり重ねたりといった工夫を凝らして
ステージをクリアするパズルゲームです。全10ステージ。

お題について

「space」宇宙モノか隙間モノか…と漠然と考える。

大きな分類で考えると

で二分されそう。

うーん、どっちかって言うとシューティングゲームかなぁ。
なんて思っていたのだけど。どうも食指が動かない。

まぁ、グラディウスとかのような名作をイメージしてしまうのは
必然というかそういうのしか頭に浮かばない。
じゃあ、カジュアルゲーム
これも前にやったしなぁ。と考え直す。
「!」
と来たのは隙間は隙間でも箱をどこに置くか?といったスキマを埋める系のゲーム。
じゃあこれにしてみようかな、と作り始めた。

まず初めに

ざっくりイメージを固める。

ステージがあって、荷物は1つまたは複数で、荷物を指定の位置に置くことが出来たらクリア

というシンプルなものをイメージ。
spaceは「スキマに置け!」みたいな語呂合わせで何とか乗り切ってしまえばいいや。
と軽く考えていたが、全体的な雰囲気を宇宙っぽくしようと考える。

blender

ちょっと怪しげなキューブにすれば、それっぽく見えるだろう。
でもモデルのUV画像を取得して編集するにはどうすればいいんだ?
という事で調査して手順化して下記にまとめておきました。

ghoul-life.hatenablog.com

モデルづくり

ゲーム内で使ったモデルはunich-chanを覗いて全て自作です。
今思えばunich-chanも止めて宇宙服の人とかにすれば良かったかなー。

キューブはこんな感じでBlenderで。

f:id:ghoul_life:20171121131441p:plain

やっとプログラムに入る

テンプレートやTweenアニメーションエンジンは一切使わないで全部自作。
unityの基本APIのみで実装していきます。

ステージ構成

大きく分けて3つに分類しました。

  • Floor
  • Gimmick
  • Player
Floor

足場になるオブジェクト。プレイヤーは動かすことが出来ないシンプルなもの。コードもシンプル。
起点と終点をセット出来て、配置して命令(InitMove)を出すと終点に移動してから固定される。

また、Stateを持っていて、基点となるGameManagerが状態を監視している。
StateがCreateFinishになると「あ、ステージ作成完了したんだな」と分かるよくあるヤーツです。

今回のゲームでは1つしか無かったが、親クラス(FloorObj)で管理していて、
色々なオブジェクトを使えるようにしている。

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

public class FloorManager : SingletonMonoBehavior<FloorManager> {

    protected STATE_STAGE _state;
    protected List<GameObject> gameObjList;
    public GameObject floorObjPrefab;
    private const float ADD_INIT_Y_VAL = 20.0f; // 初期位置の高さ。ここから降ってくる

    // Update is called once per frame
    void Update () {
        // ステージ作成中
        if (State == STATE_STAGE.CREATE)
        {
            bool isCreateFinish = true;
            foreach (var go in gameObjList)
            {
                var fo = go.GetComponent<FloorObj>();
                if (fo.IsInitMove) // まだ初期化動作が終わっていない
                {
                    isCreateFinish = false;
                    break;
                }
            }
            if (isCreateFinish)
            {
                State = STATE_STAGE.CREATE_FINISH;
            }
        }
    }

    public void Init()
    {
        Clean();
        if (gameObjList == null)
        {
            gameObjList = new List<GameObject>();
        }
    }
    /**
     * Create Stage Floor
     */
    public void Create()
    {
        State = STATE_STAGE.CREATE;

        var stageDatas = Define.GetFloorList();

        var xLen = stageDatas.Length;
        for(var xIdx = 0; xIdx < xLen; xIdx++)
        {
            var zLen = stageDatas[xIdx].Length;
            for(var zIdx = 0; zIdx < zLen; zIdx++)
            {
                var yLen = stageDatas[xIdx][zIdx];
                for (var yIdx = 0; yIdx < yLen; yIdx++)
                {
                    var targetPos = new Vector3(xIdx, yIdx, zIdx);
                    var p = new Vector3(xIdx, yIdx + ADD_INIT_Y_VAL, zIdx);
                    CreateFloorObj(targetPos, p);
                }
            }
        }
    }

    public void Clean()
    {
        State = STATE_STAGE.CLEAN;
        CleanGameObjList();
        State = STATE_STAGE.CLEAN_FINISH;
    }

    protected void CleanGameObjList()
    {
        if (gameObjList != null)
        {
            for (var i = 0; i < gameObjList.Count; i++)
            {
                var go = gameObjList[i];
                Destroy(go.gameObject);
            }
            gameObjList.Clear();
        }
    }

    //-------------------------------------------------------------------
    /**
     * floor prefab create
     */
    private void CreateFloorObj(Vector3 targetPos, Vector3 initPos)
    {
        var go = Instantiate(floorObjPrefab);
        var fo = go.GetComponent<FloorObj>();
        if (fo)
        {

            go.transform.SetParent(this.transform);
            go.transform.position = initPos;
            fo.targetPos = targetPos;
            fo.waitTime = Random.Range(0, 2.0f);
            fo.InitMove();

            gameObjList.Add(go);
        }
        else
        {
            Debug.LogError("ERR CreateFloorObj. please add FloorObj script!!");
            Destroy(go);
        }
    }

    public STATE_STAGE State
    {
        get { return _state; }
        set { _state = value; }
    }

    public bool isCreate()
    {
        return _state == STATE_STAGE.CREATE;
    }
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class FloorObj : MonoBehaviour {

    public Vector3 targetPos; // ここに向かって動く
    public float waitTime; // 動き出すまでに待機時間を設定してランダム性を出す
    private bool isInitMove = true;
    private float time = 20.0f;
	// Use this for initialization
	void Start () {
	}
	
	// Update is called once per frame
	void Update () {
	}

    public void InitMove()
    {
        if (isInitMove)
        {
            StartCoroutine(_InitMove());
        }
        
    }

    private IEnumerator _InitMove()
    {
        yield return new WaitForSeconds(waitTime);

        while(Vector3.Distance(this.transform.position, targetPos) > 0.01f)
        {
            this.transform.position = Vector3.MoveTowards(this.transform.position, targetPos, time * Time.deltaTime);
            yield return new WaitForEndOfFrame();
        }
        this.transform.position = targetPos;
        isInitMove = false;

        yield return null;
    }

    public bool IsInitMove
    {
        get { return isInitMove; }
    }
}
Gimmick

ゴールも荷物も全てギミックとしました。
Carry , Beam といった子クラスがあり、クラス別に独自の動作が出来る。
Rigidbodyは使わず、Colliderのみ。
その中のCarryを抜粋。当たり判定はRaycastで行っています。
詳しくは後述。

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

public class Carry : GimmickObj {

    private bool isMove;
    private float moveTime = 1.0f;
    private float downMoveTime = 5.0f;
    private HIT_DIR hitDir;

    Ray leftRay;
    Ray rightRay;
    Ray forwardRay;
    Ray backRay;
    Ray downRay;

    // Use this for initialization
    void Start () {
        isMove = false;
        hitDir = HIT_DIR.NONE;
    }

    void FixedUpdate()
    {
        var p = this.transform.position;

        RaycastHit hit;
        leftRay = new Ray(p, Define.vectorLeft);
        rightRay = new Ray(p, Define.vectorRight);
        forwardRay = new Ray(p, Define.vectorForward);
        backRay = new Ray(p, Define.vectorBack);
        downRay = new Ray(p, Define.vectorDown);
        var distance = 1.0f;

        if (!isMove)
        {
            // 落下判定
            if (!Physics.Raycast(downRay, out hit, distance, LayerMask.GetMask("Floor", "Gimmick")))
            {
                if (!AudioManager.Instance.IsPlaySE()) AudioManager.Instance.PlaySE(AudioManager.SE.FALL_BOX);
                MoveDown();
            }

            // player接触判定
            if (Physics.Raycast(leftRay, out hit, distance, LayerMask.GetMask("Player")))
            {
                hitDir = HIT_DIR.LEFT;
            }
            else
            if (Physics.Raycast(rightRay, out hit, distance, LayerMask.GetMask("Player")))
            {
                hitDir = HIT_DIR.RIGHT;
            }
            else
            if (Physics.Raycast(forwardRay, out hit, distance, LayerMask.GetMask("Player")))
            {
                hitDir = HIT_DIR.FORWARD;
            }
            else
            if (Physics.Raycast(backRay, out hit, distance, LayerMask.GetMask("Player")))
            {
                hitDir = HIT_DIR.BACK;
            }
            else
            {
                hitDir = HIT_DIR.NONE;
            }
        }
    }

    public bool MoveLeft() { return Move(Define.vectorLeft , moveTime); }
    public bool MoveRight() { return Move(Define.vectorRight, moveTime); }
    public bool MoveForward() { return Move(Define.vectorForward, moveTime); }
    public bool MoveBack() { return Move(Define.vectorBack, moveTime); }
    public bool MoveUp() { return Move(Define.vectorUp, moveTime); }
    public bool MoveDown() { return Move(Define.vectorDown, downMoveTime); }

    // 移動
    private bool Move(Vector3 movePos , float tmpMoveTime)
    {
        if (isMove) { return false; }
        RaycastHit hit;
        var ray = new Ray(this.transform.position , movePos);
        var distance = 1.0f;
        // 衝突判定
        if (Physics.Raycast(ray, out hit, distance, LayerMask.GetMask("Floor" , "Gimmick")))
        {
            var fo = hit.collider.gameObject.GetComponent<FloorObj>();
            var gio = hit.collider.gameObject.GetComponent<GimmickObj>();
            if (fo != null || gio != null)
            {
                // 障害物に衝突している
                return false;
            }

        }


        isMove = true; // 移動中状態フラグを立てる
        var p = this.transform.position;
        p += movePos;

        MoveControl.Instance.Move(this.gameObject, p, tmpMoveTime, FinishMove);

        return true;
    }

    private void FinishMove()
    {
        isMove = false; // 移動完了でフラグを下ろす
    }

    public HIT_DIR HitDir
    {
        get { return hitDir; }
    }
}
Player

プレイヤーです。これも1つしかなかったが、実はFloor同様複数作ることが出来るようになってます。
プレイヤーを切り替えながら橋を架けるようなステージがあっても面白いかも。
と思いましたが、一旦保留に。

オブジェクトの移動

上でお見せしているCarryのソースコードを見れば一目瞭然ですが、
オブジェクトは前後上下左右でRayを飛ばしていて、

「今何とどのように接触しているか?」

を常に取得するようにし、それによって動作を変えるようにしました。
行動を起こす前にも同様にチェックして障害物が無いかを確認。

ここは最後まで悩みました。2Dだと

{0,1,1,0,0,1,0,1}
{0,1,1,0,1,1,0,1}
{0,1,1,0,1,1,0,1}
{0,0,0,0,0,0,0,1}

といったマップを持っておいて、
マップ情報を元に通れるか通れないかを判定することがありますが、今回は不採用。

また、移動処理だけ行うクラスを用意してそこで移動させています。
callbackを設定しておいて、完了通知が飛ばせるようにしてあります。

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

/**
 * objectの移動を司る
 */
public class MoveControl : SingletonMonoBehavior<MoveControl> {

    public void Move(GameObject obj, Vector3 targetPos, float moveTime , System.Action callback)
    {
        StartCoroutine(_Move(obj , targetPos , moveTime , callback));
    }

    private IEnumerator _Move(GameObject obj, Vector3 targetPos, float moveTime , System.Action callback)
    {
        // ある程度まで近づいたら
        while (Vector3.Distance(obj.transform.position, targetPos) > 0.01f)
        {
            obj.transform.position = Vector3.MoveTowards(obj.transform.position, targetPos, moveTime * Time.deltaTime);
            yield return new WaitForEndOfFrame();
        }
        obj.transform.position = targetPos; // 強制的に指定位置に
        
        callback(); // 完了通知

        yield return null;
    }
}


(今思えばSphereでRaycast飛ばして範囲に入ったら、お互いのforwardで内積でも取ればどこで接触しているか取れそう
だなーとは思った…今度やってみよう。)

ジャンプ

最初はありませんでした。ほんとに倉庫番というゲームまんまだったので。

オリジナル要素をもう少し入れたいなーと思った時に3Dらしく上下にも動かしたいなと。
入れてみたら思いの外面白かったのでそのまま採用へ。

MoveControlでMove -> Moveです。
なので二段以上の壁でジャンプすると上には行くが、前には進めないという現象が出るようになっています。
これはこれでカワイイのでそのままにしました。

f:id:ghoul_life:20171121135710g:plain

オブジェクトの再配置

オブジェクト、プレイヤーを画面外に落とすと、初期位置に戻ります。

これ実は「デバッグ機能」でした。

ステージから落ちれない

という仕様にするのが一般的ですが
実装を後回しにしてて、「落ちたら初期位置に戻しゃいいや」と適当スクリプトでガシガシ先に進めてた所
これを利用したステージを思いつき、コレ良いんじゃないかなとそのまま採用へ。

アセットについて

unitychanとSkyboxで使用しました。

周りを見るとみんな一杯使ってるんだなーと。
一週間だからどんどん使った方が良さそうではありますが。
勉強第一。DIY精神で自作頑張ります。(←unityちゃん使ってるヤツが言うセリフではない)

感想

仕事がずっと過渡期でして。
拘束十二時間超えがまれによくある会社とかおかしくないですか。

久しぶりにunity触って、過去の作品の実装とか思い出しながらやってましたが、
思ったよりも忘れてなかった。

自分はこういうゲームが好きだ。
ここ一週間、毎日通勤中にステージ考えて、
evernoteにガシガシアイデアメモってるのも楽しかった。
「家帰ったらUIガガッとやるぞ!」なんて思いながら仕事してました。

もし良かったらちょっとだけやってみて下さいー。
そんなに時間は取らせない…はず。

【Unity】Blenderで簡単にuv展開をする手順まとめ。そして作ったものをUnityで使いたい

お久しぶりです。ぐーるです。
ゾンビみたいな暮らしをしています。

随分と間が空いてしまったのですが、unity1weekがまた始まったので、
久しぶりにブログ更新しようと思いました。

枠付きのcubeを作りたい

枠付きのcubeはこんな感じのもの。

f:id:ghoul_life:20171115165349p:plain

オシャレですよね。ゲームに使うとちょっとしたアクセントになるのでは無いかと。
このぐらいなら自分でも作れるんじゃないか、と思い立ったのが今回になります。

Blenderでcubeを作ってuv展開して色を塗ればそれっぽくなるだろう。
初心者でも簡単簡単…。で、どうやるんだ?
という考えから手順をまとめてみました。

なお、クッソ初心者向けです。

BlenderでcubeをUV展開する手順

step1

Blenderを立ち上げます。

f:id:ghoul_life:20171115165405p:plain

そして適当に画面をクリックしてスプラッシュを消します。

f:id:ghoul_life:20171115165433p:plain

step2

いきなりですが、

メインビューの右上にあるツマミを左にグィーとドラッグ

して下さい。

f:id:ghoul_life:20171115165448p:plain

するとビューを2つにすることが出来ます。
この方が分かりやすいかと思いますのでこうしました。

f:id:ghoul_life:20171115165513p:plain

step3

左のビューは「編集モード」に
右のビューは「UV/画像エディタ」に
しておきます。

f:id:ghoul_life:20171115165532p:plain
f:id:ghoul_life:20171115165544p:plain

step4

シームを付けます。

シームとはuv展開をする時にどこに切れ込みを入れ、アジの開きのように展開するか?

という基準線になります。これを入れておかないとuv展開することが出来ません。

左ビューのcubeの頂点を全部選択しておいて(Aキー)
左ペインの「シェーディング/uv」 => 「シームを付ける」を押下します

f:id:ghoul_life:20171115165604p:plain

すると線に赤い色が付きます。これがシームになります。

step5

やっと展開です。
左ペインの「シェーディング/uv」 => 「展開」 => 「展開」
と選択します。

f:id:ghoul_life:20171115165620p:plain

右側に開いておいたuv画像エディタにuv展開された図が展開されるはずです。
上手く表示されない場合はシームが正しく付いていない可能性があるので、
step4からやりなおしてみましょう。

f:id:ghoul_life:20171115165638p:plain

step6

ここで終わりじゃなく、これを編集したいのです。
なので、エクスポートします。

右ビューの下ペイン「uv」 => 「uv配置をエクスポート」
で出力することが出来ます。適当な名前と場所に出力しましょう。

f:id:ghoul_life:20171115165700p:plain

step7

編集ツールでテキトーに編集します。
Windowsペイントでも全然OKですよ!

f:id:ghoul_life:20171115165716p:plain

step8

編集したら、その画像を適用します。
Blenderに戻って右側ビュー下ペインの「開く」を押下してstep7で編集した画像を選択します。

f:id:ghoul_life:20171115165729p:plain

右ビューに編集した画像が表示されます。

f:id:ghoul_life:20171115165740p:plain

step9

もう反映されています。
左ビューに行き、シェーディングをテクスチャにしてみましょう。

f:id:ghoul_life:20171115165802p:plain

すると編集した画像が適用されたcubeが見えるはずです。

  • 上手く表示されない?

なんか部分部分しか反映されないんだけど?
ライティングやカメラの関係が上手く表示されていないのかもしれません。
unityで使う関係上、ライティングやカメラは必要無いので、消しちゃいましょう

f:id:ghoul_life:20171115165846p:plain

「右クリック」 => 「削除」 などでOKです。(xキーでもいける?)

step10

unityで使ってみましょう。
fbxで適当に出力してD&Dで持っていけばOKです。
なお、テクスチャは別で入れる必要がありました。
「fbxとpngを入れる」 => 「pngからMaterialを作る」 =>「fbxをSceneに配置してMaterialを適当」
オブジェクトに適用すればOKのはずです。

f:id:ghoul_life:20171115165907p:plain
f:id:ghoul_life:20171115165915p:plain

fbxに画像を含めるには
nn-hokuson.hatenablog.com
ここに手順が!神さまーありがとうございます!

感想

ステップ10までありますが、実際やってみると15分程度で終わりました。
慣れればもっとスイスイ出来ると思います。

これが理解できればきっと複雑なモデルでもuv展開することが出来る…はず!
あー、それにしてもBlender難しい!

unity1weekの進捗はダメです。

f:id:ghoul_life:20171115171013g:plain

【Unity】アクションゲームの「動く床」を実現するには【Tips】

最近ゲームを作ってまして、そこから何かネタが無いかなーと思うのですが、
これと言って目新しいものが無く…。
その開発過程でちょっとだけ気になった個所をメモ代わりに残しておこう。

動く床

アクションゲームにはよくあるギミックとして、

動く床

というものがある。
地面が動き、タイミングを合わせてジャンプしないと先に進めないというよくあるやつだ。

f:id:ghoul_life:20171008233955g:plain

これをunityで作ってみよう。

床を作る

cubeを作成してスケールを10,1,10。これを動く床とします。

f:id:ghoul_life:20171008234236p:plain

アニメーションを床に付与する

animation > createして適当にtransform.positonのxを動かすようなものを作成

f:id:ghoul_life:20171008234404p:plain

プレイヤーを作る

その上に乗っかるプレイヤーを適当に用意する。
今回はQueryChanを使いました。

f:id:ghoul_life:20171008234655p:plain

Control.cs
(仮実装なんでとりあえずやっつけ…。)

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

public class Control : MonoBehaviour {

    private const float SPEED = 5.0f;
    public Animator anim;
    private float jumpTime = 0;
    private bool isJump = false;
	
	// Update is called once per frame
	void Update () {

        var hz = Input.GetAxis("Horizontal");
        var vt = Input.GetAxis("Vertical");

        if(Mathf.Abs(hz + vt) > 0)
        {
            anim.SetBool("Walk", true);
        }
        else
        {
            anim.SetBool("Walk", false);
        }




        if (Input.GetButtonDown("Jump"))
        {
            if (!isJump)
            {
                anim.SetTrigger("Jump");
                isJump = true;
                jumpTime = 0;
            }
        }

        float jumpVal = 0;

        if (isJump)
        {
            if (jumpTime < 1.0f)
            {
                jumpVal += 2 * SPEED * Time.deltaTime;
            }
            else if (jumpTime < 2.0f)
            {
                //pos.y -= Time.deltaTime;
            }
            else
            {
                isJump = false;
            }
            jumpTime += Time.deltaTime;
        }

        var pos = this.transform.position;
        pos.x += hz * SPEED * Time.deltaTime;
        pos.y += jumpVal;
        pos.z += vt * SPEED * Time.deltaTime;
        
        this.transform.position = pos;

        if (Input.GetKeyDown(KeyCode.UpArrow))
        {
            transform.eulerAngles = new Vector3(0,0,0);
        }
        if (Input.GetKeyDown(KeyCode.DownArrow))
        {
            transform.eulerAngles = new Vector3(0, 180, 0);
        }
        if (Input.GetKeyDown(KeyCode.LeftArrow))
        {
            transform.eulerAngles = new Vector3(0, 270, 0);
        }
        if (Input.GetKeyDown(KeyCode.RightArrow))
        {
            transform.eulerAngles = new Vector3(0, 90, 0);
        }

    }
}
とりあえず実行してみる

早速実行してみるとこうなる。

f:id:ghoul_life:20171008235015g:plain

おかしい。おかしいぞ。
上に乗っかった時は動く床の影響を受けて欲しい。

解決策

では、どうするか?こうすればOKだ。

子要素にする

コードにするとこんな感じ

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

public class MoveFloor : MonoBehaviour {
    private void OnCollisionEnter(Collision collision)
    {
        collision.gameObject.transform.SetParent(this.transform);
    }

    private void OnCollisionExit(Collision collision)
    {
        collision.gameObject.transform.SetParent(null);
    }
}

これを床に付与して実行してみよう

f:id:ghoul_life:20171008235406g:plain

これでよくある動く床を実装出来た。

別の手段

別の手段としては床をRigidbodyにして、AddForceで動かす
(PhysicsMaterialで滑りを低く)
という手もあるらしいけど、ちょっと試していない。

凄い久しぶりにunityの事書けた…。最近更新も滞ってたし。
もっとメモ代わりにどんどん書きたい。

え、最終回?

え、今日最後なの…!?
はぁあぁあぁあ〜生きる気力があああ〜〜。

f:id:ghoul_life:20170927002635p:plain

9月は忙しいけど、休日多くてなんか楽しい

なんかやりたいけど忙殺されておる!
休日多くてunityでちょいちょいミニプチゲーム作ったりしてます。
記事に出来るほど見識溜まってないけど…。

9月は会社的には別れの季節でもあって、
8月のボーナス支給に合わせて辞めた人が多い。
よってその作業が回ってくるのだ。

立つ鳥跡を濁さずと言うけど、後を濁しまくって辞めていく人も中には居る。
何にも引き継ぎしてなかったりしてね…ヒドイぜーツライぜー。

そんな中でも癒やしてくれるのは深夜アニメだったりする。
f:id:ghoul_life:20170920001809p:plain
ああ、明日も頑張るぞい。

暑い。夏はもう終わったはずなのに

まだ終わってなかったのか。
9月ももう半ば。あっという間だ。

もう少しunityについてなんかやりたいけど何がいいだろうか。
苦手なパーティクルでも勉強してみようか。

f:id:ghoul_life:20170915001903p:plain

【Unity】 メッシュをカットした後のcappingを作ろう

以前

以前メッシュカッターについて記事にした事があった。

ghoul-life.hatenablog.com

この時はなるべく簡潔な話にしたく、オブジェクトを切った後の「蓋」については
特に触れなかったのだが、ちょっとやる機会があったので、書いておこうと思う。

メッシュ断面図

メッシュカッターはオブジェクトをポリゴン単位で考え、一つずつ丁寧に処理していく。
カットラインと交点がある場合は、新たに頂点が2つ作られる。
四角形だった場合は以下のような断面になる。
図だと少し分かりにくいが、二つに分かれたオブジェクト両方ともこのようになる。

f:id:ghoul_life:20170913001850p:plain

蓋の考え方

そしてここに蓋を作るのだが、蓋をどう作るか?を考える。
四角形ならどうにでもなりそうだが、円形や人型のオブジェクトだったらどうする?

f:id:ghoul_life:20170913001905p:plain

そういった事なども簡潔に解決する方法として、「中心点を作る」という方法がある。
この2点と中心点を繋いでいく、にすれば簡単な計算で作れそうだ。

f:id:ghoul_life:20170913001920p:plain

実装

まずは交点となった新たに作られた点を別リストとして取っておく

// どちらにもカウントがあるということはplateと交差しているポリゴンということ
if (group1PosList.Count > 0 && group2PosList.Count > 0)
{
    CalcCrossPoint(plane, group1PosList, group2PosList); // 2.planeとの交点を求める

    // 3.両方のグループともに交点を入れる
    group1PosList.Add(_pos1);
    group1PosList.Add(_pos2);

    // capping用の表
    group1CapPosList.Add(_pos1);
    group1CapPosList.Add(_pos2);


    group2PosList.Add(_pos1);
    group2PosList.Add(_pos2);

    // capping用裏側
    group2CapPosList.Add(_pos1);
    group2CapPosList.Add(_pos2);
}

中心点を求める方法はそこまで難しくない。
頂点をすべて足して、頂点数で割ればいい。

// 中心点を求める
Vector3 center = Vector3.zero;
foreach(var v in cappingPosList)
{
    center += v;
}
center = center / cappingPosList.Count;


そして交点となる2点と中心点を繋いでいく…のだけど、
カッターとなるplateのどちら側にあるかによって蓋の向きが変わる。
この事に注意して、indexを割り振っていく。

var idx0 = centerIdx;
var idx1 = i;
var idx2 = i + 1;

var cross = Vector3.Cross(capPosList[idx2] - capPosList[idx0], capPosList[idx1] - capPosList[idx0]);
var inner = Vector3.Dot(cross, plane.normal);

// plateに対してどちら側の蓋かによって計算が変わるのに注意
if (isFront)
{
    if(inner < 0)
    {
        idx0 = idx2;
        idx2 = centerIdx;
    }
}
else
{
    if (inner > 0)
    {
        idx0 = idx2;
        idx2 = centerIdx;
    }
}

完成

後は全ての頂点とindexをまとめて、
オブジェクトを作れば完成だ。

f:id:ghoul_life:20170912233913g:plain

f:id:ghoul_life:20170913143608g:plain

f:id:ghoul_life:20170913143650g:plain

複雑なオブジェクトでもちゃんと蓋が出来ている。
全体をまとめたプロジェクトごとgithubに公開している。
他にもMaterialを付与したり、Rigidbodyを付与したりという遊びも可能だ。
参考にしてほしい。

github.com