ビールが飲みたい。

ゲーム開発の備忘録などを書きます。

続。Excelでマスターデータ作ってVBA使ってJsonで出力する

こちらの記事の続編的な物です。

eggame.hateblo.jp

今更ながらVBA-JSONなる便利なライブラリを使って、もっと簡単にJSON出力のためのコードを書ける事に気づいたため、続編として残しておきます。

VBA-JSONのインポートと参照設定

こちらのリポジトリからダウンロード。

VBEを開き、ファイル > インポートからJsonConverter.basをインポート。 標準モジュールにJsonConverterが作成されればオーケー。

このままだと日本語がエスケープされてしまうので、 JsonConverterをダブルクリックして中身のjson_Encode関数の

Case 0 To 31, 127 To 65535

Case 0 To 31, 127 

に書き換えましょう。

Windows版ですが)NewでDictionaryを使うため、 ツール > 参照設定 からMicrosoft Scripting Runtimeをチェック入れる。 Macの場合は確認していないのでGithubのREADMEに従って下さい。

ついでにMicrosoft ActiveX Data Objects x.x Libraryにチェック入れてなかったらどれかに入れる。 (僕は2.8をチェックしました。)

今回のテーブル

f:id:eggame:20200403160221p:plain

コードを書く

コメントをいっぱい書いたので行数使ってますが、 コメントを消したら大分短くわかりやすいです。

Option Explicit

Sub CreateJson()
  
  Const sheetName = "item", root = "item"
  Dim targetFilePath As String
  Dim row As Integer, col As Integer, count As Integer
  
  'Jsonにコンバートするためのオブジェクトを用意するわけですが、
  'Collectionは[]でくくられる配列が作成され、
  'Dictionaryは{}でくくられる連想配列が作成されます。
  
  '今回はJsonUtilityの仕様に合わせて
  '{ "item":[{"key":"value"}, {"key":"value"}] }のような形を目指すため
  '初っ端はDictionaryをSetします。
  Dim jo As New Dictionary
  
  '出力パスを作成
  targetFilePath = ThisWorkbook.path & "\item.json"

  '{"item":[]} の形にする
  jo.Add root, New Collection
  
  With Worksheets(sheetName)
    
    'ループを開始する行番号を入れて下さい。
    row = 2 '行
    col = 1 '列
    
    'Collectionのindexは1からスタートだけど
    '関係が近い場所でカウントを進めたかったため、
    '初期値は0にしている
    count = 0
    
    Do
      jo(root).Add New Dictionary '配列に連想配列を追加
      count = count + 1           '配列のindexを進める
      
      'ここで連想配列に詰める
      Do
        jo(root)(count).Add .Cells(1, col).value, .Cells(row, col).value
        col = col + 1
      '空白のセルに当たるまで、列をループする
      Loop Until IsEmpty(.Cells(1, col).value) = True
      
      col = 1 'ループ抜けたら列カウント初期化
      row = row + 1
    '空白のセルに当たるまで、行をループする
    Loop Until IsEmpty(.Cells(row, 1).value) = True
  
  End With
  
  'joをJsonにコンバートしつつ、ファイルに書き込む
  WriteToFile JsonConverter.ConvertToJson(jo, Whitespace:=2), targetFilePath
  
  MsgBox ("出力完了")
End Sub

'ファイルに書き込む専用の関数
Private Function WriteToFile(ByVal json As String, ByVal path As String)
  Dim stm As New ADODB.Stream
  
  'ファイルが存在したら一旦削除
  If Dir(path) <> "" Then
    Kill path
  End If
  
  stm.Charset = "UTF-8"
  stm.LineSeparator = adLF
  stm.Open

  stm.WriteText json, 1
  stm.SaveToFile path, 2
  stm.Close
End Function

JSON出力結果

{
  "item": [
    {
      "data_id": 1,
      "data_name": "木の棒",
      "description": "故郷の木の枝を拾いました",
      "hp": 0,
      "attack": 1,
      "defense": 1,
      "speed": 1,
      "assetBundleName": "item_wood"
    },
    {
      "data_id": 2,
      "data_name": "竹やり",
      "description": "一般的な武器です",
      "hp": 0,
      "attack": 2,
      "defense": 2,
      "speed": 2,
      "assetBundleName": "item_bamboo_spear"
    },
    {
      "data_id": 3,
      "data_name": "錆びたナイフ",
      "description": "この龍の紋章は何でしょう。",
      "hp": 0,
      "attack": 3,
      "defense": 1,
      "speed": 3,
      "assetBundleName": "item_rested_knife"
    },
    {
      "data_id": 4,
      "data_name": "なべのふた",
      "description": "投げたい。",
      "hp": 5,
      "attack": 0,
      "defense": 5,
      "speed": 0,
      "assetBundleName": "item_pot_lid"
    }
  ]
}

備考

stringを連結していくより処理は重くなりますが、コードはとてもスッキリします。

【Unity1週間ゲームジャム お題「あつい」】レリックを獲得した時にパラメータ上昇効果を与える仕組みについて。

はじめに

Unity1週間ゲームジャム お題「あつい」に参加し、無事投稿したのでその時の話を簡単にまとめようと思います。

僕の参加は今回で6回目?7回目?

Unity 1週間ゲームジャム | 無料ゲーム投稿サイト unityroom - Unityのゲームをアップロードして公開しよう

作成したゲームについて

【Mayor's Survive】

Mayor's Survive | 無料ゲーム投稿サイト unityroom - Unityのゲームをアップロードして公開しよう

f:id:eggame:20180910163512g:plain

  • レリック買う
  • 街を作ってお金を稼ぐ
  • 襲ってくる敵を倒す

