ぐーるらいふ

底辺。

【Unity1week】一週間ゲームジャム「ぎりぎり」に参加 前編?【Unity】

前置き

Unity unity1week「ぎりぎり」に参加しました。
今回作ったのはこちら。脱出ゲーム。

f:id:ghoul_life:20180612201305p:plain
https://unityroom.com/games/giriescape


なんでこんなめんどくさいのを選んだんだ…と。

パッと見でわかる。

  • アイテムマスタ
  • メッセージマスタ

が必要だということが。


最初にお題の「ぎりぎり」と聞いて、最初に思いついたのがコレで、
一度挑戦してみたかったなーと思ってたので、「まぁいいか、やってみよう」と挑戦することにしました。

設計について

基本的には

  • キャラが移動
  • オブジェクトをターゲット
  • アクションボタン押下でメッセージが出る
  • ギミックがあればギミックを処理
  • 結果メッセージYES
  • 結果メッセージNO

というような流れになるだろうと想定。

この時メッセージは所持しているアイテムによって違う、
またはアイテムを使用するとメッセージが変わる

といった具合だ。

すでにめんどくさい雰囲気が漂っている。

ざっとフローをまとめるとこんな感じだ。

f:id:ghoul_life:20180612173846p:plain
f:id:ghoul_life:20180612173856p:plain


力尽きたので、プログラム編は後日

【Unity】 【有料Assets】 Simple Bones Animationを使って適当に取ってきたキャラにオリジナルアニメーションを作る

お疲れ様です。ぐーるです。
お久しぶりになってしまった。
サラリーマンや学生も期の変わりは忙しいんですよね。

ProBuilderはどうした

やってるんですが、まだ記事的にまとまってなくて…
もうすぐUnityの勉強会もあるんで、そちらの情報と合わせて記事にしたいな

ProBuilderで学ぶレベルデザイン

Simple Bones Animation

Unity Asset Storeでいい感じのキャラを見つけた!
早速使ってゲーム作るぞ!
走りはこれで、攻撃はこれで…
あれ、ゲームクリア時になんかポーズしたいのにいい感じなの無いじゃん!

なんてことがあった時、3Dモデルが作れない自分を呪い諦めていたんですが、
どうやらこんな便利なツールがあると汗人柱さんが記事にしてたのを見つけました。

www.asset-sale.net

(ちょっと古い記事ですが。ほんと神)

おお、これイイじゃん、早速使ってみよう!と思いたち、買いました。
(有料です。4.95ドル…!)

assetstore.unity.com

事前準備

キャラクターはこいつを使います。

assetstore.unity.com


可愛い宇宙服シューターです。ライトなSFシューティングのプレイヤーにぴったり。
だが

  • Idle
  • Run
  • Run Back
  • Left
  • Right

しかアニメーションが無く、実際に撃つようなモーションはありませんでした。
(2018/04/13。updateで追加されたりするかもしれません。)
これに撃つアニメーションを作りたいと思います。

新規プロジェクトを立ち上げ、Simple Bones AnimationとSpace ManをImportしておきます。

1.space manのRigをHumanoidからLegacyに変える

これを行わないとSimple Bones Animationが上手く動かない

f:id:ghoul_life:20180413215704p:plain
f:id:ghoul_life:20180413215729p:plain

2.space_map_modelのPrefabを配置する

配置したらAnimatorを削除し、Animationに付随しているAnimation Clipを一旦全て外す。
これで新たなAnimation Clipを作ることができるようになります。
space manのRootを選択して、Add ScriptからSimple Bonesスクリプトを追加する
Root Nodeが空の状態になるので、Space Manの Pelvisをセットする。

f:id:ghoul_life:20180413215745p:plain

ここまでやるともうボーンがSceneビューに見えるはず。

3.Shot.animを作成

Animationが空っぽの状態でAnimationビューを開くと 新たなAnimation ClipがCreate出来る。
Createを押下して、Shot.animとしてanimファイルを作成

すると、Simple BoneのAnimationにShotが選択されている状態になる。
これで準備完了です。

f:id:ghoul_life:20180413215803p:plain

4.アニメーションを作る

SimpleBonesのAnimationから「Create Animation Keys」にあるPosition , Rotation , Scaleのいずれかを押下すると
ボーン周りのTransformがAnimationビューに紐付けることが出来る。
が、今回は使いません!

