20201119のC#に関する記事は4件です。

【Unity】今更ScriptableObject入門

最初に

どうも、ろっさむです。

今回は「Unityを使っているなら皆知ってるよね?え?知らないの?なんで?」レベルの機能である「ScriptableObject」についてまとめていこうと思います。
僕は知りませんでした。

ScriptableObjectとは

ゲームやアプリの中で変化せず、あちこちで共用するデータを格納する時に便利なクラスです。例えば、敵のパラメータがよく例として挙げられています。

個別のゲームオブジェクト等にアタッチはせず、都度アセットをロードして使用することになります。なので余計なオブジェクトがなく、エンジン側からのコールバックを殆ど受け取りません。

敵Aのステータスとして仕様書側でHP100と決まっていた場合、コード上でHPを定義する場合に以下の手法があります(実際パラメータとなるとHPだけじゃなくて色んな情報が入ってきます)。

  • ハードコーディング(クラス内に直接書く)
class EnemyA
{
    // これだとEnemyAのインスタンスの数だけHP分のメモリも確保されていく
    const int MaxHP = 100;
}
class EnemyA
{
    // これだとEnemyAが画面上にいなくてもメモリ上にはEnemyAのHP分のメモリは確保されてしまう
    public static int MaxHP = 100;
}
  • CSVで定義して読み込む
// これもEnemyAのインスタンスの数分csvを読み込む必要があったり、csvをコードで読み取りやすい形式に変更が必要
敵名,MaxHP,xxx.....
Goblin,100,xxx.....

他にもJsonなどで実装できますが、もう一つの手法としてScriptableObjectを知っておくと良いかもしれません。
ScriptableObjectを継承したパラメータ定義用クラスを作成し、Unityのアセットとして扱うことでEnemyAのインスタンスがどれだけ作られてもパラメータの数値等を参照する際にはパラメータ定義用アセットを一つ見ればわかるようになります。また、EnemyAが出現しないマップなど、EnemyAが画面上に出てこない場合はそもそも読み込まなければメモリも確保されません。つまり、無駄にメモリを確保しなくてもよくなるわけですね。また、パラメータ部分だけ別アセットとして用意している状態となるので、コンフリクトもしづらく、値の調整も行いやすくなります。

パラメータ調整の他にも表情のblendshapeの名前リストの格納や、UIに使用するテキストデータ等にも使用することができるでしょう。応用すればイベントシステムも作成することができるようです。

ScriptableObject自体はUnityエディタでもよく使用されているようなので、使いこなすことができれば様々な恩恵が受けられそうです。

また、ScriptableObject は一応ゲーム中に変化するデータを扱う事も出来ますが、実機ではゲームが終了した後にデータは全て初期値に戻るため、セーブデータ的な使い方はできません(イベント期間中のステータスバフなどなら使えるかもしれませんが)。逆にエディタ上で値を変更すると、そのままアセットに直保存されます。「エディタ上で実行中にパラメータ等の値を変更してゲームバランスを調整する」という工程がこの仕様によって非常に楽に進めることができます。

具体的には、アセットファイルへの書き込みは AssetDatabaseクラスから行われ、スクリプト側からパラメータ等の値を変更した際には明示的に AssetDatabaseSaveAsset() を呼び出す必要があります。ただ、このSaveAsset()は実機での起動中は呼び出すことができません(UnityEditor.dllへの参照がないため)。

このように、マスターデータの更新の頻度がそこまで高くなく、ゲーム内に組み込みたい場合には ScriptableObject は役に立ちます。逆に、ソシャゲのようなマスターデータの更新頻度が高く、チート対策も必要で…という場合にはデータが必要になるタイミングで都度Json形式などでサーバからデータをDLする仕組みの方が安心できます。

では実際にどのように作成して、使えば良いのか、というところですが、こちらもそこまで難しくはありません。

ScriptableObjectの使用方法

