投稿記事

Unityの記事 (10)

Unity開発メモ #9 セーブスロットシステムの実装

EasySave3によるセーブスロットシステムの実装

EasySave3というアセットでヘホゲのセーブシステムを構築しているという情報を得たのでヌケルさんもこちらのアセットを使用してシステムを構築しています


とても簡単にセーブ & ロードが実装できてやさしいアセットだと思います!


https://assetstore.unity.com/packages/tools/utilities/easy-save-the-complete-save-data-serializer-system-768




ただし値段は円安でやさしくない



変数のセーブとロード

EasySave3はオートセーブやゲーム内のオブジェクトの位置情報を保存するなど高度な機能があるが、今回のゲームでは変数さえ保存できればいいのでとても単純です。


各セーブスロットに変数をセーブする

ES3.Save関数を使います
https://docs.moodkie.com/easy-save-3/es3-api/es3-methods/es3-save/

public static void Save<T>(string key, T value, string filePath, ES3Settings settings)

<>にはデータ型、第1引数でセーブデータのキーを設定し、第2引数にはキーに格納する具体的なデータを入れ、第3引数はオプショナルで設定した場合は書き込み先のファイル名となります

下はインスペクターで指定した数字のスロットにゲームの進捗状況を管理する変数を格納しているProgressDataクラスの変数をセーブするコードです

ついでにセーブ時間も記録しています


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

public class SaveGame : MonoBehaviour
{

public int saveSlotNumber; // インスペクターで指定した1~3の数字のスロットにゲームの変数(ProgressData)をセーブする

// セーブボタンをクリックしたときに呼び出す
public void Save()
{
    string saveFileName = "save" + saveSlotNumber + ".es3"; // 例 saveSlotが1のときはsave1.es3にデータを保存する
    DateTime now = DateTime.Now; // 現在時刻の取得

    ES3.Save<int>("NowDay", ProgressData._NowDay, saveFileName);
    ES3.Save<int>("NowCharacter", ProgressData._NowCharacter, saveFileName);
    ES3.Save<bool>("Night", ProgressData._Night, saveFileName);

中略

    ES3.Save<int>("SavedMonth", now.Month, saveFileName);
    ES3.Save<int>("SavedDay", now.Day, saveFileName);
    ES3.Save<int>("SavedHour", now.Hour, saveFileName);
    ES3.Save<int>("SavedMinute", now.Minute, saveFileName);
    ES3.Save<int>("SavedSecond", now.Second, saveFileName);
}

}



各セーブスロットにセーブされた変数を出力する

セーブファイルからデータを取り出すときはES3.Loadを使います
https://docs.moodkie.com/easy-save-3/es3-api/es3-methods/es3-load/


public static T Load<T>(string key, string filePath, T defaultValue, ES3Settings settings)


<>には同じくデータ型を、第1引数でロードするデータのキーを設定し、第2引数では読込先のセーブファイルの名称を指定、defaultValueでは何も保存されていない場合に読み込むデフォルト値を設定します


ES3.Loadで読み取ったデータをゲームオブジェクト上のTextに反映させたりするようなコードを書けば、ゲーム画面上にどのようなセーブデータが保存されているかを表示することができるので、セーブスロットシステムの外観が整います



アイコン・日付表示・テキスト・進捗バー・セーブ時間・登録者数・やみ度の表示を各セーブスロットに保存されたデータをもとに表示しています


ロードしたデータをゲームに反映する

各セーブスロットのロードボタンを押したときに下記の処理を実行すれば実現できます

・ロードSceneに飛ばす
・ES3.Loadを使って必要な変数をゲーム内の進捗管理変数に代入する
・再開すべきSceneに戻す


コードにすると下のような感じです
ロードときはセーブファイルが存在するか確認してなければ処理を止める処理を入れておいた方がよさそうです



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

