ぐーるらいふ

底辺。

【Unity】InkPainterをちょっと理解してみたかった

お疲れ様です。
年末商戦とか関係なく休日が潰れてしまうぐーるです。
ゼノブレイド2を買ったのですが、全然出来なくて困ってます。

InkPainter

突然ですが、InkPainterってご存知でしょうか
f:id:ghoul_life:20171212221512p:plain
https://www.assetstore.unity3d.com/jp/#!/content/86210
Unityでスプラトゥーンごっこが出来ちゃう!しかも無料!

製作者さんはこちら
esprog.hatenablog.com
twitter.com

「おおー、すげー」で終わらせたいのだけど、

内部的にどうなってるのかな?
最小限で必要な要素ってなんだろう?

とふと気になったのでちょっと調べてみました。

最小構成の全体の流れ

まずは流れを掴むためにnormalmapもheightmapも無視した状態の最小構成を調査しました。
最小限必要な処理の流れを抜き出してみると

  1. materialからmainTextureを取り出して、RenderTextureにコピー
  2. materialのrenderTextureとmainTextureを差し替える(renderTextureがセットされた状態になる)
  3. bufferを利用してrenderTextureにインクを描画する

というすごくシンプルな実装という事がわかった。

最小構成に整理したスクリプト

オリジナルのInkPainterを読むと色々と機能があるが、
最低限必要なものだけを抜き出して整理したスクリプトはこんな感じ。


最小構成のシェーダー

shaderもcgincで共通化されていた箇所を取り出してきて、
1ソース内に収めて、整理したらこんな感じ。


Unityプロジェクトに落とし込む

1.Unityでprojectを作る
f:id:ghoul_life:20171212190149p:plain

2.planeを生成してカメラを調整でもしておく
f:id:ghoul_life:20171212190153p:plain

3.Scriptを作る(ソースは上に)
f:id:ghoul_life:20171212190211p:plain

4.Shaderを作る(同じくソースは上に)
f:id:ghoul_life:20171212190218p:plain

5.リソースを用意する(brushとmainTexture)
f:id:ghoul_life:20171212190208p:plain
f:id:ghoul_life:20171212190248p:plain
f:id:ghoul_life:20171212190257p:plain
↑使用したリソース二つなのだけど、見えないかも…?

6.materialを作り、planeにアタッチ
f:id:ghoul_life:20171212190156p:plain

7.Scriptもplaneにアタッチ
f:id:ghoul_life:20171212190221p:plain

8.実行する
f:id:ghoul_life:20171212190446p:plain
マウスでドラッグした箇所にブラシで設定したテクスチャの
形に描画をすることが出来るようになっており、
ゲームを止めて起動し直すと全てが初期化されているのが
確認出来ました。(リソースを破壊してない)

感想

この実装ではマウスでやってるけど、ボールが転がった跡にしたり、
一定時間で消したりとか使用用途はありそう。

平たく言えば

「テクスチャの上にブラシテクスチャで描画出来るようにしただけ」

になってしまったが、
mainTextureをコピーしてRenderTextureを作り、
それを利用してカスタマイズした表現をするという方法を理解して貰えればと。

normalmapも合わせて色の変化やにじみとかもやれるようにすると
もっと表現の幅が広がりそうなのでもしかすると続きを書くかも。

とりあえず一旦ここまで。
ゼノブレイド2やるぞーやるぞー…。

追記

Unity 5.6.0以前で開くと、エラーが出ていた。
Shaderに

#include "UnityCG.cginc"

が必要みたいだったので、追記しておきました。

【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の事書けた…。最近更新も滞ってたし。
もっとメモ代わりにどんどん書きたい。

【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

【Unity】第5回 unity1week 「フロー」の開発

unity1week襲来

もう大分時間が経ってしまったけど、書く。

naichiさんのunityroomで定期的に開催されているunityの一週間でゲームを作るイベント

f:id:ghoul_life:20170905004123p:plain
https://unityroom.com/unity1weeks


もう5回目にもなる。
unityを始めてまだまだ日が浅いけど、なんか色々パッと出来るようになってきた。
(と思いたい)

見知らぬ、お題

毎回あるお題が出て基本的にそれに沿った形で開発を行う。
今回のお題は「フロー」。

フローとか言われると仕事感が凄いというか、
エスカレーションフローを思い出すというか…。
とにかくイメージが悪い。
(勝手に浮かんでくる自分が悪い。仕事柄しょうがないのだけど…。)

鳴りすぎる、Slack

お題見てからどうしよっかなーと頭抱えたかったのだが、
そんな事とは関係なくSlackが鳴り止まなかった。

どうも8月はなんか忙しい。
毎年4月〜5月は桜の季節だからか全体的に緩いムードなのだが、
梅雨のどんよりした季節から加速していき、夏場にピークを迎える傾向にある。

というわけで結局技術的な追求なども特に出来ず、
やれる事をやる事に。

イデア、風呂の向こうに

フロー…フロー…とりあえず風呂入って考えよう。
といった具合で特にアイデアが出ないまま時間が過ぎていく…。
流れるように水の上を滑る!
みたいなものを作ってみたが、なんというか今一つ面白くならず…。
マリオ64の滑り台パチモンみたいなもんになってしまった。