流れとしては

  1. ScriptableObject派生クラスの作成
  2. ScriptableObject派生のクラスをアセット化
  3. パラメータを設定
  4. ScriptableObject派生のクラスを使用

となります。
順番に見ていきましょう。

ScriptableObject派生のクラスを作成

単体のデータの塊だけを扱う(Enemy1種類につき1つのAsset)

こちらは簡単です。ScriptableObjectを派生したクラスを作成していきましょう。

using UnityEngine;

// CreateAssetMenu属性を使用することで`Assets > Create > ScriptableObjects > CreasteEnemyParamAsset`という項目が表示される
// それを押すとこの`EnemyParamAsset`が`Data`という名前でアセット化されてassetsフォルダに入る
[CreateAssetMenu(fileName = "Data", menuName = "ScriptableObjects/CreateEnemyParamAsset")]
public class EnemyParamAsset : ScriptableObject
{
    // データ群の先頭をstringにして名前等に設定するとInspectorで見たときに項目TOPに表示されるので見やすくなります。
    public string EnemyName = "スライム";

    // privateでも[SerializeField]をつけることでInspectorで確認できるようになります。
    [SerializeField]
    int MaxHP = 100;

    ...
}

複数のデータの塊を扱う(Enemy数種類を1つのAssetに含む)

もしもパラメータ自体が増えたり、複数種類のパラメータを一つのAssetの中に持ちたい場合には、別途データ用のstructやclasssを用意すると良いでしょう。

using UnityEngine;

[CreateAssetMenu(fileName = "Data", menuName = "ScriptableObjects/CreateEnemyParamAsset")]
public class EnemyParamAsset : ScriptableObject
{
    public List<EnemyParam> EnemyParamList = new List<EnemyParam>();
}

// System.SerializeField属性を使用することで、Inspector上で変更した値がアセットに保存されるようになります
[System.Serializable]
public class EnemyParam
{
    public string EnemyName = "スライム";

    [SerializeField]
    int MaxHP = 100;

    ...
}

ScriptableObject派生のクラスをアセット化

先ほどのコードであれば、Editor上からAssets > Create > ScriptableObjects > CreasteEnemyParamAssetという項目を押すことでEnemyParamAssetDataという名前でアセット化されてassetsフォルダに入ります。

ただ、もし外部ファイル(json,csv...etc)からパラメータを読み込んでScriptabeObjects側に流し込む場合には、アセット化する機能を持つクラスとScriptableObjects派生クラスで分けて制作していく流れになるかと思います。

using UnityEngine;

public class EnemyParamAsset : ScriptableObject
{
    public List<EnemyParam> EnemyParamList = new List<EnemyParam>();
}

[System.Serializable]
public class EnemyParam
{
    public string EnemyName = "スライム";

    [SerializeField]
    int MaxHP = 100;

    ...
}
using System.IO;
using UnityEditor;
using UnityEngine;

// AssetDatabaseを使用しているため、ビルド時には含めないようにしないとビルドエラーが起きる
#if UNITY_EDITOR
public static class CreateEnemyParamDataAssetFromCsv
{
    private const string AssetPath = "Assets/Resources/Data/Enemy/";
    private const string CsvPath = "Assets/Data/Status/Enemy/xxxx.csv"

    // MenuItem属性を付けることでEditorの上部メニューに`ScriptableObjects > CreateEnemyParamAsset`が表示されます
    // 押下すると`CreateEnemyParamDataAsset()`が実行されます
    [MenuItem("ScriptableObjects/CreateEnemyParamAsset")]
    private static void CreateEnemyParamDataAsset()
    {
        var enemyParamAsset = CreateInstance<EnemyParamAsset>();

        // この辺で外部ファイルパスを用いてデータを読みこみ、
        // 作成したenemyParamAssetに値を流し込む処理を挟む....
        // 例えばenemyParamAsset.EnemyParamList.Add(hogeParam); 的な

        // 流し込んだ後は実際に作成します
        // ここで作ったアセットの置き場所であるパスの指定もできます
        var assetName = $"{AssetPath}{enemyType}Data.asset";
        AssetDatabase.CreateAsset(enemyParamAsset, assetName);

        // Asset作成後、反映させるために必要なメソッド
        AssetDatabase.Refresh();
    }
}
# endif