public class LoadGame : MonoBehaviour
{

public int saveSlotNumber; // インスペクターで指定した1~3の数字のスロットからゲームの変数(ProgressData)をロードする

// ロードボタンをクリックしたときに呼び出す
public void Load()
{
    string saveFileName = "save" + saveSlotNumber + ".es3"; // 例 saveSlotが1のときはsave1.es3のデータをロードする

    if(!ES3.FileExists(saveFileName)) // ファイルが存在していない場合
    {
        return;
    }

    SceneManager.LoadScene("Loading"); // ロード画面に飛ばす

    // データをロードしProgressDataに格納していく

    ProgressData._NowDay = ES3.Load<int>("NowDay", saveFileName, defaultValue: 0);
    ProgressData._NowCharacter = ES3.Load<int>("NowCharacter", saveFileName, defaultValue: 2);
    

後略


ファイルの保存場所について



LocationをFile、DirectoryをPersistent Data pathに指定すると、C:/Users/Owner/AppDataに保存ファイル(es3ファイル)が作成される。
多くのUnity製アプリがここを保存場所にしており、安全と思われる。 (Unity標準ログ出力もここ)



LocationをDataPathにした場合はゲームフォルダ内の「ゲーム名_Data」に保存される。こちらの方が場所はわかりやすいか



Persistent Data pathの方が面倒ゴトが少なく安全そうなので、そちらで実装すると思います



他にもセーブファイルの暗号化機能とかもあるのですが、オンラインゲームではないのでセーブデータを改造して遊んでもらって全くかまわないので、あえてしないと思います




PlayerPrefsによるコンフィグ設定の保存

PlayerPrefはUnityにデフォルトで備わったデータ保存機能ですが、
種種の理由からセーブファイルの保存にはあまり推奨されていなようです

ただ言語設定やマスター音量等のコンフィグ設定であれば使ってもよさそう説も見かけたので、それぞれ保存するようにしてみました



以下はスライダーバーUIにアタッチするだけで、マスター音量の変更に加えて、変更した値を保存してゲーム再開時にその音量でプレイできるように作成したコードです

これで前回音量を下げたのにゲームを再開したら爆音でBGMがなるという事故が防げます、たぶん



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

public class MasterVolumeControl : MonoBehaviour
{

public Slider masterVolumeSlider;
private const string masterVolumeKey = "MasterVolume";

private void Start()
{
    // PlayerPrefsから保存されたマスター音量の値を読み込む
    float savedVolume = PlayerPrefs.GetFloat(masterVolumeKey, 1f);
    masterVolumeSlider.value = savedVolume;
    SetMasterVolume(savedVolume);

    // スライダーの値が変更されたときに呼び出されるイベントを登録
    masterVolumeSlider.onValueChanged.AddListener(OnMasterVolumeChanged);
}

private void OnMasterVolumeChanged(float volume)
{
    // スライダーの値が変更されたときに呼び出される関数
    SetMasterVolume(volume);
}

private void SetMasterVolume(float volume)
{
    // マスター音量を設定する
    AudioListener.volume = volume;

    // PlayerPrefsにマスター音量の値を保存する
    PlayerPrefs.SetFloat(masterVolumeKey, volume);
    PlayerPrefs.Save();
}

}





実績やアーカイブ(いわゆる回想)に関してはPlayerPrefsではなく、セーブスロットとは別のグローバルセーブデータをEasySave3で作成して管理しようと思います

この記事が良かったらチップを贈って支援しましょう!

チップを贈るにはユーザー登録が必要です。チップについてはこちら

Unityのポストエフェクトで遊びまくった成果


何かの演出に使えそうなポストエフェクト(ゲーム画面全体に適応する特殊効果)を作ってました
動画は作ったShader Graphとゲーム画面への適用結果です

たのしい



参考にした動画

Distortion Effect

https://www.youtube.com/watch?v=MaVmsOF4pXY

Glitch Effect

https://www.youtube.com/watch?v=VgBv6OrYY7E

Retro Effect

https://www.youtube.com/watch?v=k9g2LaBrirI

Pixelate Effect

https://www.youtube.com/watch?v=siiqnXA156Y

Marble Effect

https://www.youtube.com/watch?v=pr0CWXOBV4Y

その他

https://www.youtube.com/watch?v=Ylq2y6Qez3s

この記事が良かったらチップを贈って支援しましょう!

チップを贈るにはユーザー登録が必要です。チップについてはこちら

Unity開発メモ #8

フルスクリーンのポストプロセスをシェーダーグラフでつくる

ポストプロセスとは、レンダリングされたゲーム画面全体に最後にくわえられる特殊エフェクトのようなものです

FPSゲームで「ダメージを受けた際に視界が赤く」なったり、「ダンジョンに入るときに画面がフェードアウト」したり、卑猥なものがうつって突如「画面全体がモザイクになった」り、「いけないおくちゅりをのんだときに画面が揺れ」たりといった例がわかりやすいと思います


今回はUnity 2022.2から標準機能として搭載されたシェーダーグラフによるフルスクリーンエフェクト作成によって、「いけないおくちゅりをのんだときに画面が揺れ」る効果を作ってみました


(これのためにUnityのバージョン上げました、バージョン上げて不具合でなかったのでよかったです)

参考動画

https://www.youtube.com/watch?v=7Cd7kQl6N50

https://www.youtube.com/watch?v=AQGgwgo51lo

https://www.youtube.com/watch?v=Ylq2y6Qez3s

https://www.youtube.com/watch?v=E_oDKQU3e94&t=0s

参考記事

https://docs.unity3d.com/ja/Packages/com.unity.render-pipelines.universal@14.0/manual/Setup.html

https://qiita.com/generosity_honda/items/f091f8edc7d938c7b2fb

https://www.hanachiru-blog.com/entry/2022/12/22/120000


実装時のメモ

Fullscreen ShaderGraphがない

Create -> ShaderGraph -> URP -> Fullscreen ShaderGraphは、Unity 2022.2以降でないとありません

どのUniversal Renderer assetにFull Screen Pass Renderer Featureを追加するの?

Edit -> ProjectSettings -> Quality -> Rendering -> Render Pipeline Assetから使用しているRender Pipeline Assetを探し出し、InspectorからRendering -> Renderer Listに設定されているUniversal Renderer Dataを調べればOK
Asset > Settingに入ってるやつだった

とりあえず画面そのままのレンダリング結果をだしたい

URP Sample Bufferノード(BlitSource)でだせます

↑したんだけど画面が灰色一色のままなんだけど

シェーダーは保存しないと反映されません

シェーダーのオンオフをスクリプトで制御したい

chatGPTで出力されたコードをいじくって下のようなものをこさえました
とりあえず動きますが、もっとスマートな方法があったら教えてください


using UnityEngine;
using UnityEngine.Rendering.Universal;
using System.Collections.Generic;
using UnityEngine.SceneManagement;
using System.Collections;
public class UseDrag : MonoBehaviour
{

public Renderer2DData rendererData; // Renderer 2D Data への参照

(中略)

// 指定したフィーチャーを有効または無効にするメソッド
private void SetFeatureEnabled(string featureName, bool isEnabled)
{
    if (rendererData != null)
    {
        List<ScriptableRendererFeature> features = rendererData.rendererFeatures;

        foreach (var feature in features)
        {
            if (feature.name == featureName) // 指定した名前のフィーチャーを見つけた場合
            {
                feature.SetActive(isEnabled); // フィーチャーの有効・無効を設定する
                return;
            }
        }

        Debug.LogWarning($"Feature '{featureName}' not found in the Renderer 2D Data.");
    }
    else
    {
        Debug.LogWarning("Renderer 2D Data reference is not set.");
    }
}

(中略)

}



Drag 01 (Full Screen Pass Renderer Feature)のチェックをスクリプトでオンオフする挙動です

その他

遅延処理使いたいけどなぜかInvokeもコルーチンもうまくいかない

根本的な解決にはなっていないがTaskを使う方法もある
使うにはまず System.Threading.Tasks を読み込む必要がある。
await を使用して待機させるには、実行されるメソッドに async が付与されている必要がある。
単純に1秒待たせたいだけならこんな感じ

private async void DelayMethod()
{
await Task.Delay(1000);
Debug.Log("DelayMethod");
}

https://12px.com/blog/2016/11/unity-delay/

キャンバスの子要素のキャンバスを親より上に表示させたい

CanvasコンポーネントをアタッチしてOverride SortingいチェックをいれるとSort順を独立して設定できる

↑をやったら子要素のキャンバスがクリックできなくなった!

Graphic Raycasterコンポーネントをアタッチしないとキャンバスにはレイキャストが設定されないのでアタッチする

UIのトグルとかで別のオブジェクトのActiveを操作したい

Objectをインスペクターで指定してSetActiveを弄れる

画面のクリックをブロックしたい

画面全体最前面にレイキャストを持った透明なオブジェクトをかぶせる

テキストのレイキャストの無効化方法

インスペクターで設定できないのでコードをアタッチして対応

画像ファイル等の置き換え方法

Unityに同じ名前のファイルをドラッグアンドペーストしても新規で連番ファイルが作成されるだけ
正しくはShow in Explorer (win) で Explorerを開いてそこで置き換える


進捗

多言語対応システムはひとまず完成でテスト中

日付・時間遷移のシステム周りもおおむね完成しました

本編部分の前にシステム面をまずあらかた完成させてしまうつもりです


今後技術的に新規学習しなければならない要素としては、Live2Dのスクリプト制御、セリフ送り・選択肢分岐機能、ボイス・SE・BGM制御、セーブ・ロード機能、グローバルな回想・実績管理機能あたりかと思われます


今回はUnityの学習のためにあえて一人で1から完成させる感じなので、なるべくシンプルにプリミティブに、完成優先で進めたいと思います

この記事が良かったらチップを贈って支援しましょう!

チップを贈るにはユーザー登録が必要です。チップについてはこちら

Unity開発メモ #7

イラストやイベントより先にゲームシステムとUIをいっきに作ってしまいます。
とりいそぎデスクトップ画面風UIやスマホ風UIを実装しました。

女の子をひどい目にあわせたあとに、その子から恨みつらみを込めたメッセージが返ってきたりします、たぶん。

ドラッグで移動するUI

下のようなコンポーネントを作成して移動させたいUIにアタッチして自身をインスペクターで移動対象のオブジェクトに設定すればいけました

using UnityEngine;
using UnityEngine.EventSystems;

public class ObjectDrag : MonoBehaviour, IPointerDownHandler, IDragHandler
{

public Transform moveTarget;      // 移動対象のオブジェクト

private bool isDragging = false;   // ドラッグ中かどうかのフラグ
private Vector3 offset;            // ドラッグ開始位置からのオフセット

public void OnPointerDown(PointerEventData eventData)
{
    // クリック時にドラッグ操作を開始
    isDragging = true;
    offset = moveTarget.position - GetMouseWorldPosition();
    moveTarget.transform.SetAsLastSibling(); // ヒエラルキーで一番下に移動し、前面に表示される
}

public void OnDrag(PointerEventData eventData)
{
    if (isDragging)
    {
        // ドラッグ中のマウスのワールド座標を取得
        Vector3 mousePosition = GetMouseWorldPosition();
        // 移動対象のオブジェクトをドラッグ位置に移動
        moveTarget.position = mousePosition + offset;
    }
}

private Vector3 GetMouseWorldPosition()
{
    // マウスのスクリーン座標をワールド座標に変換
    Vector3 mousePosition = Input.mousePosition;
    mousePosition.z = -Camera.main.transform.position.z;
    return Camera.main.ScreenToWorldPoint(mousePosition);
}

}

クリックした要素を同じレイヤー間で一番前に表示させる

上のコードでも使用していますが、クリックしたさいに下記で実現可能
(オブジェクト).transform.SetAsLastSibling();

TextMeshProで日本語・韓国語・中国語(簡体字)を表示するための文字リスト

TextMeshProに多言語表示するには、あらかじめ使用し得るすべての文字リストを用意してフォントアセットを用意しなければならないという異様な面倒くささがあります。

NotoSansCJKという日本語・中国語・韓国語対応フォントを使用し、下記URLから文字リストを引っ張ってきて対応させました。これで今のところ文字化けせずに各言語が表示されています。

日本語

ttps://gist.github.com/kgsi/ed2f1c5696a2211c1fd1e1e198c96ee4

韓国語

ttps://gist.github.com/stakira/2ef5a2cd35739b9274a4e53e8d08a551)

思っていた以上に種類があって驚きました

中国語(簡体字)

ttps://gist.github.com/z-rui/4cb3431f2bcf26ea39cd569a58e66003/

URPでのポストプロセス実装

Build-inの場合(古いやり方?)とは違うので注意
新規でパッケージをインストールする必要はないです

【Unity】URPでポストプロセス(Post-processing)を使う手順https://www.hanachiru-blog.com/entry/2020/05/09/180000

Volumeコンポーネントをアタッチしたオブジェクトの有効・無効を切り替えれば、ポストプロセスの有効無効を切り替えられます。

URPでUIにポストプロセスをかけずゲーム画面にだけ適用する

カメラを二つ用意し、ゲーム画面を移すポストプロセスを適用するカメラと、UIだけを移すポストプロセスを適用しないカメラとして設定し、二つを重ねることで実現可能なようです。

URPでPostProcessingの効果をゲーム画面だけに適用する
https://takap-tech.com/entry/2021/10/17/031130

UIアセットなどの使い方

インポートしたフォルダに入っているプレハブとかをコピーしてUnpackedしてプレハブ化を解除していじるなどすればよさそうでした。

画面上の一定範囲をクリックしたときにUIを非表示にする

onMouseやEventTrigerでやろうとしたらなぜか後ろに他のキャンバスがあるときに動作しなかったので、マウスが画面内の一定範囲にあるときにクリックされたかどうかで判定するようにしました。これなら問答無用で動作します。
スマホUIを出現させた後、スマホ以外の場所をクリックするとスマホUIを閉じるといった使い方です。

using UnityEngine;

public class DisableCanvasOnEdgeClick : MonoBehaviour
{

public Canvas targetCanvas;

private void Update()
{
    if (Input.GetMouseButtonDown(0))
    {
        Vector2 clickPosition = Input.mousePosition;

        // 画面の左右端の範囲を計算
        float leftBoundary = 402f;
        float rightBoundary = Screen.width - 402f;

        // クリック位置が左右端の範囲内にあるか判定
        if (clickPosition.x <= leftBoundary || clickPosition.x >= rightBoundary)
        {
            // クリックされた位置が左右端の範囲内なのでキャンバスを非アクティブにする
            targetCanvas.gameObject.SetActive(false);
        }
    }
}

}

指定した複数のTextMeshProオブジェクトに一括して指定したテキストを出力する

publicでオブジェクトをリストで指定すると、インスペクター上で複数オブジェクトを設定できるようになります。
あとはfor分でそれらのオブジェクトに配列に格納されたテキストを順番に代入していけばOKです

public class DisplaySearchResults : MonoBehaviour
{

public TextMeshProUGUI[] textObjects;

void Start()
{
    if (textObjects.Length != ProgressData._SearchResults.Length)
    {
        Debug.LogError("Number of text objects does not match the number of texts.");
        return;
    }

    for (int i = 0; i < textObjects.Length; i++)
    {
        if (textObjects[i] != null)
        {
            textObjects[i].text = ProgressData._SearchResults[i];
        }
        else
        {
            Debug.LogWarning("TextMeshPro object is null at index " + i);
        }
    }
}

}

これで『魔法少女マジカルハート』にあった、「女の子の名前で検索した際の検索結果と関連ワードが表示される」システムを実装しました。

名前の最初の位置文字を入力しただけでフルネームとともに目を覆いたくなるような関連ワードで検索結果を汚染された女の子はかわいそうでいいね!

この記事が良かったらチップを贈って支援しましょう!

チップを贈るにはユーザー登録が必要です。チップについてはこちら

Unity開発メモ #6

ローカライズに対応したテキスト一元管理システムを構築しました

Unityでローカライズを見据えたテキストの一元管理システムが完成しました!

全てのゲーム内テキストを一つのcsvファイルで一括管理し、スプレッドシート等で閲覧・編集を可能にしました。1ボタンでのゲーム全体の言語切り替えにも対応させました。

ローカライズを容易にするために必要な要素

① すべてのゲーム内テキストは置換可能でなければならない
② よって、テキストを個別のオブジェクトやコードに直接記入したり画像化してはならない
③ 任意のデータベースで使用場所・各言語版テキストを一元管理し、スプレッドシート等で閲覧・編集を容易にする

今回も英語や中国語等にローカライズ予定ですので、スプレッドシート等で一元管理できることは翻訳家や翻訳会社への依頼を視野にいれたときのためにも必要です。

実装方法

① スプレッドシートでテキストデータを記載する。1列目はラベルとし、2列目以降に各言語版テキストを格納する。csvファイルを出力する。
② Unityでcsvファイルを読み込み、各行毎に分割して配列に格納する。
③ さらに各行をcsvの区切り文字で分割して、1列目のラベルをキー、2列目以降から言語設定によって選択される列のテキスト内容を値とする辞書に格納する。
(ついでにここで制御文字を変換したりして改行なども使えるようにする)
④ 辞書のキーを引数に入力すると、対応したテキストを出力するメソッドを作成する。
⑤ ゲームでテキストを記載する際は、④のメソッドを使用する。

さらにInspecterにラベルを記載したらテキスト内容を対応する値に書き換えるスクリプトも作成してUIテキストのコンポーネントとして追加しておけば、UIのテキストも一括書き換えできます。

このシステムのおかげで前作のときのように各言語版毎にゲームをビルドしたりする手間がはぶけそうですし、管理もとても簡単になります。


ラベルの列が辞書のキーに設定されます。キーに対応した値は、言語設定が日本語のときは2列目のテキストが格納され、英語なら3列目のテキストが格納されるといったように言語設定で切り替わります。

画像のようなcsvファイルから言語設定に応じた辞書を作ったあとは、辞書のキーを引数とし値のテキストを戻り値とするメソッドを作ります。


UnityとC#、そしてプログラミングをはじめてまだ1週間ですが、開発者人口の多いUnityだけあって各種ドキュメントが充実しており、スムーズに学習とゲーム開発をすすめられています。

今回導入したいシステム、取り入れたい演出はおぼろげながらどのようにすれば実装できるのか想像ができる状態にはなったので、開発がとん挫することはなさそうで一安心です。


できれば今月~来月中には基本となるゲームシステムを完成させたいと思います。
並行してかわいそうな目に遭う女の子のデザインも準備します。

今回の主人公は、vTuber兼配信者の女の子です

この記事が良かったらチップを贈って支援しましょう!

チップを贈るにはユーザー登録が必要です。チップについてはこちら

« 1 2

月別アーカイブ

記事を検索