20200518のUnityに関する記事は7件です。

【Unity】非同期処理を理解する〜コルーチン編〜

コルーチンとは

  • 非同期処理できる。
  • 関数を任意のタイミングで中断・再開できる機能。
  • 複数の処理を疑似並列できる。(完全な並列ではない)
  • Unity のメインスレッドで処理する。
  • 結果は返せない。
  • GameObject と処理が紐づく。
  • IEnumerator 型を戻り値とした関数を定義する。
  • コルーチンを実行したいときは StartCoroutine(string コルーチン名) または StartCoroutine(IEnumerator型の変数)
  • コルーチンを停止(中断)したいときは StopCoroutine(string コルーチン名) または StopCoroutine(IEnumerator型の変数)

コルーチンの主要な機能一覧

機能 詳細
yield return null 1フレーム分処理を中断、次のフレームで続きの行を処理
yield break コルーチンを途中で終了、再開はできない
yield return new WaitForSeconds(float seconds) 指定した seconds 秒、コルーチンを中断する
yield return new WaitUntil(Func predicate) predicate の関数の返り値が true になったら処理を再開する
yield return new WaitWhile(Func predicate) predicate の関数の返り値が false になったら処理を再開する
yield return StartCoroutine() 指定したコルーチンを実行、完了するまで後続処理は行わない
StopAllCoroutines() コルーチンを全て止める

yield return null

yield return null は、1フレーム分処理を中断して、次のフレームで続きの行を処理します。

using System.Collections;
using UnityEngine;

public class Test : MonoBehaviour
{
    void Start()
    {
        IEnumerator sample1 = Sample1();
        IEnumerator sample2 = Sample2();
        StartCoroutine(sample1);
        StartCoroutine(sample2);
    }

    IEnumerator Sample1()
    {
        Debug.Log("Sample1 Start.");
        yield return null;            // Sample1()の処理は1フレーム待機
        Debug.Log("Sample1 End.");
    }

    IEnumerator Sample2()
    {
        Debug.Log("Sample2 Start.");
        yield return null;            // Sample2()の処理は1フレーム待機
        Debug.Log("Sample2 End.");
    }
}

だから、結果は下の通りで、
1. Sample1 Start.
2. Sample2 Start.
3. Sample1 End.
4. Sample2 End.
の順で実行される。
image.png

yield break

yield break は、コルーチンを途中で終了します。
再開はできないです。

using System.Collections;
using UnityEngine;

public class Test : MonoBehaviour
{
    void Start()
    {
        IEnumerator sample1 = Sample1();
        IEnumerator sample2 = Sample2();
        StartCoroutine(sample1);
        StartCoroutine(sample2);
    }

    IEnumerator Sample1()
    {
        Debug.Log("Sample1 Start.");
        yield break;                  // Sample1() の処理はここで止める
        Debug.Log("Sample1 End.");
    }

    IEnumerator Sample2()
    {
        Debug.Log("Sample2 Start.");
        yield break;                  // Sample2() の処理をここで止める
        Debug.Log("Sample2 End.");
    }
}

コルーチンを途中で終了したので、Sample1 End.Sample2 End. は出力されません。
image.png

yield return new WaitForSeconds(float seconds)

yield return new WaitForSeconds(seconds) は、指定した秒数の間、コルーチンの実行を待ちます。

using System.Collections;
using UnityEngine;

public class Test : MonoBehaviour
{
    void Start()
    {
        IEnumerator sample1 = Sample1();
        IEnumerator sample2 = Sample2();
        StartCoroutine(sample1);
        StartCoroutine(sample2);
    }

    IEnumerator Sample1()
    {
        Debug.Log("Sample1 Start.");
        yield return null;            // Sample1()の処理は1フレーム待機
        Debug.Log("Sample1 End.");
    }

    IEnumerator Sample2()
    {
        Debug.Log("Sample2 Start.");
        yield return new WaitForSeconds (1.0f); // Sample2()の処理は1秒待機
        Debug.Log("Sample2 End.");
    }
}

Sample2 End. が、他より1秒経過してから出力されるようになります。
image.png

yield return new WaitUntil(Func predicate)

yield return new WaitUntil(Func<bool> predicate) は、predicate で指定した関数が true を返したときに再開します。

using System.Collections;
using UnityEngine;

public class Test : MonoBehaviour
{
    bool flg = false;
    void Start()
    {
        IEnumerator sample1 = Sample1();
        IEnumerator sample2 = Sample2();
        StartCoroutine(sample1);
        StartCoroutine(sample2);
    }

    IEnumerator Sample1()
    {
        Debug.Log("Sample1 Start.");
        yield return new WaitUntil(() => flg == true); // flg が true になるまで処理が止まる
        Debug.Log("Sample1 End.");
    }

    IEnumerator Sample2()
    {
        Debug.Log("Sample2 Start.");
        yield return null;
        Debug.Log("Sample2 End.");
        flg = true; // Sample2()の最後に flg を true にする
    }
}

flg が true になるまで、 Sample1 End. が出力されないため、下記のような結果になります。
image.png

yield return new WaitWhile(Func predicate)

yield return new WaitWhile(Func<bool> predicate) は、predicate で指定した関数が false を返したときに再開します。
WaitUntil(Func<bool> predicate) の逆バージョン。
使い方は上と一緒なので省略。

yield return StartCoroutine()

yield return StartCoroutine() は、指定したコルーチンを実行し、完了するまで処理を中断します。