外部ファイルを読み込んでScriptableObjects側に流し込む方法だと、外部ファイルの値がマスターデータ扱いで、ScriptableObjectはそのマスターデータをゲームに流し込むためのクッション扱いになります。
この方法には以下のメリット・デメリットがあります。

メリット:

  • 外部ファイルでの値がマスターデータとなり、gitなどでの変更差分を確認することができる
  • Editor上でアセット自体の値の書き換えを行うと即座にアセットに反映されて前の値は消されてしまうが、マスターデータは別にあるため、以前の値がわからなくなるということがなくなる。
  • 複数人でのパラメータ調整がしやすくなる

デメリット:

  • マスターデータからScriptableObjectに反映させるコード上の実装の手間がある
  • Editor上でアセット自体の値の書き換えを行って調整した際に、値をマスターデータ側に反映させる作業が必要になる。

ちなみにcsv等の外部ファイルからScriptableObjectを作成する無料アセットも存在しているのでチェックしてみると良いかもしれません。

https://assetstore.unity.com/packages/tools/integration/csv-serialize-135763?aid=1101l4PmM&utm_source=aff
image.png

これでAssetが指定したフォルダ下に作られるようになりました!
後は実際にInspectorなどから必要に応じてアセットの値を直接変更できます。

ScriptableObject派生のクラスを使用

あとは使い方ですが、こちらもシンプルにResources.Load等でデータを取得して使用するだけです。

public class EnemyBase : MonoBehaviour
{
    public enum EnemyType
    {
         Goblin,
         ...
    }

    EnemyType enemyType;

    public void ReadEnemyDataAsset()
    {
        var path = $"Data/Enemy/{enemyType.ToString()}";
        var enemyData = Resources.Load(path) as EnemyParamAsset;

        // あとは読み込んだ値を使って諸々セットなどの処理を行う
        ...

    }
}

もしくは可能そうならEditor上でScriptableObjectのアセットをアタッチする方法もあります。

最後に

ScriptableObjectは知ってみると非常に使い勝手が良く、Unityの実行速度にも優しい応用のきく機能です。
データ管理と使用は使えそうなら ScriptableObject を使用して効率的に行っていきましょう!

参考

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

【Unity】ゲームの実績システムを実装してみた。

ゲームの実績システムを実装してみた。【Unity】

UnityでスマホアプリのStgを作っているのですが、実績システムを実装するにあたって、自分がどうやって作ったかを
備忘録的に書いていきたいと思います。

そもそも実績システムとは...

自分がプレイ中にした行動に対して一定の規定をクリアしたら報酬がもらえるシステム。
どうすれば実績システムが作れるかを考えていきます。

まず、実績システムを作るために、2つの要素が必要と考えました。

1 プレイした時に実績としてデータが保存される機構。
2 保存されたデータを取得し、一定の規定をクリアしたら報酬を与える機構。

この二つの要素をクリアすれば実績システムが作れたといえると思います。
まず、1番目のプレイした時に実績としてデータが保存される機構を作っていきました。

実績保存クラス.C#
public class SaveAchievement : MonoBehaviour
{
    public static SaveAchievement saveAchievement;
    public int enemyKillCount,gameoverCount,clearCount;
}

ここでは、実績を保存するためのスクリプトを作りました。
public staticで宣言することによって他のスクリプトから取得することを容易にし、
敵を倒したときの変数、ゲームオーバーになった時の変数、クリアした時の変数を
用意したので準備はばっちりです。ここに他のスクリプトから値を足していくことで
実績システムを作れると思います(敵を倒したときにSaveAchievementのenemyKillCountに
+1するというようなコードを書く)