を繰り返しながらできるだけ長く生存するゲームです。 ゲームジャムに合わない時間泥棒ゲーですが、色々試したい物があったためつい(^^ゞ

Survival Shooter Tutorial - Asset Store

こちらのUnity公式TutorialであるSurvival Shooterを魔改造したゲームでもあります。

動的な値の変更に強そうな無料アセット見つけた!

ちょっと前に流行ったダンジョンメーカーや、最近どハマリしていたSlay the Spireのように、様々なレリックを獲得するとそのゲーム中レリックに応じた多岐にわたる効果が受けられる仕組みがあります。

僕はそういうの大好きなので、自分でどうにか作れないかここのところずっと考えていたのですが、どうも僕の勉強不足で効率のいい発想が浮かばない日々が続いていました。

で、今回の開発中に探し出した主力兵器が下記のアセットです。 assetstore.unity.com

キャラクターのステータスのAttackとかDefenceとか、ゲーム開発者が自由に項目を作ってプレイ中の動作に大きな影響を与えるパラメータの事を”stat”、まとめたものを"stats"などと呼ばれるようですが、このアセットはその辺りの操作を楽に出来るCharacterStatクラスを提供するアセットです。

無料アセットですので、本体の内容はダウンロードしてみてください。 解説動画もあります。 www.youtube.com

ここではそれをどう利用したのかを記述していきます。

レリックを獲得するとどういう流れてステータスが強化されるのか

CharacterStatアセットの中で使うクラス

  • CharacterStat.cs
    floatでBaseValueという基礎値を持つフィールドや、Valueという色んな効果を加味した最終的な値を持つプロパティがあり、StatModifyがリストに追加されたらDirtyフラグが立ち、次にアクセスがあった時にStatModifyの値を読み取りつつValueが再計算されます。
  • StatModify.cs
    CharacterStatのValueに与える効果を定義するクラスです。float Valueで効果量、Typeで加算や乗算など、Orderでどのタイミングでその値を加えるか、SourceでそのStatModifyを作ったオブジェクトをキャッシュし、そのオブジェクトが無くなった時に効果を外す処理が出来るようになります。←StatModifyを作ったオブジェクトが削除の命令を出した時に、オブジェクト自身を元に削除の対象となるStatModifyを探し出すことが出来るようにします。

作ったクラス

  • Relic.cs
    レリックを作るための親クラスです。
    今回はぱぱっとアイテム系を作るため、1アイテム1ScriptableObject(以下SO)方式にしています。
  • PlayerRelic.cs
    プレイヤーに効果を及ぼすRelicSOを作るクラスです。
  • PlayerInventory.cs
    プレイヤーが効果を受け取るためのMonoBehaviourです。Playerに影響を与えるCharacterStatが生えてます。
    PlayerRelicにこのクラスを渡して、CharacterStatを操作します。
  • RelicManager.cs
    獲得したレリックを記憶しておくためのシングルトンクラスです。本来こんな大雑把な管理はしないと思いますが、今回は大急ぎでコードを書いていたため、レリックをここに集めてしまいました。
    レリックがここに登録されると同時に、対象に効果を与える処理が走ります。(ついでに図鑑に登録します。)

レリックを作るコード詳細

Relicクラス

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

namespace U1W.Atsui
{
    public class Relic : ScriptableObject
    {
        public AssetProperty asset;
        public int Cost = 100;
        public int rare = 0;

        static Dictionary<string, Relic> _relicDic = null;
        static public Dictionary<string, Relic> RelicDic
        {
            get { return _relicDic ?? (_relicDic = Resources.LoadAll<Relic>("Relics").ToDictionary(data => data.name, data => data)); }
        }
    }
}

あらゆるレリックを作るための基底クラスです。
AssetPropertyは名前とか説明文とかアイコンとか、どこのアイテムでも使うようなフィールドをまとめたクラスです。

Costで購入金額、rareでレアリティを設定します。
rareはアイテムの出現率に影響を及ぼし、今回は5段階分かれています。
この辺はゲームの仕様に合わせて変わる部分ですね。

作ったレリックをまとめて吸い上げるのも、ここでstaticなDictionaryを作って読み込んでいます。
最初にRelicDicにアクセスがあった時にResourcesフォルダの中のRelicsフォルダ下にある全てのRelicSOをキャッシュし、RelicDic[アセット名]で取得できるようにしています。
これもUnityゲームジャム用取り急ぎコードな感じですが、急いで作る時は重宝しています。

PlayerRelicクラス

using UnityEngine;
using Kryz.CharacterStats;
using UniRx;

namespace U1W.Atsui
{
    [CreateAssetMenu(fileName ="New PlayerRelic", menuName ="U1W/Atsui/PlayerRelic")]
    public class PlayerRelic : Relic
    {
        public int StrengthBonus;
        public int VitalityBonus;
        public int SpeedBonus;
        [Space]
        public float StrengthPercentBonus;
        public float VitalityPercentBonus;
        public float SpeedPercentBonus;

        public void Equip(PlayerInventory c)
        {
            // ボーナス項目が0じゃなかったら効果を与える群
            if (StrengthBonus != 0)
                c.Strength.AddModifier(new StatModifier(StrengthBonus, StatModType.Flat, this));
            if (VitalityBonus != 0)
                c.Vitality.AddModifier(new StatModifier(VitalityBonus, StatModType.Flat, this));
            if (SpeedBonus != 0)
                c.Speed.AddModifier(new StatModifier(SpeedBonus, StatModType.Flat, this));

            if (StrengthPercentBonus != 0)
                c.Strength.AddModifier(new StatModifier(StrengthPercentBonus, StatModType.PercentMult, this));
            if (VitalityPercentBonus != 0)
                c.Vitality.AddModifier(new StatModifier(VitalityPercentBonus, StatModType.PercentMult, this));
            if (SpeedPercentBonus != 0)
                c.Speed.AddModifier(new StatModifier(SpeedPercentBonus, StatModType.PercentMult, this));

            Debug.Log("Equip!");

            // ゲームのメインステートがタイトル画面になったら効果を全て解除する。
            SceneManager.Instance.State
                .FirstOrDefault(x => x == GameState.Title)
                .Subscribe(x => Unequip(c))
                .AddTo(SceneManager.Instance);
        }

        public void Unequip(PlayerInventory c)
        {
            c.Strength.RemoveAllModifiersFromSource(this);
            c.Vitality.RemoveAllModifiersFromSource(this);
            c.Speed.RemoveAllModifiersFromSource(this);
        }
    }
}

プレイヤーに効果を及ぼすレリックを作成するためのクラスです。
RelicクラスからSOを継承していて、[CreateAssetMenu]属性をここでつけることで、UnityのメニューからPlayerRelicクラスの作成が出来るようにしています。

作ったSOはこんな感じでResources/Relics以下にズラッと。

f:id:eggame:20180910174257p:plain

ここではプレイヤーのどの値にどのくらいの効果を及ぼすのかを設定し、Relic獲得時にRelicManagerの操作によってPlayerInventoryにあるCharacterStatフィールドにStatModifyを与えます。

このクラスが多種多様な効果を及ぼすキモで、ショップになにか影響を及ぼすレリックを作りたければclass ShopRelic : Relic、人口増加や仕事で稼げる金額などに効果を与えたければclass CityRelic : Relicなどを作り、RelicManagerに操作してもらうための関数を定義する感じになります。

レリックをプレイヤーに与えるコード詳細

PlayerInventoryクラス

using Kryz.CharacterStats;
using UnityEngine;
using UniRx;
#if UNITY_EDITOR
using UnityEditor;
#endif

namespace U1W.Atsui
{
    public class PlayerInventory : MonoBehaviour {

        [SerializeField] PlayerHealth health;
        [SerializeField] PlayerMovement move;
        [SerializeField] PlayerShooting shooting;

        public CharacterStat Strength;
        public CharacterStat Vitality;
        public CharacterStat Speed;

        void Start () {
            Strength = new CharacterStat(shooting.damagePerShot);
            Vitality = new CharacterStat(health.vit);
            Speed = new CharacterStat(move.speed);

            Strength.UpdateValue
                .Subscribe(x => shooting.damagePerShot = (int)x)
                .AddTo(this);

            Vitality.UpdateValue
                .Subscribe(x => health.vit = (int)x)
                .AddTo(this);

            Speed.UpdateValue
                .Subscribe(x => move.speed = x)
                .AddTo(this);
        }
        
#if UNITY_EDITOR
        private void OnGUI()
        {
            GUILayout.BeginArea(new Rect(0, 0, 100, 200));
            {
                using (new GUILayout.VerticalScope(GUI.skin.box))
                {
                    GUILayout.Label("STR: " + Strength.Value);
                    GUILayout.Label("VIT: " + Vitality.Value);
                    GUILayout.Label("AGI: " + Speed.Value);
                }
            }
            GUILayout.EndArea();

        }
#endif
    }
}

プレイヤーのHPや攻撃力、移動速度などを一箇所で参照して値を書き換えられるようにし、CharacterStatの値が更新されたらそれぞれの値を書き換えるためのPlayer GameObjectにくっつけるコンポーネントです。
普段はRelicManagerにプレイヤーに対するRelicリストを持たせるよりこちらに持たせる方が良さそうな気がしますが、ちゃちゃっと表示など作る都合上、今回Inventoryは値を操作するだけのクラスにしました。

Startで各コンポーネントの初期値を元にCharacterStatをnewし、その後はUniRxのReactivePropertyでCharacterStatに作ったUpdateValueにより更新通知がきたら各コンポーネントの値を書き換えてプレイヤーに影響を与えます。

※追記18/9/10 22:30

PlayerInventoryなんてコンポーネントにしていますが、こういう用途なら

public class PlayerStatModifier : MonoBehaviour, IStatModifier
{
}

のようにしてPlayerInventoryを受け取るのではなくIStatModifierを受け取って操作した方がいろいろ汎用性が高いと思われます。
どうですか、お客さん!?(Interface不慣れ)


ここでちょっとCharacterStatの中身の話。

そう、実はCharacterStatはisDirtyがtrueになった後Valueにアクセスがあると計算が行われるため、StatModifyを与えただけじゃDirtyフラグが立つだけになります。

CharacterStatのサンプルではValueをUpdateで読み取っているため大した影響はありませんが、僕は普段値が更新されたら通知して反映されるように作っているため、StatModifyを与えたらすぐ更新して通知してくれないと困ります。

ので、UniRxを利用してCharacterStat.csに以下のような改造を施しました。

// このReactivePropertyを生やす
FloatReactiveProperty _updateValue = new FloatReactiveProperty();
public IReadOnlyReactiveProperty<float> UpdateValue { get { return _updateValue; } }

// 中のコードは書きませんが、
isDiry = true;
// となっている部分の下に
_updateValue.Value = Value;
// を記述して、すぐに更新して通知されるようにしています。

RelicManagerクラス

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

namespace U1W.Atsui
{
    public class RelicManager : Singleton<RelicManager>
    {
        [Serializable]
        public class Data
        {
            public List<string> relics = new List<string>();
        }

        // ListをJsonUtilityで保存するためクラスに入れています。
        public Data relicData = new Data();
        [SerializeField] PlayerInventory player;
        
        public void AddRelic(Relic relic)
        {
            if (relic is PlayerRelic)
            {
                PlayerRelic data = (PlayerRelic)relic;
                data.Equip(player);
            }

            DataHolder.Instance.AddBook(relic);

            relicData.relics.Add(relic.name);
        }

        public void RemoveRelic(Relic relic)
        {
            // 実は今回1プレイが終了するとシーン読み直ししており、
            // 全てのレリックがリセットされる仕様のため、
            // この関数全く使っていません。

            // 実際の運用時には、この場で
            // relicData.relics[relic.name].UnEquip(対象);
            // を呼び出すなり、
            // リストからレリックを削除した時にそのレリックの
            // 効果を外す処理を予め仕込んでいる必要があります。
            relicData.relics.Remove(relic.name);
        }
        
        public void Resume(Data data)
        {
            foreach (var relic in data.relics)
            {
                if(Relic.RelicDic.ContainsKey(relic))
                    AddRelic(Relic.RelicDic[relic]);
            }
        }

        // TODO もっと軽く出来る
        public List<Relic> LootBox(int count = 3)
        {
            Debug.Log("Relic.RelicDic.Count: " + Relic.RelicDic.Count);

            var list = new Dictionary<Relic, int>();
            foreach (var name in Relic.RelicDic.Keys)
            {
                if (!relicData.relics.Contains(name))
                {
                    list.Add(Relic.RelicDic[name], 21 - (Relic.RelicDic[name].rare * 5));
                }
            }
            count = Mathf.Min(count, list.Count);

            if (count <= 0) return null;

            List<Relic> result = new List<Relic>();

            for (var i = 0; i < count; i++) {
                var relic = WeightedRandomizer.From(list).TakeOne();
                result.Add(relic);
                list.Remove(relic);
            }

            return result;
        }
    }
}

Relicを一元管理するシングルトンクラスです。
レリックの追加や削除、ついでに重みを元にランダムでレリックを引っ張り出す関数が作ってあります。(これは機能分けたほうが良いやつ)

ここで重要な部分は

        public void AddRelic(Relic relic)
        {
            if (relic is PlayerRelic)
            {
                PlayerRelic data = (PlayerRelic)relic;
                data.Equip(player);
            }

            DataHolder.Instance.AddBook(relic);

            relicData.relics.Add(relic.name);
        }

というレリックが追加された時の処理で、RelicSOはポリモーフィズムで派生させているため単純に受け取ったRelicからはどうやってもPlayerRericで作った機能が引っ張り出せません。が、

if(relic is PlayerRelic)


で受け取ったレリックがPlayerRelic型かどうかが判断できます。
これを元に(PlayerRelic)relicとすることで型変換を行うと、PlayerRelicとしての機能やフィールドの値が使用できるようになります。

これがShopRelicだったらif(relic is ShopRelic)で、CityRelicだったらif(relic is CityRelic)で判断が出来て、その中に作った関数を使う処理を書けば効果を発揮させる事が出来るわけです。

ついでにDataHolder.Instance.AddBook(relic);で図鑑登録しつつ、レリックリストに追加します。

結果できたもの

f:id:eggame:20180910182039g:plain

うむ、良い感じです。

感想

  • CharacterStatは無料で勉強になる!
  • ちょうど実験していた機能を組み合わせたゲームで、見栄えがちょっと...。っていうかいつも見栄えがちょっと....。
  • UX頑張れるようになりたいですね!

次回も頑張っていきたい。

【Unity】スキルなどの色んな種類の効果に対応するためのコードを考え中

※追記18/9/11

この記事もポリモーフィズムによる効果の種類作成に関わるものですので、よろしければ御覧下さい。

eggame.hateblo.jp

MapPinEffectから派生したHealEffect等は、復元した後は is で判定できるものの、jsonから復元する際には相変わらず型がわからないので、enumによるType判定は必要になります。

概要

オヒサシブリデス

RPGのスキルなどの”効果”を設計していて、もうちょっとスマートに出来ないかなぁと悩んだため、記事として公開しました。
備忘録でもあります。

全体像を先に言いますと、ポリモーフィズムで効果の発動をカスタマイズしつつ、パラメータをScriptableObjectを使って保存して実機で動かします。

動作環境

  • Unity 2017.4.9f1

とりあえず動かしたコード

まずはコード

すごろくゲームのフィールドで使用出来るスキルです。
※開発中なので仮置きなフィールドが多数。あまり参考にならなそうな気がします。

スキルのModel

MapPinTemplate.cs

    [Serializable]
    public class MapPinTemplate : Resource
    {
        public const string PREFIX = "MapPin";

        // 所有上限、下限などを設定する値群。
        public StackProperty stackProperty = new StackProperty();
        // ほぼ全てのマスターデータに共通して設定する値群。
        public AssetProperty asset = new AssetProperty();

        //移動力
        public int move;
        
        //使用ロケーション(どのタイル上で使用可能か)
        public List<TileType> tileType = new List<TileType>() { TileType.Field };

        //反映される値(何の効果を発生させるか(数字をいじる、タイルに敵を出現させる...など))
        public List<MapPinAbility> abilitys = new List<MapPinAbility>();
        
        //アニメーション(別で登録して引っ張り出す)
        public int animIdx;
    }

”ファイア”とか”ケアル”とかいった物を定義するクラス。

ここで大事なのはabilitysがそのスキルを発動した際に実行する効果を持っていること。
1つのスキルに複数効果を持たせて発動できるようにしています。


MapPinAbility.cs

    [Serializable]
    public class MapPinAbility
    {
        // 能力名
        public string name;

        //使用トリガー(自ら、ダイス扱いした時...など)
        public UsePinTrigger trigger;
        //適用対象(自分、その場にいる敵、狙ったターゲット、狙ったタイル、モンスター全員、敵プレイヤー全員....などなど)
        public UsePinTarget target;
        public UsePinTargetGroup targetGroup;
        public int targetCount;
        //効果時間(即時、ターン数、ターン経過後)
        public UsePinEffectTime effectTime;

        // スキルの効果を設定する各種
        public MapPinEffectType effectType;
        public string effectJson;
        [NonSerialized] public MapPinEffect effect;
    }

スキルの持つ効果名や、その発動条件、効果を持つ部分です。
大事なのは、ここのMapPinEffect型のフィールドが実行時に効果を処理する部分だということです。

何やら下3つが怪しい雰囲気を醸し出していますが、以下にMapPinEffectを記載した後説明します。

効果を処理するオブジェクト

MapPinEffectという名前の、効果を解決する部分は、以下の構成でポリモーフィズムを作っています。

  • public abstract class MapPinEffect
    あらゆる効果を一つのフィールドに持たせるための基底クラス

  • public abstract class CharacterMapPinEffect : MapPinEffect
    これは継承の仕方の一例です。フィールドにいるキャラクターに対して効果を適用するスキルの処理に使用します。

  • public class HealEffect : CharacterMapPinEffect
    これも一例です。回復の効果を解決するのに使用します。

MapPinEffect.cs

    [System.Serializable]
    public abstract class MapPinEffect
    {
#if UNITY_EDITOR
        // マスターデータエディターに入力を作るために使用します。
        public virtual MapPinEffect Draw(string json) { return null; }
#endif
    }

効果を保存するフィールドを作るための大本です。
Editor拡張でマスターデータの編集を行っているため、Editor Windowから編集のためのレイアウトを呼び出す関数が定義してあります。


CharacterMapPinEffect.cs

    [System.Serializable]
    public abstract class CharacterMapPinEffect : MapPinEffect
    {
        public virtual void Resolve(Character character) { }
    }

今回、効果はResolveという名前の関数を実行すると対象の値が書き換えられますが、ここでまず値をいじる対象を引数として定義します。
対象のパターンの数だけこれと同様のクラスが増えます。

(引数、interfaceにした方が良いかも)


HealEffect.cs

   [Serializable]
    public class HealEffect : CharacterMapPinEffect
    {
        public int fixedHp;
        public int fixedSp;
        [Range(0, 1)] public float ratioHp;
        [Range(0, 1)] public float ratioSp;

        public override void Resolve(Character character) {
            // 回復を与える(固定)
            character.Heal( fixedHp, fixedSp);

            // 回復を与える(割合)
            character.Heal( Mathf.FloorToInt(character.hpMax * ratioHp), Mathf.FloorToInt(character.spMax * ratioSp));
        }

#if UNITY_EDITOR
        public override MapPinEffect Draw(string json)
        {
            var effect = JsonUtility.FromJson<HealEffect>(json);
            fixedHp = EditorGUILayout.IntField("HP回復量(固定)", effect.fixedHp);
            fixedSp = EditorGUILayout.IntField("SP回復量(固定)", effect.fixedSp);
            ratioHp = EditorGUILayout.Slider("HP回復量(割合)", effect.ratioHp, 0f, 1f);
            ratioSp = EditorGUILayout.Slider("SP回復量(割合)", effect.ratioSp, 0f, 1f);
            return this;
        }
#endif
    }

効果の詳細を記述する部分です。
効果の種類の数だけこれと同様のクラスが増えます。
アニメーション等もここで発火する予定ですが、今は回復効果だけ入っています。

話を怪しげな3つのフィールドに戻して...

MapPinAbility.csで見たこれらの怪しげなフィールドについてです。

public MapPinEffectType effectType;
public string effectJson;
[NonSerialized] public MapPinEffect effect;

なおMapPinEffectTypeはenumです。

  • effectTypeで実行時に作るMapPinEffectの種類を特定します
  • effectJsonには、UnityEditorで値を編集した物をjsonシリアライズした物が入ります。
  • effectには実行時にeffectTypeとeffectJsonを使ってHealEffectなどをインスタンス化します。

なぜこの構成になっているのか。

Unityの仕様(ではないかも)で、ポリモーフィズムは単純にシリアライズした物を復元することが出来ません。
もうちょっと具体的にいうと、

HealEffect effect = JsonUtility.FromJson<MapPinEffect>(json);

ではHealEffectが正しくデシリアライズ出来ません。

HealEffect effect = JsonUtility.FromJson<HealEffect>(json);

ならいけます。

それぞれ1クラス1ファイルで定義した上でそれをScriptableObjectとして保存する事で一応設定した値を保持出来るのですが、めっちゃファイル数増えてやだーーー!!!と思ったので、一旦パラメータをjson(string)で保存しておき、実行時にeffectTypeを見てそれに対応するeffectのインスタンスを作り、そのeffectにjsonをデシリアライズ、という事をしています。

もうちょっと具体的にいうとFactoryパターンで

MapPinfactory factory = new MapPinfactory();
MapPinEffect effect = factory.create(effectType, effectJson);

などと書くわけですがfactoryのcreateでは

switch(type){
    case MapPinEffectType.Heal:
        return JsonUtility.FromJson<HealEffect>(effectJson);
}

とかするわけです。

これらが今回のモヤモヤ部分。もっとスマートな方法は無いものか。

※追記 これをEditorで設定するとどんな感じになるか

こんな感じで効果を分けて書けるようにしてます。

f:id:eggame:20180902191245g:plain

今気づいたのですが、効果説明を書く場所を忘れていました。
とはいえ効果説明は自動生成した方が良さそうですね。

思っていること

ScriptableObjectを使っているものの、ゆくゆくはサーバーからマスターデータをjsonで渡したいと考えているのでこういう形にしました。が、ちょっと煩雑に見える。

今はマスターデータの編集をUnity Editorで行っているものの、結局Excel管理のほうがコピペとかちょっとした値の変更とか楽だなーと思います。
が、このjson部分はExcelでどう処理したら....と悩みもあったり。
(これもVBAで引数に値渡したらjson返す関数書いて解決かしら?)

まとめ

  • 実行時に効果のフィールドだけjsonをデシリアライズするとかしないやり方は無いものか。
  • Editor拡張でマスターデータEditor作るのツラい
  • 走り書きしたので後でもう少し読みやすくするかも。(僕がこう言う時、実際にやることは少ないですが。)
  • このコードブロック見辛くない!?ってか、全体的に見づらくない!?テーマ変えようかな・・・。 テーマ変えました。

Unity1week お題「space」に参加したときのこと

はじめに

Unity1週間ゲームジャム お題「space」に参加し、無事投稿したのでその時の話を簡単にまとめようと思います。

僕の参加は今回で5回目。

Unity1週間ゲームジャムとは

Unityroomで行われる、naichiさん(@naichilab)主催の1週間でお題に沿ったゲームを制作するゲームジャムです。

f:id:eggame:20170829185948p:plain

Unity 1週間ゲームジャム | 無料ゲーム投稿サイト unityroom - Unityのゲームをアップロードして公開しよう

スタンスは「ゲームを作るきっかけになれば」というもので、基本的にUnityを使ったどんな物でもあり。期限過ぎちゃっても完成しなくても全然オッケー!という非常に条件がゆるく参加しやすいイベントです。

作成したゲームについて

【ディープ・ディガー ミカン】

ディープ・ディガー ミカン | 無料ゲーム投稿サイト unityroom - Unityのゲームをアップロードして公開しよう

f:id:eggame:20171123133724g:plain
宇宙を飛び回り、星から資源を発掘するゲームです。

「ミカン」......「みかん」......「未完」......!(おや?隙間風かな?)

ミカンがいつか取れると良いですね!

使用したAsset

BGM。 (初使用)

ちょっとしたカメラ演出時のエフェクト (初使用)

UI全般の素材 (初使用)

ステージを自動生成する処理の一部に利用 (初使用)

外部ツールTexturePackerでまとめたファイルのインポートに利用

テクスチャーの掘削などに利用

軽量に動作するスクロールビューの構築

フォントの表示

サウンドマネージャー

細かなアニメーションの制御

データの保存

ポストプロセス

リアクティブプログラミング

DIコンテナ。シングルトンの代わり

フォントはNasuとUbuntu、自機などのテクスチャーはopengameart.orgのpublic domain素材

いつも通り、容赦のないアセットの盛りっぷりです :)
僕が作った素材は、きれいなコンクリっぽいシームレステクスチャに藍色の色を加えて、宇宙の背景にしたくらいでしょうか。

