ぐーるらいふ

底辺。

【Unity】ビル群をランダム生成する

お疲れ様です。ぐーるです。
また間が空いてしまった…。

unity1weekがもうすぐあるので、Unity思い出さないとなぁ~と触っています。

ビル群を自動生成する

こんなにRT&いいねされたのは初。

正直大したことはしていなくて、とっても恐縮。
ランダムでscale決めて、空いている所に配置しているだけです。

早速コード

BuildingMapCreater

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

// ビル一棟ごとのデータ(位置と大きさだけ)
public class BuildingMapObjectData
{
    public int id;
    public Vector3Int position;
    public Vector3Int scale;
}

public class BuildingMapCreater
{

    // もっとマップを広くしたい場合は大きくすればOK
    private int MAP_SIZE_W = 100;
    private int MAP_SIZE_H = 100;

    private int[][] _maps;
    private static BuildingMapCreater _instance;

    private BuildingMapCreater() { }

    public static BuildingMapCreater Instance
    {
        get
        {
            if (_instance == null)
            {
                _instance = new BuildingMapCreater();
            }
            return _instance;
        }
    }

    // ビル群のマップを自動で生成する
    public List<BuildingMapObjectData> CreateMap()
    {
        var result = new List<BuildingMapObjectData>();

        
        var i = 0;
        var j = 0;
	var id = 1;
	BuildingMapObjectData mo = null;

	// 初期化(-1)埋め
        _maps= new int[MAP_SIZE_H][];
        for (i = 0; i < MAP_SIZE_H; i++)
        {
            _maps[i] = new int[MAP_SIZE_W];
            for (j = 0; j < MAP_SIZE_W; j++)
            {
                _maps[i][j] = -1;
            }
        }

        i = 0;
        j = 0;
        while (true)
        {
            mo = new BuildingMapObjectData();
            mo.id = id++;

            mo.position = Vector3Int.zero;

	    // ビルの大きさをランダムで適当に
            mo.scale.x = Random.Range(1, 20);
            mo.scale.y = Random.Range(1, 20);
            mo.scale.z = Random.Range(1, 20);

            if (!ExecBuild(mo))
            {
                break;
            }

            result.Add(mo);
        }

        return result;
    }

    // 範囲内に他のビルが重なっていないかチェックする
    private bool ExecBuild(BuildingMapObjectData mo)
    {
        while (true)
        {
            if (IsEmptyMapRect(mo.position.x - 1, mo.position.z - 1, mo.scale.x + 2, mo.scale.z + 2))
            {
                PaintMapRect(0, mo.position.x - 1, mo.position.z - 1, mo.scale.x + 2, mo.scale.z + 2);
                PaintMapRect(mo.id, mo.position.x, mo.position.z, mo.scale.x, mo.scale.z);
                return true;
            }
            else
            {
                if (mo.position.x < MAP_SIZE_W)
                {
                    mo.position.x += 1;
                }
                else if (mo.position.z < MAP_SIZE_H)
                {
                    mo.position.z += 1;
                    mo.position.x = 0;
                }
                else
                {
                    return false;
                }
            }
        }

    }

    // 指定範囲を塗りつぶす
    private void PaintMapRect(int id, int x, int y, int w, int h)
    {
        for (var yy = y; yy < y + h; yy++)
        {
            for (var xx = x; xx < x + w; xx++)
            {
                PaintMap(id, xx, yy);
            }
        }
    }

    // 指定範囲が空いているか調べる
    private bool IsEmptyMapRect(int x, int y, int w, int h)
    {
        for (var yy = y; yy < y + h; yy++)
        {
            for (var xx = x; xx < x + w; xx++)
            {
                if (!IsEmptyMap(xx, yy))
                {
                    return false;
                }
            }
        }
        return true;
    }

    // 指定位置をIDで塗る
    private void PaintMap(int id, int xx, int yy)
    {
        if (yy >= 0 && yy < _maps.Length &&
        xx >= 0 && xx < _maps[yy].Length)
        {
            _maps[yy][xx] = id;
        }
    }

    // 指定位置が空いているかチェック
    private bool IsEmptyMap(int xx, int yy)
    {
        if (yy >= 0 && yy < _maps.Length &&
        xx >= 0 && xx < _maps[yy].Length)
        {
            if (_maps[yy][xx] > 0)
            {
                return false;
            }
            return true;
        }
        return false;
    }

    // デバッグ用出力
    private void DebugOutput()
    {
        for (var i = 0; i < MAP_SIZE_H; i++)
        {
            string output = "";
            for (var j = 0; j < MAP_SIZE_W; j++)
            {
                output += "[" + _maps[i][j] + "]";
            }
            Debug.Log(output);
        }
    }
}

Stage

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

public class Stage : MonoBehaviour
{

    [SerializeField] Building _buildingPrefab;
    private List<Building> _mapObjects;