初期ポーズはIdleからコピーして持ってきます。

f:id:ghoul_life:20180413215818p:plain

そして動かしたいパーツを選択しつつ…動かして…Add Key!!
間違えたら…一度消して、直して再度Add Key!!

f:id:ghoul_life:20180414014342p:plain

5.Shotアニメーションが作れました

こんな感じでShotアニメーションが出来ました。

f:id:ghoul_life:20180414014419g:plain

簡単でしょう?と言いたいですが、自然なボーンアニメーション作るのは難しいですね…。
でもツールはほんと凄い。有料は優良なんですね!

【Unity】Rigidbodyの弾道予測線をシュミレートする

ghoul-life.hatenablog.com

前回の続きで、今回は撃つ側。

RigidbodyにAddForceを加えた場合の移動位置をシュミレートして表示する

なんていうのはみんなやっているようで、ググると大量に出てきます。

f:id:ghoul_life:20180322150647p:plain

そのまま使わせて頂くのもOKですが、自分なりにまとめたソースもメモ。

Rigidbodyの移動を予測計算

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

/**
 * Rigidbodyにvecを加えた時の弾道をシュミレート。
 */
public class BallSimulator : MonoBehaviour {

    [SerializeField]
    private GameObject _ballSimPrefab; // 何でもOK。予測位置を表示するオブジェクト

    private const int SIMULATE_COUNT = 20; // いくつ先までシュミレートするか

    private Vector3 _startPosition; // 発射開始位置
    private List<GameObject> _simuratePointList; // シュミレートするゲームオブジェクトリスト

    void Start () {
        Init();
    }
	
    void Update () {
        // デバッグ用に線を出してみる。必要無いなら無くても問題なし。
        if (_simuratePointList != null && _simuratePointList.Count > 0)
        {
            for (int i = 0; i < SIMULATE_COUNT; i++)
            {
                if (i == 0)
                {
                    Debug.DrawLine(_startPosition, _simuratePointList[i].transform.position);
                }else
                if (i < SIMULATE_COUNT)
                {
                    Debug.DrawLine(_simuratePointList[i - 1].transform.position, _simuratePointList[i].transform.position);
                }
            }
        }

    }

    // 初期化
    private void Init()
    {
        if (_simuratePointList != null && _simuratePointList.Count > 0)
        {
            foreach (var go in _simuratePointList)
            {
                Destroy(go.gameObject);
            }
        }

        // 位置を表示するオブジェクトを予め作っておく
        if (_ballSimPrefab != null)
        {
            _simuratePointList = new List<GameObject>();
            for (int i = 0; i < SIMULATE_COUNT; i++)
            {
                var go = Instantiate(_ballSimPrefab);
                go.transform.SetParent(this.transform);
                go.transform.position = Vector3.zero;
                _simuratePointList.Add(go);
            }
        }
    }

    /**
     * 弾道を予測計算する。オブジェクトを再生成せず、位置だけ動かす。
     * targetにはRigidbodyが必須です
     **/
    public void Simulate(GameObject target , Vector3 _vec)
    {
        if (_simuratePointList != null && _simuratePointList.Count > 0)
        {
            // 発射位置を保存する
            _startPosition = target.transform.position;
            var r = target.GetComponent<Rigidbody>();
            if (r != null)
            {
                // ベクトルはmassで割る
                Vector3 force = (_vec / r.mass);

                //弾道予測の位置に点を移動
                for (int i = 0; i < SIMULATE_COUNT; i++)
                {
                    var t = (i * 0.5f); // 0.5秒ごとの位置を予測。
                    var x = t * force.x;
                    var y = (force.y * t) - 0.5f * (-Physics.gravity.y) * Mathf.Pow(t, 2.0f);
                    var z = t * force.z;
                    
                    _simuratePointList[i].transform.position = _startPosition + new Vector3(x, y, z);
                }
            }
        }
    }
}

発射する側は

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.EventSystems;

/**
 * 弾を発射する
 */
public class BallShooter : MonoBehaviour {

    [SerializeField]
    private BallSimulator _ballSimurator; // 弾道予測線

    [SerializeField]
    private Slider _powerSlider; // 力を変えるスライダー

    [SerializeField]
    private GameObject _shooterBase; // 土台

