ぐーるらいふ

底辺。

【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

今回の感想

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

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

【Unity】攻撃判定タイミングをグラフィカルに作りたい 【Editor】

いきなりですが

GitHubにコード一式あげてます。ちょっと長いので、面倒な方はソースを見ると早いと思います。

github.com

格闘ゲームを作ろうとしてます。
格闘ゲームに限らずアクション要素のあるゲームには攻撃判定というものが大抵あります。

http://www.capcom.co.jp/blog/sf4/img_upload/t11151JRI_02.jpg

この攻撃判定ですが、「常に出続けてOK」なゲームもあれば、
「一時的にだけ表示してほしい」というゲームもあります。
アクションゲームの攻撃で「剣を振る」なども一時的に攻撃判定が出てほしいアクションだと思います。

攻撃判定の制御

パンチ Animation

start ---(攻撃判定発生中) ----->> end

キック Animation

start -------------(攻撃判定発生中) ----->> end

格闘ゲームで考えるとパンチならパンチ、キックならキックのアニメーションがあり、
攻撃判定が発生するタイミングとその長さをそれぞれ設定しなければなりません。

  • アニメーションの数が少ない
  • 一人で開発している、

などといった場合は
一つ一つ再生をスクリプトで拾って

10frame後に30frame間だけ判定を出す

などといったコードを埋め込んでいく事でも実現は可能です。
が、多人数で作っていたり、大規模になったりするとその限りではありません。

グラフィカルに当たり判定を制御したい!

Unity使ってるぞ!Editorだって作れちゃうんだ。
せっかくだからEditorで攻撃判定を制御するツールを作っちゃおうじゃないか。

事前準備

今回もまた以下のAssetsを使用して説明します。


f:id:ghoul_life:20170711171702p:plain
https://www.assetstore.unity3d.com/jp/#!/content/33478

f:id:ghoul_life:20170711171719p:plain
https://www.assetstore.unity3d.com/jp/#!/content/33083

以上のお二方は前回から引き続き。
また、Animationの共通化については以下の記事を参考にしてください。

ghoul-life.hatenablog.com

キャラクターに攻撃判定コライダーをつけよう

f:id:ghoul_life:20170720192725p:plain

f:id:ghoul_life:20170720192810p:plain

こんな感じで手足にCapsuleColliderと以下のスクリプトを付与します。
このCapsuleColliderが攻撃判定の大きさになります。

プログラマー作業ではなく、
ゲームデザイナーに位置や大きさなどの調整を任せる
といった分業も出来るでしょう。

また、タックルやヘッドバットといった別の箇所にも攻撃判定を付ける
といった事も可能です。

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

[RequireComponent(typeof(CapsuleCollider))]
public class HitCollider : MonoBehaviour {

    public HitType hitType;
    private CapsuleCollider collider;

    public void Disable()
    {
        if (collider == null)
        {
            collider = GetComponent<CapsuleCollider>();
        }
        collider.enabled = false;
    }

    public void Enable()
    {
        if (collider == null)
        {
            collider = GetComponent<CapsuleCollider>();
        }
        collider.enabled = true;
    }

    // colliderをSceneビューで表示してみる…。多少強引な計算があるので、
    // colliderの正確な位置とサイズを取る方法があれば…。
    void OnDrawGizmos()
    {
        if (collider && collider.enabled)
        {

            Vector3 offset = 
                transform.right * (collider.center.x / collider.height / 2) + 
                transform.up * (collider.center.y / collider.height / 2) + 
                transform.forward * (collider.center.z / collider.height / 2);

            Vector3 size = new Vector3(
                (collider.radius / 0.5f) * collider.transform.lossyScale.x,
                (collider.height / 2) * collider.transform.lossyScale.y, 
                (collider.radius / 0.5f) * collider.transform.lossyScale.z
                );

            Quaternion dir;
            switch (collider.direction)
            {
                case 0:
                    dir = Quaternion.Euler(Vector3.forward * 90);
                    break;
                case 1:
                    dir = Quaternion.Euler(Vector3.up * 90);
                    break;
                case 2:
                    dir = Quaternion.Euler(Vector3.right * 90);
                    break;
                default:
                    dir = Quaternion.Euler(Vector3.zero);
                    break;
            }
            Gizmos.color = new Color(1, 0, 0, 0.5f);
            Gizmos.DrawMesh(GetPrimitiveMesh(PrimitiveType.Capsule), collider.transform.position + offset, transform.rotation * dir, size);

        }
    }