    public void CreateMap()
    {
        StartCoroutine(_CreateMap());
    }

	
    private IEnumerator _CreateMap()
    {
        if (_mapObjects != null)
        {
            foreach (var m in _mapObjects)
            {
                Destroy(m.gameObject);
            }
            _mapObjects.Clear();
            _mapObjects = null;
        }
        _mapObjects = new List<Building>();
        var mapObjectDatas = BuildingMapCreater.Instance.CreateMap();
        foreach (var mod in mapObjectDatas)
        {
            var fs = Instantiate(_buildingPrefab, this.transform);
            fs.SettingBuildingMapObjectData(mod);
            _mapObjects.Add(fs);
            yield return new WaitForEndOfFrame();
        }
    }
}

Building

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

public class Building : MonoBehaviour
{
    [SerializeField] int _id;
    private bool _isInitialize = false;
    private Vector3 _targetPosition = Vector3.zero;
    private float INIT_TIME = 10.0f;

    // Update is called once per frame
    void Update()
    {
        if (_isInitialize)
        {
            if (Vector3.Distance(this.transform.position, _targetPosition) > 0.01f)
            {
                this.transform.position = Vector3.MoveTowards(this.transform.position, _targetPosition, INIT_TIME * Time.deltaTime);
            }
            else
            {
                this.transform.position = _targetPosition;
                _isInitialize = false;
            }
        }
    }

    public void SettingBuildingMapObjectData(BuildingMapObjectData mapObjectData)
    {
	// dataの位置ではpivot(0,0)で計算されているため、pivot(0.5,0.5)で位置調整
        var x = mapObjectData.position.x + (mapObjectData.scale.x / 2);
        var y = (mapObjectData.scale.y + 1) / 2; // 高さはスケールに合わせる
        var z = mapObjectData.position.z + (mapObjectData.scale.z / 2);
        _id= mapObjectData.id;
        _targetPosition = new Vector3(x, y, z);

	// 最初沈ませておいて、下から迫り上がるような演出をする
        this.transform.position = new Vector3(_targetPosition.x, _targetPosition.y - mapObjectData.scale.y, _targetPosition.z);
        this.transform.localScale = mapObjectData.scale;

	_isInitialize = true;
    }
}

ビル群を生成するイメージ

まず、ランダムでビルという名のただのcubeの大きさを適当に決めます。
その後配置するのですが、この時

  • ビル同士が被らない
  • ビルとビルの間に1マス以上の隙間を空ける

この2つを実現させます。

高さは被らないため、除外して考えるので、単純に2次元配列で管理します。

0,0の位置から大きさ分の四角形が入る位置を探します。
この時、-1 , + 2をして道路分を空けるようにします。
(この辺の値を大きくするとさらに道路を広く出来ます。
また、この列や行はビル配置禁止!といった値を決めておくと大通りを作ることが出来ます)

if (IsEmptyMapRect(mo.position.x - 1, mo.position.z - 1, mo.scale.x + 2, mo.scale.z + 2))
{
    PaintMapRect(0, mo.position.x - 1, mo.position.z - 1, mo.scale.x + 2, mo.scale.z + 2);
    PaintMapRect(mo.id, mo.position.x, mo.position.z, mo.scale.x, mo.scale.z);
    return true;
}

入らなければ一マス動かす、で入る位置を探します。

入ったらその位置にIDを書き込んでおき、すでにここにはビルがありますよ、
という情報を残しておきます。

f:id:ghoul_life:20190227194818p:plain

これを繰り返して、MAP内に入り切らなくなったら終了です。

余談

記事書いてて思ったのですが、HITしたらIDからビル情報取ってきて、その大きさ足したほうが良かった

生成したビル情報使う

ビル情報のスケールはそのまま入れますが、positionはpivotの関係上、計算が必要です。

// dataの位置ではpivot(0,0)で計算されているため、pivot(0.5,0.5)で位置調整
var x = mapObjectData.position.x + (mapObjectData.scale.x / 2);
var y = (mapObjectData.scale.y + 1) / 2; // 高さはスケールに合わせる
var z = mapObjectData.position.z + (mapObjectData.scale.z / 2);

プロジェクトとして

  1. 空のGameObjectを生成して、StageスクリプトをAdd Component
  2. CubeをInspectorに生成して、BuildingスクリプトをAdd Componentして、Prefab化
  3. 作ったCubeをStageに紐づけ
  4. 実行して、Stage.CreateMap()をコールすればOK

おまけ

なんとなくビルが下からニュッと生える感じにしたかったので、
目標位置と開始位置を変えて、目標位置に到達するまで移動するようにしてます。
上から降ってくる感じとかEasingとか付けるともっと賑やかになります。
(DoTweenとか使うともっと簡単ですね)

f:id:ghoul_life:20190227180855g:plain

最後に

蓋を開けてみたらとんでもなく初歩的なスクリプト
ほんとこんなんでバズって良いんだろうかという気になってます。
実はみんなFracture側の記事を期待してたりして。

【Unity】Android Nativeプラグイン開発 最小構成でなるべくわかりやすくまとめた

Androidでネイティブプラグイン

Androidでネイティブプラグイン開発を行う時の作業手順をまとめてメモしておきます。

環境

使用した環境は以下になります。