    private Vector3 _firstPosition;
    private Rigidbody _rigidbody;
    private Vector3 _touchDownPos;

    private bool _isShot;
	// Use this for initialization
    void Start () {
        Init();
    }

    
    // Update is called once per frame
    void Update () {

        if (!EventSystem.current.IsPointerOverGameObject())
        {
            if (Input.GetMouseButtonDown(0))
            {
                _touchDownPos = Input.mousePosition;
            }
            else if (Input.GetMouseButton(0))
            {
                var tempPos = Input.mousePosition;
                Vector3 value = Vector3.zero;
                value.x = (_touchDownPos.x - tempPos.x);
                value.y = (_touchDownPos.y - tempPos.y);
                value.z = 0;
                _touchDownPos = tempPos;

                var qot1 = Quaternion.AngleAxis(value.x, new Vector3(0, 1, 0));
                var qot2 = Quaternion.AngleAxis(value.y, new Vector3(1, 0, 0));
                _shooterBase.transform.rotation *= qot1;
                this.transform.rotation *= qot2;
            }
        }


        Debug.DrawLine(this.transform.position, this.transform.position + this.transform.forward , Color.blue);
        
        // 発射中はシュミレートを止める
        if (!_isShot)
        {
            var vec = this.transform.forward * _powerSlider.value;
            _ballSimurator.Simulate(this.gameObject, vec);
        }
    }

    public void Init()
    {
        _firstPosition = this.transform.position;
        _rigidbody = this.GetComponent<Rigidbody>();
        _rigidbody.isKinematic = true;
        _powerSlider.value = 10.0f;
        _isShot = false;
        
    }

    //--------------------------------------------------------------------------
    // 発射(Buttonから呼ぶとか)
    public void Shoot()
    {
        _isShot = true;
        var vec = this.transform.forward * _powerSlider.value;
        _ballSimurator.Simulate(this.gameObject , vec);
        _rigidbody.isKinematic = false;
        _rigidbody.AddForce(vec, ForceMode.Impulse);
    }
}

こんな感じでOK。

動かす

f:id:ghoul_life:20180322151038g:plain

  • 発射方向を変えるとそれに合わせて予測線も変わるか
  • 発射する力を変えるとそれに合わせて予測線も変わるか
  • massを変えると予測線も変わるか
  • 予測線に沿って飛ぶか

f:id:ghoul_life:20180322151137g:plain

この辺りに注目。

この次は

砲台を突然作り出したのはProbuIlderの勉強のため

assetstore.unity.com

今回でなんちゃって簡易砲台(作ろうとしているゲームでは砲台ではないんだけど)が出来上がったので、
次回からProBuilderを使用したステージ制作に入っていければいいな

【Unity】スライド操作で回転する砲台を作りたかったお話

何も考えずに砲台を作る

まずは図を観てほしい。

f:id:ghoul_life:20180320200335p:plain

左右ドラッグで横回転、上下ドラッグで縦回転というシンプルなものだ。
で、発射ボタンか何かを押下するとforward方向へ発射されるという仕様と思って欲しい。
シンプルでいいんで、オブジェクトは弾だけで、自分で飛んでいくって感じ。

で、直感で作ったソースはこれ。テキトー。

Vector3 _touchDownPos;
void Update(){
  if (Input.GetMouseButtonDown(0))
  {
      _touchDownPos = Input.mousePosition;
  }
  else if (Input.GetMouseButton(0))
  {
      var tempPos = Input.mousePosition;
      Vector3 value = Vector3.zero;
      value.x = (_touchDownPos.x - tempPos.x);
      value.y = (_touchDownPos.y - tempPos.y);
      value.z = 0;
      _touchDownPos = tempPos;

      var qot1 = Quaternion.AngleAxis(value.x , new Vector3(0,1,0)); // 横回転
      var qot2 = Quaternion.AngleAxis(value.y , new Vector3(1,0,0)); // 縦回転

      this.transform.rotation *= qot1 * qot2;// Quartanion同士は掛け算で合体させる
  }

  // forwardチェック用のデバッグライン
  Debug.DrawLine(this.transform.position, this.transform.position + (this.transform.forward * 5));
}

動かしてみると

で、実際に動かすと、

  • 左右スライド -> いい感じに回る

f:id:ghoul_life:20180320200525g:plain

  • 上下スライド -> いい感じに上下に向いてくれる