2番目の保存されたデータを取得し、一定の規定をクリアしたら報酬を与える機構も作りました。

実績報酬クラス.C#
public class RewardAchievement : MonoBehaviour
{
    int enemyKillJudgment = 10;//実績規定
    if(enemykillCount <=enemyKillJudgment //{
    //もしもタスクオブジェクトの(実績条件の)達成量が達成規定よりも上回ったら    
        //報酬を与える
    }
}

これで、実績システムは完成となります。

おわりに

実績システムの大枠は作れたのですが、やはり中身がどれだけ充実できているかという所だと思うので、中身を書くときに見やすいコードを意識してい書いていきたいものです。

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

ゲームの実績システムを実装してみた。【Unity】

ゲームの実績システムを実装してみた。【Unity】

UnityでスマホアプリのStgを作っているのですが、実績システムを実装するにあたって、自分がどうやって作ったかを
備忘録的に書いていきたいと思います。

そもそも実績システムとは...

自分がプレイ中にした行動に対して一定の規定をクリアしたら報酬がもらえるシステム。
どうすれば実績システムが作れるかを考えていきます。

まず、実績システムを作るために、2つの要素が必要と考えました。

1 プレイした時に実績としてデータが保存される機構。
2 保存されたデータを取得し、一定の規定をクリアしたら報酬を与える機構。

この二つの要素をクリアすれば実績システムが作れたといえると思います。
まず、1番目のプレイした時に実績としてデータが保存される機構を作っていきました。

実績保存クラス.C#
public class SaveAchievement : MonoBehaviour
{
    public static SaveAchievement saveAchievement;
    public int enemyKillCount,gameoverCount,clearCount;
}

ここでは、実績を保存するためのスクリプトを作りました。
public staticで宣言することによって他のスクリプトから取得することを容易にし、
敵を倒したときの変数、ゲームオーバーになった時の変数、クリアした時の変数を
用意したので準備はばっちりです。ここに他のスクリプトから値を足していくことで
実績システムを作れると思います(敵を倒したときにSaveAchievementのenemyKillCountに
+1するというようなコードを書く)

2番目の保存されたデータを取得し、一定の規定をクリアしたら報酬を与える機構も作りました。

実績報酬クラス.C#
public class RewardAchievement : MonoBehaviour
{
    int enemyKillJudgment = 10;//実績規定
    if(enemykillCount <=enemyKillJudgment //{
    //もしもタスクオブジェクトの(実績条件の)達成量が達成規定よりも上回ったら    
        //報酬を与える
    }
}

これで、実績システムは完成となります。

おわりに

実績システムの大枠は作れたのですが、やはり中身がどれだけ充実できているかという所だと思うので、中身を書くときに見やすいコードを意識してい書いていきたいものです。

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

Ignite UI for Blazor データチャートを設置してみよう - Ignite UI for Blazor で始めるSPA開発 - ⑤

本記事は、インフラジスティックスが2020年10月に新たにリリースした Blazor アプリケーション対応のコンポーネントライブラリーである Ignite UI for Blazor の各種コンポーネントの実装を通してモダンWEBアプリケーションの開発に触れてみようという趣旨で作成しております。

Blazor 及び Ignite UI for Blazor については、以下の弊社ブログ記事も有用ですので是非ご参照ください。

ASP.NET Core Blazor を採用すべきアプリケーションは? Blazor を採用すべきポイントまとめ
https://blogs.jp.infragistics.com/entry/2020/10/12/133447
Ignite UI for Blazor 公式リリース!
https://blogs.jp.infragistics.com/entry/igniteui-blazor-release
ASP.NET Core Blazor - Blazor WebAssembly と Blazor Server の違いは? 5つのポイント
https://blogs.jp.infragistics.com/entry/2020/10/30/152834

