20190218のUnityに関する記事は11件です。

Unity 宴 会話シーンのメッセージ送りをゲームパッドで行う方法

読者対象
・Unity初心者の方
・Project内に宴を使った会話シーンを組み込み済

目標
・Unityのアセット宴を使って会話シーンを追加したProjectを
 ゲームパッドでも文字送り出来るようにする(ジャンプボタンを押した時に文字送りする)

筆者
・Unity初心者
・和尚本のお写経をしてアクションゲームを作成
・宴をアセットストアで購入し、使い方を勉強中

1.はじめに

Unityでアクションゲームのお写経が完成し、宴のアセットを使って会話シーンをゲーム内に入れ込むことが出来ました。
クリックやEnterを押すと会話も進んでいくし、動作も問題ない。
しかし、アクションゲームは、ゲームパッドでも遊びたいでしょう~ということで、ゲームパッドのボタンを押すと宴で作った会話シーンも進むようにしたいと考えました。

何はなくとも公式のマニュアルが肝心だー!
宴のマニュアル(http://madnesslabo.net/utage/?page_id=9794)
マニュアル > UIを変更する > キーボード入力など、独自の入力処理を実装する

にアクセスしました。

しかし、初心者の私には????だらけの内容で、書いてある意味すら分かりません。
初心者からの目線としての印象ですが、宴の公式マニュアルは、プログラム未経験者には、難易度が非常に高いと感じました。
プログラム未経験者でも簡単にノベルゲームが作れるという触れ込みのアセットの割に、初心者殺しのマニュアルだな~という印象が拭えませんでした。

2.何をすれば希望の動作を行えるのか?

諸先輩方のお話しによれば「クリック判定の仕方を変える」の項目に書いてある。
『自分が呼びたい判定の仕方、タイミングで、「AdvUguiManager」の「OnPointerDown」を呼びます。』
を行えばゲームパッドでの操作が可能になるらしい。

しかし、私はここで困った事がありました。
「AdvUguiManager」が、どこにあるのか?どうやって「OnPointerDown」を呼びだすのかといったことが分からないのです。

3.AdvUguiManagerがついたオブジェクトはどこあるの?

「OnPointerDown」は、Scene >Hierarchy >AdvEngine >UI InputManagerに格納されていました。

あらかじめ画像のようにUIをPlayerのオブジェクトにセッティングしておきます。
WS003819.JPG

4.どうやって「OnPointerDown」を呼び出そうか?

・シーン内にAdvUguiManagerがついたゲームオブジェクトがある
・自分のスクリプトにAdvUguiManager型の変数を用意して、
 そのオブジェクトをインスペクタでセットしておく
・キーパッドが押されたらOnPointerDownを呼び出すコードを
 自分が作成したPlayerを操るスクリプトに追加する
どうやらこれらのことを行えば今回やりたい動作を行えるようだ。

ゲームパッドで現在動いているボタン("Jump")に「OnPointerDown」のコードを埋め込めば手っ取り早いか?と考えた私は、

public AdvUguiManager advUguiManager;
advUguiManager.OnPointerDown()

を追加しました。……が、動かない! 以下のエラーが出てきます。

引数 0 を指定するメソッド'OnePointerDown'のオーバーロードはありません。

このエラーメッセージは`PointerEventData` って型の引数が必要との意味らしいので、以下に修正。

advUguiManager.OnPointerDown(new PointerEventData())

すると今度はこんなエラーが・・・

型または名前空間の名前'PointerEventData'が見つかりませんでした(using ディレクティブまたはアセンブリ参照が指定されていることを確認してください)。

5.どうやってエラーを倒そうか?

どうやら先程のエラーは「今 using している namespace 達の中には PointerEventData って型のクラスか構造体が見当たらないよ!」という意味らしい。

C# は、namespace を using することで「その namespace に含まれるクラスとか構造体とかを使えるようになる」仕様なので、上の方にある「using なんたら~」を書き足さないといけなのです。

ということで、以下の必要な記述を追加しました。

using Utage;    //宴を使うために必要な記述
using UnityEngine.EventSystems; //PointerEventDataを使うために必要な記述
public AdvUguiManager advUguiManager;   //宴標準のAdvUguiManagerマネージャー

// ジャンプボタンを押した
    public void PushJumpButton () {
        if (canJump) {
            goJump = true;
            //宴の会話送りを進める動作を呼び出す
            advUguiManager.OnPointerDown(new PointerEventData(EventSystem.current));   
        }
    }

やったーーーーーー! 動いたーーーーっ! 動きましたよっ! 先生いぃーーーーーーーヽ(゚∀゚)ノ

6.まとめ

・宴のイベントを呼び出す際には OnPointerDown メソッドを自前で呼び出す必要がある
・OnPointerDown メソッドは PointerEventData 型の引数を1つ要求する
・PointerEventData は `UnityEngine.EventSystems` namespace 以下に居るので using の追加が必要
・PointerEventData の初期化には EvemtSystem のインスタンスが必要

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

UnityでHTTPに接続する

Unityのバージョン: Unity 2017.4.13f1
https://unity3d.com/jp/get-unity/download/archive

zipcloud 郵便番号検索API
http://zipcloud.ibsnet.co.jp/doc/api

Unity側の設定

  1. Hierarchyに空のゲームオブジェクトを作成して名前をMainControllerにします。
  2. 名前がMainControllerのC#ファイルを作成して、ゲームオブジェクトのMainControllerのコンポーネントに追加します。
  3. UI > Textオブジェクトを作成して、MainControllerスクリプトのプロパティText Resultにセットします。 01.png
  4. UI > Buttonオブジェクトを作成して、On Clickプロパティを+ボタンで追加します。
  5. 追加したOn ClickプロパティにMainControllerオブジェクトをセットして、FunctionにMainControllerのOnClickメソッドを指定します。 02.png

UnityからHTTPにGET接続する方法

MainController.cs
using System.Collections;
using UnityEngine;
using UnityEngine.Networking;

public class MainController : MonoBehaviour
{
    //接続するURL
    private const string URL = "http://zipcloud.ibsnet.co.jp/api/search?zipcode=7830060";

    //ゲームオブジェクトUI > ButtonのInspector > On Click()から呼び出すメソッド
    public void OnClick()
    {
        //コルーチンを呼び出す
        StartCoroutine("OnSend", URL);
    }

    //コルーチン
    IEnumerator OnSend(string url)
    {
        //URLをGETで用意
        UnityWebRequest webRequest = UnityWebRequest.Get(url);
        //URLに接続して結果が戻ってくるまで待機
        yield return webRequest.SendWebRequest();

        //エラーが出ていないかチェック
        if (webRequest.isNetworkError)
        {
            //通信失敗
            Debug.Log(webRequest.error);
        }
        else
        {
            //通信成功
            Debug.Log(webRequest.downloadHandler.text);
        }
    }
}

UnityからHTTPにPOST接続する方法

MainController.cs
using System.Collections;
using UnityEngine;
using UnityEngine.Networking;

public class MainController : MonoBehaviour
{
    //接続するURL
    private const string URL = "http://zipcloud.ibsnet.co.jp/api/search";

    //ゲームオブジェクトUI > ButtonのInspector > On Click()から呼び出すメソッド
    public void OnClick()
    {
        //コルーチンを呼び出す
        StartCoroutine("OnSend", URL);
    }

    //コルーチン
    IEnumerator OnSend(string url)
    {
        //POSTする情報
        WWWForm form = new WWWForm();
        form.AddField("zipcode", 1000001);

        //URLをPOSTで用意
        UnityWebRequest webRequest = UnityWebRequest.Post(url, form);
        //UnityWebRequestにバッファをセット
        webRequest.downloadHandler = new DownloadHandlerBuffer();
        //URLに接続して結果が戻ってくるまで待機
        yield return webRequest.SendWebRequest();

        //エラーが出ていないかチェック
        if (webRequest.isNetworkError)
        {
            //通信失敗
            Debug.Log(webRequest.error);
        }
        else
        {
            //通信成功
            Debug.Log(webRequest.downloadHandler.text);
        }
    }
}

UnityでJSONデータを読み込んでSerializableクラスに入れる方法

MainController.cs
using System.Collections;
using UnityEngine;
using UnityEngine.Networking;
using UnityEngine.UI;

public class MainController : MonoBehaviour
{
    //接続するURL
    private const string URL = "http://zipcloud.ibsnet.co.jp/api/search";
    //JSONデータを表示するUI > Textオブジェクト
    public Text textResult;

    //ゲームオブジェクトUI > ButtonのInspector > On Click()から呼び出すメソッド
    public void OnClick()
    {
        //コルーチンを呼び出す
        StartCoroutine("OnSend", URL);
    }

    //コルーチン
    IEnumerator OnSend(string url)
    {
        //POSTする情報
        WWWForm form = new WWWForm();
        form.AddField("zipcode", 1000001);

        //URLをPOSTで用意
        UnityWebRequest webRequest = UnityWebRequest.Post(url, form);
        //UnityWebRequestにバッファをセット
        webRequest.downloadHandler = new DownloadHandlerBuffer();
        //URLに接続して結果が戻ってくるまで待機
        yield return webRequest.SendWebRequest();

        //エラーが出ていないかチェック
        if (webRequest.isNetworkError)
        {
            //通信失敗
            Debug.Log(webRequest.error);
        }
        else
        {
            //通信成功
            //ZipクラスにJSONデータを格納する
            Zip zip = JsonUtility.FromJson<Zip>(webRequest.downloadHandler.text);
            //zipクラスに格納したJSONデータをゲームオブジェクトUI > Textに出力する
            textResult.text = zip.message + "," + zip.results.Length + "," + zip.status;
            foreach (ZipResult zr in zip.results)
            {
                textResult.text += string.Format("\n{0},{1},{2},{3}", zr.address1, zr.address2, zr.address3, zr.prefcode);
            }
        }
    }
}

読み込むJSONデータ

JSONデータ.txt
{
    "message": null,
    "results": [
        {
            "address1": "東京都",
            "address2": "千代田区",
            "address3": "千代田",
            "kana1": "トウキョウト",
            "kana2": "チヨダク",
            "kana3": "チヨダ",
            "prefcode": "13",
            "zipcode": "1000001"
        }
    ],
    "status": 200
}

JSONデータを格納するためのSerializableクラス

Zip.cs
[System.Serializable]
public class Zip
{
    public string message;
    public ZipResult[] results;
    public int status;
}
ZipResult.cs
[System.Serializable]
public class ZipResult
{
    public string address1;
    public string address2;
    public string address3;
    public string kana1;
    public string kana2;
    public string kana3;
    public string prefcode;
    public string zipcode;
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

大学生が有料スマホゲームを作った全てを公開するよ(4)ゲームの構造・パズルとアクションの仕組み(後編)

大学生が有料スマホゲームを作った全てを公開するよ(3)ゲームの構造・パズルとアクションの仕組み(前編)
の続き。

今回は、使ったスクリプトコンポーネントを全て公開しておく。
前回の記事も参考にしながら見てくれると良いと思う。

細かく触れると多すぎるので、
今回は工夫した点を特記する程度になってしまい申し訳ない。

もし需要があれば、掘り下げた記事も書くかもしれない。
スクリプトは、クリックすると表示されるよ。

作ったスクリプト及びコンポーネント一覧

ステージオブジェクト

スクリプト

プレイヤー、ブロック、敵、アイテム、その他のステージオブジェクトが継承する親クラス
SetStats関数では、int配列でオブジェクトの初期化情報を取得する。

StageObject.cs
StageObject.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class StageObject : MonoBehaviour
{
    public SOType sotype = SOType.Unknown;
    public string SOstate = "";
    protected GameObject GC;

    public void Start()
    {
        Init();
    }

    /// <summary>
    /// Sets the stats.
    /// </summary>
    /// <param name="stats">int配列ください</param>
    public virtual void SetStats(int[] stats)
    {

    }

    protected virtual void Init()
    {
        //GameControllerのGameObjectを取得
        GC = GameObject.FindGameObjectWithTag("GameController");
    }

    public virtual void OnShiftStart()
    {

    }

    public virtual void OnShiftEnd()
    {

    }

public virtual void Die(){
        Destroy (gameObject);
    }

    //ゲッターの意味...
    public SOType GetSOType(){
        return sotype;
    }
}

コンポーネント

ステージオブジェクトは以下のコンポーネントを持っている。

  • Transform
  • ParticleSystem
  • CircleCollider 2D
  • Rigidbody 2D

ただし、必要に応じて以下の場合もある。

  • CircleCollider2DをBoxCollider2Dで代用
  • Animatorコンポーネントを持つ

あまり重要ではないので、基本は同じだと考えてもらって良い。

プレイヤー

pertica_shift_low.gif

Character.cs親クラス
PlayerクラスとEnemyクラスはChracter.cs継承している。

基本的にはRigidbodyCircleColliderを使って物理挙動を制御している。

入れ替わり時の時を止める処理は、timescale0にすることで実現してる。
エフェクト関係はtimescaleの影響を受けないように、Unscaledの設定にしている。

Character.cs
Character.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;

public class Character : StageObject {
    protected int HP;

    public bool isDead(){
        bool isdead = false;

        if (HP <= 0) 
            isdead = true;

        return isdead;
    }

    public virtual void TakeDamage(int damage){
        HP -= damage;
        if (isDead ()) {
            Die ();
        }
    }

    public virtual void Die(){
        Destroy (gameObject);
    }

    public int GetHP(){
        return HP;
    }
}

Player.cs
Player.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;

public class Player : Character {

    //姿を消すときに消すオブジェクト
    public GameObject[] objBodys; 

    //パーティクルの親
    private GameObject parParticle;
    //Animator
    private Animator thisAnim;
    //Dieエフェクト
    public GameObject preDieEffect;

    //enable fire flag
    public bool canFire = true;

    //前回銃を打ってからの経過時間
    private float TimeFromFire;
    //前回エモートしてからの経過時間
    private float TimeFromEmote;

    //先行入力中か
    bool duringTypeAhead;
    Vector3 TypeAheadTapPos;
    int TypeAheadCount;

    //無敵フラグ(すりぬける)
    public bool isInvulnerable = false;


    protected override void Init(){
        base.Init ();

        HP = 1;
        parParticle = GameObject.Find ("Particle");
        thisAnim = GetComponent<Animator> ();
    }

    void Update () {

        if (isInvulnerable)
        {
            GetComponent<Rigidbody2D>().simulated = false;
        }

        //タイマーを進める
        TimeFromFire += Time.deltaTime;
        TimeFromEmote += Time.deltaTime;

        //待機アニメーション
        IdlingAnimation();

        //シフト中はシフト処理
        if (isShifting)
            Move ();
    }

    void IdlingAnimation(){
        float TriggerTime = 10;
        float EmoteTime = 2;

        if ((TimeFromFire > TriggerTime && TimeFromFire < TriggerTime + EmoteTime) 
            || (TimeFromEmote > TriggerTime && TimeFromEmote < TriggerTime + EmoteTime)) {
            if (thisAnim.GetInteger ("Emote") == 0) {
                int r = Random.Range (1, 3);
                thisAnim.SetInteger ("Emote", r);
            }
        } else {
            thisAnim.SetInteger ("Emote", 0);
        }
        if (TimeFromFire < TriggerTime || TimeFromEmote > TriggerTime + EmoteTime) {
            TimeFromEmote = 0;
        }
    }


    //指定された場所に敵がいるかを判定(posはscreenpoint)
    //いればtransformを、いなければnullを返す
    Transform EnemyExists(Vector3 pos){
        pos.z = 10;
        Vector2 wpos = Camera.main.ScreenToWorldPoint (pos);
        Ray ray = Camera.main.ScreenPointToRay(pos);

        RaycastHit2D hit;

        if (hit = Physics2D.Raycast(wpos, -Vector2.up)) {
            Transform objectHit = hit.transform;
            return objectHit;
        }
        return null;
    }

    //シフトする相手
    private GameObject objShiftTarget;
    //シフト中かどうか
    public bool isShifting = false;
    //シフト前のプレイヤーの位置
    private Vector2 FirstPlayerPos;
    //シフト前の相手の位置
    private Vector2 FirstTargetPos;

    public GameObject preShiftingPlayer;
    public GameObject preShiftingTarget;

    private GameObject objShiftingPlayerParticle;
    private GameObject objShiftingTargetParticle;

    public GameObject GetTarget()
    {
        return objShiftTarget;
    }

    //シフトする
    public void Shift(GameObject obj){
        //シフト中はシフトしない
        if(isShifting) return;

        objShiftTarget = obj;
        FirstPlayerPos = transform.position;
        FirstTargetPos = obj.transform.position;

        //対象オブジェクトのOnShiftを呼ぶ
        StageObject objSO = obj.GetComponent<StageObject>();
        objSO.OnShiftStart();

        //particle生成
        objShiftingPlayerParticle = Instantiate(preShiftingPlayer, Vector3.zero, Quaternion.identity, parParticle.transform);
        objShiftingTargetParticle = Instantiate(preShiftingTarget, Vector3.zero, Quaternion.identity, parParticle.transform);

        //rigidbodyのsimulatedをoffに(ぶつかったりしなくなる)
        gameObject.GetComponent<Rigidbody2D> ().simulated = false;
        objShiftTarget.GetComponent<Rigidbody2D> ().simulated = false;

        //SetActiveでやってみる(Updateが呼ばれないのでだめ)
        //gameObject.SetActive(false);
        //objShiftTarget.SetActive (false);

        //先行入力開始
        StartCoroutine(TypeAheadCoroutine());

        //時を止める
        Time.timeScale = 0;

        //SE
        SEPlayer.PlaySE("shift");

        //フラグ建築
        isShifting = true;
    }

    //Shiftするスピード
    private float SHIFTSPEED = 4;

    //対象のGameObjectと入れ替わる演出(FixedUpdatで呼び続ける)
    public void Move(){

        Vector2 PlayerPos = gameObject.transform.position;
        Vector2 TargetPos = objShiftTarget.transform.position;

        //徐々に移動していくパターン
        gameObject.transform.position += ((Vector3)FirstTargetPos - (Vector3)PlayerPos)*SHIFTSPEED*Time.unscaledDeltaTime;
        objShiftTarget.transform.position += -((Vector3)FirstTargetPos - (Vector3)PlayerPos)*SHIFTSPEED*Time.unscaledDeltaTime;

        Vector2 lError = new Vector2 (3f, 3f);
        Vector2 Error = new Vector2(0.05f, 0.05f);

        //入れ替わる時のパーティクル
        objShiftingPlayerParticle.transform.position = PlayerPos;
        objShiftingTargetParticle.transform.position = TargetPos;

        //近づいた時
        if (Mathf.Abs (PlayerPos.x - FirstTargetPos.x) < lError.x && Mathf.Abs (TargetPos.y - FirstPlayerPos.y) < lError.y) {
            //カメラ操作
            MoveCamera mc = Camera.main.gameObject.GetComponent<MoveCamera> ();
            if(mc != null){
                mc.SetState (MoveCamera.CameraState.GoToTarget);
            }
        }

        //シフト完了時
        if (Mathf.Abs (PlayerPos.x - FirstTargetPos.x) < Error.x && Mathf.Abs (TargetPos.y - FirstPlayerPos.y) < Error.y) {
            //position最後の微調整
            gameObject.transform.position = FirstTargetPos;
            objShiftTarget.transform.position = FirstPlayerPos;

            //simulatedをtrueに
            gameObject.GetComponent<Rigidbody2D> ().simulated = true;
            objShiftTarget.GetComponent<Rigidbody2D> ().simulated = true;

            //SetActiveでやってみる(Updateが呼ばれなくなるのでだめ)
            //gameObject.SetActive(false);
            //objShiftTarget.SetActive (false);

            //時を進める
            Time.timeScale = 1;

            //先行入力によるFire
            if (TypeAheadCount > 0) {
                TypeAheadFire ();
            }

            //先行入力終了
            duringTypeAhead = false;

            //フラグ回収
            isShifting = false;

            //移動対象のオブジェクトのOnShiftEndを呼ぶ
            objShiftTarget.GetComponent<StageObject>().OnShiftEnd();
        }
    }

    void SetRendererEnabled(GameObject obj, bool enabled){
        if (obj == gameObject) {
            foreach (GameObject objBody in objBodys) {
                objBody.GetComponent<Renderer> ().enabled = enabled;
            }
        }else{
            Renderer[] objRenderers = obj.GetComponentsInChildren<Renderer> ();
            foreach (Renderer objRenderer in objRenderers) {
                objRenderer.enabled = enabled;
            }
        }
    }

    public override void Die(){
        //SceneManager.LoadScene (SceneManager.GetActiveScene().name);
        //SE
        SEPlayer.PlaySE("gameover");
        PlayerPrefs.SetInt ("DieCount", PlayerPrefs.GetInt ("DieCount", 0) + 1);
        Debug.Log(PlayerPrefs.GetInt("DieCount"));
        gameObject.SetActive(false);
        Instantiate(preDieEffect, gameObject.transform.position, Quaternion.identity, transform.parent);
    }

    public GameObject preShiftBullet;

    /// <summary>
    /// Exists the UI.
    /// </summary>
    /// <returns><c>true</c>, if UI was existed, <c>false</c> otherwise.</returns>
    /// <param name="pos">スクリーン座標を渡してください。また、UIのアンカーは左下にしてください</param>
    bool existUI(Vector3 pos){
        bool exist = false;
        GameObject[] objUIs = GameObject.FindGameObjectsWithTag ("UI");
        GameObject objUICanvas = GameObject.FindGameObjectWithTag ("UICanvas");
        if (objUICanvas == null)
            return exist;
        Vector2 UIscale = objUICanvas.GetComponent<RectTransform> ().localScale;
        foreach (GameObject objUI in objUIs) {
            RectTransform rectUI = objUI.GetComponent<RectTransform> ();
            Vector3 UIpos = objUI.transform.position;
            Vector3 dsize = rectUI.sizeDelta;
            Vector3 lbUIpos = UIpos - dsize;
            Vector3 ruUIpos = UIpos + dsize;
            //UI座標からスクリーン座標に変換
            Vector3 screenUIpos = RectTransformUtility.WorldToScreenPoint(Camera.main, UIpos);
            Vector3 screenlbUIpos = screenUIpos - dsize*UIscale.x*40;
            Vector3 screenruUIpos = screenUIpos + dsize*UIscale.y*40;
            if (screenlbUIpos.x < pos.x && pos.x < screenruUIpos.x && screenlbUIpos.y < pos.y && pos.y < screenruUIpos.y) {
                exist = true;
            }

            Debug.Log ("lb " + screenlbUIpos);
            Debug.Log ("ru " + screenruUIpos);
            Debug.Log ("m " + pos);
        }
        return exist;
    }

    public void Fire(){
        if (!canFire)
            return;

        if(ExistBullet())
            return;

        Vector3 pos = Input.mousePosition;
        pos.z = 10;
        Vector3 wpos = Camera.main.ScreenToWorldPoint (pos);
        Vector3 velocity = (wpos - transform.position).normalized;

        //先行入力
        if (duringTypeAhead) {
            TypeAheadTapPos = wpos;
            if (TypeAheadCount < 1) {
                TypeAheadCount++;
            }
        }

        //シフト中は打たない
        if(isShifting) return;

        GameObject objItems = GameObject.Find ("Items");
        GameObject objBullet;
        objBullet = Instantiate (preShiftBullet, transform.TransformPoint (0, 0, 0), Quaternion.identity, objItems.transform);

        //弾に力を与える
        objBullet.GetComponent<ShiftBullet> ().Fire(velocity);

        //タイマーリセット
        TimeFromFire = 0;

        //SE
        SEPlayer.PlaySE("bullet");
    }

    private Coroutine reloadCoroutine;
    void TypeAheadFire(){
        Vector3 pos = TypeAheadTapPos;
        Vector3 velocity = (pos - transform.position).normalized;

        GameObject objItems = GameObject.Find ("Items");
        GameObject objBullet;
        objBullet = Instantiate (preShiftBullet, transform.TransformPoint (0, 0, 0), Quaternion.identity, objItems.transform);

        //弾に力を与える
        objBullet.GetComponent<ShiftBullet> ().Fire(velocity);

        //タイマーリセット
        TimeFromFire = 0;

        //SE
        SEPlayer.PlaySE("bullet");


        //先行入力数を減らす
        TypeAheadCount--;
    }

    public override void TakeDamage (int damage)
    {
        if (isShifting) {
        } else {
            HP -= damage;
            if (isDead ()) {
                Die ();
                GC.GetComponent<Main> ().Reset (0.5f);
            }
        }
    }

    //何もしなくなる
    public void Sleep(){
        isShifting = true;
    }

    float startTime = 0f; //シフト開始から先行入力開始までの時間
    float endTime = 0.3f;   //シフト開始から先行入力終了までの時間
    IEnumerator TypeAheadCoroutine(){
        yield return new WaitForSeconds (startTime);
        duringTypeAhead = true;
        yield return new WaitForSeconds (endTime - startTime);
        duringTypeAhead = false;
    }


    //弾がステージ内に存在するか
    private bool ExistBullet()
    {
        bool existbullet = false;
        GameObject objItems = GameObject.Find ("Items");
        GameObject objShiftBullet = SOManager.FindStageObject(objItems, SOType.ShiftBullet);
        if (objShiftBullet == null)
        {
            existbullet = false;
        }
        else
        {
            existbullet = true;
        }

        return existbullet;
    }
}

ブロック

pertica_reflectionwall.gif

全てBlockクラスを継承しているが、今のところ処理はなくBlockクラスは意味がなくなってしまった。

SwitchWallに関しては見た目をパーティクルに頼っているため、SetActiveの切り替えは違和感があった。(一瞬でパーティクルが消えるため)
スイッチ壁の切り替えの時は、ParticleSystemをPlayしたりStopして対応している。

Wall.cs
Wall.cs
public class Wall : Block {

    public GameObject CollideEffect;

    protected override void Init(){
        base.Init ();
        sotype = SOType.Wall;
    }

    void OnCollisionEnter2D(Collision2D coll){
        foreach (ContactPoint2D contact in coll.contacts) {
            Instantiate (CollideEffect, (Vector3)contact.point, Quaternion.identity);
        }
    }
}

ReflectionWall.cs
ReflectionWall.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class ReflectionWall : Block {

    protected override void Init(){
        base.Init ();
        sotype = SOType.ReflectionWall;
    }

    void OnCollisionEnter2D(Collision2D coll){
        if (coll.gameObject.tag == "UnshiftableItem") {
            if (coll.gameObject.GetComponent<StageObject> ().sotype == SOType.ShiftBullet) {
                SEPlayer.PlaySE ("reflection");
            }
        }
    }
    //反射処理はbulletに任せている
}

SwitchWall.cs
SwitchWall.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class SwitchWall : Block
{

    public bool EnabledStart;
    public ParticleSystem[] WallMovers;
    public GameObject BreakEffect;

    private bool isEnabled = true;

    public ParticleSystem EnergyBall;
    private Vector3 oldPos;

    protected override void Init()
    {
        base.Init();

        sotype = SOType.SwitchWall;

        if (EnabledStart == true)
            SetEnabled(true);
        else
            SetEnabled(false);

        isEnabled = EnabledStart;
    }

    public void ClearLineEffect()
    {
        EnergyBall.gameObject.SetActive(false);
        if (isEnabled)
        {
            EnergyBall.gameObject.SetActive(true);
        }
    }

    /// <summary>
    /// Sets the stats.
    /// </summary>
    /// <param name="stats">[0] (0:非表示, 1:表示)状態からスタート</param>
    public override void SetStats(int[] stats)
    {
        base.SetStats(stats);

        if (stats.Length == 0)
        {
            return;
        }

        switch (stats[0])
        {
            case 0:
                EnabledStart = false;
                break;
            case 1:
                EnabledStart = true;
                break;
            default:
                Debug.Log("Invalid number");
                break;
        }
    }

    private GameObject break_effect;

    //壁を消したり現れたりします
    public void SwitchEnabled()
    {
        isEnabled = !isEnabled;
        if (isEnabled)
        {
            gameObject.GetComponent<BoxCollider2D>().enabled = true;
            gameObject.GetComponent<ParticleSystem>().Play();

            for (int i = 0; i < transform.childCount; i++)
                transform.GetChild(i).gameObject.SetActive(true);
            //destroy effect
            Destroy(break_effect);
        }
        else
        {
            gameObject.GetComponent<BoxCollider2D>().enabled = false;
            gameObject.GetComponent<ParticleSystem>().Stop();
            for (int i = 0; i < transform.childCount; i++)
                transform.GetChild(i).gameObject.SetActive(false);

            //Effect
            break_effect = Instantiate(BreakEffect, transform.position, Quaternion.identity, transform);
            break_effect.GetComponent<particleAttractorMove>().target = SOManager
                .FindStageObject(transform.parent.gameObject, SOType.SwitchWallButton).transform;
        }
    }

    //壁を出現させます
    public void SetEnabled(bool state)
    {
        gameObject.GetComponent<BoxCollider2D>().enabled = state;
        for (int i = 0; i < transform.childCount; i++)
            transform.GetChild(i).gameObject.SetActive(state);
        switch (state)
        {
            case true:
                gameObject.GetComponent<ParticleSystem>().Play();
                break;
            case false:
                gameObject.GetComponent<ParticleSystem>().Stop();
                break;
        }
    }

    public bool GetEnabled()
    {
        return isEnabled;
    }
}

pertica_dog.gif

Enemyクラスを親クラスとしている。
元々、HPを設けるつもりだったが今は必要ないので名残で使ってしまっている。

壁にぶつかった時の反射に関しては、Rigidbodyを使っている。
Physics2Dの設定でVelocity Thresholdを0にしないと低速の跳ね返りが無視されてしまい、変な跳ね返り方をするので要注意。

Character.cs
Character.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;

public class Character : StageObject {
    protected int HP;

    public bool isDead(){
        bool isdead = false;

        if (HP <= 0) 
            isdead = true;

        return isdead;
    }

    public virtual void TakeDamage(int damage){
        HP -= damage;
        if (isDead ()) {
            Die ();
        }
    }

    public virtual void Die(){
        Destroy (gameObject);
    }

    public int GetHP(){
        return HP;
    }
}

Enemy.cs
Enemy.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Enemy : Character {
    protected int direction; //左-1 右1
    protected float speed;  //移動速度

    protected GameObject objPlayer;
    protected Player cmpPlayer;

    private Coroutine corAttackCoroutine;

    protected override void Init(){
        base.Init ();

        direction = -1;
        speed = 2;
        HP = 1;
        objPlayer = GameObject.FindGameObjectWithTag ("Player");
        cmpPlayer = objPlayer.GetComponent<Player> ();

        corAttackCoroutine = StartCoroutine(AttackCoroutine ());
    }

    void Update(){
        Move ();
        Attack ();
    }

    //進む方向を変更します
    protected void ChangeDirection(){
        direction = -direction;
    }

    //プレイヤーとぶつかった時の処理
    protected void OnCollideWithPlayer(){
        cmpPlayer.TakeDamage (10000);
    }

    //updateで呼ばれるmove関数です。ご自由に。
    public virtual void Move(){

    }

    //updateで呼ばれるattack関数です。ご自由に。
    public virtual void Attack(){

    }

    //最初に一度だけ呼ばれるattackコルーチンです。ご自由に。
    public virtual IEnumerator AttackCoroutine(){
        yield return new WaitForSeconds(0f);
    }
}

Dog.cs
Dog.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Dog : Enemy {

    Rigidbody2D R2D;

    protected override void Init(){
        base.Init ();
        R2D = gameObject.GetComponent<Rigidbody2D> ();
        R2D.velocity = transform.rotation * Vector2.up * speed;
    }



    /// <summary>
    /// Sets the stats.
    /// </summary>
    /// <param name="stats">[0] 向き</param>
    public override void SetStats (int[] stats)
    {
        base.SetStats (stats);
        if (stats.Length == 0) {
            transform.rotation = Quaternion.AngleAxis (90, Vector3.forward);
        } else {
            transform.rotation = Quaternion.AngleAxis (stats [0] * 45, Vector3.forward);
        }


        if (stats.Length == 0) {
            GetComponent<Rigidbody2D> ().constraints = RigidbodyConstraints2D.FreezePositionY;
        }else{
            if (stats [0] == 0 || stats [0] == 4) {
                GetComponent<Rigidbody2D> ().constraints = RigidbodyConstraints2D.FreezePositionX;
            } else if (stats [0] == 2 || stats [0] == 6) {
                GetComponent<Rigidbody2D> ().constraints = RigidbodyConstraints2D.FreezePositionY;
            } else {
                Debug.Log ("Invalid Rotation");
            }
        }
    }

    protected virtual void Update(){
        //速度が0だったら強制的に動かします
        if (GetComponent<Rigidbody2D> ().velocity == Vector2.zero) {
            if (R2D != null)
            {
                R2D.velocity = transform.rotation * Vector2.up * speed;
            }
        }

        //斜め移動の場合、xとyの値を揃えます
        if (R2D.velocity.x != 0 && R2D.velocity.y != 0) {
            R2D.velocity = new Vector2 (R2D.velocity.x / Mathf.Abs (R2D.velocity.x), R2D.velocity.y / Mathf.Abs (R2D.velocity.y));
        }

        //速度を正規化します
        if (GetComponent<Rigidbody2D>().velocity.magnitude != 1)
        {
            if (R2D != null)
            {
                R2D.velocity = R2D.velocity.normalized * speed;
            }
        }
    }

    void OnCollisionEnter2D(Collision2D coll){
        R2D.velocity = R2D.velocity.normalized * speed;
        //プレイヤーとぶつかったら何かします
        if (coll.gameObject.tag == "Player") {
            cmpPlayer.TakeDamage (10000);
        }
    }
}

Pig.cs
Pig.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Pig : Dog{
    public GameObject prePigLazer;
    public bool host;

    LineRenderer[] LRs;
    EdgeCollider2D[] EC2Ds;
    GameObject[] PigLazers;

    List<GameObject> Pigs;

    protected override void Init ()
    {
        base.Init ();
        Pigs = SOManager.FindStageObjects (transform.parent.gameObject, SOType.Pig);
        host = true;
        foreach (GameObject Pig in Pigs) {
            if (Pig.GetComponent<Pig> ().host && Pig != gameObject) {
                host = false;
            }
        }
        LRs = new LineRenderer[Pigs.Count];
        EC2Ds = new EdgeCollider2D[Pigs.Count];
        PigLazers = new GameObject[Pigs.Count];
        if (host) {
            for (int i = 0; i < Pigs.Count; i++) {
                PigLazers[i] = Instantiate (prePigLazer, transform.parent);
                LRs[i] = PigLazers[i].GetComponentInChildren<LineRenderer> ();
                EC2Ds[i] = PigLazers[i].GetComponentInChildren<EdgeCollider2D> ();
            }
            SetLinePosition ();
        }
    }

    protected override void Update(){
        base.Update ();
        if (host) {
            SetLinePosition ();
        }
    }

    void SetLinePosition(){
        Vector3[] poses3 = new Vector3[Pigs.Count + 1];
        Vector2[] poses2 = new Vector2[Pigs.Count + 1];
        for(int i=0; i < Pigs.Count; i++){
            poses3 [i] = Pigs [i].transform.position;
            poses2 [i] = Pigs [i].transform.position;
        }
        poses3 [poses3.Length - 1] = poses3 [0];
        poses2 [poses2.Length - 1] = poses2 [0];
        for (int i = 0; i < Pigs.Count; i++) {
            LRs [i].SetPosition (0, poses3 [i]);
            LRs [i].SetPosition (1, poses3 [i + 1]);
            Vector2[] ecpos = { poses2 [i], poses2 [i + 1] };
            EC2Ds[i].points = ecpos;
        }
    }
}

アイテム

pertica_clearhole.gif
pertica_dimensioncrack.gif

親クラスとしてItem.csを使っている。
Cubeは元々Rockという名前だったのでスクリプトの名前がRock.csになってしまっている。

ShiftBulletでは、衝突時にDie関数を呼ぶかどうかで反射するかを決めている。

Item.cs
Item.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Item : StageObject {

    protected GameObject objPlayer;
    protected Player cmpPlayer;

    protected override void Init(){
        base.Init ();
        objPlayer = GameObject.FindGameObjectWithTag ("Player");
        cmpPlayer = objPlayer.GetComponent<Player> ();
    }

    public override void Die (){
        Destroy (gameObject);
    }
}

Cube.cs
Rock.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Rock : Item {
    protected override void Init ()
    {
        base.Init ();
        sotype = SOType.Rock;
    }
}

ClearHole.cs
ClearHole.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;

public class ClearHole : Item {
    bool hasEnter;

    float RotateSpeed = 50;
    float MoveSceneDelay = 1;
    public string LoadSceneName = "Clear";

    private GameObject ScreenPanel;

    protected override void Init ()
    {
        base.Init ();
        ScreenPanel = GC.GetComponent<Main>().ScreenPanel;
    }

    void OnTriggerEnter2D(Collider2D coll){
        //trialTagがあったらクリアしません
        if (GameObject.FindGameObjectWithTag ("trialTag") != null) {
            if (coll.gameObject.tag == "Player") {
                //プレイヤーを無敵にします
                coll.GetComponent<Player>().isInvulnerable = true;
                Debug.Log ("Clear!!!!!!!!!");
            }
            return;
        }

        if (coll.gameObject.tag == "Player") {
            if (hasEnter == false) {
                //プレイヤーを回転させます
                coll.GetComponent<Rigidbody2D> ().angularVelocity = Mathf.PI * RotateSpeed;
                hasEnter = true;
                //プレイヤーを無敵にします
                coll.GetComponent<Player>().isInvulnerable = true;
                //もしもEndステージを初めてクリアしたらMessageシーンに飛びます
                if (DataManager.GetPlayStageData() == 71 && DataManager.LoadProgress() == 71) {
                    LoadSceneName = "Message";
                }
                //Fadeinします
                ScreenPanel.GetComponent<Animator>().SetBool("FadeIn",true);
                //Progressをセーブします
                DataManager.SaveProgress();
                //BGMをFadeoutします
                BGMPlayer.FadeoutBGM();
                //数秒後シーンを移動します
                StartCoroutine (MoveSceneCoroutine ());
            }
        }
    }

    IEnumerator MoveSceneCoroutine(){
        Time.timeScale = 0.5f;
        yield return new WaitForSeconds (MoveSceneDelay);
        Time.timeScale = 1;
        SceneManager.LoadScene (LoadSceneName);
    }
}

SwitchWallButton.cs
SwitchWallButton.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class SwitchWallButton : Item {

    List<GameObject> SwitchWalls;

    void Start(){
        sotype = SOType.SwitchWallButton;
        SwitchWalls = SOManager.FindStageObjects(transform.parent.gameObject, SOType.SwitchWall);
    }

    public override void OnShiftStart()
    {
        base.OnShiftStart();
        foreach (GameObject switchWall in SwitchWalls)
        {
            switchWall.GetComponent<SwitchWall>().ClearLineEffect();
        }
    }

    public override void OnShiftEnd()
    {
        base.OnShiftEnd();
        foreach (GameObject switchWall in SwitchWalls)
        {
            switchWall.GetComponent<SwitchWall>().PlayLineEffect();
        }
    }

    //同列のオブジェクトをswitchする
    void Switch(){
        Transform parent = gameObject.transform.parent;
        GetComponent<Animator> ().SetTrigger ("ChangeColor");
        foreach (Transform child in parent) {
            if (child.GetComponent<StageObject>().GetSOType() == SOType.SwitchWall) {
                child.GetComponent<SwitchWall> ().SwitchEnabled ();
            }
        }
        //SE
        SEPlayer.PlaySE("switch");
    }

    //同列のオブジェクトのswitchwallをenabledをtrueに
    void SwitchTrue(){
        Transform parent = gameObject.transform.parent;
        foreach (Transform child in parent) {
            if (child.GetComponent<StageObject>().GetSOType() == SOType.SwitchWall) {
                child.GetComponent<SwitchWall> ().SetEnabled (true);
            }
        }
    }

    //同列のオブジェクトのswitchwallをenabledをfalseに
    void SwitchFalse(){
    Transform parent = gameObject.transform.parent;
        foreach (Transform child in parent) {
            if (child.GetComponent<StageObject> ().GetSOType () == SOType.SwitchWall) {
                child.GetComponent<SwitchWall> ().SetEnabled (false);
            }
        }
    }

    //ぶつかったら状態を変更
    void OnCollisionEnter2D(Collision2D coll){
        StageObject cmpSO = coll.gameObject.GetComponent<StageObject> ();
        //例外
        if(cmpSO.GetSOType() == SOType.ShiftBullet) return;
        if (coll.gameObject.tag == "Block") return;
        Switch ();
    }
}

DimensionCrack.cs
CimensionCrack.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class DimentionCrack : Item {

    public bool entered = false;
    public GameObject enteredObject;

    private List<GameObject> DCs;
    public GameObject PairDimentionCrack;
    private DimentionCrack PairDC;

    public Vector3 viewPos;
    public bool isVisible;

    [SerializeField]
    GameObject EnableEffect;

    protected override void Init(){
        base.Init ();
        GameObject objItem = GameObject.Find ("Items");
        DCs = SOManager.FindStageObjects (objItem, SOType.DimentionCrack);
        sotype = SOType.DimentionCrack;
        PairDimentionCrack = SOManager.FindStageObject (transform.parent.gameObject, SOType.DimentionCrack, gameObject);
        PairDC = PairDimentionCrack.GetComponent<DimentionCrack> ();
    }

    void OnTriggerEnter2D(Collider2D coll){
        if (SetPairDC() != 1)
            return;
        SOType colltype = coll.GetComponent<StageObject> ().sotype;
        if (PairDC.isVisible == false || isVisible == false)
            return;
        if (coll.tag == "ShiftableItem" || coll.tag == "ShiftableEnemy" || coll.tag == "UnshiftableItem"){
            if (!PairDC.entered) {
                Vector3 TelePoint = new Vector3 (0, 0, 0);
                coll.gameObject.transform.position = PairDimentionCrack.transform.TransformPoint (TelePoint);
                entered = true;
                enteredObject = coll.gameObject;
                EnableEffect.GetComponent<ParticleSystem> ().Emit (30);
                SEPlayer.PlaySE ("warp");
            } else {
                PairDC.entered = false;
            }
        }
    }

    void OnTriggerExit2D(Collider2D coll){
        if (coll.gameObject == PairDC.enteredObject && PairDC.entered) {
            PairDC.entered = false;
            entered = false;
            EnableEffect.GetComponent<ParticleSystem> ().Emit (30);
        }
    }

    void Update(){
        viewPos = Camera.main.WorldToViewportPoint (transform.position);
        SetVisible (viewPos);

        if (SetPairDC() == 1 && isVisible && PairDC.isVisible) {
            EnableEffect.SetActive (true);
            transform.localScale = new Vector3(1,1,1) * 1f;
        } else {
            EnableEffect.SetActive (false);
            transform.localScale = new Vector3(1,1,1) * 0.4f;
        }
        if (enteredObject == null && PairDC.enteredObject == null) {
            entered = false;
        }
    }

    void SetVisible(Vector3 viewpos){
        bool isvisible = false;
        if (0f < viewpos.x && viewpos.x < 1f && 0f < viewpos.y && viewpos.y < 1f) {
            isvisible = true;
        }
        isVisible = isvisible;
    }

    int SetPairDC(){
        int visibleCount = 0;
        foreach(GameObject DC in DCs){
            DimentionCrack cmpDC = DC.GetComponent<DimentionCrack> ();
            if (cmpDC.isVisible && DC != gameObject) {
                PairDimentionCrack = DC;
                PairDC = cmpDC;
                visibleCount++;
            }
        }
        return visibleCount;
    }
}

ShiftBullet.cs
ShiftBullet.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System;

public class ShiftBullet : Item {


    public float LIFESPAN;
    public float BULLETSPEED;

    public GameObject DieEffect;


    private SOType[] DontDieTypes = new SOType[]{SOType.ReflectionWall,SOType.TeleportWall};

    protected override void Init(){
        base.Init ();
        StartCoroutine (DieCoroutine());
        sotype = SOType.ShiftBullet;
    }

    void Update(){
        //when out of camera
        Vector3 vpos = Camera.main.WorldToViewportPoint (transform.position);
        Vector3 limitpos = new Vector3 (0f, 0f, 0f);
        if (vpos.x < limitpos.x || 1 - limitpos.x < vpos.x || vpos.y < limitpos.y || 1 - limitpos.y < vpos.y) {
            Destroy(gameObject);
        }
    }


    //colliderがぶつかった時の処理
    void OnCollisionEnter2D(Collision2D coll){
        //入れ替われる相手とぶつかったら何かします
        if (coll.gameObject.tag == "ShiftableEnemy" || coll.gameObject.tag == "ShiftableItem") {
            cmpPlayer.Shift (coll.gameObject);
        } else {
            //入れ替われない相手とぶつかったら何かします(ただし、isTriggerがfalseの奴とはぶつかれないのでそっちでやって
            //SE
            SEPlayer.PlaySE("disapear");
        }

        //当たっても消えないやつ
        if ( 0 <= Array.IndexOf(DontDieTypes, coll.gameObject.GetComponent<StageObject> ().GetSOType ()))
            return;

        Die ();
    }

    //発射します
    public void Fire(Vector2 velocity){
        gameObject.GetComponent<Rigidbody2D> ().velocity = velocity * BULLETSPEED;
    }

    IEnumerator DieCoroutine(){
        yield return new WaitForSeconds (LIFESPAN);
        Destroy (gameObject);
    }

    public override void Die ()
    {
        Instantiate (DieEffect, transform.position, Quaternion.identity);
        base.Die ();
    }
}

その他のステージオブジェクト

pertica_pig.gif

PredictionLine(予測線)は、LineRendererをコンポーネントに持つ。
プレイヤーからレイを飛ばしてぶつかった位置をLineRendererに設定する事で実現している。

PigLazerは、LineRendererEdgeCollider2Dをコンポーネントに持つ。
レーザーはプレイヤーとの当たり判定のみ行い、レーザーの端点の設定はPig.csで行う。

PredictionLine.cs
PredictionLine.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class PredictionLine : MonoBehaviour {
    GameObject player;
    LineRenderer line;

    void Start(){
        player = GameObject.FindGameObjectWithTag ("Player");
        line = GetComponent<LineRenderer> ();
    }

    void Update(){
        if (Input.GetMouseButton (0)) {
            line.enabled = true;
            Vector3 tappos = Input.mousePosition;
            tappos.z = 10;
            Vector3 wtappos = Camera.main.ScreenToWorldPoint (tappos);

            Vector3 direction = (wtappos - player.transform.position).normalized;

            int layerMask = Physics2D.DefaultRaycastLayers & ~(1 << 8);
            RaycastHit2D hit = Physics2D.Raycast (player.transform.position, direction, 12, layerMask);
            if (hit) {
                line.SetPosition (0, player.transform.position);
                line.SetPosition (1, hit.point);
            } else {
                line.SetPosition (0, player.transform.position);
                line.SetPosition (1, player.transform.position + direction * 12);
            }

        } else {
            line.enabled = false;
        }
    }
}

PigLazer.cs
PigLazer.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class PigLazer : StageObject {

    void OnTriggerEnter2D(Collider2D coll){
        if (coll.tag == "Player") {
            GameObject player = GameObject.FindGameObjectWithTag ("Player");
            //Why am i using HP?
            if (player != null) {
                player.GetComponent<Player> ().TakeDamage (10000);
            }
        }

        if (coll.GetComponent<StageObject> ().sotype == SOType.ShiftBullet) {
            coll.GetComponent<ShiftBullet> ().Die ();
        }
    }
}

カメラ

このゲーム(pertica)のカメラはかなり特殊だ。
まず1つのシーンにカメラが3つある。

  • ScreenCamera
  • MainCamera
  • StarCamera

ほとんどはMainCameraの映像を利用する。
2Dの為、MainCameraはProjectionをOrthographicに設定する。
でもそれだと背景の星立体感が無くなった。

そこで、背景専用StarCameraを用意しMainCameraの子オブジェクトにした。

また、perticaは画面のアスペクト比が変わるとゲームが成り立たない。
全ての端末でアスペクト比を揃える必要があった。

でもそれだとスマホの画面に余白が出来てしまう。
余白部分をタップしても反応させるためにScreenCameraを用意したんだ。

MoveCamera.cs
MoveCamera.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;

public class MoveCamera : MonoBehaviour {

    //どうやって作ればいいのかわかんねー教えてー

    public enum CameraState{
        Stay,LookClearHole, TracePlayer, GoToTarget, LookTitle, LookTitleMove
    }

    private GameObject objPlayer;
    private GameObject objCamera;
    private CameraState cstate = CameraState.TracePlayer;
    private GameObject objClearHole;

    private Player cmpPlayer;

    Vector3 sPivot; //pivot for smabro camera
    float sX = 10 * Mathf.Sqrt(3); //camera length of x for smabro camera
    float sD = 1; //rate of camera move

    void Start(){
        Init ();
    }

    void Init(){
        if (SceneManager.GetActiveScene ().name == "Title" && TitleManager.firstTitleScene) {
            cstate = CameraState.LookTitle;
            TitleManager.firstTitleScene = false;
        }
        //object initialize
        objCamera = GameObject.FindGameObjectWithTag ("MainCamera");
        objPlayer = GameObject.FindGameObjectWithTag ("Player");
        cmpPlayer = objPlayer.GetComponent<Player> ();
        GameObject[] objUsIs = GameObject.FindGameObjectsWithTag ("UnshiftableItem");
        foreach(GameObject objUsI in objUsIs){
            StageObject SO = objUsI.GetComponent<StageObject> ();
            if(SO != null && SO.sotype == SOType.ClearHole){
                objClearHole = objUsI;
            }
        }
        if (objClearHole != null) {
            Vector3 CorrectClearHolePos = new Vector3 (objClearHole.transform.position.x, objClearHole.transform.position.y, -10f);
            transform.position = CorrectClearHolePos;
        }
    }

    void Update(){
        switch(cstate){
        case CameraState.Stay:
            break;
        case CameraState.TracePlayer:
            if (CorrectPosz (objPlayer.transform.position).x == transform.position.x)
                SetState (CameraState.Stay);
            TracePlayer_Move ();
            break;
        case CameraState.LookClearHole:
            LookClearHole_Move ();
            break;
        case CameraState.LookTitle:
            StartCoroutine (LookTitleCoroutine ());
            break;
        case CameraState.LookTitleMove:
            GoToTarget_Move (objPlayer, 1);
            break;
        case CameraState.GoToTarget:
            GoToTarget_Move (objPlayer, 3);
            break;
        }
    }

    public void SetState(CameraState state){
        //外部からstateを変えるためのやつ
        switch (state) {
        case CameraState.LookClearHole:
            cstate = CameraState.Stay;
            StartCoroutine (SetStateCoroutine (0.3f, CameraState.LookClearHole));
            break;
        case CameraState.TracePlayer:
            cstate = CameraState.TracePlayer;
            Debug.Log ((objPlayer.transform.position).x - transform.position.x);
            if ((objPlayer.transform.position).x < transform.position.x) {
                sPivot = transform.position + Vector3.forward * 10 + new Vector3 (sX, 0, 0);
            } else {
                sPivot = transform.position + Vector3.forward * 10 - new Vector3 (sX, 0, 0);
            }
            break;
        case CameraState.GoToTarget:
            cstate = CameraState.GoToTarget;
            break;
        case CameraState.Stay:
            cstate = CameraState.Stay;
            break;
        }
    }

    public CameraState GetStats()
    {
        return cstate;
    }

    //Change State to some state after some seconds
    IEnumerator SetStateCoroutine(float time, CameraState state){
        yield return new WaitForSeconds (time);
        //球を打てなくする
        cmpPlayer.canFire = false;
        cstate = state;
    }

    void TracePlayer_Move(){
        transform.position = new Vector3 (objPlayer.transform.position.x, objPlayer.transform.position.y, -10f);
    }

    //move camera per frame
    void LookClearHole_Move(){
        float MoveSpeed = 5 + (objClearHole.transform.position - objPlayer.transform.position).magnitude * (1f/10f);
        float Error = 0.2f;
        objCamera.transform.position += (CorrectPosz(objPlayer.transform.position) - objCamera.transform.position).normalized * Time.unscaledDeltaTime * MoveSpeed;
        //移動完了時
        if ((CorrectPosz (objPlayer.transform.position) - objCamera.transform.position).magnitude < Error) {
            cmpPlayer.canFire = true;
            SetState (CameraState.Stay);
        }
    }

    //correct position.z for camera
    Vector3 CorrectPosz(Vector3 pos){
        Vector3 CorrectPos = new Vector3 (pos.x, pos.y, -10f);
        return CorrectPos;
    }

    void GoToTarget_Move(GameObject objTarget, float speed){
        Vector3 TargetPos = objTarget.transform.position;
        transform.position += (CorrectPosz(TargetPos) - transform.position) * Time.unscaledDeltaTime * speed;
        float Error = 0.1f;

        if ((CorrectPosz (TargetPos).x + Error > transform.position.x) &&
            (CorrectPosz (TargetPos).x - Error < transform.position.x) &&
            (CorrectPosz (TargetPos).y + Error > transform.position.y) &&
            (CorrectPosz (TargetPos).y - Error < transform.position.y)) {
            SetState (CameraState.Stay);
        }
    }

    IEnumerator LookTitleCoroutine(float time = 1f){
        yield return new WaitForSeconds (time);
        cstate = CameraState.LookTitleMove;
    }
}

FixCameraSize.cs
FixCameraSize.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class FixCameraSize : MonoBehaviour {
    public float x_aspect = 16.0f;
    public float y_aspect = 9.0f;

    private void Awake()
    {
        Camera camera = GetComponent<Camera>();
        Rect rect = calcRect(x_aspect, y_aspect);
        camera.rect = rect;
    }

    private Rect calcRect(float width, float height)
    {
        float target_aspect = width / height;
        float window_aspect = Screen.width / (float)Screen.height;
        float scale_height = window_aspect / target_aspect;
        Rect rect = new Rect(0.0f, 0.0f, 1.0f, 1.0f);

        if(1.0f > scale_height)
        {
            rect.x = 0;
            rect.y = (1.0f - scale_height) / 2.0f;
            rect.width = 1.0f;
            rect.height = scale_height;
        }
        else
        {
            float scale_width = 1.0f / scale_height;
            rect.x = (1.0f - scale_width) / 2.0f;
            rect.y = 0.0f;
            rect.width = scale_width;
            rect.height = 1.0f;
        }

        return rect;
    }
}

ゲームマネージャー

SpawnObject関数は画面をタップした位置(Input.mousePosition)を想定している。
その為、スクリーン座標からワールド座標への変換を行い、カメラのz座標を引くことで生成位置を調整している。

Main.cs
Main.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;

public class Main : MonoBehaviour {

    public GameObject[] Objects;
    public GameObject EnemiesParent;

    public GameObject ScreenPanel;

    public GameObject preLoadMenuHole;

    private GameObject objPlayer;
    private Player cmpPlayer;


    void Start(){
        Init ();
    }

    void Init(){
        objPlayer = GameObject.FindGameObjectWithTag ("Player");
        cmpPlayer = objPlayer.GetComponent<Player> ();
    }

    /// <summary>
    /// Reset the specified WaitTime.
    /// </summary>
    /// <param name="WaitTime">Wait time.</param>
    public void Reset(float WaitTime = 1){
        //ShiftBulletを消す
        GameObject[] objs = GameObject.FindGameObjectsWithTag ("UnshiftableItem");
        foreach (GameObject obj in objs) {
            if (obj.GetComponent<StageObject> ().sotype == SOType.ShiftBullet) {
                obj.GetComponent<StageObject> ().Die ();
            }
        }
        StartCoroutine (ResetCoroutine (WaitTime));
        FadeIn ();
    }

    public void LoadMenu(){
        //ShiftBulletを消す
        GameObject[] objs = GameObject.FindGameObjectsWithTag ("UnshiftableItem");
        foreach (GameObject obj in objs) {
            if (obj.GetComponent<StageObject> ().sotype == SOType.ShiftBullet) {
                obj.GetComponent<StageObject> ().Die ();
            }
        }
        Instantiate (preLoadMenuHole, objPlayer.transform.position, Quaternion.identity);
        StartCoroutine (LoadMenuCoroutine ());
    }

    IEnumerator ResetCoroutine(float WaitTime){
        Time.timeScale = 0.35f;
        BGMPlayer.FadeoutBGM ();
        yield return new WaitForSeconds (WaitTime);
        Time.timeScale = 1f;
        SceneManager.LoadScene (SceneManager.GetActiveScene().name);
    }

    float LoadMenuDelay = 2;
    string LoadSceneName = "Title";
    IEnumerator LoadMenuCoroutine(){
        yield return new WaitForSeconds (LoadMenuDelay);

        SceneManager.LoadScene (LoadSceneName);
    }

    void Update () {
        Vector3 MousePos = Input.mousePosition;
        if(Input.GetKeyDown(KeyCode.Alpha0))
            SpawnObject(Objects[0], EnemiesParent, MousePos);

        if(Input.GetKeyDown(KeyCode.Alpha1))
            SpawnObject(Objects[1], EnemiesParent, MousePos);

        if(Input.GetKeyDown(KeyCode.Alpha2))
            SpawnObject(Objects[2], EnemiesParent, MousePos);

        if(Input.GetKeyDown(KeyCode.Alpha3))
            SpawnObject(Objects[3], EnemiesParent, MousePos);

        if(Input.GetKeyDown(KeyCode.Alpha4))
            SpawnObject(Objects[4], EnemiesParent, MousePos);

        if(Input.GetKeyDown(KeyCode.Alpha5))
            SpawnObject(Objects[4], EnemiesParent, MousePos);
    }

    //オブジェクトをステージに生成します。
    //ひとまずposはscreenpoint
    void SpawnObject(GameObject obj, GameObject parent, Vector3 pos){
        pos.z = 10;
        Vector3 wpos = Camera.main.ScreenToWorldPoint (pos);
        Instantiate (obj, wpos, Quaternion.identity, EnemiesParent.transform);
    }

    //フェードイン
    public void FadeIn(){
        ScreenPanel.GetComponent<Animator> ().SetBool ("FadeIn", true);
    }
}

PlayStageController.cs
PlayStageController.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.Linq;

public class PlayStageController : MonoBehaviour {

    MoveCamera MC;
    public GameObject tutorialHighlight;

    void Start(){
        //BGM
        BGMPlayer.FadeinBGM ("game");
        //BGMPlayer.PlayBGM("game");
        //initialize
        MC = GameObject.FindGameObjectWithTag("MainCamera").GetComponent<MoveCamera>();
        MC.SetState (MoveCamera.CameraState.LookClearHole);
        if (DataManager.LoadProgress() == 11)
        {
            Tutorial tutotiate = gameObject.AddComponent<Tutorial>();
            Transform[] touchlist = GameObject.Find("Items").GetComponentsInChildren<Transform>();
            touchlist = touchlist.Where(c => c.name != "Items" && c.parent.name == "Items").ToArray();
            tutotiate.TouchList = touchlist.Select(c => c.gameObject).ToList();
            tutotiate.playerComp = GameObject.FindGameObjectWithTag("Player").GetComponent<Player>();
            tutotiate.cameraComp = GameObject.FindGameObjectWithTag("MainCamera").GetComponent<MoveCamera>();
            tutotiate.highlightPrefab = tutorialHighlight;
        }
    }
}

デコードオブジェクト

事前に作成したテキストファイルをもとにステージを生成する。

Composit Collider 2Dのついたオブジェクトを親に指定する事で、
壁と壁の間をすり抜けないように工夫したりしている。

DecodeStageScript.cs
DecodeStageScript.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.Linq;

public class DecodeStageScript : MonoBehaviour {
    public StageObjectData stageObjectData;

    // Use this for initialization
    void Awake () {
        string text;
        GameObject[] trials = GameObject.FindGameObjectsWithTag("trialTag");
        if (trials.Length == 0)
        {
            TextAsset textAsset = DataManager.GetPlayStagecodeText();
            if(textAsset == null)
            {
                textAsset = (Resources.Load("Data/StageCode_Data") as StageCodeData).stagecodeText[0][0];
            }
            text = textAsset.text;

        }
        else
        {
            GameObject trialObject = trials[0];
            text = trialObject.GetComponent<EncodeStage.TrialCode>().encodetext;
        }

        string[] textline = text.Split('\n');

        foreach(string stageobjectText in textline.OrderBy(c=> int.Parse(c.Split(' ')[0])).ToList())
        {
            string[] indexString = stageobjectText.Split(' ');
            int objectID = int.Parse(indexString[0]);
            float objectx = float.Parse(indexString[1]);
            float objecty = float.Parse(indexString[2]);
            int groupID = int.Parse(indexString[3]);
            int[] abilityIDList = indexString.Skip(4).Select(x => int.Parse(x)).ToArray();
            //Debug.Log(abilityIDList.Count);
            var objectData = stageObjectData.objectList[objectID];
            GameObject objectPrefab = objectData.Prefab;
            GameObject instance;
            if (objectData.NoParent)
                instance = Instantiate(objectPrefab);
            else
            {
                if (objectData.haveID)
                {
                    if (GameObject.Find(objectData.parentName.ToString() + groupID) == null)
                    {
                        GameObject oldparent = Resources.Load<GameObject>("Prefab/" + objectData.parentName.ToString());
                        GameObject newParent = Instantiate(oldparent, GameObject.Find(oldparent.tag).transform);
                        newParent.name = objectData.parentName.ToString() + groupID;
                        /*if(objectData.Prefab.tag == "Block")
                        {
                            newParent = newParent.transform.Find("Walls").gameObject;
                        }*/
                        instance = Instantiate(objectPrefab, newParent.transform);
                    }
                    else
                    {
                        instance = Instantiate(objectPrefab, GameObject.Find(objectData.parentName.ToString()+groupID).transform);
                    }
                }
                else
                {
                    GameObject parent;
                    if((parent = GameObject.Find(objectData.parentName.ToString())) == null)
                    {
                        GameObject parentPrefab = Resources.Load<GameObject>("Prefab/" + objectData.parentName.ToString());
                        parent = Instantiate(parentPrefab, GameObject.Find(parentPrefab.tag).transform);
                        parent.name = objectData.parentName.ToString();
                    }
                    instance = Instantiate(objectPrefab, parent.transform);
                }

            }

            instance.transform.position = new Vector2(objectx, objecty);

            if(abilityIDList != null)
            {
                instance.GetComponent<StageObject>().SetStats(abilityIDList);
            }
        }

    }    
}

UI

UIは特にスクリプトを作っていない。
uGUIButtonOnClickをインスペクター上で設定するだけで済ませている。
大抵GameManagerの関数を呼び出す事が多い。

BGM・SE

BGM、SEstaticなクラスを作る事で簡単に音を流せるように工夫している。

BGMSourceScriptを持つゲームオブジェクトを作成。
BGM_Dataという名前のスクリプタブルオブジェクトを作る。

BGM_Dataにファイルkeyを設定する。
BGMPlayer.PlayBGM(key)で簡単に再生が可能。
SEも同様の手順で使える。

BGMSourceScript.cs
BGMSourceScript.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class BGMSourceScript : MonoBehaviour {


    void Awake()
    {
        if (GameObject.FindGameObjectsWithTag("BGMSource").Length > 1)
        {
            Destroy(gameObject);
        }
        else
        {
            DontDestroyOnLoad(gameObject);
            BGMPlayer.SetAudioSource(gameObject.GetComponent<AudioSource>());
            BGMPlayer.SetBGM(Resources.Load("Data/BGM_Data") as BGMData);
        }
    }


    void Update()
    {
        if (BGMPlayer.IsFadeoutBGM())
        {
            if (BGMPlayer.GetBGMVolume() <= 0)
            {
                BGMPlayer.SetBGMVolume(0);
                BGMPlayer.StopBGM();
                BGMPlayer.SetFadeoutBGM(false);
                BGMPlayer.ResetBGM();
            }
            else
            {
                BGMPlayer.SetBGMVolume(BGMPlayer.GetBGMVolume() - BGMPlayer.GetFadeoutBGMtime());
            }
        }
        if (BGMPlayer.IsFadeinBGM())
        {
            if (BGMPlayer.GetBGMVolume() >= BGMPlayer.GetMaxVolume())
            {
                BGMPlayer.SetBGMVolume(BGMPlayer.GetMaxVolume());
                BGMPlayer.SetFadeinBGM(false);
            }
            else
            {
                BGMPlayer.SetBGMVolume(BGMPlayer.GetBGMVolume() + BGMPlayer.GetFadeinBGMtime());
            }
        }
    }
}

SESourceScript.cs
SESourceScript.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class SEPlayer{
    static GameObject audioObject;
    static SEData SE_Data;
    //AudioSource audioSource=null;

    /*public SEPlayer(string keyname)
    {
        audioSource = audioObject.AddComponent<AudioSource>();
        audioSource.clip = SE_Data.SEList[keyname];
        audioSource.volume = SE_Data.SEVolumeList[keyname] * PlayerPrefs.GetFloat("SEVolume", 1);
    }*/

    public static void SetAudioSourceObject(GameObject source)
    {
        audioObject = source;
    }

    public static void SetSE(SEData data)
    {
        SE_Data = data;
    }

    public static void PlaySE(string keyname)
    {
        /*if (IsPlaying() == true)
        {
            StopSE();
        }*/
        AudioSource audioSource = audioObject.AddComponent<AudioSource>();
        audioSource.clip = SE_Data.SEList[keyname];
        audioSource.volume = SE_Data.SEVolumeList[keyname] * PlayerPrefs.GetFloat("SEVolume", 1);
        audioSource.Play();
    }

    /*public void StopSE(string keyname)
    {
        audioSource.Stop();
    }*/

    /*public bool IsPlaying()
    {
        return audioSource.isPlaying;
    }*/
}

BGMPlayer.cs
BGMPlayer.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class BGMPlayer{
    static AudioSource audioSource;
    static BGMData BGM_Data;
    static bool Fadeoutflag = false;
    static bool Fadeinflag = false;
    static float maxVolume;
    static float fadeouttime;
    static float fadeintime;

    public static void SetAudioSource(AudioSource source)
    {
        audioSource = source;
    }

    public static void SetBGM(BGMData data)
    {
        BGM_Data = data;
    }

    //BGM再生(音量設定なし)
    public static void PlayBGM(string keyname)
    {
        if (IsPlaying() == false)//シーン再読み込みのために消さない
        {
            audioSource.clip = BGM_Data.BGMList[keyname];
            audioSource.Play();
            audioSource.volume = BGM_Data.BGMVolumeList[keyname] * PlayerPrefs.GetFloat("BGMVolume", 1);
            maxVolume = audioSource.volume;
        }
    }

    // volumeの変更
    public static void ChangeVolume(float volume){
        if (volume < 0) volume = 0;
        else if (volume > 1) volume = 1;

        audioSource.volume = BGM_Data.BGMVolumeList[audioSource.clip.name] * PlayerPrefs.GetFloat("BGMVolume", 1);
    }

    public static float GetMaxVolume()
    {
        return maxVolume;
    }

    //BGM停止
    public static void StopBGM()
    {
        audioSource.Stop();
    }

    //フェードアウト
    public static void FadeoutBGM(float pertime=0.009f)
    {
        Fadeinflag = false;
        Fadeoutflag = true;
        fadeouttime = pertime;
    }

    public static float GetFadeoutBGMtime()
    {
        return fadeouttime;
    }

    //フェードイン
    public static void FadeinBGM(string keyname,float pertime=0.008f)
    {
        if (IsPlaying() == false && audioSource.clip != BGM_Data.BGMList[keyname])//シーン再読み込みのために消さない
        {
            StopBGM();
        }
        if (audioSource.clip != BGM_Data.BGMList[keyname])
        {
            audioSource.clip = BGM_Data.BGMList[keyname];
            audioSource.Play();
        }
        maxVolume = BGM_Data.BGMVolumeList[keyname] * PlayerPrefs.GetFloat("BGMVolume", 1);
        Fadeoutflag = false;
        audioSource.volume = 0;
        Fadeinflag = true;
        fadeintime = pertime;
    }

    public static float GetFadeinBGMtime()
    {
        return fadeintime;
    }

    public static void ResetBGM()
    {
        audioSource.clip = null;
    }

    public static bool IsFadeinBGM()
    {
        return Fadeinflag;
    }

    public static void SetFadeinBGM(bool flag)
    {
        Fadeinflag = flag;
    }

    public static bool IsFadeoutBGM()
    {
        return Fadeoutflag;
    }

    public static void SetFadeoutBGM(bool flag)
    {
        Fadeoutflag = flag;
    }

    public static void SetBGMVolume(float volume)
    {
        audioSource.volume = volume;
    }

    public static float GetBGMVolume()
    {
        return audioSource.volume;
    }

    public static bool IsPlaying()
    {
        return audioSource.isPlaying;
    }
}

SEPlayer.cs
SEPlayer.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class SEPlayer{
    static GameObject audioObject;
    static SEData SE_Data;
    //AudioSource audioSource=null;

    /*public SEPlayer(string keyname)
    {
        audioSource = audioObject.AddComponent<AudioSource>();
        audioSource.clip = SE_Data.SEList[keyname];
        audioSource.volume = SE_Data.SEVolumeList[keyname] * PlayerPrefs.GetFloat("SEVolume", 1);
    }*/

    public static void SetAudioSourceObject(GameObject source)
    {
        audioObject = source;
    }

    public static void SetSE(SEData data)
    {
        SE_Data = data;
    }

    public static void PlaySE(string keyname)
    {
        /*if (IsPlaying() == true)
        {
            StopSE();
        }*/
        AudioSource audioSource = audioObject.AddComponent<AudioSource>();
        audioSource.clip = SE_Data.SEList[keyname];
        audioSource.volume = SE_Data.SEVolumeList[keyname] * PlayerPrefs.GetFloat("SEVolume", 1);
        audioSource.Play();
    }

    /*public void StopSE(string keyname)
    {
        audioSource.Stop();
    }*/

    /*public bool IsPlaying()
    {
        return audioSource.isPlaying;
    }*/
}

コメント

需要があるか謎だが、スクリプトを全文公開してみた。

結構冗長な書き方をしてたり、コメント文が少ない。
実はいくつかバグも見つかってる...。

読むのが大変だと思うけど、実際に動いているコードだから信用はして良い。

このコードでどんな事が出来るかは、ゲームを見た方が早いかもしれない。

このゲームperticaは以下のリンクからインストールできるよ。

さて、次回はゲームのデザインの話でも書こうかな。

大学生が有料スマホゲームを作った全てを公開するよ(1)イントロダクション
大学生が有料スマホゲームを作った全てを公開するよ(2)開発環境とゲームの構成
大学生が有料スマホゲームを作った全てを公開するよ(3)ゲームの構造・パズルとアクションの仕組み(前編)
大学生が有料スマホゲームを作った全てを公開するよ(4)ゲームの成り立ち・パズルとアクションの仕組み(後編)

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Unity】【Android】ローカルストレージにファイルを出力する

ローカルストレージにファイルを出力する

※Unity2018.3.3f1を利用しています。

Androidのローカルストレージ内にXMLファイルを出力しようとしたのですが
ファイル自体はできあがるのに、なぜか中身が出力されません。

そういやManifestファイルを作っていなかったと思い、
Unityマニュアルを確認したところ、以下のように記載されていました。


Unity は、Player Settings とアプリケーションがスクリプトから呼び出す Unity APIs に基づいて、自動的に必要なパーミッションを加えます。例えば、以下のようなものが含まれます。

  • Network クラスにより INTERNET パーミッションを加えます。
  • バイブレーションの使用 (Handheld.Vibrate など) により VIBRATE を加えます。
  • InternetReachability プロパティーにより ACCESS_NETWORK_STATE を加えます。
  • Location APIs (LocationService など) により ACCESS_FINE_LOCATION を加えます。
  • WebCamTexture APIs により CAMERA パーミッションを加えます。
  • Microphone クラスにより RECORD_AUDIO を加えます。

と言うことは、Player Settingsに何らかの設定値がありそうです。
実際、以下のような項目がありました。

  • Write Access
    • External(SDCard) に設定すると、SD カードなどの外部ストレージへの書き込みアクセスを有効にし、対応する権限を Android マニフェストに追加します。 開発ビルドではデフォルトで有効になっています。

デフォルトでは「Internal」になっていましたが
SDカードに出力したい訳ではないので、Internalのままで良いだろうと思っていました。

ところが

ファイルは一向に出力されず・・・。

manifestファイルをPlaginsフォルダに入れることで、
Unityが自動で生成したmanifestを上書きできるようですが
manifestファイル自体は作成してくれないので、ゼロから自作しなくてはなりません。
ちょっとそれはめんどくさい

Write PermissionをExternalにしてみる

その1.png

ものは試しで、Write Permissionの値をExternal設定にしてみたところ
実機の内部ストレージにXMLファイルが出力されていました!
なんてこった。

考察

PlayerのWrite Permissionを「External(SDCard)」に設定することで
内部ストレージにXMLファイルを出力することができました。

クラスをそのまま出力したかったので、ファイル出力には
DataContractSerializerを使用したのですが
UnityEngine.PlayerPrefsなるクラスも準備されているようでして。

もしかして、これを使っていたら
Unityが自動的にパーミッションを加えていてくれたのかな?

おまけ

Unity Editorで動作させる場合と実機で動作させる場合とで
保存先のパスが変わってきます。
その場合、UNITY_EDITORプリプロセッサを使用してパスを切り替えると良いです。

    var filename = "SaveData.xml";
#if UNITY_EDITOR
    // Unity Editor上
    var filepath = Path.Combine(@"C:\", filename);
#else
    // それ以外
    var filepath = Path.Combine(UnityEngine.Application.persistentDataPath, filename));
#endif

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ML-Agentsで自動運転シミュレーション

はじめに

「自動運転シミュレーション」は車を壁や他の車に衝突させないように運転させる学習環境です。
車が壁やほかの車に衝突するとエピソードが完了し、長く運転し続けた方が報酬が多くなるように設定している。

使ったもの

方法

学習実行のコマンド

MacでUnity ML-Agentsの環境を構築する(v0.5.0対応)を参照

AnacondaNavigatorから作成した環境のTermialを起動する。

Terminal上で .../ml-agents-master/ml-agentsまで移動して、
$mlagents-learn trainer_config.yamlのパス --env=hoge1 --run-id=hoge2 --train
hoge1はUnityでBuildした実行ファイルの名前。ただし、拡張子(.exe, .app)は抜く。
hoge2は出力される学習モデル(ml-agents-master/ml-agents/models/hoge2-0/hoge1_hoge2.bytes)の名前。

ソースコードの構成

  • CarAcademy.cs:Academy(Academy.csのオーバーライド)
  • Brain.cs:Brain(ML-Agents/Scripts/Brain.cs)
  • CarAgent.cs:Agent
  • RayPerception.cs:Agentの衝突判定(ML-Agents/Examples/SharedAssets/Scripts/RayPerception.cs)

ソースコードの詳細

CarAcademy.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using MLAgents;

public class CarAcademy : Academy
{
    public override void AcademyReset()
    {

    }

    public override void AcademyStep()
    {

    }
}
CarAgent.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Sprites;
using MLAgents;

public class CarAgent : Agent {
    // 参照
    private CarRayPerception rayPer;
    private Rigidbody rigidbody;

    // 変数
    private Vector3 initPosition; //初期位置
    private Quaternion initRotation; // 初期向き
    private bool crush; //衝突フラグ

    //初期化時に呼ばれる
    public override void InitializeAgent()
    {
        // 参照
        this.rayPer = GetComponent<CarRayPerception> ();
        this.rigidbody = GetComponent<Rigidbody> ();

        // 変数
        this.initPosition = this.transform.position;
        this.initRotation = this.transform.rotation;
    }

    public override void AgentReset(){
        // 車の初期化
        this.transform.position = this.initPosition;
        this.transform.rotation = this.initRotation;
        rigidbody.velocity = new Vector3 (0, 0, 0);
        rigidbody.angularVelocity = new Vector3 (0, 0, 0);
        this.crush = false;
    }

    //Stateの取得
    public override void CollectObservations(){
        float rayDistance = 50.0f;
        float[] rayAngles = { 0f, 45f, 90f, 135f, 180f, 110f, 70f };
        string[] detectableObjects;
        detectableObjects = new string[] { "car", "wall" };
        AddVectorObs (rayPer.Perceive (rayDistance, rayAngles, detectableObjects, 1f, 0f));
    }

    // フレーム毎に呼ばれる
    public override void AgentAction(float[] vectorAction, string textAction){
        // アクション
        //float handle = Mathf.Clamp (vectorAction [0], -1.0f, 1.0f) * 1.5f; //教材のコードでは上手くいかなかった。
        float handle = Random.Range(-1.0f,1.0f)*3.0f;

        // 車の向きと加速度の指定
        this.gameObject.transform.Rotate (0, handle, 0);
        this.rigidbody.velocity = this.gameObject.transform.rotation * new Vector3 (0, 0, 20); //速度調整できる。

        // 報酬
        AddReward (0.001f);

        // エピソード完了
        if (this.crush)
            Done ();
    }

    // オブジェクト衝突時に呼ばれる
    void OnCollisionEnter(Collision collision){
        this.crush = true;
    }
}

Unityの画面

Hierarchy

学習環境内で独立して行動するAgent:jeep
学習環境を管理するAcademy:CarAcademy
Agentが観測した「状態」に応じて、Agentの「行動」を決定するBrain:CarBrain, PlayerBrain
carHierarchy.PNG

Scene

上から見下ろすと8台のjeepが原点を中心に配置される。
scene.PNG

CarAcademy(Academy)

CarAcademy.csを加える。
carAcademy.PNG

CarBrain(Brain)

Brain.csを加える。
carBrain.PNG

jeep(Agent)

jeepを8台用意する。その内1台のjeepにCameraを加えて実行時の走行の様子を確認する。
また、CarAgent.csとRayPerception.csを加える。CarAgent.csは3つのパラメータを持ち、jeepがCarSpeedの速さでTimeOut秒ごとに最大でMaxAngle度向きを変える。
jeep1.PNG

結果

100000回学習したもの

GreenTrue1.gif

200000回学習したもの

GreenTrue2.gif

300000回学習したもの

GreenTrue3.gif

学習後、Terminalから下のコマンドを打ち込む。
$tensorboard --logdir=summaries

TensorBoard 1.7.0 at http://WINDOWS-1O4MM0L:6006 (Press CTRL+C to quit)が表示されたら、URL(http://WINDOWS-1O4MM0L:6006 )からTensorBoardに移動。
TensorBoardを開くと次のように学習状況のグラフが表示される。
tecsorboard.PNG
マゼンタ:100000回, 深緑:200000回, シルバー:300000回学習した結果。

以下がそれぞれのグラフの説明。

  • Info/Lesson:レッスンの進捗状況(カリキュラム学習)
  • Info/cumulative_reward:平均累積報酬のグラフ(継続して増加し上下の振れ幅が小さいことが期待される。)
  • Info/entropy:Actionのランダムな動きの割合(継続的な減少が期待される。)
  • Info/episode_length:エピソードの平均長(今回は長いほうが良いので増加が期待される。)
  • Info/learning_rate:学習率のグラフ(時間とともに継続して減少する。)
  • Info/policy_loss:Actionの方策の変化の割合(学習成功時に減少し継続して減少することが期待される。)
  • Info/value_estimate:将来の報酬の予測(学習成功時に増加し継続して増加することが期待される。)
  • Info/value_loss:将来の報酬予測と実際の報酬の乖離(報酬は安定したら減少することが期待される。)

参考文献

Unityではじめる機械学習・強化学習 Unity ML-Agents実践ゲームプログラミング

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

LookingGlassのパラメータをunityのインスペクターから変える方法

経緯

・とある使い方のためLookingGlassのパラメータをコードでいじる方法が紹介されていた
・インスペクターからいじれるんだけどなぁ…ってモヤモヤした。

やりかた

1.HoloPlayerCaptureを選択
2.インスペクターをデバッグに切り替え
3.QuiltコンポーネントのConfigを開く
4.各パラメータいじり放題!!(パラメータによっては描画が崩れます)
image.png

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

なんでもDCCツールじゃなくてUnityでやりたい人向けまとめ

概要

ちょっとウェイト塗りなおしたい、ちょっとテクスチャスタンプ追加したい、ちょっとモーション修正したい、みたいなときにDCCツールを使わずにUnityで済ませられたらうれしい、という話です。
よく聞かれるのでまとめておきます。

ポリゴンモデリング

Pro Builder…おおまかに壁のサイズを出したりするのに使います。
(package manager)

UModeler…Pro Builderより高機能、無理やり剣とかの小物もモデリングできます。曲面が多いと無限につらい。
https://assetstore.unity.com/packages/tools/modeling/umodeler-80868

プロシージャルモデリング(パラメトリックモデリング)

Archimatix Pro…非破壊のノードベースモデリング、Houdiniに近いやつがあったはず。
https://assetstore.unity.com/packages/tools/modeling/archimatix-pro-59733

スキニング、ボーン追加

Puppet3D…スカートに骨を足したり、揺れ物を増やすための終端ボーンを追加したり、よく見ると破綻してたウェイトをこっそり修正したりします。
https://assetstore.unity.com/packages/tools/animation/puppet3d-111554

Substance Painter

Surforge…ディティールアップ向け、柔らかい系に使うのはいけるのかな。
https://assetstore.unity.com/packages/tools/level-design/surforge-79171

アニメーション作成

Very Animation…ゼロから手付するのにおすすめ。Blendshapeのキーも打てる
https://assetstore.unity.com/packages/tools/animation/very-animation-96826

UMotionPro…モーキャプデータ修正。貧者のMotionBuilderです。
https://assetstore.unity.com/packages/tools/animation/umotion-pro-animation-editor-95991

まとめ

この辺で作業した結果をUnity FBX Exporterで出力して調整したりします。

https://assetstore.unity.com/packages/essentials/fbx-exporter-101408

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Vuforia】★5の状態から、認識精度をさらに上げる(ブルブルを軽減する)

壁にマーカーを貼り付けた状態でARしているのですが、
★5なのに、ちょいちょいブレるんやけど!!と思ったので、備忘録です。

目次
ターゲットサイズ(ARマーカー)
認識精度が影響する要因
印刷用紙
Extended Trackingの有効化

ターゲットサイズ(ARマーカー)

カメラとターゲット間の距離が開くほど、ターゲットを大きくする必要がある。
・少なくとも12cm推奨だそう
・検出可能な距離は、大体、ターゲットサイズの10倍。
 例) 30cm幅のターゲットの場合・・・30cm × 10倍 = おおよそ300cm(3m)の距離まで検出可能

その他要因

・ターゲットが部屋の光でテカっていないか? →印刷用紙
・ターゲットがヨレていないか?       →印刷用紙
・端末カメラのフォーカスモード
・アップロード画像を320px以上のものにすると◎ ※アンチエイリアス処理後の、画像ターゲットの検出率に影響を与えるのを回避する

印刷用紙

・光沢のない用紙を使用する → EPSON フォトマット紙 A4 50枚 KA450PM がオススメ
・水平なマーカーにするために、厚紙使用や、ボードに貼り付ける等。

Extended Tracking(拡張トラッキング)を有効にする

カメラからマーカーが外れても、表示し続けるようになります。
マーカーをギリギリ認識の状態でブルブルしてるのが、軽減します。

設定方法 ※試した環境 Unity : 2018.2.16f / Vuforia Unity Extention Version : 8.0.10

①Windows > Vuforia Configuration をクリック
image.png

②少し下の方にある [Device Tracker]の Track Device Pose にチェック
③Tracking mode は POSITIONALに
image.png

参考にしたページ

Tutti Lab - http://tuti107.hatenablog.com/entry/2016/12/01/163148
Vuforia資料庫 - https://www.cnsrd.jp/vuforia-library/howto/index.html
 
 
 
 
これでかなりブルブルが安定するはーず。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Unity】Standard AssetsがImport Packageに表示されていないときの対処方法

背景

Unity5 3D/2Dゲーム開発 実践入門 作りながら覚えるスマートフォンゲーム制作(吉谷 幹人) | 書籍 本 | ソシムの本で勉強しているときに、「Assets」→「Import Package」から、Standard Assetsを選択してください。的な操作がありました。
しかし、自分のUnityは、Custom Packageしか表示されていません。

import_package.png

動作確認環境

  • Windows 10
  • Unity 2018.3.1f1

解決方法

「Window」→「Asset Store」を開いて、Standart Assetsと入力して検索してください。
次に、画像の一番上にあるUnity Technologiesと書いてあるタブをクリックしてください。

standard_assets2.png

ピンクのImportボタンを押下してください。
Importしたいものにチェックを入れて、Importボタンを押下すればStandard Assetsを使用することができます。

standard_assets.png

終わりに

おそらくUnityが日々バージョンアップしていくなかで、Standard AssetsはAssets Storeからインストールするものになったのだと思います。

ググり能力が低いせいか、なかなか解決できませんでした。
忘れては困ると思ったので、自分の備忘録がてら記事にしました。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

UnityでGamePadの個数を取得する

はじめに

こんにちは、避雷です。今回は現在開発中の「BoosterBoots」というゲームで複数のコントローラを一つのPCに接続するタイプのマルチプレイを実装する過程で得た知見を公開します。というか助けて…
https://twitter.com/lucknknock/status/1095883926503968768

実装

Input.GetJoystickNames().Lengthの利用

Unityでゲームパッドの接続台数を調べる
こちらの記事を参考にして接続されたコントローラの個数を取得しようとしました。

var controllerNames = Input.GetJoystickNames();

Debug.Log(controllerNames.Length);

この手法はランタイム中にコントローラの抜き差しが発生しない状態では正しい個数を取得できるのですが、ランタイム中にコントローラの抜き差しが発生すると、「コントローラ名が""(空白)のコントローラが存在する扱いになる」という不具合があり、コレのせいで抜き差しするたびに接続コントローラ数が増えたり増えなかったりします。

対策方法

誤って生成されるコントローラは常に""(空白)という名称なので、これを除去してあげてカウントすれば正しいコントローラ個数を取得することができます。

        string[] cName = Input.GetJoystickNames();
        currentConnectionCount = 0;
        for (int i = 0; i < cName.Length; i++)
        {
            if(cName[i] != "")
            {
                currentConnectionCount++;
            }
        }

こうすれば正しいコントローラ数を取得できます。

残る問題点

今回の解決策は決して抜本的なものではなく、その場しのぎの杜撰な対応なので、Unityのコントローラ同時接続周りの滅茶苦茶具合が直ったわけではありません。残るバグの一例として

  • コントローラの接続順が入れ替わる
  • 存在しないはずのコントローラが接続される
  • そもそもキーコンフィグを動的に変更できない

などがあります。Unity2018.3から実装された新しいInputSystemではある程度改善されているらしいのですが、いかんせん資料が少ないです…
Unity2018.2以前でこれをやるにはお高めのアセットを買うしかなさそうで結構辛いです。誰か助けて…

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Unityちゃんが落下する際の設定ミス

事象

Unityちゃんを動かしてみようとしたところ、開始直後にすり抜けて落下していった

事象発生までの設定作業

①AssetStoreからUnityちゃんをダウンロード
②Prefabs内からUnityちゃんをドラッグ&ドロップ
③ControllerをThirdPersonAnimatorControllerに変更
④IdleChangeとFaceUpdateをデタッチ
⑤ThirdPersonUserControlをAddComponent
⑥位置を調整

原因

コライダーがUnityちゃんの位置より上に位置していたため、当たり判定が上に位置していた

解決

コライダーのCenterを調整して、Unityちゃんがすり抜けをしないようにした

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む