f:id:ghoul_life:20180320200547g:plain

「完成だね!」
とは行かず、上下を角度を付けた状態で
左右に回すと斜めに回ってしまう。(当たり前だけど)

f:id:ghoul_life:20180320200613g:plain

やはり自然に回ってほしい

数学的には上の動きで全く問題ないのだけど、
こう回るのが直感的でゲーム的な理想だと思う。

f:id:ghoul_life:20180320200731g:plain

解決法(暫定)

とりあえず土台でやってしまった。

f:id:ghoul_life:20180320202037p:plain

  • 左右スライド -> 土台を回す
  • 上下スライド -> 弾の角度を調整
  var qot1 = Quaternion.AngleAxis(value.x , new Vector3(0,1,0));
  var qot2 = Quaternion.AngleAxis(value.y , new Vector3(1,0,0));
  _shooterBase.transform.rotation *= qot1; // 土台を回す
  this.transform.rotation *= qot2; // 砲塔を回す

というオブジェクトの分離で理想の動きになりました。

打ち終わったらまた弾に元に戻って欲しかったり、
発射位置がわからないと困ることも多いし…と土台があるとそれはそれで便利。

そもそも砲台だって土台と砲塔とって別れとるやんけ!
そう作ればええんや!

ってだけなんですが、計算で上手く回転軸を取ったり出来る…?

【Unity】【uGUI】 滑らかにスクロールするチャットウィンドウのようなものを作りたかった話

お疲れ様です。ぐーるです。
いきなりですけど、表題のようなものが作りたくて四苦八苦してました。
結局自作に終わったんですが、ちょっと経緯をメモっておこうかと。

ScrollRectで余裕っしょ

UnityにはScrollRectがあって、それはnormalizedPositionっていう値を持っていて、
その値でスクロール位置を制御できます。

docs.unity3d.com

f:id:ghoul_life:20180316174936p:plain

こちらを利用しようとまず考えました。

追加する行について

今回作ろうとしていたのはチャットウィンドウなんで、文字の長さや改行によって
追加される行の高さはまちまちな訳です。
これはLayoutElementと自分計算で行けるなということで

  _messageText.text = strMessages[Random.Range(0, strMessages.Length)]; // 適当なメッセージ
  _messageText.rectTransform.sizeDelta = new Vector2(_messageText.rectTransform.sizeDelta.x, _messageText.preferredHeight);
  _backGroundImage.GetComponent<RectTransform>().sizeDelta = new Vector2(_backGroundImage.GetComponent<RectTransform>().sizeDelta.x, _messageText.preferredHeight + 40);

こんな感じで、テキストの高さから計算して設定すればとりあえずいいでしょう。

スクロールイメージ

最初に起こしたイメージは

f:id:ghoul_life:20180316175418p:plain

というイメージでAddする度に0.0fへ
毎回スクロールさせてあげればいいじゃん。シンプル。

実際これは問題無く実装出来ました。

void Update () {
        if (isScroll)
        {
            var val = _scrollRect.verticalNormalizedPosition;
            if(val > 0){
                val -= scrollSpeed * Time.deltaTime;
                _scrollRect.verticalNormalizedPosition = val;
            }
            else
            {
                _scrollRect.verticalNormalizedPosition = 0;
                isScroll = false;
            }
        }
}

void AddRow()
{
            var go = Instantiate(_rowPrefab);
            go.transform.SetParent(_tableContent.transform);
            isScroll = true;
}

工夫すると

チャットシステムとはいえ、単純に作るとInstantiateの嵐になってしまいます。
なので、出来るだけコンポーネントを使い回したいなと考えました。

f:id:ghoul_life:20180316174956p:plain

このようになると単純なスクロールでは上手くいかず、
少し計算が必要になりました。

float contentHeight = _tableContent.GetComponent<RectTransform>().sizeDelta.y; // 全体の長さ
float beforeTextHeight = row.GetComponent<RectTransform>().sizeDelta.y; // 前の行の高さ
float afterTextHeight = row.Create(); // Createの中では、ランダムにメッセージを生成している。行の高さを返却

float hDelta = contentHeight - beforeTextHeight + afterTextHeight; // 前回のを消し、新しい高さを計算
float normal = afterTextHeight / hDelta; // 最終的なスクロール位置