本連載の過去記事は以下となります。

第1回
https://qiita.com/MNakae_IG/items/5d89b404257d93742e45
第2回
https://qiita.com/MNakae_IG/items/7a833f7a6bc85fbb125d
第3回
https://qiita.com/MNakae_IG/items/a590126cb520255dbcf8
第4回
https://qiita.com/MNakae_IG/items/56b82d4e9e08d40082aa

第5回目の本記事では、前回、Ignite UI for Blazor のデータグリッドで使用したものと同じデータソースを使ってチャートの設置をしていきます。

新規ページの追加

第2回の時と同じような流れで新しいページを設置していきます。 /chart というURLでアクセスすると表示されるように「Chart.razor」ファイルを新規に作成します。メニューにも新たにリンクアイテムを追加し、以下のように新規ページを設置しました。
(新規ページ作成方法の詳細に関してはこちらの記事をご参照ください。)

Image from Gyazo

このページにチャートを設置してきます。

データソースの用意

チャートコンポーネントにバインドするためのデータソースを用意します。第2回の時と同じ流れで、厚生労働省がコロナウイルス対策関連で公開している「各都道府県の検査陽性者の状況」データを HttpClient で取得し、使用します。
(データモデルの定義および HttpClient でデータを取得する方法の詳細についてはこちらの記事をご参照ください。)

# ファイル名「Chart.razor」を以下のように変更します。
@page "/chart"
@using IgniteUI.Blazor.Controls
@using System.Collections.ObjectModel;
@using IgniteUI_for_Blazor.Models
@inject HttpClient Http
@inject IIgniteUIBlazor IgniteUIBlazor
...
@code {
    private Covid19Japan Covid19Japan;
    public ObservableCollection<Area> ChartData { get; set; }

    protected override async Task OnInitializedAsync()
    {
        base.OnInitialized();
        ChartData = new ObservableCollection<Area>();
        var data = await Http.GetFromJsonAsync<Covid19Japan>("https://www.stopcovid19.jp/data/covid19japan.json");
        @foreach (var area in data.Area)
        {
            this.ChartData.Add(area);
        }
    }
}

以上で ChartData をデータソースとして準備することが出来ました。

円グラフ(パイチャート)の設置

Ignite UI for Blazor は様々なタイプのチャートを提供していますが、その中のひとつ、円グラフを使って、各都道府県ごとの「PCR検査陽性者数」を表示していきます。まず円グラフを利用するために、「Chart.razor」の @Code 内に以下のように追記します。

# ファイル名「Chart.razor」を以下のように変更します。
protected override async Task OnInitializedAsync()
{
    base.OnInitialized();
    PieChartModule.Register(IgniteUIBlazor);
    ...
}

次に HTML 側に PieChart コントロールを配置し、データソースをバインドします。

# ファイル名「Chart.razor」を以下のように変更します。
<h3>都道府県別陽性者数円グラフ</h3>
@if (ChartData != null)
{
    <PieChart Height="600px" Width="100%"
                DataSource="ChartData"
                LabelMemberPath="Name_jp"
                ValueMemberPath="Npatients"
                LegendLabelMemberPath="Name_jp"
                OthersCategoryThreshold="4"
                OthersCategoryType="OthersCategoryType.Percent"
                OthersCategoryText="その他"
                StartAngle="0"
                Outlines="white">
    </PieChart>
}

実行すると以下のように表示されます。

Image from Gyazo

先ほどの PieChart コントロールに渡したプロパティ値の中で、OthersCategoryType="OthersCategoryType.Percent" および、OthersCategoryThreshold="4" としていますが、これはグラフ全体のうち4%以下のデータに関しては「その他」という項目としてひとまとめにするという設定です。陽性者数の割合が大きい県を示すためにこのような設定としました。
このグラフを見ると五大都市圏のある都道府県が陽性者数の75%以上を占めていることがわかります。またその中でも、首都圏が全体のおよそ半分を占めているということもわかります。