そして、最小構成で作成します。
aarを使わず、より不要な物をそぎ落としたjarで組み込みます。

なるべく処理に不要な物は排除し、本当に処理を行うのに必要なものは
何なのか?という事に着目してまとめてます。
なるべく、わかりやすく。Step By Stepで。

Android Studioを使ってUnityプラグインを作ろう

手順1 プロジェクトを作成

Android Studioを立ち上げ、新規プロジェクトを「Add No Activity」で作成します。

f:id:ghoul_life:20190126020326p:plain:w300
f:id:ghoul_life:20190126020559p:plain:w300

手順2 ライブラリモジュールを作成

プロジェクトが出来たら、ライブラリモジュールを作成します。

Android Studioの上部メニューバーからFile > New > New Module
そして、「Android Library」を選択して作成します。

f:id:ghoul_life:20190126020926p:plain
f:id:ghoul_life:20190126021255p:plain:w300
f:id:ghoul_life:20190126021041p:plain:w300

手順3 必要ないファイルの削除

ライブラリモジュール以外は必要ないため、
作成したプロジェクトフォルダを開き、最初に作られたappを削除します。

f:id:ghoul_life:20190126021732p:plain:w300

そしてsetting.gradleからもappを消します。

include ':app' ':unitypluginsamplelibrary'
->
include ':unitypluginsamplelibrary'

f:id:ghoul_life:20190126021919p:plain:w300

手順4 Unityクラスライブラリの追加

AndroidでNative連携Pluginを作る時、AndroidからUnityの機能を呼びたい事があります。
そういった機能をAndroidから使えるように、Unityが用意しているAndroid用クラスライブラリをPlugin側に組み込む必要があります。
例えば、Unity HubでUnity2018.3.2f1の場合はここにあります。

C:\Program Files\Unity\Hub\Editor\2018.3.2f1\Editor\Data\PlaybackEngines\AndroidPlayer\Variations\mono\Release\Classes

これをモジュールのlibsの中にコピーして配置します。

f:id:ghoul_life:20190126022258p:plain:w300

手順5 ビルドスクリプトの修正

ビルドスクリプト(モジュールの直下にあるbuild.gradle)を以下のように整理修正します。
(compileSdkVersionなどは各環境に合わせて下さい)

apply plugin: 'com.android.library'

android {
    compileSdkVersion 27

    defaultConfig {
        minSdkVersion 15
        targetSdkVersion 27
        versionCode 1
        versionName "1.0"
    }

    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }

}

dependencies {
    compileOnly fileTree(dir: 'libs', include: 'classes.jar')
}

// buildしたjarをカレントに持ってきてリネーム
task createJarFile(type: Copy) {
    // 環境によって出力先がよく変わるみたい?intermediates-jarsとかもあるらしい
    from('build/intermediates/packaged-classes/release/')
    into('.')
    include('classes.jar')
    rename('classes.jar', 'UnityPluginSample.jar')
}
createJarFile.dependsOn(build)

新規追加したタスク「createJarFile」はUnityに組み込むjarをビルド後に抜き出し、リネームするタスクです。こういうのを作ると楽になります。

実はこの辺りでちょっと困ったことがあったので、その件についてまとめました。

ghoul-life.hatenablog.com

aarを使わないこの記事の方針では実はここまでしなくても良かったりします。
興味があれば一読してみてください。

手順6 configurationの設定

Android StudioからcreateJarFileタスクを実行出来るように構成を設定します。
Android StudioのEdit ConfigurationsからGradleを選択して、以下を参考に設定してください。

+ボタン > Add New Configuration > Gradle
Name : 自由に
Gradle Project : プロジェクトフォルダを選択
Tasks : createJarFileと入力

f:id:ghoul_life:20190126023556p:plain:w300
f:id:ghoul_life:20190126023708p:plain:w300

手順7 テスト用ファイルの削除

本来は必要であるべきと思われるのですが、最小構成なので、ここでは省きます。
モジュール作成時に自動で追加されるTestコードを削除します。

f:id:ghoul_life:20190126024030p:plain:w300

手順8 実際に使用するコードを記述する

ここまで来てようやっとコードがかけます。長い。
ソースファイルを新規追加し、以下のようにコードを記述します。
手順4でライブラリを正しくlibsの下に配置していて、手順5でgradleの設定が出来ていれば
com.unity3d.playerパッケージが使えるようになるはずで、エラーが出ないと思われます。
(エラーが出るときはその辺りを見直すとよい)

package com.example.unitypluginsamplelibrary;

import com.unity3d.player.UnityPlayer;
import java.util.Random;

public class HelloAndroidNativePlugin {

    public static void Execute()
    {
        Random r = new Random();
        UnityPlayer.UnitySendMessage("AndroidNativeManager" , "FromAndroid" , "Hello Unity Android Plugin. Rand." + r.nextInt());
    }
}

手順9 ビルドしてJarを作成する

Build VariantsをReleaseにして、ConfigureをcreateJarFileに合わせ、RunすればOKです。
BUILD SUCCESSと出れば、正常にプラグインが作成出来ています。