これにコインでも付けて一番下まで滑ろう!なんて適当案が最初に思いついたのだが、
どうしよっかなぁ、このまま詰めるしか無いのか…と
イデアが降りてくるのを待ったが、やっぱりSlackが鳴り止まない。

ベッドダイバー

もう飛んだ。

朝Slackで起こされる ->
家からリモートで暫定対応して、会社へ ->
午前中MTG ->
午後客先など ->
日付変わるぐらいに帰宅 ->
就寝

というループが続き、気がつけば金曜日。
冷や汗しか出ない。

静止した頭の中で

テキトーにBlenderモデリングする!

  • >豆腐
  • >お箸

テキトーにFractureして壊してみる!

  • >予めオブジェクト分けといて、死んだらRigidbody.AddForceでぶっ飛ばす

苦手なAnimationに挑戦!
→何度か使ってるんだけど、毎回戸惑う!

後は割と過去の遺産を掘り起こしてUIやらなんやらそのまま使いました。

死に至るBlender、そして

豆腐とお箸はこれで作りました。
すげー苦手なBlenderを頑張ってみる。
後で書く

FractureとModel

予め壊れたものにしておいて、AddForceでぶっ飛ばす…だけ

f:id:ghoul_life:20170905003447p:plain

メッシュカッターからcapping、そしてvolonoi法(もどき)について
まとめきれないいいい。

Anchor

unityにはanchorという考えを使いたい場合、ちょっとだけ工夫が必要だ。
色々手はあるが、親子構造にして、親オブジェクトをanchorの代わりにするのが単純だ。
お箸ではこれを使ってる

せめて、ゲームらしく

(これが書きたかっただけ)
出来上がったのがこれ。

最初はすげー難しくて自分で作ってクリア出来ないというひどい出来w
動く床は失敗だった…。
うまく追従してくれなくて、どうしようかなと一旦そのままにしている。

こういうアクションゲーム作ったことある人はunity使いに多いと思うが、
基本的にコード記述量が少ない。
基本誤魔化し誤魔化しでなんとか形にしておいた…程度。

命の選択を

ラクガキしてる方が多くないかw
キーボードよりペンタブ触ってる方が長かったんじゃないかw
違うよこれはただの現実逃避だよ!

とりあえず夏休みを取りたい。
この炎上案件さえ終わればきっと取れる。
そう信じたい。

【Unity】第四回 unity1week 「夏」の開発について【レールの作り方】

第四回 Unity一週間ゲームジャムに参加

https://unityroom.com/assets/unity1week-d28da101073cc56f5238563f033ad780466b71b1605362b9fc351e7135ed1c4b.png
Unity 1週間ゲームジャム | 無料ゲーム投稿サイト unityroom - Unityのゲームをアップロードして公開しよう

一回目から参加し続けてもう四回目にもなってしまった。
参加者もちょっと少なかったのかもしれない。
イカドラクエのせいだ。


結論から言えば、今回も結果は振るわなかったのだけど、
まだまだUnity学びたいし頑張ろうと思っている。

イデアについて

とりあえず

  • イカ
  • 花火
  • お化け

この辺りが夏の記号になりそうだなと漠然と考えた。

で、とりあえずスイカ割らせてみたり。

切り方はMeshCutterで数回ランダムに切っているだけ。
毎回結果が変わるのが面白い所。

だが、スイカ割りは特に人気で被りまくりそう。
花火も上に同じ。

最近気になっていたレール式ランゲームをちょっとやってみたかったので、
サーフィンを題材に今回はそれに。

レール式ランゲーム

決められたレールの上を走るランゲーム。
ジェットコースターやトロッコとかイメージすればOK。
例えば右方向へ向かって走るランゲームと比べて、
3Dを活かしたダイナミックな動きを出すことが容易に出来る。
また制御しやすい。(一回転を作ったり、この曲がり角で敵を出す!とか)

unityに落とし込む

  • ランダムにルートを生成する
  • 無限に走ることが出来ないとならない
  • 敵の配置
  • ゲームとしての体裁を整える

敵の配置やゲームとしての体裁を整える辺りは長くなりすぎるので割愛。
今回はルート生成についてコードレベルで紹介。

ルート生成のアルゴリズム

一言で言えばルートはpointの集合体です。
「次に向かうポイント」
をどんどん点で打っていけばOKです。

流れとしては

  • ポイントをざっくり置く
  • 滑らかに整える
  • デコレーションする
  • 描画する

という流れになります。

ポイントをざっくり置く

まずはじめにポイントをざっくりと置いていきます。

for (var i = 1; i < LINE_CNT; i++)
{
    var distance = Random.Range(1, 5); // 距離の力
    range = Random.Range(-3, 3) * 30; // ランダムな方向を向ける
    lastAngle = Mathf.Clamp(lastAngle + range, 0, 180); // 前回の角度から0度~180度の間でランダムな方向を出す

    float angle = lastAngle * Mathf.PI / 180;
    // 極座標の公式で計算する
    path.LinePoints[i] = path.LinePoints[i - 1] + new Vector3(Mathf.Cos(angle), 0, Mathf.Sin(angle)) * distance;
}