ネタ出しの話

週前半に仕事が詰まっていることが確定していたので、元々あまり参加する気はありませんでしたが、お題を見た時にフッと色々案が思い浮かんできたので、一応やれそうだったらこうしよう!という形で妄想はしていました。

「space」ということで、最初に考えたネタは商隊が町から町へ積荷を運んでいくゲームで、食糧と積荷をバランスよくスペースに詰め込んで食糧を消費しながら移動していくという物でした。

ちょうどRPGっぽいゲームを作り始めていて、ワールドマップを移動する演出に何か良いものがないか考えていたため、それに引っ張られています。

お題発表から3時間位で出したネタ。

コンセプトをノートに書いて写メをTwitterにアップしました。

が、しかし!

これ単体では全然楽しくなさそう!!!

仕事も忙しいだろうし、ノれるネタじゃないからやっぱやめよう。

と思った結果、ツイ消しして寝ることに。
わずか2時間の命でした。

しかしなんだかんだいって考え始めると止まらなくなる質なので、翌日も仕事しながらネタは考えます。

ストレートにspaceを宇宙と捉えることと、第3回目で使って以来タンスの肥やしになっていたDestructible 2Dを使った発掘ゲームにしよう!と月曜日には決めてました。

数日経って、予定していた仕事が後ろにずれて暇ができたため、水曜日辺りには結局作り始めます。