PieChart のその他のプロパティなど、詳細に関しましては以下のドキュメントをご参照ください。
Blazor 円チャートの概要

積層型棒チャートを設置する

次は同じデータソースを用いて、違った切り口でデータをチャート表示します。
以下の「Blazor 積層型 100 縦棒チャート」を利用して、「PCR検査陽性者数」を100%とした時の「現在の感染者数」、「快復者数」、「死亡者数」、「重症者数」、「不明数」をそれぞれパーセンテージで、各都道府県ごとに表示します。

Blazor 積層型 100 縦棒チャート

積層型のデータチャートを利用するために「Chart.razor」の @Code 内に以下のように追記します。

    protected override async Task OnInitializedAsync()
    {
        ...
        DataChartCoreModule.Register(IgniteUIBlazor);
        DataChartStackedModule.Register(IgniteUIBlazor);
        ...
    }

次に HTML 側に DataChart コントロールを配置し、データソースをバインドします。

<h3>累計陽性者数における内訳割合</h3>
<DataChart Height="400px" Width="100%" IsVerticalZoomEnabled="false" IsHorizontalZoomEnabled="false">
    <CategoryXAxis Name="xAxis" DataSource="ChartData" Label="Name_jp" />
    <NumericYAxis Name="yAxis" MinimumValue="0" />
    <Stacked100ColumnSeries XAxisName="xAxis" YAxisName="yAxis" DataSource="ChartData" ShowDefaultTooltip="true" AreaFillOpacity="1">
        <StackedFragmentSeries ValueMemberPath="Ncurrentpatients" />
        <StackedFragmentSeries ValueMemberPath="Nexits" />
        <StackedFragmentSeries ValueMemberPath="Ndeaths" />
        <StackedFragmentSeries ValueMemberPath="Nheavycurrentpatients" />
        <StackedFragmentSeries ValueMemberPath="Nunknowns" />
    </Stacked100ColumnSeries>
    <ItemToolTipLayer></ItemToolTipLayer>
</DataChart>

CategoryXAxis つまり横軸を都道府県名とし、StackedFragmentSeries として、積層表示するデータを指定しています。

また、各積層カラムに対して色を指定し、どの色が何のデータの割合を示すのかを分かりやすくするための表記を追記すると、最終画面は以下のようになります。

Image from Gyazo

本記事制作時点では、このチャートを見ると、北海道と鳥取県において、感染者数の累計に対して現在の感染者数の割合が大きい状態となっていることが分かります。

Image from Gyazo

同じデータソースを用いていても、異なる種類のチャートを利用することで新しい気づきを得ることができました。

積層型 100 縦棒チャートのその他のプロパティなど、詳細に関しましては以下のドキュメントをご参照ください。
Blazor 積層型 100 縦棒チャート

まとめ

このように、チャートを使ってデータを表示することで、数字が羅列されたグリッドとは異なり、イメージとして直感的に情報を捉えることが出来るようになります。Ignite UI for Blazor には、本記事では紹介しきれなかった様々な種類のチャートが実装されています。詳細は以下のドキュメントをご参照ください。

Blazor チャートシリーズ

今回作成したサンプルアプリケーションは以下よりダウンロードいただけます。

Ignite-UI-for-Blazor_5.zip

Ignite UI for Blazor トライアル版を利用するには

Ignite UI for Blazor はトライアル板での試用が可能です。
トライアル版を利用するためには以下のページよりアカウントの作成を行ってください。登録より30日間、弊社のテクニカルサポートをご利用いただくことが出来ますのでお気軽にお問い合わせください。

https://iam.infragistics.com/Account/Register

また、製品をご購入をご検討のお客様は以下のページよりお気軽にお問い合わせください。

https://jp.infragistics.com/about-us/contact-us

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