pointはランダムな方向と、角度から位置を計算します。
極座標の公式

線の長さ(r)と角度(θ)で到達点座標(x,y)が求まる
(x,y) = (r cosθ , r sinθ)

イメージとしてはこんな感じでまだガタガタな点が作られます。

f:id:ghoul_life:20170731181931p:plain

滑らかに整える

点を細かくして補完していきます。
この計算をどのようにするかによって補完の仕方が変わります。
http://d.hatena.ne.jp/nakamura001/20111117/1321539246
イージングのアルゴリズムを応用して、緩急を作り出したりしても面白い!

/// <summary>
/// 線を滑らかに補完する
/// </summary>
/// <param name="linePoints"></param>
/// <param name="smoothness"></param>
/// <returns></returns>
private Vector3[] SmoothCurve(Vector3[] linePoints , float smoothness)
{
	if (smoothness < 1.0f) smoothness = 1.0f;

	var pointsLength = linePoints.Length;
	var curvedLength = (pointsLength * Mathf.RoundToInt(smoothness)) - 1;
	var curvedPoints = new List<Vector3>(curvedLength);

	float t = 0.0f;
	List<Vector3> points;
	for (int i = 0; i < curvedLength + 1; i++)
	{
		t = Mathf.InverseLerp(0, curvedLength, i);
		points = new List<Vector3>(linePoints);
		for (int j = pointsLength - 1; j > 0; j--)
		{
			for (int k = 0; k < j; k++)
			{
				points[k] = (1 - t) * points[k] + t * points[k + 1];
			}
		}
		curvedPoints.Add(points[0]);
	}

	return (curvedPoints.ToArray());
}

青い線が補完後の線になります。
ちょっと滑らかさが強いかもしれませんね…。

f:id:ghoul_life:20170731182144p:plain

デコレーションする

一旦パスを作った後、ゲーム的な要素を追加します。
敵を置いたり、ループ作ったり。

基本的に完成したパスを再度舐めつつ、
ランダムに処理をしていけばOK

var EFFECT_IDX = 20; // 20個目までの点には効果を出さないように~とか。
for (var i = EFFECT_IDX ; i < path.LinePoints.Length ; i++)
{
    var pos = path.LinePoints[i];
    // 効果がランダムで発動
    switch (Random.Range(1, 5))
    {
    	// 障害物を出す、とか
    	//case XXX:
	    //     ~~~
	    //     break;
	}
}

描画する

無くてもいいのですが、
見せてもいいのです。

LineRendererにまとめて入れるだけ

_lineRenderer.positionCount = _linePoints.Length;
_lineRenderer.SetPositions(_linePoints);

あれ、SetVertexCount だったような…とちょっと気になるヤーツ。

https://docs.unity3d.com/ja/540/ScriptReference/LineRenderer.SetVertexCount.html

変わったようです。

処理速度について

もうここまででもいいんじゃね?って話ですが
ゲームにした場合、気になるのは処理速度です。
どうしても計算はちょっと長いのでストレートに処理していたらプチフリーズしてしまうでしょう。
そして、パスの終点まで到達した時に処理をしていたのでは、遅れてしまうでしょう。

  • 並列に処理する
  • 予め次を計算しておく

この2つを満たせばOKです。

並列に処理するのは何でもいいです。
Coroutineでも、Taskでも、Threadでも…。
慣れ親しんだCoroutine辺りでとりあえずいいのではないでしょうか。

次を用意しておく処理はこんな感じで

// indexが半分を過ぎたら。ここはもっと早くてもいいかも。ゲームスピードとの兼ね合い
var len = currentPath.LinePoints.Length;
if (index > 0.5f * len && isCreateNext)
{
    var lastPoint = currentPath.LastPoint; // 最後のposition
    var lastAngle = currentPath.LastAngle; // 最後の角度
    // 続きのサブを作成しておく
    StartCoroutine(CreateRailPath(lastPoint, lastAngle , subPath));

    isCreateNext = false; // 何度も作らせないために
}

// 最後まで来たらswapする
if (index > len - 1)
{
    // 予め作っておいたサブとカレントを切り替える
    var tmpPath = currentPath;
    currentPath = subPath;
    subPath = tmpPath;
    isCreateNext = true; // 次を作らせるフラグをON
    index = 0;
}

currentとsubを用意しておいて、
終わり際にswapする!というだけ。
後は進捗をチェックしつつ、次を用意させるだけです。

f:id:ghoul_life:20170731183130p:plain

進んでいくと

f:id:ghoul_life:20170731183153p:plain

交互に次が生成されるのがわかると思います。
この辺りだけgithubにまとめておきました。
よく分からない所はこちらを参考に。

github.com

今回の感想

というわけで第四回も終わりました。
今回も大変だった…。手が遅くて後手後手に…。
イデアは相変わらずヒドイもんで、
センスが腐ってんなと思います。

次こそはもうちょっと良いものを作りたい。