時間もあまり無いし、アセットいっぱい使ってね月間*1なので、宇宙空間を360°飛び回るゲームで何かのサンプルプロジェクトをベースにしようと考え、3つくらいサンプルゲームを見ましたが、Destructible 2Dに同梱されていたサンプルゲームがイメージにドンピシャだったため、これをベースにすることに。

unityroom & 1week GAMEJAMの投稿作品にアセットを登録してバウチャーをゲットしよう! – Unity公式 Asset Portal

進捗動画としてアップするといつも以上に反応があったため、「このシステムを基礎としたアプリいけるかも?」と思った結果、予め期限ぶっちぎる前提で素材収集と強化をしていけるゲームに仕上げることにし、アセットも容赦なく突っ込んでいくことに決定しました。

(自力でここまで作り上げたんか...!って感動された方、申し訳ない。感動をお返し致します😱)

そして無限に広がっていく風呂敷......。

今もなお、風呂敷は広がっています。 正に宇宙!!!

Landscaperを利用した、オブジェクト半自動生成装置の話

少し前にAssetStore企画のスクリーンショットコンテストに投稿しており、その際にTerrain上に、GameObjectを地形に合わせて自動生成するアセットを利用しました。

お題にチャレンジ!Vol.2: 大草原にたたずむ小さな家のスクリーンショット – Unity公式 Asset Portal