    private Mesh GetPrimitiveMesh(PrimitiveType type)
    {

        GameObject gameObject = GameObject.CreatePrimitive(type);
        Mesh mesh = gameObject.GetComponent<MeshFilter>().sharedMesh;
        DestroyImmediate(gameObject);

        return mesh;

    }

}

スクリプトはコライダーのON/OFFを制御するのに使います。
HitTypeは後述。

攻撃判定種別を付けよう

今回は格闘ゲームを題材にしているため、
攻撃判定は複数あることにします。

  • 左手
  • 右手
  • 左足
  • 右足

の4つとします。
ゲームによっては、ひじやひざ、または頭突きや肩なんてものもあるかもしれません。
が、今回はシンプルにこの4つにします。
これを定義しているのが、HitTypeです。

public enum HitType
{
    LEFT_HAND = 1,
    RIGHT_HAND = 2,
    LEFT_LEG = 4,
    RIGHT_LEG = 8
}

こんなただのenumです。
値はbit演算のためにこのようにしてます。
その1で対応したHitColliderに左手なら左手、右足なら右足といったHitTypeをセットしておきます。

まずはAnimationClipを再生するEditorを作る

Editor沼に足を踏み入れたような気がします。
AnimationClipなんて、Inspectorの再生窓使えば見れるやんけ!
という声が聞こえてきそうですが、それをあえて自分で作ってみる。

(EditorWindowの作り方の説明などはいくらでも出てくるので割愛)

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

// 単純なサンプルエディタ
public class AnimClipSampleEditor : EditorWindow
{

    // アニメーションクリップを再生させるオブジェクト
    private Animator animObject;
    // 再生したいアニメーションクリップ
    private AnimationClip animClip;

    [MenuItem("Window/AnimClipSampleEditor")]
    static void Open()
    {
        GetWindow<AnimClipSampleEditor>();
    }

    private float value;

    void OnGUI()
    {
        GUILayout.BeginHorizontal();
        GUILayout.Label("character : ", GUILayout.Width(110));
        animObject = (Animator)EditorGUILayout.ObjectField(animObject, typeof(UnityEngine.Animator), true);
        GUILayout.EndHorizontal();
        EditorGUILayout.Space();

        GUILayout.BeginHorizontal();
        GUILayout.Label("animation clip : ", GUILayout.Width(110));
        animClip = (AnimationClip)EditorGUILayout.ObjectField(animClip, typeof(UnityEngine.AnimationClip), true);
        GUILayout.EndHorizontal();
        EditorGUILayout.Space();

        if (animObject == null || animClip == null)
        {
            GUILayout.Label("Please Setting character and animation clip");
            EditorGUILayout.Space();
            EditorGUI.BeginDisabledGroup(true);

        }

        GUILayout.BeginHorizontal();
        value = EditorGUILayout.Slider(new GUIContent("TimeLine"), value, 0, 1, GUILayout.Width(300));
        GUILayout.EndHorizontal();

        if (animObject == null || animClip == null)
        {
            EditorGUI.EndDisabledGroup();
        }

        //--------------------------------------------------------------------

        if (animObject != null && animClip != null)
        {
            animClip.SampleAnimation(animObject.gameObject, value);
        }

        Repaint();
    }

}

そして適当にキャラクターをシーンに配置して、
AnimationClipをセットすれば、
スライダーでアニメーションを動かすことが出来ます。

f:id:ghoul_life:20170720193802p:plain

攻撃判定を付与するには

色々なアプローチがあります。

  • 自力Scriptアプローチ、
  • AnimatorからStateBehaviorでUpdateのframeを見る

などなんでも良いのですが、
今回はAnimationClipからEventを呼び出すアプローチで行きたいと思います。

AnimationClipにはEventを付与することが出来ます。

f:id:ghoul_life:20170720195028p:plain

これによって、AnimationClipの最後にEventを付与して、アニメーションの終了判定を行ったり
していた過去もあるかもしれません。