using System.Collections;
using UnityEngine;

public class Test : MonoBehaviour
{
    bool flg = false;
    void Start()
    {
        IEnumerator sample1 = Sample1();
        StartCoroutine(sample1);
    }

    IEnumerator Sample1()
    {
        Debug.Log("Sample1 Start.");
        IEnumerator sample2 = Sample2();
        yield return StartCoroutine(sample2); // Sample2() を実行が終わるまで待機
        Debug.Log("Sample1 End.");
    }

    IEnumerator Sample2()
    {
        Debug.Log("Sample2 Start.");
        yield return null;           // Sample2()の処理は1秒待機
        Debug.Log("Sample2 End.");
    }
}

結果は下のような感じです。
image.png

StopAllCoroutines()

名前の通り、コルーチンの全てを止めます。
Behavior 上で実行されている全てを止めます。

using System.Collections;
using UnityEngine;

public class Test : MonoBehaviour
{
    void Start()
    {
        IEnumerator sample1 = Sample1();
        IEnumerator sample2 = Sample2();
        StartCoroutine(sample1);
        StartCoroutine(sample2);
        StopAllCoroutines(); // 全てのコルーチンを止める
    }

    IEnumerator Sample1()
    {
        Debug.Log("Sample1 Start.");
        yield return null;
        Debug.Log("Sample1 End.");
    }

    IEnumerator Sample2()
    {
        Debug.Log("Sample2 Start.");
        yield return null;
        Debug.Log("Sample2 End.");
    }
}

コルーチンを途中で終了したので、Sample1 End.Sample2 End. は出力されません。
image.png

終わりに

Unity 初心者なので、間違いがあったら教えてくれるとありがたいです。
今まで雰囲気で非同期処理を書いていたので、しっかり勉強するために記事を書くことにしました。
いまのところ、async/await編、UniRx編を書く予定です。
挫折したらごめんなさい。

参考文献

コルーチン - Unity マニュアル
【C#/Unity】コルーチン(Coroutine)とは何なのか
Unityのコルーチン機能を使う

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

スマホゲーム作る人はEventTriggerを使え!!(キャラをUI上の方向キーで動かす)

実現できること

下の写真のようにボタンを設置して押したらキャラが動くようになる。
スクリーンショット 2020-05-18 20.14.44.png

やること

(1)ボタンを作る

なんでもいいのでボタンを配置する
スクリーンショット 2020-05-18 20.14.36.png
スクリーンショット 2020-05-18 20.14.44.png

(2)スクリプトを書く

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

public class PlayerManager : MonoBehaviour
{
    //画面の上のボタンによる移動
    bool rightmove;
    bool leftmove;

    void Update()
    {
        if (rightmove == true)
        {
            transform.position += new Vector3(4f * Time.deltaTime, 0, 0);
        }
        if (leftmove == true)
        {
            transform.position += new Vector3(-4f * Time.deltaTime, 0, 0);
        }
    }

    public void rightButtonDown()
    {
        rightmove = true;
    }
    public void rightButtonUp()
    {
        rightmove = false;
    }
    public void leftButtonDown()
    {
        leftmove = true;
    }
    public void leftButtonUp()
    {
        leftmove = false;
    }

(3)EventTriggerを追加する

スクリーンショット 2020-05-18 20.19.13.png
Pointer Down と Pointer Upをそれぞれ作って
PLayerManager.csがついたオブジェクトをドラッグする
そして、のところから任意のものを選択する

以上。

わからないことがあれば
@e_san_desuyo まで

追記:動くスピードはスクリプトから変更可能です

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

【Unity】webgl向けにビルドしたときのvectorのずれを直す方法

現状

以下のソースを用いてprefabをインスタンス化しているのですが、Unity上で動かしたときとWeb上にアップロードした後でオブジェクトが作られる位置がずれてしまいます。
Unity上ではWebGLのデフォルトのサイズである960x600で作っており、index.htmlのstyleも960×600なのですがなぜずれてしまうのでしょうか?
また、直す方法はないでしょうか?

ソース
// prefabのkomaをインスタンス化する。
GameObject koma = (GameObject)Instantiate(obj,new Vector3(332,291.5,0),Quaternion.identity,komaParent);

▼Unity上で動かしたとき
無題1.png

▼Webサーバー上で動かしたとき
無題2.png

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

Update内で処理を待つ

N秒おきにCUBEの表示をONOFFする。

task/async/await

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.Threading.Tasks;

public class Delay2 : MonoBehaviour {