f:id:ghoul_life:20190126024718p:plain:w300

これでAndroid側の作業は完了です。

UnityでAndroid Pluginを組み込む

こちらはぐっと簡単です。

手順1 作成したプラグインを配置

Assets/Plugins/Android

以下に配置します。上の手順ならjarファイルをそこにポンと置けばOKです。

f:id:ghoul_life:20190126025305p:plain

手順2 プラグインの機能を使用するスクリプトを作成

組み込んだプラグインの機能を使うスクリプトを記述します。
AndroidJavaClassでクラスを指定し、Callで呼びたい関数を指定すればOKです。

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

public class AndroidNativeManager : MonoBehaviour
{
    [SerializeField] Text _androidMessageText;

    public static readonly string ANDROID_NATIVE_PLUGIN_CLASS = "com.example.unitypluginsamplelibrary.HelloAndroidNativePlugin";

    // Start is called before the first frame update
    void Start()
    {
        ResetMessage();
    }

    public void CallAndroidPlugin()
    {
        using (AndroidJavaClass androidJavaClass = new AndroidJavaClass(ANDROID_NATIVE_PLUGIN_CLASS))
        {
            androidJavaClass.CallStatic("Execute");
        }
    }

    public void ResetMessage()
    {
        _androidMessageText.text = "";
    }

    public void FromAndroid(string message)
    {
        _androidMessageText.text = message;
    }
}

apkを作成し、実機で動かしてみる

実際にapkをビルドして、それを実機で動かしてテストします。
自分はAndroid Emuratorで実行しました。

f:id:ghoul_life:20190126030157g:plain

Unity -> Android -> Unity
と問題なく処理が動いているのを確認できました。

【Unity】Android Native連携Pluginを開発してたら has been replaced with 'variant.getPackageLibraryProvider()'.

Androidのネイティブ連携開発

UnityでAndroidのNative連携Pluginの開発をしていたらこんなエラーに出くわした。

WARNING: API 'variantOutput.getPackageLibrary()' is obsolete and has been replaced with 'variant.getPackageLibraryProvider()'.
It will be removed at the end of 2019.
For more information, see https://d.android.com/r/tools/task-configuration-avoidance.
To determine what is calling variantOutput.getPackageLibrary(), use -Pandroid.debug.obsoleteApi=true on the command line to display a stack trace.
Affected Modules: helloplugin

エラー内容を見れば、
「variantOutput.getPackageLibrary()は2019年には消えるから、variant.getPackageLibraryProvider()に
置き換えて下さい」
といった具合なのですが、ちょっと対応に戸惑ったのでメモしておこう。

発生した状況

Android Studio 3.3を利用して素直にプロジェクトを作るとデフォルトで

com.android.tools.build:gradle:3.3.0

を利用することになり、そしたら遭遇した。

前提として

AndroidでNative連携Pluginを作る時、AndroidからUnityの機能を呼びたい事がよくある。
そういった機能を使う時に、Unityが用意しているAndroid用クラスライブラリをPlugin側に
組み込む必要がある。
例えば、Unity HubでUnity2018.3.2f1を利用している場合はここにある。

C:\Program Files\Unity\Hub\Editor\2018.3.2f1\Editor\Data\PlaybackEngines\AndroidPlayer\Variations\mono\Release\Classes

Macとかならこの辺りを参考に
docs.unity3d.com

原因

build.gradleに記載してあるこれが原因。

android.libraryVariants.all { variant ->
    variant.outputs.each { output ->
        output.packageLibrary.exclude('libs/classes.jar')
    }
}

これは、AndroidでUnityの機能を使うためにlibs/classes.jarに配置して使用するが、
Unity側に含めると重複エラーになってしまうので、aarに固める時には省きたい。
その省く処理を行っている。

間違った対応メモ

android.variants.all{ variant ->
    variant.packageLibararyProvider.each { output ->
        output.configure{
            exclude “libs/classes.jar”
        }
    }
}

ストレートに直すとこうだ。
エラーは出なくなるが、これでは上手くいかず、classses.jarがPluginに入ってしまった。

そもそもPackageLibraryProviderに代わっているため、流れてくる内容が違う。
中身はTaskProviderでDebug , Releaseになってるような感じだった(ちょっと詳しくはわからないけど)
なので、そこでexcludeとかやっても特に効果が無い。

解決について

ただやりたいことは、

ビルド時にlibs/classes.jarを省きたい

これだけなんだ。

つまりこうすればいい

dependencies{
    compileOnly fileTree(dir:”libs” , file: “classes.jar”)  
    .....
}

(もちろんpackageLibrary~の部分は全部消していい)

前まではprovidedだったが、これも無くなって今はcompileOnlyになった。
これで固める時にclasses.jarを省くことが出来る。

【Unity】第10回unity1week「10」ベルトスクロールアクションシステムについて

お疲れ様です。ぐーるです。
今回はunity1weekで提出した
「School Bag Fight」
f:id:ghoul_life:20181127003802p:plain
School Bag Fight | 無料ゲーム投稿サイト unityroom - Unityのゲームをアップロードして公開しよう
についての記事にしようと思います。