攻撃判定を開始するタイミングでEventを発行して、終了タイミングで再度Eventを発行すれば良さそうです。

HitTypeの判定について

攻撃判定を開始した時

  • 「左手の攻撃判定開始」 -> 「判定終了」
  • 「右足の攻撃判定開始」 -> 「判定終了」
  • 「左手と右手の攻撃判定開始」 -> 「判定終了」

といった命令が送られてくると考えます。

こうしました。

イベントを送る側(Animation Clip)

    private int CreateHitEvent()
    {
        int result = 0;

        if (toggleLeftHand) result = (result | (int)HitType.LEFT_HAND);
        if (toggleRightHand) result = (result | (int)HitType.RIGHT_HAND);
        if (toggleLeftLeg) result = (result | (int)HitType.LEFT_LEG);
        if (toggleRightLeg) result = (result | (int)HitType.RIGHT_LEG);

        return result;
    }
    
    void OnGUI(){
        //~~
        if (GUILayout.Button("Add Hit Event")){
            var serialied = new SerializedObject(animClip);
            serialied.Update();

            var events = new List<AnimationEvent>();

            onHitEventStart = new AnimationEvent();
            onHitEventStart.time = startTime;
            onHitEventStart.functionName = "OnHitEventStart";
            onHitEventStart.intParameter = CreateHitEvent();
            events.Add(onHitEventStart);

            onHitEventEnd = new AnimationEvent();
            onHitEventEnd.time = endTime;
            onHitEventEnd.functionName = "OnHitEventEnd";
            events.Add(onHitEventEnd);

            // Animation ClipにEventを付与します
            AnimationUtility.SetAnimationEvents((AnimationClip)serialied.targetObject , events.ToArray());
            EditorUtility.SetDirty(serialied.targetObject);

            serialied.ApplyModifiedProperties();
        }
        //~~
    }

イベントを受ける側(Character)

    // hitEvtValue = 0x1111 , 0x1001 といった値が来る
    public void OnHitEventStart(int hitEvtValue)
    {
        // 該当するcolliderをON
        foreach (var hit in hitColliders)
        {
            if (((int)hit.hitType & hitEvtValue) != 0)
            {
                hit.Enable(); // HitColliderをON
            }
        }

    }

ビット演算を使い、そのフラグが立っていたら判定をONというシンプルな作りです。
他にも渡したいものがあればObjectに情報をまとめて渡すなんてのもアリなんですが、
とりあえず思いつかなかったので最小限にしてます。

あとはこれをグラフィカルにEditor化すればOK

必要なパーツは大体揃ったので、後はウィンドウにまとめて値をEditorから操作します。

f:id:ghoul_life:20170721103214p:plain

どうしてもGUI系はコードだらっと長いので、githubのコードを参考にしてください。

github.com

MinMaxSliderの範囲が攻撃判定の範囲ということにしました。
ハンドルが持ちにくく、もう少し見た目が変えられるといいんですが、
GUIStyleとか頑張って作ればいけるのかな。

攻撃判定発生タイミングはAnimationClip側に紐付いているため、
キャラクターを変えても、AnimationClipに攻撃判定発生タイミングは変わらず、
共通で扱うことが出来ます。

f:id:ghoul_life:20170721124224g:plain

そして、攻撃判定の大きさや位置はキャラクター側に紐付いているため、
キャラクター個別に設定することが出来ます。


f:id:ghoul_life:20170721123414g:plain

こうしてEditor沼へと落ちていくのだった

これで作業の分離が出来ました。(?)
出来たのか?

ゲームデザイナーやキャラクターデザイナー
攻撃判定位置や発生フレームを丸投げして、
自分はロジックを組む…という分担が出来ます。

「困難は群れで分け合え」

と、かばんさんも言っていました。

自分は完全に個人なので、自分で作って自分で使う!
という無駄な作業かもしれないのですが、勉強だと思って…。

注意点として

fbxの内側にあるAnimationClipをそのまま使おうとすると、
Eventを上手く付与出来ないかもしれません。(保存したのに反映されないなど)
その場合はfbxからAnimationClipを取り出すと、Editが自由になるため
Eventの付与も出来るはずです。
こちらも参考に!神。

baba-s.hatenablog.com