ぐーるらいふ

底辺。

【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を取り出す

【Unity1week】一週間ゲームジャム「あつい」に参加【Unity】

お疲れ様です。お久しぶりです、ぐーるです。
仕事の方ではゲームとは関係ないまま、すっかり中堅、それ以上となり
打合せ、社外MTGなど座席に余り居ないような毎日を送っています。
Unityの新規開発プロジェクトとかやりたいです。

前回からまた長い間が空いてやってきました、unity1week。
自分からするとつい先日宴ゲームジャムがあったので、
そこまで間が無かったようなそんな印象なんですが、やっぱりこっちも参加したいと。

ghoul-life.hatenablog.com


そうでもしないとunityを触る機会とか無いんだものー

お題について

これまた難しい。どうしようかウンウン唸ってみるが全然アイデアは降りてこない。
・おでん?
・太陽?
・お湯?
こういう時にマインドマップとかやるといいのだろうけど、
流れに身を任せて日々を過ごす。

とりあえず漠然と考えていたのは
「自分でキャラなにか描きたいなぁ」
だった。

相変わらず下手だが、下手の横好き、別に見られても減るもんじゃないし!
(でもリアル知り合いには言わないし、見せないw)
という気持ちで、
自分でも描けそうで、
作りやすそうで、
カジュアルで、
工数は2日ぐらいないい感じの無いかなーと案を巡らせていると
よくある正弦波とかを利用した波乗りとかどうだろうとふと思いついた。

なみのり

昔メッシュを利用したマップを作ってその上を滑らせたりしてたんで
これならすぐ出来るなーと思った。ひっくり返ったらゲームオーバーにしようと。
とりあえず枠組みはすぐ出来るし、キャラも波乗りしている女の子でいいじゃんと。

サーフィンやボディボードとかも思ったけどわかりやすく浮き輪にしました。
浮き輪なら遅くても平気だし!
と描き始める。

f:id:ghoul_life:20180917142613j:plain

ざっとこんな感じで座ってて〜髪は長めで〜
とか試行錯誤をやってるうちにどんどん深みにハマっていく。(ここがめっちゃ楽しいんだけど)
「他のバリエーションとか浮き輪も乗るタイプのものとかアニメーションとか付けたいなー」
と思っているうちに期限は刻一刻と迫っていた。

ゲームについて

もうこっちは特に言うこと無いですね。
uvスクロールで水面を動かしつつとplaneを利用してスクリプトからmesh変形させてるだけ。

    private void Wave()
    {
        _mesh = this.GetComponent<MeshFilter>().mesh; // planeのメッシュ
        _meshCollider = this.GetComponent<MeshCollider>();
        _verticies = _mesh.vertices;

        int counter = 0;
        int yLevel = 0;

        for (var i = 0; i < iLen; i++) 
        {
            for (var j = 0; j < jLen; j++)
            {
                Calc(counter, yLevel);
                counter++;
            }
            yLevel++;
        }

        _mesh.vertices = _verticies;
        _mesh.RecalculateBounds();
        _mesh.RecalculateNormals();
        

        _meshCollider.sharedMesh = _mesh;

    }

    private void Calc(int i , int j)
    {
            var x = (_verticies[i].x + this.transform.position.x) / _detailScale;
            var y = (_verticies[i].y + this.transform.position.y) / _detailScale;
            _verticies[i].z = Mathf.PerlinNoise(x, y) * _heightScale;
            _verticies[i].z -= j;
    }

これプレイが進むと自動でパラメータが切り替わっていくんだけど
この切り替わりがちょっと唐突なのが微妙だった。
今後の課題だなと。lerpで向かっていくといいのかな。

(実はこんなトリッキーな波にすることとかも出来るのです)

f:id:ghoul_life:20180917164252p:plain

絵について

結局描けたのは
・通常
・ジャンプ時
・落ちた時
の3つだけ。
あとUIパーツとかそういったものも手描きで作ってたりしてます。

次回について

次ももちろん参加します。

それとは別で次のゲームの構想は決まっていて
作り出していきたいのですが仕事が忙しい…。
平日は10-23が標準で日付跨ぐ、朝までコースもしばしば。
これで家族がいる人とかどうやって暮らしてんだろう?
とか全然わかりませんね。

あ、宴ゲームジャム、賞ほんとにありがとうございました。
ADVの話のアイデアも練っておこう!
「Unity & 宴ノベルゲーム開発入門」出版記念オンラインゲームジャム | 無料ゲーム投稿サイト unityroom - Unityのゲームをアップロードして公開しよう