お題について

また困るやつ…w
勝手に「クリスマス」とか「お正月」
とか思ってたんですが、そんなものではなかった…w
まぁ、とりあえず作りたいものを作って
後から帳尻を合わせようかな、何か作りたいものはあるかな?
と言う所から考えることにしました。

作りたいもの?

挑戦したかったのは自分で描くスプライトアニメーションでした。
でとりあえず描き始めたのが徒歩モーション。

f:id:ghoul_life:20181127211549p:plain

これが完成した辺りで、そうだファイナルファイトみたいな
ベルトスクロールアクションを作ろうかなと思いました。

コアシステムについて

日本ではベルトスクロールと言いますが、
海外ではBeat em upとか言うのかな。

一見複雑に見えます(?)が、実はソースコードはそこまで長くはならなかったです。

四つのシステム

本当のコアの部分は以下の四つのクラスで構成されています。
一部抜粋で冗長になりそうなnullチェックとかrangeチェックとか消してます。

AreaManager
EnemyFactory
Player
Enemy

AreaManager

いきなりですけどコードを。

    public Camera _mainCamera;
    private int _index;
    private bool _isNext;
    private static Area[] _areas = new Area[]
    {
        new Area(-20.0f, -0.5f, -4.0f),
        new Area(-17.0f, -0.5f, -4.0f),
        new Area(-8.0f, -0.5f, -4.0f),
        new Area(-3.0f, -0.5f, -4.0f),
        new Area(2.0f, -0.5f, -4.0f),
        new Area(7.0f, -0.5f, -4.0f),
        new Area(12.0f, -0.5f, -4.0f),
        new Area(18.0f, -0.5f, -4.0f),
        new Area(23.0f, -0.5f, -4.0f),
        new Area(28.0f, -0.5f, -4.0f),
    };
    private Area _area;

    // 次のエリアへ移動可能
    public void Next()
    {
        _index++;
        _isNext = true;
        _area = _areas[_index];
    }

    void Update()
    {
        if (_isNext)
        {
            var p = _mainCamera.transform.position;
            if (p.x >= _area._targetX)
            {
                // 次のエリアへ到達。敵出現開始
                _isNext = false;
                EnemyFactory.Instance.NextStageStart();
            }
        }
    }

まずはAreaという概念から説明します。
ベルトスクロールアクションはよく

進む->止まって敵がワラワラと出てくる->全部倒したら->また次に進めるようになる

というイベントがあります。
これを1Areaとして管理することにしました。

Areaは

・目標X位置
・右端移動可能位置(算出)
・左端移動可能位置(算出)
・上に移動可能な位置
・下に移動可能な位置

とrectと似たようなデータを持つクラスです。
AreaManagerはこれをArea数分持っています。
そしてAreaの切り替わりはCameraの位置によって決めています。

1. 敵を全部倒した
2. AreaManagerの次のエリアデータをセットして、次に進んでいいよフラグを立てる
3. フラグが立っている時は目標X位置にカメラが来るまで待機
4. 目標X位置までカメラが到達したら進んでいいよフラグを降ろし敵を出現させる

という流れになります。

3.の状態の時、横着してまして、
左(Xマイナス方向)に進むことを出来なくしています。
CineMachineとか使ってたら範囲内ならプレイヤーだけ移動、範囲外に出たらカメラごと移動
という良いUXに出来たなーと後でちょっと思いました。今後の課題

EnemyFactory

読んで字のごとく敵を生成するクラスです。

    private List<EnemyScript> _enemyList;
    private List<Bullet> _bullets;
    private int _createCount = 0;
    private int _disposeCount = 0;
    private float _popTimer = 10;
    public bool _running = false;

    // int enemyCount, float moveSpd, float countDownBeforeSpd, float countDownAfterSpd, float popSpd 
    private StageData[] _stages = new StageData[]
    {
        new StageData(1, 100, 1, 10, 5),
        new StageData(3, 5, 2, 50, 3),
        new StageData(5, 5, 2, 50, 1),
        new StageData(1, 100, 1, 30, 3),
        new StageData(5, 10, 3, 30, 2),
        new StageData(10, 10, 2, 30, 1),
        new StageData(7, 3, 5, 50, 1),
        new StageData(7, 3, 4, 50, 0.5f),
        new StageData(10, 10, 7, 50, 2),
        new StageData(1, 100, 10, 100, 1),
    };

    private StageData _stage;

    // ステージを開始
    public void NextStageStart()
    {
        _running = true;
        _stage = _stages[AreaManager.Instance.Index];
    }

    public void CreateEnemy(StageData stage, Vector3 pos){ /* 敵を生成。リストに保存 */ }
    public void CreateBullet(bool fripX, Vector3 pos, Vector3 vector){ /* 弾を生成して発射。リストに保存 */ }

    public void DisposeEnemy(EnemyScript enemy)
    {
        if(enemy != null)
        {
            _disposeCount++;
            if (_enemyList != null)
            {
                _enemyList.Remove(enemy);
            }

            Destroy(enemy.gameObject);
            // 敵を倒したカウントを計測しておいて、出現数に到達したら次のエリアへ
            if(_disposeCount == stage._enemyCount)
            {
                _running = false;
                AreaManager.Instance.Next();
            }
        }
    }

    public void DisposeBullet(Bullet bullet){ /* リストから消してDestroy */}

    void Update () {
        if (_running)
        {
            if(_createCount < _stage._enemyCount)
            {
                _popTimer += Time.deltaTime;

                if (_popTimer > _stage._popSpd)
                {
                    _popTimer = 0;
                    var pos = _player.transform.position;
                    var rand = Random.Range(0, 100);
                    if (rand > 50)
                    {
                        pos.x += 10;
                    }
                    else
                    {
                        pos.x -= 10;
                    }
                    pos.x += Random.Range(-0.5f, 0.5f);
                    pos.y += Random.Range(-0.5f, 0.5f);

                    CreateEnemy(_stage, pos);
                    _createCount++;
                }
            }
        }
    }