Debug.Log("contentHeight == " + contentHeight + " | before " + beforeTextHeight + " | after " + afterTextHeight + " | normal " + normal + " | delta " + (hDelta));
row.transform.SetAsLastSibling(); // 最下部にスライド

こんな感じで計算すると追加した行の手前に丁度フォーカスが動きます。
後はそのまま0.0fへ向かってスクロールすればOKです。

f:id:ghoul_life:20180316174856g:plain

スクロールはいい感じです。
が、全体としてはどうでしょうか。
ScrollRectの再計算なのかガタっと表示が動く事があるため、微妙に荒い感じに…。
(gifで見ると悪くなく見えますが、実際見るとちょっとガタつきがあることがあります)

結局自作?

使い回しをせず、素直にInstantiateすれば問題ないです。

自動スクロールさせない、なんていうのも良いし、
アニメーションさせず、0.0fへ飛ぶような感じでも誤魔化せそう。

ちょっとヘンな事をしようとすると上手く行かなかった…って話でした。
実装でカバー出来そうだけど、自作のほうが制御が楽?

【Unity】C#初心者だけどUniRXを使えるようになりたい

github.com

uniRXいいですよね。
バリバリなUnity Developerって感じで。

とはいえ普段C#使わないレベルの初心者だと入りづらいのも事実で、
「そもそも使うメリットとかあるの?」
「余計わかりにくくなるだけじゃないの?」
なんて思ってたりもしました。

ちょっとググってみて、
「なんかチェーンでいい感じにスッキリ書けるみたい。」
「イベント駆動で処理を飛ばしたりと自由自在らしい。」
「MessageBrockerとかBufferとか便利みたいだけどどこで使えばいいのかよくわからん。」
なんて事も分かりました。

ちょっとお硬めなメリットとかはググった先の先人達の記事を参考にして頂くとして、
初心者の自分がどうやって使い始めたかだけメモとして書いてみようと思いました。

UpdateをUniRXで置き換えよう

void Update(){
}

unityでMonoBehaviorやってたら知ってますよね。
毎フレーム処理したい時に使うアレです。

これに処理を入れると

void Update(){
     if(_isFire){
          // 火を発生させる処理
          _isFire = false; // 終わったらフラグを下ろす
     }
     // その他処理
}

こんなん書くことがあると思います。

これを言葉で説明してみると
「ファイアフラグが立っている時だけUpdateで火の処理をしたい」
という感じになります。

おお。そうか。これをUniRXに置き換えてみると

using UniRX;
using UniRX.Triggers;

void Start(){
     this.UpdateAsObserbable().Where(_ => _isFire).Subscribe(_ => {
          // 火を発生させる処理
          _isFire = false; // 終わったらフラグを下ろす
     });
}

void Update(){
// もういらない
//     if(_isFire){
//         // 火を発生させる処理
//          _isFire = false; // 終わったらフラグを下ろす
//     }
     // その他処理
}

このように置き換えることが出来る。

とにかく void Update()は大きくなりやすい。
switchでstate切り分けたり、パラメータチェックしたり。
そのため、細かくメソッドに分けたり、処理を委譲させたりして
管理しやすくする涙ぐましい努力をしているのだけど、
その辺りを助けてくれるってわけだ。

開始アニメーション処理している時は…

     this.UpdateAsObserbable().Where(_ => _isStartAnimation).Subscribe(_ => {
          // アニメーションさせるとか
          _isStartAnimation = false; // 終わったらフラグを下ろす
     });

敵が残っている時は…

     this.UpdateAsObserbable().Where(_ => _enemies.Count > 0).Subscribe(_ => {
          // 敵が残っている時
     });

HPが0になった時は…

     this.UpdateAsObserbable().Where(_ => _player._hp < 1).Subscribe(_ => {
          // HPが0以下になったら
     });

とupdate処理をブロックごとに分けて書く事が出来る。

もちろん好き好きでは良いはずだ。
毎フレーム処理をしたいならvoid Update()に処理を書くのが最もシンプルで分かりやすい。
UniRXを使うとこう書けますよというだけだ。

ボタンイベントをUniRX化する

uGUI使ってたらButtonを使うこともあるはず。
普段なら

public void OnClickButton(){
     // ボタン押されたら
}

こんなん作って、Inspectorで紐付けるはずだ。
これもUniRXで置き換えて紐付けをやめてみよう。