    void Start () {
    }
    async Task Update () {

        await Vanish();//1secまって消す

        if( gameObject.active == false) {
            //1secまってつける
            await Task.Delay (1000*1);
            gameObject.SetActive (true);

        }
    }
    async Task Vanish () {
        await Task.Delay (1000*1);
        gameObject.SetActive (false);
    }
}

メモ:

久しぶりに使ったらハマったのでメモ
```
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.Threading.Tasks;
public class Delay : MonoBehaviour {
void Start () {
Func3(); //これは遅延する
}
async Task Update () {
//Func3(); //ここから読んだら遅延しない。
}
async void Func3() {
int cnt = 0;
await Task.Delay(1000);
Debug.Log(cnt.ToString());
cnt++;
await Task.Delay(1000);
Debug.Log(cnt.ToString());

    cnt++;
    await Task.Delay(1000);
    Debug.Log(cnt.ToString());
}

}

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

Unityで『Unexpected character』というエラーが出ているが、おかしい場所(部分)が見当たらない。

 実現したいこと

  • スクリプトのエラーの解消
  • Unityで作成したプログラムを動かしたい

 困っていること

Unityで『Object2Terrain』のスクリプトを書いていたのですが、下記のようなメッセージが出た上、メニューアイテムにこのプログラムが追加されないので困っています…。プログラムはVisual studio 2019で書きました。

 発生しているエラーメッセージ

No MonoBehaviour scripts in the file, or their names do not match fale name.

 試したこと

- クラス名の修正
- 大文字、小文字のミスがないか確認
- using System.Collection;の追加

using UnityEngine;
using UnityEditor;
using System.Collection;

 コード比較(修正前、修正後)

 修正前のコード

using UnityEngine;
using UnityEditor;

public class Object2Terrain : EditorWindow
{

    [MenuItem("Terrain/Object to Terrain", false, 2000)]
    static void OpenWindow()
    {

        EditorWindow.GetWindow<Object2Terrain>(true);
    }

    private int resolution = 512;
    private Vector3 addTerrain;
    int bottomTopRadioSelected = 0;
    static string[] bottomTopRadio = new string[] { "Bottom Up", "Top Down" };
    private float shiftHeight = 0f;

    void OnGUI()
    {

        resolution = EditorGUILayout.IntField("Resolution", resolution);
        addTerrain = EditorGUILayout.Vector3Field("Add terrain", addTerrain);
        shiftHeight = EditorGUILayout.Slider("Shift height", shiftHeight, -1f, 1f);
        bottomTopRadioSelected = GUILayout.SelectionGrid(bottomTopRadioSelected, bottomTopRadio, bottomTopRadio.Length, EditorStyles.radioButton);

        if (GUILayout.Button("Create Terrain"))
        {

            if (Selection.activeGameObject == null)
            {

                EditorUtility.DisplayDialog("No object selected", "Please select an object.", "Ok");
                return;
            }

            else
            {

                CreateTerrain();
            }
        }
    }

    delegate void CleanUp();

    void CreateTerrain()
    {

        //fire up the progress bar
        ShowProgressBar(1, 100);

        TerrainData terrain = new TerrainData();
        terrain.heightmapResolution = resolution;
        terrain.SetDetailResolution(resolution, 100);
        GameObject terrainObject = Terrain.CreateTerrainGameObject(terrain);

        Undo.RegisterCreatedObjectUndo(terrainObject, "Object to Terrain");

        MeshCollider collider = Selection.activeGameObject.GetComponent<MeshCollider>();
        CleanUp cleanUp = null;

        //Add a collider to our source object if it does not exist.
        //Otherwise raycasting doesn't work.
        if (!collider)
        {

            collider = Selection.activeGameObject.AddComponent<MeshCollider>();
            cleanUp = () => DestroyImmediate(collider);
        }

        Bounds bounds = collider.bounds;
        float sizeFactor = collider.bounds.size.y / (collider.bounds.size.y + addTerrain.y);
        terrain.size = collider.bounds.size + addTerrain;
        bounds.size = new Vector3(terrain.size.x, collider.bounds.size.y, terrain.size.z);

        // Do raycasting samples over the object to see what terrain heights should be
        float[,] heights = new float[terrain.heightmapWidth, terrain.heightmapHeight];
        Ray ray = new Ray(new Vector3(bounds.min.x, bounds.max.y + bounds.size.y, bounds.min.z), -Vector3.up);
        RaycastHit hit = new RaycastHit();
        float meshHeightInverse = 1 / bounds.size.y;
        Vector3 rayOrigin = ray.origin;

        int maxHeight = heights.GetLength(0);
        int maxLength = heights.GetLength(1);

        Vector2 stepXZ = new Vector2(bounds.size.x / maxLength, bounds.size.z / maxHeight);

        for (int zCount = 0; zCount < maxHeight; zCount++)
        {

            ShowProgressBar(zCount, maxHeight);

            for (int xCount = 0; xCount < maxLength; xCount++)
            {

                float height = 0.0f;

                if (collider.Raycast(ray, out hit, bounds.size.y * 3))
                {

                    height = (hit.point.y - bounds.min.y) * meshHeightInverse;
                    height += shiftHeight;

                    //bottom up
                    if (bottomTopRadioSelected == 0)
                    {

                        height *= sizeFactor;
                    }

                    //clamp
                    if (height < 0)
                    {

                        height = 0;
                    }
                }

                heights[zCount, xCount] = height;
                rayOrigin.x += stepXZ[0];
                ray.origin = rayOrigin;
            }

            rayOrigin.z += stepXZ[1];
            rayOrigin.x = bounds.min.x;
            ray.origin = rayOrigin;
        }

        terrain.SetHeights(0, 0, heights);

        EditorUtility.ClearProgressBar();

        if (cleanUp != null)
        {

            cleanUp();
        }
    }

    void ShowProgressBar(float progress, float maxProgress)
    {

        float p = progress / maxProgress;
        EditorUtility.DisplayProgressBar("Creating Terrain...", Mathf.RoundToInt(p * 100f) + " %", p);
    }
}

 修正後のコード

using UnityEngine;
using UnityEditor;
using System.Collections;

public class Object2Terrain : EditorWindow {

    [MenuItem("Terrain/Object to Terrain", false, 2000)]
    static void OpenWindow() => EditorWindow.GetWindow(typeof(Object2Terrain))(true);

    private int resolution = 512;
    private Vector3 addTerrain;
    int bottomTopRadioSelected = 0;
    static string[] bottomTopRadio = new string[] { "Bottom Up", "Top Down"};
    private float shiftHeight = 0f;

    void OnGUI () {

        resolution = EditorGUILayout.IntField("Resolution", resolution);
        addTerrain = EditorGUILayout.Vector3Field("Add terrain", addTerrain);
        shiftHeight = EditorGUILayout.Slider("Shift height", shiftHeight, -1f, 1f);
        bottomTopRadioSelected = GUILayout.SelectionGrid(bottomTopRadioSelected, bottomTopRadio, bottomTopRadio.Length, EditorStyles.radioButton);

        if(GUILayout.Button("Create Terrain")){

            if(Selection.activeGameObject == null){

                EditorUtility.DisplayDialog("No object selected", "Please select an object.", "Ok");
                return;
            }

            else{

                CreateTerrain();
            }
        }
    }

    delegate void CleanUp();

    void CreateTerrain(){    

        //fire up the progress bar
        ShowProgressBar(1, 100);

        TerrainData terrain = new TerrainData();
        terrain.heightmapResolution = resolution;
        GameObject terrainObject = Terrain.CreateTerrainGameObject(terrain);

        Undo.RegisterCreatedObjectUndo(terrainObject, "Object to Terrain");

        MeshCollider collider = Selection.activeGameObject.GetComponent<MeshCollider>();
        CleanUp cleanUp = null;

        //Add a collider to our source object if it does not exist.
        //Otherwise raycasting doesn't work.
        if(!collider){

            collider = Selection.activeGameObject.AddComponent<MeshCollider>();
            cleanUp = () => DestroyImmediate(collider);
        }

        Bounds bounds = collider.bounds;    
        float sizeFactor = collider.bounds.size.y / (collider.bounds.size.y + addTerrain.y);
        terrain.size = collider.bounds.size + addTerrain;
        bounds.size = new Vector3(terrain.size.x, collider.bounds.size.y, terrain.size.z);

        // Do raycasting samples over the object to see what terrain heights should be
        float[,] heights = new float[terrain.heightmapWidth, terrain.heightmapHeight];    
        Ray ray = new Ray(new Vector3(bounds.min.x, bounds.max.y + bounds.size.y, bounds.min.z), -Vector3.up);
        RaycastHit hit = new RaycastHit();
        float meshHeightInverse = 1 / bounds.size.y;
        Vector3 rayOrigin = ray.origin;

        int maxHeight = heights.GetLength(0);
        int maxLength = heights.GetLength(1);

        Vector2 stepXZ = new Vector2(bounds.size.x / maxLength, bounds.size.z / maxHeight);

        for(int zCount = 0; zCount < maxHeight; zCount++){

            ShowProgressBar(zCount, maxHeight);

            for(int xCount = 0; xCount < maxLength; xCount++){

                float height = 0.0f;

                if(collider.Raycast(ray, out hit, bounds.size.y * 3)){

                    height = (hit.point.y - bounds.min.y) * meshHeightInverse;
                    height += shiftHeight;

                    //bottom up
                    if(bottomTopRadioSelected == 0){

                        height *= sizeFactor;
                    }

                    //clamp
                    if(height < 0){

                        height = 0;
                    }
                }

                heights[zCount, xCount] = height;
                   rayOrigin.x += stepXZ[0];
                   ray.origin = rayOrigin;
            }

            rayOrigin.z += stepXZ[1];
              rayOrigin.x = bounds.min.x;
              ray.origin = rayOrigin;
        }

        terrain.SetHeights(0, 0, heights);

        EditorUtility.ClearProgressBar();

        if(cleanUp != null){

            cleanUp();    
        }
    }

    void ShowProgressBar(float progress, float maxProgress){

        float p = progress / maxProgress;
        EditorUtility.DisplayProgressBar("Creating Terrain...", Mathf.RoundToInt(p * 100f)+ " %", p);
    }
}

 修正したのは良いが

修正しましたが、そしたらまたエラーメッセージが…。

Assets\Editor\object2terrain.cs(1,1):error CS1056: Unexpected character"

そんなメッセージが出たので再度修正作業を行いました。

 2回目の修正後のコード

using UnityEngine;
using UnityEditor;
using System.Collections;

public class Object2Terrain : EditorWindow {

    [MenuItem("Terrain/Object2Terrain", false, 2000)]
    static void OpenWindow() => EditorWindow.GetWindow(typeof(Object2Terrain))(true);

        private int resolution = 512;
    private Vector3 addTerrain;
    int bottomTopRadioSelected = 0;
    static string[] bottomTopRadio = new string[] ("Bottom Up", "Top Down");
    private float shiftHeight = 0f;

    void OnGUI () {

        resolution = EditorGUILayout.IntField("Resolution", resolution);
        addTerrain = EditorGUILayout.Vector3Field("Add terrain", addTerrain);
        shiftHeight = EditorGUILayout.Slider("Shift height", shiftHeight, -1f, 1f);
        bottomTopRadioSelected = GUILayout.SelectionGrid(bottomTopRadioSelected, bottomTopRadio, bottomTopRadio.Length, EditorStyles.radioButton);

        if(GUILayout.Button("Create Terrain")){

            if(Selection.activeGameObject == null){

                EditorUtility.DisplayDialog("No object selected", "Please select an object.", "Ok");

                                return;
            }


                    else{

                CreateTerrain();
            }
        }
    }


        delegate void CleanUp();

    void CreateTerrain(){   

        //Fire up the progress bar
        ShowProgressBar(1, 100);

        TerrainData terrain = new TerrainData();
        terrain.heightmapResolution = resolution;
        GameObject terrainObject = Terrain.CreateTerrainGameObject(terrain);

        Undo.RegisterCreatedObjectUndo(terrainObject, "Object2Terrain");

        MeshCollider collider = Selection.activeGameObject.GetComponent<MeshCollider>();
        CleanUp cleanUp = null;

        //Add a collider to our source object if it does not exist.
        //Otherwise raycasting doesn't work.
        if(!collider){

            collider = Selection.activeGameObject.AddComponent<MeshCollider>();
            cleanUp = () => DestroyImmediate(collider);
                    }


        Bounds bounds = collider.bounds;    
        float sizeFactor = collider.bounds.size.y / (collider.bounds.size.y + addTerrain.y);
        terrain.size = collider.bounds.size + addTerrain;
        bounds.size = new Vector3(terrain.size.x, collider.bounds.size.y, terrain.size.z);

        // Do raycasting samples over the object to see what terrain heights should be
        float[,] heights = new float[terrain.heightmapWidth, terrain.heightmapHeight];  
        Ray ray = new Ray(new Vector3(bounds.min.x, bounds.max.y + bounds.size.y, bounds.min.z), -Vector3.up);
        RaycastHit hit = new RaycastHit();
        float meshHeightInverse = 1 / bounds.size.y;
        Vector3 rayOrigin = ray.origin;

        int maxHeight = heights.GetLength(0);
        int maxLength = heights.GetLength(1);

        Vector2 stepXZ = new Vector2(bounds.size.x / maxLength, bounds.size.z / maxHeight);

        for(int zCount = 0; zCount < maxHeight; zCount++){

            ShowProgressBar(zCount, maxHeight);

            for(int xCount = 0; xCount < maxLength; xCount++){

                float height = 0.0f;

                if(collider.Raycast(ray, out hit, bounds.size.y * 3)){

                    height = (hit.point.y - bounds.min.y) * meshHeightInverse;
                    height += shiftHeight;

                    //bottom up
                    if(bottomTopRadioSelected == 0){

                        height *= sizeFactor;
                    }

                    //clamp
                    if(height < 0){

                        height = 0;
                                  }
                }

                heights[zCount, xCount] = height;
                rayOrigin.x += stepXZ[0];
                ray.origin = rayOrigin;
            }

            rayOrigin.z += stepXZ[1];
            rayOrigin.x = bounds.min.x;
            ray.origin = rayOrigin;
        }

        terrain.SetHeights(0, 0, heights);

        EditorUtility.ClearProgressBar();

        if(cleanUp != null){

            cleanUp();    
        }
    }

    void ShowProgressBar(float progress, float maxProgress){

        float p = progress / maxProgress;
        EditorUtility.DisplayProgressBar("Creating Terrain...", Mathf.RoundToInt(p * 100f)+ " %", p);
    }

 しかし!

Assets\Editor\object2terrain.cs(1,1):error CS1056: Unexpected character"

このメッセージが消えず…。なにか対処法はありますか!?

自分にはミスがわからないので救世主が欲しいです…。

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

SpeedGun (SR3600)で遊んでみた話

ボールや車などの速度を図ることのできるSpeedGunを触る機会があり、
面白そうだったのでPCに接続してUnityでデータを取得できるようにしてみた時のメモ。

SpeedGun選定

いろいろ種類はあったけど、外部出力機能としてRJ45(RS232-C変換付き)を搭載した
SR3600というモデルがあったのでこれを使うことにした。

接続テスト

シリアル通信で拾えるか試してみる。
RS232-Cの仕様通りに接続したがデータは拾えなかったので、
RJ45のピンアサインを元にArduinoと接続したところ、
文字化けはしているが何かしらのデータは流れてきた。

serialTest.png

ちなみに結線は以下の通り。

connection.png

流れてきたデータを見ると経験上、バイナリデータだと思われるのでアスキーに変換してみる。

データ変換と解析

ArduinoのString(bytes, HEX)を使って16進数に変換してみたところ、
規則性のあるデータが取れた。

hex.png

何度もボールを投げてデータを収集し、
どのデータが何を指しているのかをデータを解析する。

description byte
1 startByte 0x00
2 Fixed 0x20
3 Fixed 0x03
4 Fixed 0x06
5 100の位 [numbers]
6 10の位 [numbers]
7 1の位 [numbers]
8 Fixed 0x03
9 マイル or メートル 0x5A or 0x59
10 Fixed 0x05
11 Fixed 0x0B
12 Fixed 0x03
13 checksum? 0x06
14 Fixed 0x3D
15 endByte 0x79

Unityとの連携

ArduinoでUnityにデータを送るスクリプトを書く。

SpeedGun.ino
#define START_BYTE 0
#define END_BYTE 121

int bytes;

String str_data;
String str_array;

bool isStart = false;

void setup() {
  // put your setup code here, to run once:
  Serial.begin(1200);
}

void loop() {
  // put your main code here, to run repeatedly:
  if (Serial.available()) {

    bytes = Serial.read();
    if (bytes == START_BYTE) {
      isStart = true;
      str_array = "";
      str_data = "";
    }
    else if (bytes == END_BYTE) {
      str_array = str_array + String(END_BYTE, HEX);
      Serial.println(str_array);
      isStart = false;
    }


    if (isStart == true) {
      str_data = String(bytes, HEX);
      if (str_data.length() == 1) {
        str_data = "0" + str_data;
      }
      str_array = str_array + str_data + ",";
    }
  }
}

//========================

// [miles or km]
// km   ->  5A
// miles -> 59

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

// [numbers]
// 0 -> 0x06  
// 1 -> 0x67 
// 2 -> 0x33 
// 3 -> 0x66 
// 4 -> 0x19 
// 5 -> 0x65 
// 6 -> 0x32 
// 7 -> 0x64 
// 8 -> 0x0C  
// 9 -> 0x63 

//========================

Unityでシリアル通信するスクリプトを書いてArduinoから送られてきたHexを数字に変換して表示する。

toNumber.cs
    void ReceivedLine(string data){
        Debug.Log (data);

        string[] arr =  data.Split(',');
        int score = int.Parse(toNumber(arr[3]) + toNumber(arr[4]) + toNumber(arr[5]));
        string scoreStr = score.ToString();

        unit.text  = toUnit(arr[8]).ToString();
    }

    string toNumber(string s){

        string num = "0";
        switch(s){
            case "63" : num = "9"; break;
            case "0c" : num = "8"; break;
            case "64" : num = "7"; break;
            case "32" : num = "6"; break;
            case "65" : num = "5"; break;
            case "19" : num = "4"; break;
            case "66" : num = "3"; break;
            case "33" : num = "2"; break;
            case "67" : num = "1"; break;
            case "06" : num = "0"; break;
        }
        return num;
    }

デモ

出来上がったプロトタイプがこちら

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

【Unity(C#)】unity1weekで共同開発に初挑戦した振り返り

unity1week

不定期で行われているunity1weekというゲームジャムに参加しました。

Unityを使って1週間でゲームを作るイベントです。
UnityがインストールされたPCとインターネット環境があればどなたでも参加可能です。
月曜0時にお題が発表されます。日曜20時までにゲームを作って投稿しましょう。
お題については多少こじつけでも大丈夫。ゲーム作りを楽しむことを心がけてください。

【引用リンク】:unityroom

共同開発

私自身参加は6回目なのですが、今回の参加においては共同開発に挑戦しました。
こんな感じのノリで始まりました。
GWunity.PNG

この友人のお仕事は、プログラムを書いてお金をもらうお仕事でもないですし、
理系でコンピューターサイエンスを専攻してたとかでもない、"ただただ興味がある"
という状態だったのでまずは一緒に何か動かしてみることにしました。

Day0

unity1week開催前日にDiscordで通話して画面共有を行いながら
同じ手順を踏んでもらう形でボールをキー入力で動かすというゲーム?を作りました。

Mitsu0.gif

using UnityEngine;

/// <summary>
/// Inspectorで動かしたいSphereにAdd Component
/// </summary>
public class BallMove : MonoBehaviour
{
    //ボールを動かす力を調整
    //[SerializeField]でInspectorに表示
    //[Range(最小値,最大値)]でInspectorにスライダーを表示
    [SerializeField, Range(0, 100)] private float power = 10;

    //Rigidbody:物理挙動に関する設定を簡単に扱える便利Component
    //Rigidbodyの入れ物を用意する
    private Rigidbody rb;

    //最初にStart関数内に書いた処理が実行される
    private void Start()
    {
        //先程用意した入れ物にRigidbodyを入れる

        //この.ゲームオブジェクトから.コンポーネントを取得<Rigidbodyを指定>
        //this.gameObject.GetComponent<Rigidbody>();
        rb = this.gameObject.GetComponent<Rigidbody>();
    }

    //Start関数内に書いた処理が実行されたあと、Update関数内に書いた処理が実行される
    //Updateは毎フレーム呼ばれる
    //1秒間に60回フレームが更新されるなら→60fps
    private void Update()
    {
        //Rigidbodyクラスに定義されているAddForceという関数を使う

        //もし、右矢印キーを押していたら、、、
        if (Input.GetKey(KeyCode.UpArrow))
        {
            //ワールド空間の指定した方向に任意の力を加える
            rb.AddForce(Vector3.forward * power);
        }

        //もし、下矢印キーを押していたら、、、
        if (Input.GetKey(KeyCode.DownArrow))
        {
            //ワールド空間の指定した方向に任意の力を加える
            rb.AddForce(Vector3.back * power);
        }

        //もし、右矢印キーを押していたら、、、
        if (Input.GetKey(KeyCode.RightArrow))
        {
            //ワールド空間の指定した方向に任意の力を加える
            rb.AddForce(Vector3.right * power);
        }

        //もし、左矢印キーを押していたら、、、
        if (Input.GetKey(KeyCode.LeftArrow))
        {
            //ワールド空間の指定した方向に任意の力を加える
            rb.AddForce(Vector3.left * power);
        }

}

友人に教えている最中に
あまりにも書いてあることがわからなさすぎて技術書を噛みちぎろうとした日を思い出しました。
(まあまあの値段がしたので踏みとどまりました)

何もわからない状態からなら、丁寧すぎて逆に理解しづらいかな?という懸念は一旦捨てて良い
と仮説を立てて"くどい"くらいにコメントを書きました。

Day1

unity1weekは、開始と同時にお題が発表され、
そのお題に関連性のあるゲームを開発するというルールがあります。

今回のお題は「密」でした。

Day1はどんなゲームを作るかアイデア出しを行いました。

アイデア出しの進め方としては、「密 熟語」で検索して
アイデアのベースになりそうなワードを列挙しました。

そして、そのワードからイメージを派生し、ゲーム性、実現可能性について検討しました。


★実際のメモ

Day1メモ.PNG

アイデア出しに関してはunity1week online共有会 #1(24:10~)
でお話してるので参考にしてみてください。

実際にできたアイデアのデザインモックがこちらです。
Getting Over It With Bennett Foddy × モンスト
をイメージしたゲーム性です。

キャプチャ.PNG

Day2

下記リンクを参考にUnity Collaborateの環境構築を完了させました。

【参考リンク】:【Unity】Unity Collaborateでリアルタイム通信コンテンツの制作フローを効率化

いきなりのGitはハードルがさすがに高いと思ったので試みましたが、
大正解でした。本当に簡単にプロジェクトの共同編集が可能なので、超おススメです。

Day3

この日からはひたすら実装です。
先ほどのデザインモックの段階でゲーム性の関してはある程度固まっていましたが、
"何を"飛ばすかがまだ決まっていませんでした。

キャプチャ.PNG

ただ、引っ付いた際、発射する際のアニメーションを自分で作るのが面倒だったので
手足の無い無機質な生命体の方が楽できそうだなと
実装可能性の観点からこの時点でアイデアは絞れていました。

手足の無い無機質な生命体で思いついたのはスライムです。
壁に密着するというアイデアにシナジーも生まれます。

スライムでアセットストアで検索した際に欲しかった形の
無機質な生命体を発見したので採用しました。
SlimePBR.PNG

ただ、スライムの引っ付いたり弾んだりする質感を実装で再現するのが面倒だったので
ここでもう一段階、実装を楽にするための設定を考えました。

ズバリ、磁力です。

この設定の付与で実装のイメージが一気に固まりました。

この日はキャラクターを"飛ばす"、それに伴う"補助線"の実装で終了しました。

Mitsu1.gif

Mitsu2.gif

Day4

磁力の実装に着手しました。

Mitsu3.gif

磁力の実装には万有引力の公式を利用しています。


F[N]:質量m₁[kg]と質量m₂[kg]の2つの物体の間に働く万有引力の大きさ
r[m]:物体間の距離
G:万有引力定数(G=6.673×10-11N・m2/kg2) ※今回はここを係数にして力を調整可能に

F = G\frac{m₁m₂}{r^2}

ただ、このまま利用するとm₁とm₂の物体間距離が遠くなればなるほど
力が弱くなる
という性質になり、なかなか気持ち良い挙動にはなりませんでした。

なので、m₁とm₂の物体距離が遠くなればなるほど力が強くなる式に変更して利用してみました。

F = Gm₁m₂r^2

望み通りの挙動になったのですが、
質量は今回関与する必要はないので下記が最終的な計算式です。

F = Gr^2

私がほしかったのはただの指数関数だったわけですね。

コード

磁力として扱うオブジェクトにアタッチ
/// <summary>
/// 磁力の表現 万有引力の計算式を利用
/// S,Nでそれぞれベクトルが逆になる
/// 磁力として扱うオブジェクトにアタッチ
/// </summary>
public class MagnetFunction : MonoBehaviour
{
    private enum MagnetState
    {
        S,
        N
    }

    [SerializeField] private float _accelerationScale;
    [SerializeField] private MagnetState _magnetState;

    private Rigidbody _collisionObjRigidbody;
    private Vector3 _direction;

    private float _distance;
    private float _magnetPower;

    private void OnTriggerEnter(Collider other)
    {
        //Start関数内で名指しで取得する実装でも良いかも
        _collisionObjRigidbody = other.gameObject.transform.root.GetComponent<Rigidbody>();
    }

    private void OnTriggerStay(Collider other)
    {
        //衝突したキャラがSかNの判定を入れた方がよさそう
            switch (_magnetState)
            {
                case MagnetState.S:
                    // 星に向かう向きの取得
                    _direction = this.gameObject.transform.position - other.transform.position;
                    // 星までの距離の2乗を取得
                    _distance = _direction.magnitude;
                    _distance *= _distance;
                    // 万有引力計算
                    _magnetPower = _accelerationScale * _distance;
                    // 力を与える
                    _collisionObjRigidbody.AddForce(_magnetPower * _direction.normalized, ForceMode.Force);
                    break;

                case MagnetState.N:
                    // 星に向かう向きの取得
                    _direction =  other.transform.position - this.gameObject.transform.position;
                    // 星までの距離の2乗を取得
                    _distance = _direction.magnitude;
                    _distance *= _distance;
                    // 万有引力計算
                    _magnetPower = _accelerationScale * _distance;
                    // 力を与える
                    _collisionObjRigidbody.AddForce(_magnetPower * _direction.normalized, ForceMode.Force);
                    break;
            }
    }
}

InspectorでS極N極それぞれの役割を変更できるようにしました。
S,Nそれぞれの機能でクラスを用意しても良いかと思います。

磁力としてBox Colliderのみを保持するオブジェクトを用意し、
その子に磁石として扱うオブジェクトを配置します。

磁力のコライダーの範囲内に留まっている間は
磁力が働く仕組みです。

NCube.PNG

Day5

友人から稼働時間を確保できると連絡がきたので
タイトルシーンを作ってもらうことにしました。

簡単なデザインモックを渡した後、
適当に調べてもらって分からないことがあれば
逐一、Slackで聞いてもらう形を取りました。

TitleMock.PNG

すぐ作ってくれたので助かりました。
友人もUnityもすごい。
MitsuMasuo1.gif

私は 磁力の調整、および発射パワー調整を行いました。

Day6

下記を頑張りました。

・ステージの実装(チュートリアルっぽいやつ)
・音探して編集して入れたり
・パワーの入力タイミングに制限を加えた
・パワー入力タイミングに関して画面に出す画像を用意してプレイヤーに伝えられるようにした
・フェードインアウト実装

Mitsu5.gif

フェードインアウト

DoTween(無料版)を使って実装しました。

実際に使ったコードはもっと乱雑でしたが、
次回からはこのようなUtilityを用意しておいて最小限の労力で臨もうかと思ってます。

using DG.Tweening;
using UnityEngine;
using UnityEngine.EventSystems;

/// <summary>
/// 任意のUIを表示、非表示に関するユーティリティークラス
/// アニメーション中はUIいじれない
/// </summary>
public static class UIAppearTweenAnimeUtility
{
    /// <summary>
    /// UI表示
    /// </summary>
    /// <returns>シーケンス</returns>
    public static Sequence AppearUI(RectTransform uiRectTransform)
    {
        EventSystem eventSystem = EventSystem.current;

        float appearDuration = 0.5f;

        return DOTween.Sequence()
            .OnStart(()=>eventSystem.enabled = false)
            .Append(uiRectTransform.DOScale(Vector3.one, appearDuration))
            .OnComplete(()=>eventSystem.enabled = true);
    }

    /// <summary>
    /// UI非表示
    /// </summary>
    /// <returns>シーケンス</returns>
    public static Sequence DisappearUI(RectTransform uiRectTransform)
    {
        EventSystem eventSystem = EventSystem.current;

        float disappearDuration = 0.5f;

        return DOTween.Sequence()
            .OnStart(()=>eventSystem.enabled = false)
            .Append(uiRectTransform.DOScale(Vector3.zero, disappearDuration))
            .OnComplete(()=>eventSystem.enabled = true);
    }
}

ポイントとしては
・メソッドがSequence(シーケンス)を返す
・EventSystemのenableを操作するヤンキーコング1

の二点です。

メソッドがSequenceを返す

DoTweenのSequenceはいろいろな処理を任意の順番、タイミングで実行することができます。
また、SequenceとSequenceを繋いで実行することもできます。

呼び出し側でUIアニメーションの処理に追加して
新しい処理を行わせることが可能になります。

UIのアニメーション処理の終了後に音を再生
AppearUI(uiRectTransform)
    .AppendCallback(()=>_fadeOpenAudioSource.Play())
    .Play();

アセットのインポート直後の初期設定ではSequenceが自動で実行される設定になっているかと思うので、
ツールバーから設定を変更しておくとScriptから実行できます。
DoTweenPanel.png

TweenAutoPlay.PNG

EventSystemのenableを操作するヤンキーコング

UIアニメーションの処理を行っている間は入力を受け付けないようにしたかったので
EventSystemごとenableをオフにしています。
そのため、一時的にすべてのEventSystem関連の入力が受け取れない状態になります。
(Unityゲーム開発者ギルドで教えてくださった方ありがとうございます)

EventSystem eventSystem = EventSystem.current;
eventSystem.enabled = true

Day7

結果的に私は毎度のことのように〆切に間に合わなかったのですが、
この日はリトライ処理の実装やステージ解放の実装を行っていました。

【参考リンク】:【Unity(C#)】覚えゲーのリトライポイント実装
【参考リンク】:【Unity(C#)】PlayerPrefsを使用したステージ解放の実装

Mitsu6.gif

友人にはステージを作ってもらっていました。
ゲーム性を与えるセンス抜群&ツールを使いこなす速度がバケモノで助かりました。
MMGameSS.PNG

振り返り

評価はなかなかシビアな感じでした。
MMReview.PNG

操作性

特に操作性の部分ではGetting Over It With Bennett Foddyを意識した難易度設計から、
難しい→操作しづらい という
印象の変換が無意識下で行われてしまったのではないか、という分析をしています。

Getting Over It With Bennett Foddyは確実に名作ですが、
操作をしやすいか質問して、"はい"という回答は得られにくい
かと思います。

Getting Over It With Bennett Foddyは
操作しづらいことで大きなゲーム性が生まれています。

つまり何が言いたいかというと、
評価項目の何かしらが欠損していることによってゲーム性が拡張されるゲーム
unity1weekで上位を狙いにくいということです。

文字起こししてみると当たり前ですが、
意外と見落としがちな要素かと思います。

操作性を振り返っての結論としては、
unity1weekで上位に食い込めなかったからといって
おもしろくないゲームであるという解釈にはならない
ということです。(暴論)

最後に

自分への慰めも含めて声を大にして言いますが、
みなさん胸を張って自分の作ったゲームに誇りを持ちましょう。

あと、運営、開発、実況等、本当にみなさんお疲れさまでした。
合わせて、お礼の言葉で締めさせていただきます。ありがとうございました。

(技術的な誤り、誤植等ありましたらお気軽にコメントください)

参考リンク

[Unity] 惑星に向かって物体が落ちるようにする
Unity で EventSystem.current が null になってる問題
[Unity] DOTweenのSequenceを使ってアニメーションを結合する

【作ったゲーム】:MAGNET MONSTER


  1. ヤンキーコングはヤンキーコーディングを略した造語です。 

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