f:id:eggame:20171123125823p:plain

(ありがたいことに大賞をいただき、ユニティちゃんグッズもらえる事になりました。やったね嬉しい!)

この草原、GameObjectで草2万個くらい生やしてます。

Terrainの凹凸の傾斜角度や、塗られているテクスチャーの種類(草とか土地面とか岩肌とか)を見て、それに沿ってGameObjectをいい感じに生成してくれるアセットです。

この時はパーリンノイズによりTerrainを自動的に隆起させるプログラムを書き、Terrainに隆起を発生させた後、草大量に生やしました。草。

f:id:eggame:20171123130522p:plain

(実際の所、カメラに映る範囲だけ生やしたいため、カメラに映らない範囲は禿げ上がってます。草原のように見えますが、砂漠です。

spaceマップの作り方

  1. 縦横深さと隆起のサイズを決めます
  2. Terrainに自動で隆起を発生させるプログラムを書き、隆起を発生させます。
    youtu.be
  3. Landscaperで利用するScriptableObjectにGameObjectのバラ巻き方を色々設定し、生成します。
    youtu.be
  4. 生成したままだとTerrainの隆起に合わせて角度や位置が設定されているので、これらを実際利用するpositionとrotationに変換します。
    youtu.be
  5. この生成したSceneをGameObjectのみ存在するSceneとして保存しておき、ステージ開始時にAdditiveで追加します。(この際、自機の出現ポイント(0,0)にオブジェクトがないことを確認します。)

生成したいものとして登録する際の設定項目に、重みづけ、距離感、Terrainの傾斜、Terrainのどのテクスチャー上に生やすか、など設定でき、星も敵もなんでもまとめていい感じに自動生成出来るので、かなり手間が省けました。

本当はその作業すらゲーム内で自動的に行えると良いと思いますので、後々時間をかけてアルゴリズムを考えるかもしれません。
(面倒なのでこの方式でそのまま運用するかもしれません)

まとめ

良かった点

  • 削るの気持ちィィ
  • Terrainについて理解が進んだ
  • Multi Scene Edditingをもっと積極的に使ってもいいかと思い始めた。(今はあまり使っていないため)

反省点

期限内に出来た機能は、裏方を優先したため(装備選択機能とかショップ機能とか)、ゲーム部分は星に弾を撃って削って近寄るだけ、避けたりしないとゲームオーバーになるような常時何かしなきゃいけないような要素もなく、クリア目的も規定時間が経過するのを待つだけで緩急全くないステージ。

ぶっちゃけて言えば、このゲーム退屈です!!!

装備切り替えなんてまず捨てて、最初にメインの遊びの部分から作るべきだと学びました。(5回目でようやく)

感想

ネタ的には良いと思うんですが、上手く期間内には調理できなかったので、悔しさありますね。
このゲームは今後も調整していく予定なので、興味がありましたら下記のページをご覧下さい。

https://trello.com/b/U8PCJEdI/update

絶対に面白いゲームにしてやるぞ!

*1:UnityroomとAssetStoreのコラボ企画で、ゲームを投稿し利用したアセットを登録して、ゲームページを申請するとAssetStoreで利用できるバウチャーがもらえる企画を開催中です。

Excelでマスターデータを作り、JsonでScriptableObjectに読み込ませる備忘録

※ 2020-04-03 なんと続編的なものを書きました

eggame.hateblo.jp

はじめに

この記事は、Excelでマスターデータを作り、jsonとして出力し、UnityのJsonUtilityを利用してScriptableObjectに読み込ませるまでの一連の流れを列挙した備忘録です。

暇ができたから記事を書いたものの、ExcelをUnityで直接読み込んだりした方が楽なので、特に理由が無ければこちらの記事等を参考にされた方が良いかと思います。

robamemo.hatenablog.com

ExcelからJSONを出力したかったり、JSONを読み込ませたかったり、サーバーからJSONAPIで受け取る予定だけどまだサーバー準備中だから......みたいなちょっと変わった事情がある方が参考になるかなぁという感じの記事です。

そういうやり方もできるンダナーくらいの流し読み推奨。

※10/4 12:00 追記

ScriptableObjectの何が良いのかってスライドをUnity様が丁度同じ日にアップしていました。

www.slideshare.net

Excel等からScriptableObjectを生成する方法も少し記述されていて、こちらの手法が使えればよりスマートに事が進むと思います。

バージョン情報

  • Excel 2016
  • Unity 2017.1.0f3

ExcelからJSONで出力する

Excelを開き、item.xlsmなど適当な名前で保存します。
.xlsx形式だと保存後に書いたマクロが消えてしまうのでご注意下さい。

まずはマスターデータを作ります。
f:id:eggame:20171003165153p:plain
今回はこんな感じ。

次にテーブルをjsonで出力出来るよう、vbaを書きます。
f:id:eggame:20171003171618g:plain

  • メニューの[開発] > [Visual Basic]をクリック
  • [挿入] > [標準モジュール] を選択肢し、Module1を作成
  • マクロを記述(今回動かしたコードは下記に記載しました。)
  • utf-8で保存したいため、とあるライブラリを使用します。
     [ツール] > [参照設定] から
    Microsoft ActiveX Data Objects
    の適当なバージョンにチェック
  • 実行ボタンよりマクロを実行

targetFilePath変数に出力時のファイル名が設定されています。 コードは下記の通り。

Option Explicit

Sub CreateJson()

  Dim stringData As String
  Dim targetFilePath As String
  
  Dim sheetName As String
  Dim key As String
  Dim value As String
  
  Dim row As Integer
  Dim col As Integer
    
'****
' Init
'****
  targetFilePath = ThisWorkbook.Path & "\item.json"
  
  'ファイルを一旦削除
  If Dir(targetFilePath) <> "" Then
    Kill targetFilePath
  End If


'****
' 処理
'****
  Dim stm As ADODB.Stream
  Set stm = New ADODB.Stream
  
  stm.Charset = "UTF-8"
  stm.LineSeparator = adLF
  stm.Open

  
  sheetName = "Sheet1"
  'ループを開始する行番号を入れて下さい。
  row = 3
  
  stringData = stringData + "{"
  stringData = stringData + """" + "item" + """" + ":["
    
  Do
    stringData = stringData + "{"
    
    col = 1
    Do
      
      key = """" + CStr(Worksheets(sheetName).Cells(2, col).value) + """"
      value = """" + CStr(Worksheets(sheetName).Cells(row, col).value) + """"
      stringData = stringData + key + ":" + value
      
      col = col + 1
      
      If IsEmpty(Worksheets(sheetName).Cells(row, col).value) = False Then
        stringData = stringData + ","
      End If
      
    Loop Until IsEmpty(Worksheets(sheetName).Cells(row, col).value) = True

    row = row + 1
    
    If IsEmpty(Worksheets(sheetName).Cells(row, 1).value) = True Then
      stringData = stringData + "}"
    Else
      stringData = stringData + "},"
    End If
    
  Loop Until IsEmpty(Worksheets(sheetName).Cells(row, 1).value) = True
  
  stringData = stringData + "]}"
  
  stm.WriteText stringData, adWriteLine
  stm.SaveToFile targetFilePath, adSaveCreateOverWrite
  stm.Close
  
  MsgBox ("出力完了")
  
  End Sub

詳しくは解説しませんが、stringData変数にテーブルから読み取ったデータをどんどん繋げていき、最後にtargetFilePathで設定したファイルへまとめて書き込みます。
この設定の場合、jsonファイルはxlsmファイルと同じディレクトリに出力されます。

以下のようなjsonファイルが作成されます。(出力後に整形しました。)

item.json

{
  "item": [
    {
      "idx": "item1",
      "name": "木の棒",
      "discription": "故郷の木の枝を拾いました。",
      "hp": "0",
      "attack": "1",
      "diffence": "1",
      "speed": "1",
      "assetbundle": "item_wood"
    },
    {
      "idx": "item2",
      "name": "竹やり",
      "discription": "一般的な武器です。",
      "hp": "0",
      "attack": "2",
      "diffence": "2",
      "speed": "2",
      "assetbundle": "item_bamboo_spear"
    },
    {
      "idx": "item3",
      "name": "錆びたナイフ",
      "discription": "この竜の紋章は何でしょう。",
      "hp": "0",
      "attack": "3",
      "diffence": "1",
      "speed": "3",
      "assetbundle": "item_rusted_knife"
    },
    {
      "idx": "item4",
      "name": "なべのふた",
      "discription": "投げたい。",
      "hp": "5",
      "attack": "0",
      "diffence": "5",
      "speed": "0",
      "assetbundle": "item_pot_lid"
    }
  ]
}

最後に、シートにボタンを用意し、押すと出力されるようにしておくと少し楽になります。 f:id:eggame:20171003171816g:plain

  • [開発] > [挿入] > [ボタン]をクリック
  • クリック長押ししながら適当なサイズへマウスを移動
  • マウスを離すと機能を登録するウィンドウが出現するので、処理を書いたプロシージャ名を選択

うまく設定出来れば、クリックするとjsonが出力されるようになります。

JSONを読み込むための準備

ここからUnityでの作業に移ります。

読み込むjsonの形と一致するよう、マスターデータクラスを作ります。
フィールド名とjsonのkeyが一致していないと無視されてしまうので、ご注意下さい。
JsonUtilityでシリアライズするため、各クラスへ[Serializable]をつけます。

ItemMasterData.cs

using System;
using System.Collections.Generic;

[Serializable]
public class ItemMasterData{

    public List<ItemData> item = new List<ItemData>();

    [Serializable]
    public class ItemData
    {
        public string idx;
        public string name;
        public string discription;
        public int hp;
        public int attack;
        public int defense;
        public int speed;
        public string assetbundle;
    }
}

マスターデータを入れるScriptableObjectを作ります。

ItemSO.cs

using UnityEngine;

[CreateAssetMenu]
public class ItemSO : ScriptableObject {
    public ItemMasterData itemMasterData;
}

ScriptableObjectの作り方は色々あると思いますが(マスターデータ用クラスと統合しちゃったり)、今回はわけて作りました。

jsonファイルをStringで読み込むスクリプトを用意します。

JsonHelper.cs

using UnityEngine;
using System;
using System.IO;
using System.Text;

public class JsonHelper
{
    /// <summary>
    /// JSONファイルをStringで読み込みます。
    /// </summary>
    /// <param name="filePath">streamingAssetsフォルダからのパス</param>
    /// <param name="fileName">ファイル名</param>
    /// <returns>jsonのstringデータ</returns>
    public static String GetJsonFile(String filePath, String fileName)
    {
        string fileText = "";

        // Jsonファイルを読み込む
        FileInfo fi = new FileInfo(Application.streamingAssetsPath + filePath + fileName);
        try
        {
            // 一行毎読み込み
            using (StreamReader sr = new StreamReader(fi.OpenRead(), Encoding.UTF8))
            {
                fileText = sr.ReadToEnd();
            }
        }
        catch (Exception e)
        {
            // 改行コード
            fileText += e + "\n";
        }

        return fileText;
    }
}

ボタンをクリックするとScriptbleObjectへjsonデータが読み込めるスクリプトを作ります。

LoadMasterDataFromJson.cs

using UnityEngine;

public class LoadMasterDataFromJson : MonoBehaviour {

    public ItemSO itemSO;

    private void Awake()
    {
        if (!itemSO)
        {
            itemSO = Resources.Load<ItemSO>("MasterData/ItemMasterData");
        }
    }

    private void LoadFromJson()
    {
        itemSO.itemMasterData = new ItemMasterData();
        itemSO.itemMasterData = JsonUtility.FromJson<ItemMasterData>(JsonHelper.GetJsonFile("/","item.json"));
    }

    private void OnGUI()
    {
        if(GUI.Button(new Rect(20, 20, 100, 50), "読み込み"))
        {
            LoadFromJson();
        }
    }
}

フォルダを作っていきます。

今回はScriptableObjectをResources.Loadで読み込むことを想定し、
Resources/MasterDataの中に格納します。

jsonファイルをStreamingAssetsフォルダから読み込むため、StreamingAssetsフォルダを作成し、item.jsonを格納します。

これで下準備完了です。

JSONをScriptableObjectへ読み込む

データを入れるScriptableObjectを作ります。
MasterDataフォルダを右クリックし、[Create] > [Item SO]を選択すると、MasterDataフォルダ内に新規オブジェクトが生成されます。
名前をItemMasterDataにしておきます。

ファイル名は、LoadMasterDataFromJson.csでの読み込み処理に影響が出ますので、別の名前にする際はLoadMasterDataFromJson.csを適宜編集して下さい。

f:id:eggame:20171003174704g:plain

LoadMasterDataFromJson.csを適当なGameObjectに追加し、シーンを再生するとGame画面にGUIボタンが出現します。
GUIボタンをクリックするとScriptableObjectにjsonファイルからデータが読み込まれます。

f:id:eggame:20171003175524g:plain

使用後、LoadMasterDataFromJson.csを使わないときはInstectorのチェックを外したり、削除したりすれば動かなくなります。

おわりに

VBAなどハードコーディングしちゃってる部分もあり、ちょっとコードがわかりづらいかもしれません。すいません。
何かあればコメント欄にお願いします。

Unity 1週間ゲームジャム「フロー」でクオータービューに挑戦しました

※8/30 16:00 Isometric 2.5D Toolsetの話にUltimate Isometric Toolkitのことを追記しました。

はじめに

祝!初エントリー!

Unity1週間ゲームジャム お題「フロー」に参加し、無事投稿したのでその時の話を簡単にまとめようと思います。

僕の参加は今回で4回目。

Unity1週間ゲームジャムとは

Unityroomで行われる、naichiさん(@naichilab)主催の1週間でお題に沿ったゲームを制作するゲームジャムです。

f:id:eggame:20170829185948p:plain

Unity 1週間ゲームジャム | 無料ゲーム投稿サイト unityroom - Unityのゲームをアップロードして公開しよう

スタンスは「ゲームを作るきっかけになれば」というもので、基本的にUnityを使ったどんな物でもあり。期限過ぎちゃっても完成しなくても全然オッケー!という非常に条件がゆるく参加しやすいイベントです。

作成したゲームについて

【Waterworks】

Waterworks | 無料ゲーム投稿サイト unityroom - Unityのゲームをアップロードして公開しよう

f:id:eggame:20170829183003g:plain

水源から水路を繋げて、ゴールへ水力を導くゲームです。

水力を伝えると効果が発動するギミックを持ったタイルも実装し、単調なパズルにならないように調整できる可能性を見せました。
(実際はステージ作成の練度が足らず、単調なステージばかりになってしまいましたが...)

使用したAsset

サウンドマネージャー (初使用)

クオータービュータイルマップの管理 (初使用)

カメラの動きの制御 (初使用)

Skybox (初使用)

エフェクト

細かなアニメーションの制御

データの保存

シェーダーの作成

ポストプロセス

リアクティブプログラミング

DIコンテナ。シングルトンの代わり

BGMは昔買って死蔵していた素材から漁ってきた落ち着いたJazzっぽいもの、
TextureはKENNEYさんのPublic Domain素材
UIはPhotoshopとFont Awesomeで作りました。

ネタ出しの話

僕がこのゲームジャムに参加する時は何かの技術学習や実験のついでになりますが、今回はお題が出る前からIsometric 2.5D Toolsetを試してみる事を決めていました。

どんなお題が来てもクオータービュー!

お題「フロー」が発表され、補足説明で「流れ」という単語が見えた時点で「水を流して導くパズル」というコンセプトが思い浮かび、その後日本語や英語で辞書を引いたりしましたが特に心変わりはなくそのまま作業に入りました。

ついついコンセプトに合わせて機能を実装する事が頭に浮かんできてそれに集中してしまうため、 実際それは面白いゲームなのかどうかを検討することがおざなりになってしまうのが悪い癖です。

Isometric 2.5D Toolsetの話

今回初使用です。参加の主目的。

オブジェクトの移動や当たり判定、物理演算などをクオータービューの形式で管理してくれるAssetです。 IsoWorldコンポーネントをつけたGameObjectを親にし、子にIsoObjectコンポーネントをつけたタイルを置くことで、 XYZ座標の動きをIsometric状態にしてくれます。

f:id:eggame:20170829181302p:plain
本ゲームのステージのhierarchy。

f:id:eggame:20170829181246g:plain
切りの良い場所に吸い付くので気持ちいい。

注意点は、IsoRigidbodyコンポーネントやIsoBoxColliderコンポーネントなど、Unity標準のRigidbodyやColliderを使用しないため、クリック時の当たり判定などはIPointerDownHandlerやUniRxのObservableDragTrigger.OnDragAsObservable()が効かなくなり、かわりにIsoPhysics.RaycastNonAlloc()などの専用に用意されているAPIを利用して実装することになります。

また、IsoWorld内の非アクティブなGameObjectは描画順などの計算の対象にならないため、非アクティブだったGameObjectをアクティブにした際、描画順序がおかしいままになる場合があります。

f:id:eggame:20170829182201g:plain

このためイベントによりタイルが表示されるようなギミックを作った場合にSetActive(true)後にどれかのタイルの座標を少し動かしてやるなどのハックが必要になります。

今思うと、意図的に再計算させるメソッドが調べたらあるかもしれませんし、GameObjectを非表示にしないでSpriteRendererだけ非アクティブにするなり鍵マークをつけるなりで常に計算してもらえるような状況を作るべきだったかもしれません。

本ゲームでは物理演算などを使用せずキャラクターの操作も無く、単にタイルを並べるだけで成立するためこのアセットの旨味はあまりなかったかもしれません。

ただ、これから物理演算を利用した追加ギミックなどの拡張をする気持ちになれば有効活用できそうです。(スイッチを押して大玉を転がして穴に入れると鍵が開くゼ◯ダっぽいギミックとか)

※追記

今回は利用しませんでしたが、外部ツールのTiledで簡単にステージ作成作業が出来ます。 活用出来ると良いかもしれません。

※追記2 8/30 16:00

Ultimate Isometric Toolkit

Isometric 2.5D Toolsetと同等の機能を持つ、もう少し評価の高いUltimate Isometric ToolkitというAssetがあります。

記事を書いた後にこちらも購入して試してみましたが、サンプルを見た時点でのIsometric 2.5D Toolsetとの違いは下記の通りです。

【Isometric 2.5D Toolsetと違う所】

  • 高低差を考慮したパスファインディングがある
  • 単一オブジェクトを用いた地形自動生成機能
  • デフォで用意してあるキャラクターコントローラーの動きが少し滑らかでジャンプが出来る
  • 錯視ステージが作れる(との説明がStoreにあるものの、サンプルシーンは無い)
  • チュートリアルがチョット多い
  • 3Dオブジェクトのソート(描画順序)には対応していない
  • Sceneのスナップが機能しない(ストアの説明には出来ると書いてある) ※Unity2017.1
  • Isometric 2.5D ToolsetはPlayMaker対応
  • Isometric 2.5D Toolsetは複数の異なった角度で計算される空間を同一シーンに持てる

ちなみにどちらもArea Effector 2D等のEffector 2Dには対応していない様子です。

3Dモデルとの親和性は薄いですが、パスファインディングが標準で用意されているのは強力ですね。

なお、Ultimate Isometric Toolkitには現在無料のLite版が存在します。
こちらで出来る事はStoreを引用すると、

  • 太陽系外惑星/エイリアン&自然のテーマのためのスプライトシート
  • アイソメトリックソーティング
  • アイソメトリック衝突検出&物理
  • カスタムエディターツール(アイソメトリックスナッピング、カスタムハンドル)
  • 1個の連続移動のためのアイソメトリックキャラクターコントローラー
  • 2個のサンプルシーン

となっており、サンプルを見たところキャラの横移動やタイルの配置をクオータービューで行うことまでは出来るという感じでした。

アイソメトリック衝突検出&物理と書いてありますがAssetにはIsoRigidbodyやIsoBoxCollider等は同梱されておらず、コレだけでは物理演算と衝突検出は出来そうにありません。

ひとまずクオータービューゲームを作ろうとしたらどんなことを考えたら良いのかを確認するために、まずコレを触ってみると良いのではないでしょうか。

水力の伝搬の話

managerにDictionary<Vector3,waterParameter>のカタチで水力の発生している座標と各方向の水力を保存しておき、Worldのタイルに変化があった際に各々のタイルがそこを参照して自分自身の水力を算出して、変化があればmanagerに再登録する仕組みを取っています。

で、自身の持っている水力を元にShaderで2枚のTextureをLerpして水力の見た目を変化させています。

こちらのShaderはShader Forgeで作りました。

大量のタイルに変化が発生するような移動を行った際、計算によるフリーズが少し発生するため、もっと効率のいい方法を思いつきたいです。

Pro Camera2Dの話

今回初使用です。

2Dでのカメラの移動、ズーム、イベントによる映すターゲットの切り替え、ついでにカメラを利用したtransitionなどがとても簡単に実装できるようになります。

本ゲームではDragでカメラターゲットを動かした際の追従アニメーション、描画範囲の制限、追加でクリア時のズームの実装に利用しました。 ほんとに簡単に気持ちいい挙動を実装できたので、今後積極的に利用してくかもしれません。

Skyboxを利用したディゾルブで画面遷移を作った話

今回はTextureは速攻で決まっていたものの背景どうしたら良いか迷い、 少し考えるのに時間を使いました。

全体的にどうするか考えている所。

その結果以前のゲームジャムで評判が良さそうだったSkyboxをグラデーションにする演出をやってみました。

で、グラデーション背景にシンプルなタイルが敷かれた様子を見て、「なんかこれちょっとオシャンティだな。」と感じた結果、 各所にオシャンティ要素を散りばめてみようと考えたわけです。

アイコンや枠線などは極力シンプルにする予定にし、 最初あまり見かけないからUI背景をすりガラスっぽくしてオシャレにしようかと目論みましたが大雑把すぎでダメ。

ここでいつも僕のゲーム画面遷移がうるさいよな...と感じたことを思い出して同一背景でキレイにシーン遷移出来ないかと考えました。

いろいろ考えた結果、同じSkyboxさえずっと表示されていれば地続きでゲームが進んでるように見えるなと思い、 フェードなどで各オブジェクトを背景に溶け込ませ浮き上がらせな感じで続いてる感を出せないかと考えました。

で、ネックになるのはタイル。そしてステージを開始すると生成されるパーティクル。

ParticleはDontDestroyOnLoadに登録して再生を制御しても良かったですが、大量のタイルはSpriteRendererで表示しているため、さらっとCanvasGroupのalphaをいじるようなことは出来そうにありません。

Worldをいじりづらいなら、Skyboxで覆ってしまえばいいじゃない!

と思った結果、サブカメラを用意してRenderTextureにSkyboxを保存し、そのRenderTextureにShaderでエフェクトをかけることで溶けるような演出を加えることに成功しました。

Shaderはこちらの動画を参考にShader ForgeでノイズTextureを元にOpacity ClipをいじるShaderを用意しました。

アニメーションをDOTweenで

あとは時間差のUIアニメーションは全てDOTweenで制御。

ステージ選択エリアのアウトラインがぐるっとひかれるのは、BGの親にMaskを用意し、MaskのImage.typeをfillにして円形に描画されるようにし、fillAmountをいじって表示しています。

まとめ

良かった点

  • クオータービューの構築について大分理解が進んだ。
  • Pro Camera2Dは今後も1軍候補。
  • 各画面遷移が流れるように繋げられたため、気持ち良かった。

反省点

  • 物理演算やキャラクターの移動などがないため、Isometric 2.5D Toolsetを使う意味が薄かった。(アプリ化できそうであれば有効活用したいです。)
  • デバッグが荒く、投稿後にギミックが最初から開放されていたりタイルの表示がおかしかったりした。
  • タイルをマウスオーバーしても変化がないため、タイルの高低差によっては意図したタイルがタップできなかったり。
  • 操作の手間や視認性が悪く、爽快感が薄い印象。
  • SEがまともに鳴らない理由が未だわかりません。
  • タイルの説明などがなく、遊び方の説明が不親切。

感想

初めてまともにブログを書いたため、きちんと要点が書けていない&考察が甘いかもしれませんが許して!

ついでに途中で編集に疲れて色々リンクはるの省いたのも許し!て!

主目的であるクオータービューの扱いに大分慣れたので個人的には大満足です。

ただ、ここまで記事を読んでいただいた通り企画のこと一切書いていないのはそれだけあんまり考えずに実装しているためで、企画力貧弱なのなんとかしたいですね...。

(見られている割にハートも少ない...。愛をください...。)

記事書くの大変なので今後も書くかどうかはわかりませんが、ゲーム制作は今後も頑張っていきたいと思います。