EnemyFactoryはstageという情報を持っています。
Stageは

・敵の出現数
・敵の移動速度、攻撃速度
・敵のポップ速度

といった難易度パラメータを持っています。
この値とAreaが1:1で繋がっています。
(だったら一つのScriptableObjectにまとめとけ!って後で思いました)

この値に沿って敵を出現させるのですが、
まぁよくある手でListでEnemyもBulletも保持します。
これはOPに戻ったりした時にまとめてリセットをかけるために持ってます。
破棄する時に一緒にListからも外す必要が出てくるのでちょい面倒ではありますが、しょうがない。

Player

Playerは操作感というか
キーを離したらぴたっと止まるようにとか
攻撃速度を早くとかキックしているときだけ若干の硬直を入れてたりといった小細工を入れている程度なんですが、
一つだけ問題が出た所はContinueのフローでした。

f:id:ghoul_life:20181126235255p:plain
こちらPlayerのAnimatorになります。

敵にやられた時、ここのdown状態のStateで止まっていて
コンティニューを選んだ時にwaitに戻るのですが、最初はここをTriggerで管理してました。
一度目は良いのですが、二度目コンティニューをしようとすると即座にwaitに遷移してしまう、

というバグを生み出してしまいました。
なので、ここをfloatに変えて、値が一定以上ならwaitに戻り、戻ったら0で上書きしておく
と修正しました。

f:id:ghoul_life:20181127213507p:plain
死亡した時の画面。ゲーム全体を止めていて
発射モーション途中で止めたいと思ってそこは意識してます。

Enemy

State管理が重要です。

SEARCH
PATROL
ATTACK_BEFORE
SHOT
ATTACK_WAIT

の五つがあります。

        switch (_enemyState)
        {
            case EnemyState.SEARCH:
                _targetPosition = _player.transform.position;
                _targetPosition.x += Random.Range(-0.1f, 0.1f);
                _targetPosition.y += Random.Range(-0.1f, 0.1f);
                _enemyState = EnemyState.PATROL;
                break;
            case EnemyState.PATROL:
                _animator.SetFloat("walk", 1.0f);
                var move = _moveSpeed * Time.deltaTime;
                transform.position = Vector3.MoveTowards(transform.position, _targetPosition, move);
                if(Vector3.Distance(transform.position, _targetPosition) <= STOP_DISTANCE)
                {
                    _animator.SetFloat("walk", 0.0f);
                    _enemyState = EnemyState.ATTACK_BEFORE;
                    /* パラメータセット */
                }
                break;
            case EnemyState.ATTACK_BEFORE:
                if (_countdownTimer > 0)
                {
                    _countdownTimer -= _countdownBefore * Time.deltaTime;
                    // playerが離れすぎたら再度searchへ
                    if(Vector3.Distance(transform.position, _player.transform.position) > 8)
                    {
                        _enemyState = EnemyState.SEARCH;
                    }
                }
                else
                {
                    _enemyState = EnemyState.SHOT;
                    /* パラメータセット */
                }
                break;
            case EnemyState.SHOT:
                _animator.SetTrigger("gun");
                var vector = (_player.transform.position - transform.position).normalized;
                _enemyFactory.CreateBullet(_spriteRenderer.flipX,this.transform.position, vector);
                _enemyState = EnemyState.ATTACK_WAIT;
                /* パラメータセット */
                break;
            case EnemyState.ATTACK_WAIT:
                if (_countdownTimer > 0)
                {
                    _countdownTimer -= _countdownAfter * Time.deltaTime;
                }
                else
                {
                    _enemyState = EnemyState.SEARCH;
                }
                break;
        }
SEARCH

プレイヤーの位置を取得して、若干ランダムで位置をずらして保持します。
その後PATROLに遷移

PATROL

SEARCHで取得した位置に移動します。
移動速度はStageパラメータで設定します。
目標地点に到達したらATTACK_BEFOREに入ります。

ATTACK_BEFORE