[SerializableField]
private Button _joinButton:

_joinButton.OnClickAsObservable().Subscribe(_ =>{
     // ボタン押されたら
});

うーんスッキリ。だけどボタンだけは紐付けないとダメか。(GetComponent?)
Zenjectのidまで連携させればボタンの紐付けすらいらなくなるケド。

[Inject(id="join_button")]
private Button _joinButton:

これはちと微妙かも?
(Buttonみたいな汎用なクラスをInjectするのはやめておいたほうが良さそう)

勉強中

まだ使い始めたばかりなので、これから少しずつ使って慣れていきたい所。
次はイベントの送出を勉強したいな。
(GameManagerみたいな親がいて、EnemyControllerのような子クラスのイベントをフックする、的なやつ)

以下コードイメージ。イメージだよ!合ってませんよ!

// 敵が倒されたらその敵に紐付いているスコアを加算して表示する
_enemyController.EnemyDestroy(_ => enemy).Subscribe(e => {
          score += e.score;
          scoreText.text = "score : " + score;
});

こういうイメージ…今まではdelegateとか使ってたけどUniRXならこんな感じで書けるのでは…!?
ととりあえずここまで。出来たらまたメモしよう。

【Unity】第7回 unity1week「当てる」参加作品「SATELLITE ONE」の開発

お疲れ様です。ぐーるです。

体調最悪のコンディションでしたが、なんとか提出することが出来ました。

思い出と実装をメモっておきたいと思います。

お題について

「当てる」かー、今回は神題だなと。
前提を考えなければアイデアはいくらでも出せそうだ、と。
デザインをほとんど必要とせずサクッと出来る、シンプルなものがいいなぁとか漠然と考えていて
降ってきたアイデアがSATELLITE ONEです。

内部設計について

いつもそうだけど物理的なunity.sceneは一つだけ。

内部的な画面は

  • OpeningScene
  • GameScene
  • ResultScene

の3つ。

各画面間の切り替えはシンプルなステートマシンで管理。

public interface IState{
	void OnStateEnter();
	void OnStateExit();
}

public class StateMachine : MonoBehaviour {

	public List<IState> states = new List<IState>();
	private IState prevState;
	private IState currState;

	public void AddState(IState state)
	{
		foreach (var st in states)
		{
			if (st == state)
			{
				return;
			}
		}
		states.Add(state);
	}

	public void RemoveState(IState state)
	{
		states.Remove(state);
	}

	public void ChangeState<T>() where T : IState
	{
		if (currState is T)
		{
			return;
		}
		if (currState != null) { 

			prevState = currState;
			prevState.OnStateExit(); // 抜ける前にOnStateExit()を呼ぶ
		}
		foreach (var st in states)
		{
			if (st is T)
			{
				currState = st;
				currState.OnStateEnter(); // 切り替わると同時にOnStateEnter()を呼ぶ
			}
		}
	}
}
/**
 * // Zenjectで紐付け
 * [Inject]
 * private MainScene _mainScene;
 * [Inject]
 * private StateMachine _stateMachine;
 * // ~~
 * _stateMachine.AddScene(_mainScene); // 起動時にStateMachineに登録しておく
 * // ~~
 * _stateMachine.ChangeScene<MainScene>(); // _mainScene画面へ切り替え
 */
public class MainScene : MonoBehavior , IState{
	//~~~~~
}

こんな感じでZenjectと組み合わせ。
楽か?って言われるとこの規模だとそこまででも無いけど
Zenject化するのが癖になってしまった。

ターゲットサイトについて

これは円形のプレーンな板をBlenderで用意しておいて、
その上で円を描くShaderを動かしています。

f:id:ghoul_life:20180226153048p:plain
f:id:ghoul_life:20180226231345g:plain