攻撃前の待機です。構えと言い換えてもいいです。
10秒のカウントダウンを行い、SHOTに遷移します。
このカウントダウン速度もパラメータで設定。
この時プレイヤーが離れすぎた場合は再度SEARCHに遷移します。

SHOT

弾を発射します。animatorに合わせて微調整入れてます。

ATTACK_WAIT

弾発射後の硬直です。
ここもパラメータ
硬直が解けたら、再度SEARCHに遷移して、プレイヤー位置を上書きします。

その他

後は普通にColliderで当たり判定を作っていたり
画面遷移、_animatorをストップさせて会話パートをゲーム中に挟んでみたりと
といった細かい実装なので割愛。

終わってみて

企画、システム(ゲーム)、デザイン
をわずか一週間で形にする…。これは大変な事です。
ちょっと無謀だったな…というチャレンジでしたが、
なんとか遊べるレベルにまで持っていけてよかったです。
そして評価、コメントまで頂けて本当に嬉しいです。

余談

作業中はよく適当にアニメを流しているんですが、今回は
ガールズアンドパンツァーをTV、OVA、劇場版、最終章と続けて二周ぐらい流してました。
次は戦車にしようかなw

【Unity】タッチした位置にuGUI(RectTransform)を表示する

お疲れ様です。ぐーるです。
最近はなんかサーバだけではなく、採用だ評価だと色んな所まで見てたりして、
ちょっとアップアップしています。色んな人と会話するのって難しいです。

unityroom以外でもなんかゲーム作ってて、ふと
「指定位置にuGUIでテキストと画像を出したいなー」
と思ったので、実装したのですが、なんか思ったように上手く行かずにちょっと困ったので
メモ代わりにまとめておきます。

タッチした位置にuGUI(RectTransform)を表示する

普通のGameObjectだったらScreenToWorldPoint()とか使いますよね。
こんな感じ。

f:id:ghoul_life:20181113000239g:plain

public GameObject cubePrefab;
	
// Update is called once per frame
void Update () {
        if (Input.GetMouseButtonDown(0))
        {
            var mousePosition = Input.mousePosition;
            mousePosition.z = 10;
            var pos = Camera.main.ScreenToWorldPoint(mousePosition);
            var cube = Instantiate(cubePrefab);
            cube.transform.position = pos;
            cube.transform.SetParent(this.transform);
        }
}

が、RectTransformではどうするんだろうと言うとこうします。
タッチした位置にInstantiateしてDoTweenでアニメーション付与して終わったら消す。

f:id:ghoul_life:20181112235926g:plain

// uGUIのprefab
public GameObject itemPrefab;

// Update is called once per frame
void Update () {
        if (Input.GetMouseButtonDown(0))
        {
            var canvas = this.GetComponent<Canvas>();
            var canvasRect = canvas.GetComponent<RectTransform>();

            Vector2 localpoint;
            RectTransformUtility.ScreenPointToLocalPointInRectangle(canvasRect, Input.mousePosition, canvas.worldCamera, out localpoint);
            var item = Instantiate(itemPrefab);
            item.transform.SetParent(this.transform);
            item.GetComponent<RectTransform>().anchoredPosition = localpoint;
            DoTweenUtil.UpToRectTransform(item);
        }
}

補足でDoTweenのコードも

    public static void UpToRectTransform(GameObject gameObject)
    {
        var rectTran = gameObject.GetComponent<RectTransform>();
        if(rectTran != null)
        {
            rectTran.DOMove(new Vector2(0 , 50) , 1.0f)
            .SetRelative(true)
            .OnComplete(() => {
                UnityEngine.Object.Destroy(gameObject);
            })
            .SetEase(Ease.OutCubic)
            .Play();
        }
    }

こんだけだけど

なるほど、RectTransformUtilityなんて便利なものがあるのね。
また一つ勉強になりました。

もうすぐunity1weekが始まりますね。お題は「クリスマス」ではないかと思っているのですが、
そんなことないか。
新型iPad ProとApple Pencilを衝動買いしてしまったのでお絵かき練習だけは継続しています。

【Unity】いい感じにデータクラスをList表示したい【Inspector】

なんか連投です。
きっとこの後間が空きます!
時間とやる気があるときにやっとけ!って事ですね。

リスト表示したい

なんか簡単なマスタデータ的なデータ持ちたくて
でもcsvにするほどでも無くて、でも折角Unity使ってるんだからオシャレにリスト表示したい。

f:id:ghoul_life:20181029235212p:plain

こんな感じにInspectorを活用したい。
調べればすぐ出てくるかもしれないけど、自分でもメモっておく。

PropertyDrawerとReordableList

ソースから。Dataクラスをリスト表示する最小限な構成を紹介

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

[System.Serializable] // <- これが大事。忘れずに
public class Data {
    public int _id;
    public string _name;
    public DataList.DataActionType _actionType;
}
using UnityEngine;

// GameObjectに付けるのはこれ。
public class DataList : MonoBehaviour
{
    public enum DataActionType
    {
        NONE,
        TYPE1,
        TYPE2,
        TYPE3
    }
    [SerializeField] Data[] _dataList;
}

以下はEditor拡張だ。

// Project/Editorの下に配置でもOK
#if UNITY_EDITOR

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

[CustomPropertyDrawer(typeof(Data))] // DataクラスのInspector表示をカスタム
public class DataPropertyDrawer : PropertyDrawer {
    public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
    {
        // 表示幅
        float[] widthes = { position.width * 0.2f, position.width * 0.5f, position.width * 0.3f };

        if (property != null)
        {
            position.width = widthes[0];
            EditorGUI.PropertyField(position, property.FindPropertyRelative("_id"), GUIContent.none); // フィールド名を指定

            position.x += position.width;
            position.width = widthes[1];
            EditorGUI.PropertyField(position, property.FindPropertyRelative("_name"), GUIContent.none);

            position.x += position.width;
            position.width = widthes[2];
            EditorGUI.PropertyField(position, property.FindPropertyRelative("_actionType"), GUIContent.none);
        }
    }
}
#endif
// Project/Editorの下に配置でもOK
#if UNITY_EDITOR

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

[CustomEditor(typeof(DataList))]
public class DataListEditor : Editor
{
    private ReorderableList _reorderableList; // ReorderableListを利用して、並び替えや+-ボタンを使えるようにする

    void OnEnable()
    {
        _reorderableList = new ReorderableList(serializedObject, serializedObject.FindProperty("_dataList"));
        _reorderableList.drawElementCallback += (Rect rect, int index, bool selected, bool focused) =>
        {
            SerializedProperty property = _reorderableList.serializedProperty.GetArrayElementAtIndex(index);
            // PropertyFieldを使ってよしなにプロパティの描画を行う(PropertyDrawerを使っているのでそちらに移譲されます)
            EditorGUI.PropertyField(rect, property, GUIContent.none);
        };
        _reorderableList.drawHeaderCallback += rect =>
        {
            EditorGUI.LabelField(rect, "id | name | action type");
        };
    }


    public override void OnInspectorGUI()
    {
        _reorderableList.DoLayoutList();
        serializedObject.ApplyModifiedProperties();
    }
}
#endif

これだけでOK

上記のコードだけで画像のようなリスト表示が出来るようになる。便利~。

【Unity】【Android Plugin】android連携で画像を読み込みたかっただけなのにちょっとハマった話

お疲れ様です。ぐーるです。
最近もお絵描きの練習と新しいゲームの開発をコツコツやってます。
もうすぐ1weekまた始まるんでしたっけ。
ヤバい。Inventoryシステムの完成を急ぎたい。

Androidプラグイン

UnityからAndroidネイティブの機能を使いたい場合はプラグインを作成する必要があります。
作り方についてはちょっと調べると沢山出てくるのでここでは割愛。
プロジェクト作って、
新しくモジュール追加でLibrary選んで、
適当にコード書いてビルドして、
aarからjar取り出すかbundlesの下から取ってきて、
Plugins/Androidの下に配置すればOKってな具合です。

まぁ大概はビルド&配置task作って楽しますかね。

画像が出ない?

画像を読み出す時はこんな感じのコードを書きます。

// res/drawable/hogehoge.pngを読み出す
Context unityContext = UnityPlayer.currentActivity.getApplicationContext();
int resourceId = unityContext.getResources().getIdentifier("hogehoge", "drawable", unityContext.getPackageName());

ふむふむ、なるほど。ではこのまま書こう。
と書いてみるとhogehoge.pngが読めない。
えー何故だ!?Plugins/Android/res/drawableの下にちゃんと置いてるよ?と。
今回はこのお話。

結論

ハッキリ言ってしまえば配置ミスです。
こう配置する必要がありました。

// Plugins/Android/の下
res/drawable-hdpi-v4/
res/drawable-ldpi-v4/
res/drawable-mdpi-v4/
res/drawable-xhdpi-v4/
res/drawable-xxhdpi-v4/
res/drawable-xxxhdpi-v4/

正解は全部作って全部にちゃんとhogehoge.png入れとけ!です。これだけ。

出来上がったjarかAndroid exportしたAndroidプロジェクトを見てみればすぐわかりますが、
res/drawableの下に置いたビルド後に画像が無くなってるんですよね。
恐らくUnity側でそこは自動でアイコンを配置したりなどで使ってて
何か入れておいても無視されてる様子。

Androidやってる人からしたら常識なのかもしれないですが、
dpiによってリソース分けられてるんですよね。すっかり忘れてました。

余談

アイコン読み出したいときはこう書きます

Context unityContext = UnityPlayer.currentActivity.getApplicationContext();
// iconを読み出す
PackageManager packageManager = unityContext .getPackageManager();
ApplicationInfo applicationInfo = null;
try
{
    applicationInfo = packageManager.getApplicationInfo(unityContext .getPackageName(), PackageManager.GET_META_DATA);
}
catch (PackageManager.NameNotFoundException e)
{
    e.printStackTrace();
    return;
}
appIconResId = applicationInfo.icon; // iconのリソースIDを取り出す