Shader "Custom/Ring" {
	Properties{
		_TargetX("Target X", float) = 0
		_TargetY("Target Y", float) = 0
		_TargetZ("Target Z", float) = 0
		_Alpha("Alpha" , float) = 0
		_Color("Color" , Color) = (0,0,0,0)
		_BG("BG" , Color) = (0,0,0,0)
	}

	SubShader{
		Tags{ "Queue" = "Transparent" "RenderType" = "Transparent" } // alphaに対応するのに必要
		Blend SrcAlpha OneMinusSrcAlpha // alphaに対応するために必要
		LOD 200

		CGPROGRAM
#pragma surface surf Standard alpha:fade
#pragma target 3.0

		float _TargetX;
		float _TargetY;
		float _TargetZ;
		float _Alpha;
		float4 _Color;
		float4 _BG;
		struct Input {
			float3 worldPos;
		};

		void surf(Input IN, inout SurfaceOutputStandard o) {
			float dist = distance(fixed3(_TargetX, _TargetY, _TargetZ), IN.worldPos);
			float val = abs(sin(dist*3.0 - _Time * 100));
			float alpha = _Alpha;
			if (val > 0.98 && alpha > 0){
				o.Albedo = _Color;
				o.Alpha = alpha;
			}
			else {
				// ↓何でも良い
			
				//discard;
				o.Alpha = 0;
				//o.Albedo = _BG;
				//o.Alpha = _BG.a;
			}
		}
		ENDCG
	}
	FallBack "Diffuse"
}
	[SerializeField]
	private Material _ringMat;
	// Update is called once per frame
	void Update () {
		// 適当なinputのラッパーだと思って下さい。なんでもいいです。
		if(_inputManager.isTouchDownOnly(0)){
			RaycastHit hit;
			Ray ray = Camera.main.ScreenPointToRay(_inputManager.GetTouchDownPosition(0));
			if (Physics.Raycast(ray, out hit)){
				var p = hit.point;
				p.y += 0.1f;
				this.transform.position = p;
				// タッチしたポイントをShaderに渡して中心点を移動した位置にする
				_ringMat.SetFloat(Shader.PropertyToID("_TargetX") , p.x);
				_ringMat.SetFloat(Shader.PropertyToID("_TargetY"), p.y);
				_ringMat.SetFloat(Shader.PropertyToID("_TargetZ"), p.z);
			}
		}
	}

ステージ制作について

SATELLITE ONEのステージは以下の2つの要素から成る。

  • フロア

そして敵は以下の要素を持っている

  • 移動ルートポイント
  • ルートポイントごとの移動速度
  • 移動イージング

回転とかは賑やかしなので割愛。

ステージ作りがとにかく面倒だなと。
フロアはしょうがないとして、敵の移動ルートを量産出来れば、
ステージがどんどん作れそうだなと考えた。

ステージ制作画面

別でステージ製作専用のエディタを用意した。

f:id:ghoul_life:20180226231457p:plain

  1. 敵を選択して選択状態に
  2. 動かしたい所をクリック
  3. AddPointボタンを押下でルートポイントを追加

という流れでどんどんルートポイントを置いていくことが出来る。
Playで動きの確認、そしてSaveでcsv出力している。
loadももちろん完備。

ここまで必要なのか?と思ったが
結局ゲーム側でも必要な機能がほとんどだったのでついでで。

csvのロードについて

ステージを追加量産するために
Application.dataPathを利用してパスでファイルを読むようにしてたら
当たり前だけどWebGLに出力したら動かない。
(開発中は紐付けなくてもcsvファイルを新規追加読み込み可能なので便利だった)

FileInfo fi = new FileInfo(Application.dataPath + "/Resources/stage/data/" + stageId + ".csv");
var result = new List<string>();
StreamReader reader = new StreamReader(fi.OpenRead());
while (reader.Peek() > -1) {
	string line = reader.ReadLine();
	result.Add(line);
}
return result;

Editor上だけ使って実ゲームではTextAsset化して泥臭く紐付けて使うことに…。
とほほ。

[SerializeField]
private TextAsset[] _stageDatas;

おまけ

人工衛星なんですが、これはUnity標準のプリミティブオブジェクトの組み合わせで作ってます。
最初は無かったんですが、余りに味気なさすぎたので急遽追加しました。
適当に置いているだけなのですが、ちょっとゲームらしくなってくれたかなと。

f:id:ghoul_life:20180226232144p:plain

感想

後はBGMのON/OFF付けたり、Post Processing Stack付けて微妙な色合いを出したり。頭痛と闘いながら調整。

docs.unity3d.com

今回もどうにか参加できました。
何故インフルエンザなんてかかるのか!?
ちゃんと予防接種受けてるのに。

前回は可愛い感じだったので、今回は硬めに。
みなさんお疲れ様でしたー!
次は可愛めで行きたい!