1. HOME»
  2. プログラミング・Web»
  3. Unity»
  4. 【絶対できる!】Unityでの2Dノベルゲームの作り方を詳しく解説

【絶対できる!】Unityでの2Dノベルゲームの作り方を詳しく解説

人魚との触れ合いを描いた小説、「ELENA 人魚と過ごした時間」のゲーム化を目指し、日々コツコツと開発を進めているのですが、もう少し時間がかかりそうです。

さて、そんなわけで、今回はUnityによるゲーム開発の状況を少しだけお見せしながら、Unityによるノベルゲーム開発の方法を紹介していきたいと思います。

目次
  1. 今回作成するノベルゲームについて
  2. Unityのダウンロードとインストールについて
  3. 新しいプロジェクトの作成
  4. シーンの名前を変更
  5. ノベルゲームの基本となる階層を作る
  6. それぞれのオフジェクトの設定
  7. GameManagerの作成
  8. テキストの表示
  9. 背景画像の表示
  10. イベントCGの表示
  11. 画像(Prefab)の削除
  12. テキストを1文字ずつ表示
  13. まとめ

今回作成するノベルゲームについて

まず、今回作成するノベルゲームについてです。
次のようなゲームを作成します。(早送りしています)

完成したノベルゲームのプレビュー

今回、行ないたいのは、次のようなものです。

  • クリックで文字を次のものに切り替える
  • 文字を一文字ずつ表示
  • 背景画像の表示
  • イベントCGの表示
  • 外部テキストファイルの読み込み
  • 外部テキストファイルによる、背景画像とイベントCGの操作

では、はじめていきましょう。

Unityのダウンロードとインストールについて

Unityのインストールがまだの方は、以下の記事を参考にしてください。

Windows

Mac

新しいプロジェクトの作成

では、今回のノベルゲーム開発用に新しいプロジェクトを作成します。

テンプレートから2D」を選択し、プロジェクト名を付け「プロジェクトを作成(Create project)」をクリックします。

プロジェクトの新規作成

シーンの名前を変更

デフォルトではシーンの名前がSampleSceneになっているので変更します。
変更するには、プロジェクト(Project)にあるAssetsの中のScenesから「SampleScene」を右クリックし、「名前を変更(Rename)」をクリックします。

シーンの名前を変更

今回は「MainScene」に変更しました。

シーンの名前をMainSceneに変更

次のようなダイアログが表示された場合は、「再ロード(Reload)」をクリックします。

再ロードをクリック

ノベルゲームの基本となる階層を作る

今回作りたいノベルゲームの階層は、次のようにしようと思います。

Unityで開発する、ノベルゲームの階層

一番後ろに「背景」を、その手前に「イベントCG」を表示します。
その手前にメッセージボックスを、さらにその手前にメッセージを表示します。

今回、立ち絵は使いませんが、背景の上に画像を表示する、という意味ではイベントCGの表示と同じですので、工夫すればでできると思います。

ではさっそく、この階層を作っていきましょう。

ヒエラルキー(Hierarchy)のなにもないところで右クリックし、「UI」、「パネル(Panel)」と選択します。

パネルを作成

すると「Canvas」が作られ、その中にパネルが作られますので、名前を「Game」に変更します。

パネルの名前を「Game」に変更

さらに、「Game」オブジェクトを右クリックして、「空のオブジェクトを作成(Create Empty)」をクリックします。

空のオブジェクトを作成

オブジェクトの名前は「Background」にします。

オブジェクト名をBackgroundに変更

再び「Game」オブジェクトを選択して、同じ手順で「空のオブジェクト」を作成し、名前を「Event」にします。

オブジェクト名をEventに変更

こんどは、パネルを作成します。
Gameオブジェクトをクリックし、「UI」から「パネル(Panel)」を選択します。

メッセージウィンドウのパネルの作成

名前は、「MessageWindow」にします。

パネルの名前をMessageWindowに変更

さらに、MessageWindowオブジェクトを右クリックして、「UI」から「テキスト – TextMeshPro(Text – TextMeshPro)」を選択します。

TextMeshProの作成

次のような画面が開いたら、「Import TMP Essentials」をクリックします。

「Import TMP Essentials」をクリック

すると、「Text(TMP)」オブジェクトが作られます。
(TMP Importerは閉じてしまって構いません)

「Text(TMP)」オブジェクトが作られた

この「Text(TMP)」オブジェクトは、「MainText」という名前に変更します。

「Text(TMP)」オブジェクトを、「MainText」という名前に変更

これで、今回のノベルゲーム開発での、階層ができました。

それぞれのオフジェクトの設定

つづいて、さきほど作ったオブジェクトの設定をしていきましょう。

Main Cameraの設定

まず、Main Cameraの設定をしていきましょう。
ヒエラルキー(Hierarchy)で、「Main Camera」を選択します。

Main Cameraオブジェクトを選択

インスペクター(Inspector)から、背景(Background)の色を黒に変えます

背景色を黒に変更

これでMain Cameraの設定の完了です。

Canvasの設定

つづいて、Canvasの設定をしていきます。
ヒエラルキー(Hierarchy)から、「Canvas」を選択しましょう。

Canvasを選択

インスペクター(Inspector)から、以下の部分を変更します。

  • レンターモード(Render Mode)を「スクリーンスペース – カメラ(Screen Space – Camera)」に変更。
  • レンダーカメラ(Render Camera)を、「Main Camera」に変更。
  • UIスケールモード(UI Scale Mode)を「画面サイズに拡大(Scale With Screen Size)」に変更。
  • さらに、参照解像度(Reference Resolution)を「1520」x「720」に変更。
  • マッチ(Match)を「1」に変更。
Canvasの設定箇所

これでCanvasの設定の完了です。

Gameオブジェクトの設定

つづいて、Gameオブジェクトの設定をしていきます。
ヒエラルキー(Hierarchy)から、「Game」を選択しましょう。

Gameオブジェクトを選択

インスペクター(Inspector)から、以下の部分を変更します。

  • アンカープリセット(Anchor Presets)を、縦が「middle」、横が「center」のものに変更。
  • 幅を「1520」、高さを「720」に変更。
  • 色(Color)を黒くし、Aは「255」に設定。
Gameオブジェクトの設定箇所

これで、Gameオブジェクトの設定は完了です。

Backgroundオブジェクトの設定

つづいて、Backgroundオブジェクトの設定をしていきます。
ヒエラルキー(Hierarchy)から、「Background」を選択しましょう。

Backgroundオブジェクトの選択

インスペクター(Inspector)で、以下の部分を変更します。

  • アンカープリセット(Anchor Presets)を縦横「stretch」のものに変更。
  • 上下左右(Top、Bottom、Left、Right)の値をすべて「0」に変更。
Backgroundオブジェクトの設定箇所

これで、Backgroundオブジェクトの設定は完了です。

Eventオブジェクトの設定

つづいて、Eventオブジェクトの設定をしていきます。
ヒエラルキー(Hierarchy)から、「Event」を選択しましょう。

Eventオブジェクトの選択

インスペクター(Inspector)で、以下の部分を変更します。

  • アンカープリセット(Anchor Presets)を縦横「stretch」のものに変更。
  • 上下左右(Top、Bottom、Left、Right)の値をすべて「0」に変更。
Eventオブジェクトの設定箇所

これで、Eventオブジェクトの設定は完了です。

MessageWindowオブジェクトの設定

つづいて、MessageWindowオブジェクトの設定をしていきます。
ヒエラルキー(Hierarchy)から、「MessageWindow」を選択しましょう。

MessageWindowオブジェクトを選択

インスペクター(Inspector)で、以下の部分を変更します。

  • 上の値を「480」に変更。
  • アンカーピボット(Anchors Pivot)のYの値を「0」に変更。
  • 色(Color)を、R「40」、G「40」、B「40」、A「180」に変更
MessageWindowオブジェクトの設定箇所

これで、MessageBoxオブジェクトの設定の完了です。

MainTextオブジェクトの設定

MainTextオブジェクトの設定をしていきます。
ヒエラルキー(Hierarchy)から、「MainText」を選択しましょう。

MainTextオブジェクトの選択

インスペクター(Inspector)で、以下のように変更します。

  • 幅(Width)を「980」、高さ(Height)を「200」に変更。
MainTextオブジェクトの設定箇所

これで、ノベルゲームの階層が完成しました。
再生してみると、次のようになります。

ノベルゲームを実行

GameManagerの作成

つづいて、スクリプトを書いていきましょう。
まずは、ゲーム全体を管理するための、GameManagerを作っていきます。

プロジェクトからAssetsを選択し、右クリック、「作成」、「フォルダー」をクリックしましょう。

Scriptsフォルダの作成

フォルダ名は「Scripts」にします。

フォルダ名をScriptsに変更

いま作ったScriptsフォルダを開き右クリック、「作成」、「C# スクリプト(C# Script)」をクリックしましょう。

C# スクリプトの作成

ファイル名は「GameManager」とします。

ファイル名をGameManagerに変更すると歯車(ギア)のアイコンになる

ここで、GameManagerのアイコンが歯車(ギア)になってしまいました。
なぜこうなるのかは分からないのですが、すこし心配になりますよね。

そこで、クラスを名前空間で管理することにします。
これで、この歯車(ギア)アイコンも解決できます。

では、たった今作成したファイル「GameManager.cs」を、以下のように書き換えてみてください。

GameManager.cs

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

namespace NovelGame
{
    public class GameManager : MonoBehaviour
    {
        // Start is called before the first frame update
        void Start()
        {
            
        }

        // Update is called once per frame
        void Update()
        {
            
        }
    }
}

GameManagerクラス全体を、NovelGameという名前空間に入れました。
すると、さきほどの歯車(ギア)アイコンが、いつものC#ファイルのアイコンになります。

GameManagerファイルが、C#ファイルのアイコンになる

さらに、スクリプトを編集していきます。
GameManagerに作った変数やメソッドなどは、他のクラスからも使いたいものがほとんどだと思いますので、Instanceプロパティを作成して、別のクラスから変数関数を呼び出せるようにします。

また、Start、Updateメソッドは、今回は使わないので削除しました。

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

namespace NovelGame
{
    public class GameManager : MonoBehaviour
    {
        // 別のクラスからGameManagerの変数などを使えるようにするためのもの。(変更はできない)
        public static GameManager Instance { get; private set; }

        void Awake()
        {
            // これで、別のクラスからGameManagerの変数などを使えるようになる。
            Instance = this;
        }
    }
}

では、ヒエラルキー(Hierarchy)で、「MainScene」右クリックして、「ゲームオブジェクト(GameObject)」から、「空のオブジェクトを作成(Create Empty)」をクリックします。

空のオブジェクトを作成

名前は「GameManager」にします。

空のオブジェクトの名前をGameManagerに変更

さらに、今作成したGameManagerオブジェクトに、さきほどのGameManagerファイルを、ドラッグ&ドロップでアタッチします。

GameManagerファイルをアタッチ

これで、GameManagerを作っていくための準備が整いました。

テキストの表示

つづいて、ノベルゲームに一番大切な、テキストの表示をしていきます。

日本語のテキストを表示できるようにする

まずはフォントの設定をしていきます。
日本語に対応した、お好きなフォントを用意しておいてください。ここでは源ノ角ゴシックを使います。

では、Unityの画面で、Assetsに「Fonts」フォルダを作成します。

Fontsフォルダの作成

さらにFontsフォルダを開き、用意しておいたフォントをドラッグ&ドロップします。

フォントの読み込み

「ウィンドウ(Window)」から、「TextMeshPro」を選択し、「フォントアセットクリエーター(Font Asset Creator)」をクリックします。

フォントアセットクリエーターを開く

ウィンドウが開いたら、次のように指定します。

フォントアセットクリエーターの設定箇所

また、「Custom Character List」には、使いたい文字を入力する必要がありますが、日本語ではとてもたくさんの文字を使いますので、入力は大変です。

しかし、そういった文字をすべてまとめてくださっている方がいますので、そちらを使わせていただこうと思います。(ありがとうございます)

GitHub Gist(kgsi/japanese_full.txt): https://gist.github.com/kgsi/ed2f1c5696a2211c1fd1e1e198c96ee4?h=1

上記からダウンロードしたtxtファイル内容をすべてコピーし、「Custom Character List」に貼り付け「Generate Font Atlas」をクリックします。

日本語に使われる文字をすべて入力して、「Generate Font Atlas」をクリック

「Save」をクリックします。

「Save」をクリック

次のように表示されたら、再び「Save」をクリックします。

再び「Save」をクリック

「フォントアセットクリエーター(Font Asset Creator)」を閉じて、ヒエラルキー(Hierarchy)から「MainText」オブジェクトを選択します。

MainTextオブジェクトを選択

インスペクター(Inspector)のFont Assetから、作ったフォントを指定します。

フォントの指定

これで、日本語のテキストを表示できるようになりました。

日本語テキストが入力可能になった

テキストファイル(novel.txt)を読み込んで、順番に画面に表示

つづいて、テキストデータを読み込んでみましょう。
今回はnovel.txtファイルにシナリオを書いていき、それをスクリプトから読み込む、というい方法にしたいと思います。

novel.txtは以下のようにしました。

novel.txt

気がつけば人魚と話していた。
水面で揺らめく金色の髪。一点の濁りもない水中に見えるのは、コーラルピンクに輝く長い尾鰭。巨大な太陽に照らされながら、その人魚はこちらに手を伸ばした。
薄紅色の柔らかな唇が、優しく上下する。
「食べられる? ゆっくり食べてね」
なんの疑問もなく、人魚から海藻を受け取った。味はせず、美味しいとも不味いとも思わなかった。
波に合わせて体が上下に揺れている。わずかな差で人魚の体も揺れている。彼女の背後に見える陸地には、建物も電線も見当たらなかった。そして、それが当たり前だった。
「寒くない?」
「大丈夫──です」
人魚はくすくすと笑った。
「翔馬、少しずつだけど良くなってきてる」
どうして彼女は俺の名前を知ってるのだろうか──翔馬は不思議な感情を抱いた。しかし、波が体を揺らす心地よさに、いつの間にかその気持ちは消えてしまった。
「ありがとうございます、エレナ──さん」
「ふふ、エレナでいいのよ──」
エレナはにっこりと微笑んだ。

プロジェクトからAssetsを選択し、Textsフォルダを作成します。

Textsフォルダの作成

Textsフォルダを開き、そこにさきほど作成したnovel.txtをドラッグ&ドロップします。

テキストファイルの読み込み

Scriptsフォルダに、C# スクリプト(C# Script)を2つ作成します。
名前は、「MainTextController」、「UserScriptManager」とします。

C# スクリプトを2つ作成

今回、シナリオを記述したnovel.txtファイルには、シナリオだけではなく、背景やイベントCGの追加や削除をするための命令も記述できるようにしたいと思います。

そこでこの記事では、そういったシナリオや命令を書いたものを、「ユーザスクリプト(UserScript)」と呼ぶことにします

画面に表示するシナリオは「MainTextController」によってコントロールし、テキストファイルに記述したユーザスクリプトは「UserScriptManager」で管理することにします。

では、ヒエラルキー(Hierarchy)で、「UserScriptManager」オブジェクトを作成しましょう。
オブジェクトは、「MainScene」右クリックして、「ゲームオブジェクト(GameObject)」から、「空のオブジェクトを作成(Create Empty)」をクリックすることで、作ることができます。

UserScriptManagerオブジェクトの作成

MainTextControllerファイルをMainTextオブジェクトに、UserScriptManagerファイルをUserScriptManagerオブジェクトに、ドラッグ&ドロップでアタッチします。

スクリプトファイルのアタッチ

スクリプトは、次のようにします。
まずは、MainTextControllerからです。

MainTextController.cs

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

namespace NovelGame
{
    public class MainTextController : MonoBehaviour
    {
        [SerializeField] TextMeshProUGUI _mainTextObject;

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

        // Update is called once per frame
        void Update()
        {
            // クリックされたとき、次の行へ移動
            if (Input.GetMouseButtonUp(0))
            {
                GoToTheNextLine();
                DisplayText();
            }
        }

        // 次の行へ移動
        public void GoToTheNextLine()
        {
            GameManager.Instance.lineNumber++;
        }

        // テキストを表示
        public void DisplayText()
        {
            string sentence = GameManager.Instance.userScriptManager.GetCurrentSentence();
            _mainTextObject.text = sentence;
        }
    }
}

つづいて、UserScriptManagerです。
Start、Updateメソッドは、今回使わないので、削除しています。

UserScriptManager.cs

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

namespace NovelGame
{
    public class UserScriptManager : MonoBehaviour
    {
        [SerializeField] TextAsset _textFile;

        // 文章中の文(ここでは1行ごと)を入れておくためのリスト
        List<string> _sentences = new List<string>();

        void Awake()
        {
            // テキストファイルの中身を、1行ずつリストに入れておく
            StringReader reader = new StringReader(_textFile.text);
            while (reader.Peek() != -1)
            {
                string line = reader.ReadLine();
                _sentences.Add(line);
            }
        }

        // 現在の行の文を取得する
        public string GetCurrentSentence()
        {
            return _sentences[GameManager.Instance.lineNumber];
        }
    }
}

GameManager.cs

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

namespace NovelGame
{
    public class GameManager : MonoBehaviour
    {
        // 別のクラスからGameManagerの変数などを使えるようにするためのもの。(変更はできない)
        public static GameManager Instance { get; private set; }

        public UserScriptManager userScriptManager;
        public MainTextController mainTextController;

        // ユーザスクリプトの、今の行の数値。クリック(タップ)のたびに1ずつ増える。
        [System.NonSerialized] public int lineNumber;

        void Awake()
        {
            // これで、別のクラスからGameManagerの変数などを使えるようになる。
            Instance = this;

            lineNumber = 0;
        }
    }
}

ヒエラルキー(Hierarchy)でMainTextオブジェクトを選択し、インスペクター(Inspector)のMain Text Objectに、シーンから「MainText」を追加します。

MainTextオブジェクトの設定

ヒエラルキー(Hierarchy)でGameManagerオブジェクトを選択し、インスペクター(Inspector)で、以下のように設定します。

  • User Script Managerでは、シーンから「UserScriptManager」を選択。
  • Main Text Controllerでは、シーンから「MainText」を選択。
GameManagerオブジェクトの設定

ヒエラルキー(Hierarchy)でUserScriptManagerオブジェクトを選択します。
インスペクター(Inspector)のText Fileで、アセットから「novel」を選択します。

UserScriptManagerオブジェクトの設定

これで再生すると、novel.txtの最初の1行が表示され、クリックで順番にテキストが表示されていきます。

ノベルゲームの実行

背景画像の表示

つづいて、背景画像を表示してみましょう。

フォルダの作成と、使いたい背景画像の追加

ではまず、表示する背景画像を入れておくフォルダを作りましょう。
フォルダ名は「Images」にしました。

Imagesフォルダを作成

さらにImagesフォルダを開いて、「Backgrounds」フォルダを作ります。

Backgroundsフォルダを作成

Backgroundsフォルダを開き、その中に使いたい背景画像をドラッグ&ドロップします。

Backgroundsフォルダに背景画像を追加

これで背景画像を追加できました。

Prefabの作成

今回、画像の表示には、Prefabを使っていこうと思います。
「Background」を右クリックし、「UI」から、「画像(Image)」を選択します。

Imageオブジェクトの作成

いま作った「Image」を選択し、インスペクター(Inspector)で、以下のように変更します。

  • アンカープリセット(Anchor Presets)を縦横「stretch」のものに変更。
  • 上下左右(Top、Bottom、Left、Right)の値をすべて「0」に変更。
Imageオブジェクトの設定箇所

そして、プロジェクト(Project)からAssetsフォルダを開き、ヒエラルキー(Hierarchy)から「Image」オブジェクトを、そこへドラッグ&ドロップします。

ImageオブジェクトのPrefabを作成

これで、「Image」というPrefabを作ることができました。

ImageというPrefabが作られた

ヒエラルキー(Hierarchy)の「Image」はもう削除(Delete)してしまいましょう。

ヒエラルキー(Hierarchy)の「Image」を削除

スクリプトを用意して背景画像を表示する

つづいて、実際に背景画像を表示してみます。
まずは、スクリプトを用意しましょう。ファイル名は「ImageManager」とします。

ImageManagerファイルを作成

さらに、ヒエラルキー(Hierarchy)で、空のオブジェクトを作成し、名前を「ImageManager」にします。

ImageManagerオブジェクトを作成

ヒエラルキー(Hierarchy)のImageManagerオブジェクトに、プロジェクトのImageManagerファイルを、ドラッグ&ドロップします。

ImageManagerファイルをアタッチ

では、「ImageManager」ファイルを編集してみましょう。以下のようにします。
また、Start、Updateメソッドは必要ないので削除しました。

ImageManager.cs

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

namespace NovelGame
{
    public class ImageManager : MonoBehaviour
    {
        [SerializeField] Sprite _background1;
        [SerializeField] GameObject _backgroundObject;
        [SerializeField] GameObject _imagePrefab;

        // テキストファイルから、文字列でSpriteやGameObjectを扱えるようにするための辞書
        Dictionary<string, Sprite> _textToSprite;
        Dictionary<string, GameObject> _textToParentObject;

        // 操作したいPrefabを指定できるようにするための辞書
        Dictionary<string, GameObject> _textToSpriteObject;

        void Awake()
        {
            _textToSprite = new Dictionary<string, Sprite>();
            _textToSprite.Add("background1", _background1);

            _textToParentObject = new Dictionary<string, GameObject>();
            _textToParentObject.Add("backgroundObject", _backgroundObject);

            _textToSpriteObject = new Dictionary<string, GameObject>();
        }

        // 画像を配置する
        public void PutImage(string imageName, string parentObjectName)
        {
            Sprite image = _textToSprite[imageName];
            GameObject parentObject = _textToParentObject[parentObjectName];

            Vector2 position = new Vector2(0, 0);
            Quaternion rotation = Quaternion.identity;
            Transform parent = parentObject.transform;
            GameObject item = Instantiate(_imagePrefab, position, rotation, parent);
            item.GetComponent<Image>().sprite = image;

            _textToSpriteObject.Add(imageName, item);
        }
    }
}

さらに、ユーザスクリプトからの画像表示の命令を取得したいので、「UserScriptManager」ファイルを次のように編集します。

UserScriptManager.cs

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

namespace NovelGame
{
    public class UserScriptManager : MonoBehaviour
    {
        [SerializeField] TextAsset _textFile;

        // 文章中の文(ここでは1行ごと)を入れておくためのリスト
        List<string> _sentences = new List<string>();

        void Awake()
        {
            // テキストファイルの中身を、1行ずつリストに入れておく
            StringReader reader = new StringReader(_textFile.text);
            while (reader.Peek() != -1)
            {
                string line = reader.ReadLine();
                _sentences.Add(line);
            }
        }

        // 現在の行の文を取得する
        public string GetCurrentSentence()
        {
            return _sentences[GameManager.Instance.lineNumber];
        }

        // 文が命令かどうか
        public bool IsStatement(string sentence)
        {
            if (sentence[0] == '&')
            {
                return true;
            }
            return false;
        }

        // 命令を実行する
        public void ExecuteStatement(string sentence)
        {
            string[] words = sentence.Split(' ');
            switch(words[0])
            {
                case "&img":
                    GameManager.Instance.imageManager.PutImage(words[1], words[2]);
                    break;
            }
        }
    }
}

また、テキストが次の行へ移動するとき、その行が文章なのか、命令なのかを判断する必要があるので、「MainTextController」ファイルも編集していきます。

MainTextController.cs

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

namespace NovelGame
{
    public class MainTextController : MonoBehaviour
    {
        [SerializeField] TextMeshProUGUI _mainTextObject;

        // Start is called before the first frame update
        void Start()
        {
            // 最初の行のテキストを表示、または命令を実行
            string statement = GameManager.Instance.userScriptManager.GetCurrentSentence();
            if (GameManager.Instance.userScriptManager.IsStatement(statement))
            {
                GameManager.Instance.userScriptManager.ExecuteStatement(statement);
                GoToTheNextLine();
            }
            DisplayText();
        }

        // Update is called once per frame
        void Update()
        {
            // クリックされたとき、次の行へ移動
            if (Input.GetMouseButtonUp(0))
            {
                GoToTheNextLine();
                DisplayText();
            }
        }

        // 次の行へ移動
        public void GoToTheNextLine()
        {
            GameManager.Instance.lineNumber++;
            string sentence = GameManager.Instance.userScriptManager.GetCurrentSentence();
            if (GameManager.Instance.userScriptManager.IsStatement(sentence))
            {
                GameManager.Instance.userScriptManager.ExecuteStatement(sentence);
                GoToTheNextLine();
            }
        }

        // テキストを表示
        public void DisplayText()
        {
            string sentence = GameManager.Instance.userScriptManager.GetCurrentSentence();
            _mainTextObject.text = sentence;
        }
    }
}

「GameManager」ファイルも、ちょっとだけ編集します。

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

namespace NovelGame
{
    public class GameManager : MonoBehaviour
    {
        // 別のクラスからGameManagerの変数などを使えるようにするためのもの。(変更はできない)
        public static GameManager Instance { get; private set; }

        public UserScriptManager userScriptManager;
        public MainTextController mainTextController;
        public ImageManager imageManager;

        // ユーザスクリプトの、今の行の数値。クリック(タップ)のたびに1ずつ増える。
        [System.NonSerialized] public int lineNumber;

        void Awake()
        {
            // これで、別のクラスからGameManagerの変数などを使えるようになる。
            Instance = this;

            lineNumber = 0;
        }
    }
}

ImageManagerオブジェクトを選択して、インスペクター(Inspector)を見ると、「Background1」、「Background Object」、「Image Prefab」という項目が増えていますので、それぞれ以下のように指定します。

  • Background1には、「アセット」から「bg1」を指定。
  • BackgroundBackground Objectには、「シーン」から「Background」を指定。
  • Image Prefabには、「アセット」から「Image」を指定。
ImageManagerオブジェクトの設定箇所

さらに、GameManagerオブジェクトを選択して、インスペクター(Inspector)から、Image ManagerにシーンからImageManagerを選択します。

GameManagerオブジェクトの設定箇所

あとは、novel.txtで、以下のように書いてみましょう。

novel.txt

&img background1 backgroundObject
気がつけば人魚と話していた。
水面で揺らめく金色の髪。一点の濁りもない水中に見えるのは、コーラルピンクに輝く長い尾鰭。巨大な太陽に照らされながら、その人魚はこちらに手を伸ばした。
薄紅色の柔らかな唇が、優しく上下する。
「食べられる? ゆっくり食べてね」
なんの疑問もなく、人魚から海藻を受け取った。味はせず、美味しいとも不味いとも思わなかった。
波に合わせて体が上下に揺れている。わずかな差で人魚の体も揺れている。彼女の背後に見える陸地には、建物も電線も見当たらなかった。そして、それが当たり前だった。
「寒くない?」
「大丈夫──です」
人魚はくすくすと笑った。
「翔馬、少しずつだけど良くなってきてる」
どうして彼女は俺の名前を知ってるのだろうか──翔馬は不思議な感情を抱いた。しかし、波が体を揺らす心地よさに、いつの間にかその気持ちは消えてしまった。
「ありがとうございます、エレナ──さん」
「ふふ、エレナでいいのよ──」
エレナはにっこりと微笑んだ。

これを再生すると、次のようになります。

ノベルゲームのプレビュー

再生したままの状態で、ヒエラルキー(Hierarchy)を確認すると、Backgroundオブジェクトの中に、「Image(Clone)」が作られているはずです。

「Image(Clone)」が作られているかどうかの確認

イベントCGの表示

つづいて、イベントCGの表示をしていきます。

まず、Imagesフォルダの中に、「EventCG」フォルダを作成します。

EventCGフォルダの作成

作ったEventCGフォルダに、使いたいイベントCGをドラッグ&ドロップします。
今回は2つの画像ファイルを追加しました。

イベントCGの読み込み

ImageManagerを、以下のようにします。

ImageManager.cs

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

namespace NovelGame
{
    public class ImageManager : MonoBehaviour
    {
        [SerializeField] Sprite _background1;
        [SerializeField] Sprite _eventCG1;
        [SerializeField] Sprite _eventCG2;
        [SerializeField] GameObject _backgroundObject;
        [SerializeField] GameObject _eventObject;
        [SerializeField] GameObject _imagePrefab;

        // テキストファイルから、文字列でSpriteやGameObjectを扱えるようにするための辞書
        Dictionary<string, Sprite> _textToSprite;
        Dictionary<string, GameObject> _textToParentObject;

        // 操作したいPrefabを指定できるようにするための辞書
        Dictionary<string, GameObject> _textToSpriteObject;

        void Awake()
        {
            _textToSprite = new Dictionary<string, Sprite>();
            _textToSprite.Add("background1", _background1);
            _textToSprite.Add("eventCG1", _eventCG1);
            _textToSprite.Add("eventCG2", _eventCG2);

            _textToParentObject = new Dictionary<string, GameObject>();
            _textToParentObject.Add("backgroundObject", _backgroundObject);
            _textToParentObject.Add("eventObject", _eventObject);

            _textToSpriteObject = new Dictionary<string, GameObject>();
        }

        // 画像を配置する
        public void PutImage(string imageName, string parentObjectName)
        {
            Sprite image = _textToSprite[imageName];
            GameObject parentObject = _textToParentObject[parentObjectName];

            Vector2 position = new Vector2(0, 0);
            Quaternion rotation = Quaternion.identity;
            Transform parent = parentObject.transform;
            GameObject item = Instantiate(_imagePrefab, position, rotation, parent);
            item.GetComponent<Image>().sprite = image;

            _textToSpriteObject.Add(imageName, item);
        }
    }
}

ImageManagerオブジェクトを選択し、インスペクター(Inspector)で、Event CG1とEvent CG2にさきほどのイベントCGを指定し、Event Objectには「Event」オブジェクトを指定します。

ImageManagerオブジェクトの設定箇所

あとは、novel.txtに、イベントCGを表示する命令を追加します。

&img background1 backgroundObject
&img eventCG1 eventObject
気がつけば人魚と話していた。
水面で揺らめく金色の髪。一点の濁りもない水中に見えるのは、コーラルピンクに輝く長い尾鰭。巨大な太陽に照らされながら、その人魚はこちらに手を伸ばした。
薄紅色の柔らかな唇が、優しく上下する。
「食べられる? ゆっくり食べてね」
なんの疑問もなく、人魚から海藻を受け取った。味はせず、美味しいとも不味いとも思わなかった。
波に合わせて体が上下に揺れている。わずかな差で人魚の体も揺れている。彼女の背後に見える陸地には、建物も電線も見当たらなかった。そして、それが当たり前だった。
「寒くない?」
「大丈夫──です」
人魚はくすくすと笑った。
「翔馬、少しずつだけど良くなってきてる」
どうして彼女は俺の名前を知ってるのだろうか──翔馬は不思議な感情を抱いた。しかし、波が体を揺らす心地よさに、いつの間にかその気持ちは消えてしまった。
「ありがとうございます、エレナ──さん」
「ふふ、エレナでいいのよ──」
エレナはにっこりと微笑んだ。

これで再生すると、このようになります。

ノベルゲームのプレビュー

イベントCGだけが表示されているように見えますが、ヒエラルキー(Hierarchy)を見ると、さきほど表示した背景画像と、今回のイベントCGが、同時に表示されていることが分かります。

背景画像とイベントCGが同時に表示されているか確認

画像(Prefab)の削除

つづいて、さきほど表示した背景画像やイベントCGのPrefabを、削除できるようにしていこうと思います。

まずはImageManagerファイルに、画像を削除するためのメソッドを作成しましょう。

ImageManager.cs

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

namespace NovelGame
{
    public class ImageManager : MonoBehaviour
    {
        [SerializeField] Sprite _background1;
        [SerializeField] Sprite _eventCG1;
        [SerializeField] Sprite _eventCG2;
        [SerializeField] GameObject _backgroundObject;
        [SerializeField] GameObject _eventObject;
        [SerializeField] GameObject _imagePrefab;

        // テキストファイルから、文字列でSpriteやGameObjectを扱えるようにするための辞書
        Dictionary<string, Sprite> _textToSprite;
        Dictionary<string, GameObject> _textToParentObject;

        // 操作したいPrefabを指定できるようにするための辞書
        Dictionary<string, GameObject> _textToSpriteObject;

        void Awake()
        {
            _textToSprite = new Dictionary<string, Sprite>();
            _textToSprite.Add("background1", _background1);
            _textToSprite.Add("eventCG1", _eventCG1);
            _textToSprite.Add("eventCG2", _eventCG2);

            _textToParentObject = new Dictionary<string, GameObject>();
            _textToParentObject.Add("backgroundObject", _backgroundObject);
            _textToParentObject.Add("eventObject", _eventObject);

            _textToSpriteObject = new Dictionary<string, GameObject>();
        }

        // 画像を配置する
        public void PutImage(string imageName, string parentObjectName)
        {
            Sprite image = _textToSprite[imageName];
            GameObject parentObject = _textToParentObject[parentObjectName];

            Vector2 position = new Vector2(0, 0);
            Quaternion rotation = Quaternion.identity;
            Transform parent = parentObject.transform;
            GameObject item = Instantiate(_imagePrefab, position, rotation, parent);
            item.GetComponent<Image>().sprite = image;

            _textToSpriteObject.Add(imageName, item);
        }

        // 画像を削除する
        public void RemoveImage(string imageName)
        {
            Destroy(_textToSpriteObject[imageName]);
        }
    }
}

つづいて、UserScriptManagerファイルに、画像を削除する命令を作ります。

UserScriptManager.cs

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

namespace NovelGame
{
    public class UserScriptManager : MonoBehaviour
    {
        [SerializeField] TextAsset _textFile;

        // 文章中の文(ここでは1行ごと)を入れておくためのリスト
        List<string> _sentences = new List<string>();

        void Awake()
        {
            // テキストファイルの中身を、1行ずつリストに入れておく
            StringReader reader = new StringReader(_textFile.text);
            while (reader.Peek() != -1)
            {
                string line = reader.ReadLine();
                _sentences.Add(line);
            }
        }

        // 現在の行の文を取得する
        public string GetCurrentSentence()
        {
            return _sentences[GameManager.Instance.lineNumber];
        }

        // 文が命令かどうか
        public bool IsStatement(string sentence)
        {
            if (sentence[0] == '&')
            {
                return true;
            }
            return false;
        }

        // 命令を実行する
        public void ExecuteStatement(string sentence)
        {
            string[] words = sentence.Split(' ');
            switch(words[0])
            {
                case "&img":
                    GameManager.Instance.imageManager.PutImage(words[1], words[2]);
                    break;
                case "&rmimg":
                    GameManager.Instance.imageManager.RemoveImage(words[1]);
                    break;
            }
        }
    }
}

テキストファイルでは、もうちょっとシナリオを長くして、画像の表示と削除を試してみます。

novel.txt

&img eventCG1 eventObject
気がつけば人魚と話していた。
水面で揺らめく金色の髪。一点の濁りもない水中に見えるのは、コーラルピンクに輝く長い尾鰭。巨大な太陽に照らされながら、その人魚はこちらに手を伸ばした。
薄紅色の柔らかな唇が、優しく上下する。
「食べられる? ゆっくり食べてね」
なんの疑問もなく、人魚から海藻を受け取った。味はせず、美味しいとも不味いとも思わなかった。
波に合わせて体が上下に揺れている。わずかな差で人魚の体も揺れている。彼女の背後に見える陸地には、建物も電線も見当たらなかった。そして、それが当たり前だった。
「寒くない?」
「大丈夫──です」
人魚はくすくすと笑った。
「翔馬、少しずつだけど良くなってきてる」
どうして彼女は俺の名前を知ってるのだろうか──翔馬は不思議な感情を抱いた。しかし、波が体を揺らす心地よさに、いつの間にかその気持ちは消えてしまった。
「ありがとうございます、エレナ──さん」
「ふふ、エレナでいいのよ──」
エレナはにっこりと微笑んだ。
&rmimg eventCG1
&img background1 backgroundObject
川を眺めていると、兄に会えそうな気がした。
稲生沢川に架かる橋の歩道で、笹倉翔馬は欄干に肘を置いて体重を預けながら、汗で湿った半袖のスクールシャツを肩まで捲った。
この川はすぐ海に出る。人が生まれ、やがて最期を迎えるように、運命に逆らうことなくゆっくりと流れていく。
失われた命は二度と戻らない。過ぎた時間は巻き戻らない。神の定めた理論に抗うことはできない──
翔馬は欄干に置いていた残りわずかのコーラを飲み干した。視界に飛び込んできた空には雲ひとつなく、その果てしない青色が兄との海を連想させる。兄との思い出の海──
「死は黒じゃなくて透明だ」と父が言っていたが、あの日、兄と見た海はまさに透き通っていた。
ペットボトルにキャップをしたとき、背後からトンと肩を叩かれた。翔馬は川を見つめたまま、
「正樹か?」
「あれ、なんで分かったんだ?」
「いつものことだろ」
「確かに」
&img eventCG2 eventObject
笑い声に振り返ると、白い歯を見せる橋本正樹と目が合った。
着崩したシャツ、耳が少し隠れるぐらいの茶髪、日に焼けた肌。左腕の大きな傷は、幼いころに三輪車で転んだときのものらしい。
翔馬とは真逆のタイプだが、どういうわけか馬が合い、一年のころに仲良くなった。
正樹は翔馬の隣で欄干に背中を預け、目の前の人魚像を眺める。橋の四隅に置かれたうちのひとつだ。
「この前、直してもらったバイク、調子いいよ。翔ちゃんのお父さんすげぇな」
「そんなことしか能がないからな」
&rmimg eventCG2
正樹は川の方を向いた。
「いや、すげぇよ。オレもバイク分解してみようかな」

これで再生すると、シナリオと画像がマッチした、ノベルゲームになります。

ノベルゲームのプレビュー

テキストを1文字ずつ表示

最後に、もう少しノベルゲームらしくするために、テキストを1文字ずつ表示できるようにしていきます。

MainTextController.cs

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

namespace NovelGame
{
    public class MainTextController : MonoBehaviour
    {
        [SerializeField] TextMeshProUGUI _mainTextObject;
        int _displayedSentenceLength;
        float _time;
        float _feedTime;

        // Start is called before the first frame update
        void Start()
        {
            _time = 0f;
            _feedTime = 0.05f;

            // 最初の行のテキストを表示、または命令を実行
            string statement = GameManager.Instance.userScriptManager.GetCurrentSentence();
            if (GameManager.Instance.userScriptManager.IsStatement(statement))
            {
                GameManager.Instance.userScriptManager.ExecuteStatement(statement);
                GoToTheNextLine();
            }
            DisplayText();
        }

        // Update is called once per frame
        void Update()
        {
            // 文章を1文字ずつ表示する
            _time += Time.deltaTime;
            if (_time >= _feedTime)
            {
                _time -= _feedTime;
                if (!CanGoToTheNextLine())
                {
                    _displayedSentenceLength++;
                    _mainTextObject.maxVisibleCharacters = _displayedSentenceLength;
                }
            }

            // クリックされたとき、次の行へ移動
            if (Input.GetMouseButtonUp(0))
            {
                if (CanGoToTheNextLine())
                {
                    GoToTheNextLine();
                    DisplayText();
                }
            }
        }

        // その行の、すべての文字が表示されていなければ、まだ次の行へ進むことはできない
        public bool CanGoToTheNextLine()
        {
            string sentence = GameManager.Instance.userScriptManager.GetCurrentSentence();
            return (_displayedSentenceLength > sentence.Length);
        }

        // 次の行へ移動
        public void GoToTheNextLine()
        {
            _displayedSentenceLength = 0;
            _time = 0f;
            _mainTextObject.maxVisibleCharacters = 0;
            GameManager.Instance.lineNumber++;
            string sentence = GameManager.Instance.userScriptManager.GetCurrentSentence();
            if (GameManager.Instance.userScriptManager.IsStatement(sentence))
            {
                GameManager.Instance.userScriptManager.ExecuteStatement(sentence);
                GoToTheNextLine();
            }
        }

        // テキストを表示
        public void DisplayText()
        {
            string sentence = GameManager.Instance.userScriptManager.GetCurrentSentence();
            _mainTextObject.text = sentence;
        }
    }
}

これで再生すると、テキストが1文字ずつ表示されます。

ノベルゲームのプレビュー

しかしこのままでは、すべての文字が表示されるまで、次の行へ進めなくなってしまいます。
そこで、まだすべての文字が表示されていないとき、クリックすると、すべての文字が表示されるようにします。

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

namespace NovelGame
{
    public class MainTextController : MonoBehaviour
    {
        [SerializeField] TextMeshProUGUI _mainTextObject;
        int _displayedSentenceLength;
        int _sentenceLength;
        float _time;
        float _feedTime;

        // Start is called before the first frame update
        void Start()
        {
            _time = 0f;
            _feedTime = 0.05f;

            // 最初の行のテキストを表示、または命令を実行
            string statement = GameManager.Instance.userScriptManager.GetCurrentSentence();
            if (GameManager.Instance.userScriptManager.IsStatement(statement))
            {
                GameManager.Instance.userScriptManager.ExecuteStatement(statement);
                GoToTheNextLine();
            }
            DisplayText();
        }

        // Update is called once per frame
        void Update()
        {
            // 文章を1文字ずつ表示する
            _time += Time.deltaTime;
            if (_time >= _feedTime)
            {
                _time -= _feedTime;
                if (!CanGoToTheNextLine())
                {
                    _displayedSentenceLength++;
                    _mainTextObject.maxVisibleCharacters = _displayedSentenceLength;
                }
            }

            // クリックされたとき、次の行へ移動
            if (Input.GetMouseButtonUp(0))
            {
                if (CanGoToTheNextLine())
                {
                    GoToTheNextLine();
                    DisplayText();
                }
                else
                {
                    _displayedSentenceLength = _sentenceLength;
                }
            }
        }

        // その行の、すべての文字が表示されていなければ、まだ次の行へ進むことはできない
        public bool CanGoToTheNextLine()
        {
            string sentence = GameManager.Instance.userScriptManager.GetCurrentSentence();
            _sentenceLength = sentence.Length;
            return (_displayedSentenceLength > sentence.Length);
        }

        // 次の行へ移動
        public void GoToTheNextLine()
        {
            _displayedSentenceLength = 0;
            _time = 0f;
            _mainTextObject.maxVisibleCharacters = 0;
            GameManager.Instance.lineNumber++;
            string sentence = GameManager.Instance.userScriptManager.GetCurrentSentence();
            if (GameManager.Instance.userScriptManager.IsStatement(sentence))
            {
                GameManager.Instance.userScriptManager.ExecuteStatement(sentence);
                GoToTheNextLine();
            }
        }

        // テキストを表示
        public void DisplayText()
        {
            string sentence = GameManager.Instance.userScriptManager.GetCurrentSentence();
            _mainTextObject.text = sentence;
        }
    }
}

これで、まだ文字の表示が途中の場合でも、クリックですべての文字を表示できるようになります。

まとめ

今回はUnity 2Dでの、ノベルゲームの作り方を紹介しました。

ノベルゲームの開発は、他のゲームと比べると簡単ではありますが、テキストを一文字ずつ表示させたり、画像を表示させたりといった、ゲーム開発の基本になる部分が含まれます。

また、今回作成したものは、現在私が開発中のノベルゲームの一部分でもあります。ゲーム公開を目指して、懸命に取り組んでいきたいと思います。

40件のコメント 「【絶対できる!】Unityでの2Dノベルゲームの作り方を詳しく解説」

  1. こんにちは!
    改変して勉強させて頂いているのですが、
    新しくスクリプトを追加して、他のクラスと同じレベルで扱いたいのですが、
    ①GameManagerにクラス名、変数名を追加
    ②呼び出す際にGameManager.Instance.変数.
    関数名()で呼び出す

    といったように他のクラス(スクリプト)と同じような呼び方をするとnull referenceで怒られてしまいます。
    使い方はこのようなやり方で正しいのでしょうか?

    1. > キクチさん
      こんにちは。コメントありがとうございます。
      返信が遅くなり、申し訳ありません。

      もしかすると、以下の方法で解決できるかもしれません。

      Unityの画面のヒエラルキーで空のオブジェクトを作って、さらにそこに作ったクラス(スクリプト)を、ドラッグ&ドロップで、アタッチしてください。
      さらにヒエラルキーから、GameManagerを選んで、インスペクターでさきほど作ったオブジェクトを指定してください。

      ぶじ動作することを願ってます。

  2. めちゃわかりやすくて汎用性高いです!
    ご質問なんですが、上記のシステムで背景画像を変更して使うなどはしているのでしょうか?
    (↑のやつだと決まった画像しか使えない?
    おそらく新しくシーンを作成されてる?)

    例えば、assetsフォルダ配下の画像フォルダを参照して、指定した名前のファイルを使用する、など、、。

    1. > あいぶさきさん
      コメントありがとうございます。

      このシステムでも背景画像を削除と追加はできるはずですが、この記事を公開してから時間が経っているので、今はかなりの改変された状態で私は使っております。
      この記事の内容も、それに合わせて更新したいとは思っているのですが、なかなか時間がとれず、まだ更新できていない状況です。

      私の環境のものは、かなりの改変を加えてしまったので、実際に試せてはいないのですが、今回の記事のシステムですと、イベントCGの削除と追加と同じ要領で、背景画像の切り替えもできると思います。
      ImageManagerクラスで、新たに背景画像用の変数(Sprite)を用意し、その変数に使いたい画像をインスペクターから指定する必要があります。

      また、assetsフォルダの画像をファイル名で参照する方法は、私も以前いろいろ試したときがありました。
      しかし最終的には、SerializeFieldを使って画像ごとの変数を定義して、その変数に使いたい画像をインスペクターから指定するのが一番スッキリする気がしたので、その方法を使っています。

      またよろしくお願いします。

  3. こんにちは。はじめまして。たった今unityを使いノベルゲームの製作の一部をやっている初心者です。
    unityの使い方に慣れず全く手も足も出なかった状態だったのですが、主さんのこのサイトのおかげで第一歩を踏み出すことができました!ありがとうございます。
    ですが、進めてくうちに新たな課題も発生してきており、その部分の解決法をおたずねしたいと思い、この度コメントを送信しております。
    内容は2つ。
    1つは、&imgと&rmimgを多用するとセリフが二重に読みあげられてしまうこと。
    2つ目は、一部の文字を読み上げ時に色変更したいという要望をかなえるための機構をどう組み込むべきか。(例:textの23行目と43行目の文字のみ赤で表示 など)

    調べてみても自力ではどうすればよいかわからなくて困ってます。
    何卒宜しくお願い致します。

    1. >マジンガーzeroさん
      はじめまして、こんにちは。コメントありがとうございます。
      私の記事を役立てていただけているとのこと、とてもうれしいです。

      まず、ひとつ目のご質問なのですが、いろいろな可能性が考えられますので、私の方ではなんとも言えないです……
      申し訳ありません。

      ふたつ目のご質問ですが、文字の色を変えたいところをcolorタグで囲むと色を変更できるはずです。
      この記事であれば、novel.txtに書かれたテキストの色を変えたい部分を次のように変更します。

      ——————
      気がつけば<color=#ff6666>人魚</color>と話していた。
      ——————

      行全体の色を変更したい場合は、行全体をcolorタグで囲めばOKです。

      またよろしくお願いします。

      1. 主さん、返信いただきありがとうございます。
        文字のカラー編集は一番になんとかしたい場所だったので、本当に助かりました。
        これからも参考にさせていただきます。
        ありがとうございました。

        1. >マジンガーzeroさん
          こんにちは。返信がおそくなり、申し訳ございません。

          こちらこそ、私のサイトをご覧いただき、ありがとうございます。
          これからもよろしくお願いします。

  4. とても参考にさせていただいています!!
    ボタンを作って、novel.txtからの指示でボタンを表示/非表示、ボタンが押された場合には、背景画像を変更ということをしたいのですが、その場合、どのようにC#ファイルを書き換えて、ボタン操作を受け取ったり、表示を切り替えたりしたら良いのか分かりません。参考になりそうなURLでも、書き換えが必要なファイルだけでも構いませんので、教えていただけると幸いです。

    1. >TNさん
      コメントありがとうございます。
      返信が遅くなってしまい、申し訳ございません。

      実際に作ることはできていないのですが、私ならこう作るかな、というものを紹介します。

      まず、イベントCGや背景画像の表示と同じように、ボタンの表示非表示を切り替えるための命令を作ります。例えば、&btnとか、&btnonとか、&btnoffとかです。
      変更するファイルは「UserScriptManager.cs」です。
      ボタンの表示非表示は、SetActive()が使えると思います。

      ボタンは、ヒエラルキーに画像を追加して、インスペクターの「コンポーネントを追加」からButtonを追加すれば作ることができます。
      あとは、背景画像を変更するメソッドを任意のファイルに作って、ボタンがクリックされたときに、そのメソッドを呼び出すようにすれば完成です。

      うまくお伝えできたか分かりませんが、成功を祈っています。
      またよろしくお願いします。

      1. ご返信ありがとうございます!
        無事にボタンが完成しました。本当に助かりました。

        次は、ボタンが押されたら画面が切り替わるようにしたいのですが、同じシーンの中で背景画像とボタンをの有効化無効化を切り替えるのと、LoadSceneを使ってシーンごと切り替えるのでは、どちらの方が作りやすいと思うかご意見をお聞かせください。
        (今、背景画像、使うボタンの変更にLoadSceneを使ってシーンごと切り替えようと思っていますが、エラーと戦っているところです。)

        1. >TNさん
          コメントありがとうございます。
          ボタンの完成、おめでとうございます。

          同じシーン上かシーンを切り替えるかについてですが、私ならば同じシーンの中に作ってしまいます。
          ただ、どういう意図でその実装をしたいのかにもよりますので、絶対にそうするとは言えないです。

          たとえば、「ゲームをはじめる」ボタンを押すと、タイトル画面からゲーム画面になる、のような切り替えでしたら、シーンごと切り替えるのがいいと思います。
          ゲームの中で、「中に入る」ボタンを押すと部屋に入る、といった場合の背景画像の切り替えでしたら、シーンを切り替えなくてもいいと思います。

          参考になれば幸いです。

          1. ご返信ありがとうございました。

            シーンの切り替えも上手く使いながら、無事完成させることができました。
            Rさんの返信が本当に心強かったです。
            初めてのゲーム作りだったのですが、こちらのページなくしては完成できなかったと思っています。
            改めて本当にありがとうございました。
            今後のご活躍を心より応援しています!!

          2. > TNさん
            こんにちは。

            ゲームが完成した、ということでですね!
            おめでとうございます。
            そして、私の記事がお役に立てたようで、私もうれしいです。
            これからも、お互い楽しくゲーム開発していきましょう。

            こちらこそ、私の記事をご覧いただき、ありがとうございます。
            私自身も、記事の更新やゲーム開発を、しっかり取り組んでいこうと思います。

            また、よろしくお願いします。

  5. 丁寧な解説ありがとうございます。ゲームを製作する際に、このページを参考にさせていただきたいと思っているのですが、このページのソースコードなどの利用規約やライセンスを教えていただいてもよろしいでしょうか。

    1. >deepさん
      コメントありがとうございます。

      ソースコードの利用規約はとくに作っていませんが、ゲーム開発でプログラミングの参考に使っていただく場合であれば、どのように使っていただいても問題ありません。
      もし規約で制限してしまうと、規約ばかりが気になって、プログラミングの学習がなかなかできなくなってしまうのではないか、というのが、私の考えです。

      ですが、イラストとストーリーは私の著作物ですので、ご使用いただけません。

      ……これでは不安になられてしまうかもしれないので、とりあえず目安として、以下を参考にしてください。

      Q記事のソースコードを参考にして作ったゲームを商用利用できますか?
      A可能です

      Q記事のソースコードを参考にして作ったゲームをネット上に公開してもいいですか?
      Aもちろんです。すばらしいです

      Q記事のソースコードを参考にして作ったゲームを公開したとき、この記事のURLを貼ったり、サイト名を記述したり、といったことをした方がいいですか?
      Aどっちでもいいです。そういうのって、書く場所に悩んだり、この書き方でいいのかと心配になったりと、苦労することは知っています

      Q年齢制限のあるゲームで利用してもいいですか?
      A可能です

      Qソースコードをそのまま別のサイトに転載してもいいですか?
      Aそういうのはあまり好きじゃないです

      Q記事のソースコードを参考にして作った独自のソースコードをネット上に公開してもいいですか?
      A問題ないです。なにも参考にせずにプログラミングができるはずがないです

      あまりにひどい使い方をしない限り、問題にはしませんので、安心してお使いください。

      あまりにひどい使い方というのは、たとえば、記事の内容を自分の著作物と主張して、こちらの記事を削除するように要請したりすることです。
      私にとって、不利益になることは禁止です。

      もし、なにかご不明な点がございましたら、お気軽にお尋ねください。

        1. >deepさん
          こちらこそ、私の記事をご覧いただき、ありがとうございます。
          またよろしくお願いします。

    1. >人さん
      コメントありがとうございます。
      さきほど、バージョン「2021.3.23f1」で試してみたのですが、空のオブジェクトを作ることはできました。
      どうして空のオブジェクトがでてこないのかは、申し訳ありませんが、こちらでは分かりません。

      またよろしくお願いします。

  6. 参考にして、推理ゲーム的なのを作っております。
    そこでご報告なのですが、背景を表示する処理にtxtファイルの&img background1 backgroundObject
    で、背景は表示できたのですが、なぜかゲーム画面のほうでこの文字が出てしまいます。
    サイト通りにしたつもりですが、よろしければ解決方法があれば幸いです。

    もう一つ、背景の処理を応用してシーンチェンジに組み込もうと思いますが、SceneManagement;みたいに可能でしょうか?これもよろしくお願いいたします。

    1. >SIROさん
      コメントありがとうございます。

      まず、ひとつ目のご質問についてです。
      MainTextController.csを確認してください。
      GoToTheNextLine()で次の行に進む、というふうに作っているので、これがうまく機能していないと、次の行に進まずにそのままコマンドが表示されてしまいます。

      次に、ふたつ目の質問についてですが、今現在、なかなか時間がとれず、実際に試すことができないので、なんともお答えすることができません。

      お力になれず、大変申し訳ございませんが、またよろしくお願いします。

  7. ちょうどノベルゲームを作ろうとしている時にこちらのページを拝見し、大変参考になりました。
    わかりやすい解説をありがとうございます。

    1. >shinkaiさん
      こちらこそ、私の記事をご覧いただき、とても嬉しいです。
      またよろしくお願いします。

  8. コメント失礼します、非常にわかりやすい解説で初心者の私にはとても助かっております。
    コメント欄を見ていても私と同じ内容の報告がないので必要がないかなと思いつつも、どうしても気になってしまったため、送信させていただきました。
    サイト通りに作成していると、途中でエラーを吐き止まってしまいました。(´-ω-`)カナシイ
    実際に出たエラーメッセージです。
    IndexOutOfRangeException: Index was outside the bounds of the array.
    NovelGame.MainTextController.GoToTheNextLine () (at Assets/Script/MainTextController.cs:34)
    NovelGame.MainTextController.Update () (at Assets/Script/MainTextController.cs:24)

    MainTextControllerの関数GoToTheNextLineからということで、しばらく原因を探していました。結果として
    string sentence = GameManager.Instance.userScriptManager.GetCurrentSentence();
    if (GameManager.Instance.userScriptManager.IsStatement(sentence))
    {
    GameManager.Instance.userScriptManager.ExecuteStatement(sentence);
    GoToTheNextLine();
    }
    GoToTheNextLineの中にあったこの部分を消したらエラーを吐かずに動作しました。
    記事の中では、目次でいう「9.背景画像の表示」からこの記述があります。9以降の手順(更新していただいたもの含め)を全て完了した後で動作させてはいないため他の条件では動くかもしれないですが、少なくとも最初から作る際にここで止まったということで報告させていただきました。
    止まった理由が分からないので、もしよろしければ原因を教えていただけたら大変学びになります。長文失礼致しました、どうぞよろしくお願いいたします。

    1. >grandpancakeさん
      コメントありがとうございます。

      私ももう一度、「9.背景画像の表示」の終わりまでの手順を試してみたのですが、そのようなエラーは表示されませんでした。

      まず、grandpancakeさんが削除した部分は、novel.txtに書かれた行が命令かどうか判断して、命令だったら命令として実行するものです。
      なのでこれを消してしまうと、たとえばゲームの途中で画像を切り替えたいときなどに、うまく動作しなくなってしまいます。

      今回のエラーについてなのですが、私の方ではそのエラーを確認することができず、原因を探ることはできませんでした。
      お役に立てず、申し訳ございません。

      1. やはりこちら側の問題ということで、なんとか動作するよう原因を探してみます。返信ありがとうございました。

          1. その後ChatGPT先生に聞いてみたところ、テキストファイルが原因だったことがわかりました。テキストファイルに改行が含まれている場合にこのメッセージが出るようです。自分の使用したファイルが友人が書いたもので、かつ連続して改行が使われていたので原因を見つけるのに時間がかかってしまいました。
            ともかく、ようやく解決しましたことお伝えします。お騒がせ致しました。(o_ _)o

          2. >grandpancakeさん
            こんにちは。ご報告ありがとうございます。
            なるほど、原因はテキストファイルの改行だったのですね。

            確かに、この記事で作成したプログラムは、テキストファイルでの連続した改行には対応していません。
            今回のプログラムをもとに、連続改行に対応させてみるのも面白いかもしれませんね。

            ぶじ解決したとのこと、私もスッキリしました。
            またよろしくお願いします。

  9. 丁寧な解説をありがとうございます。
    この機能の他に、文字の表示が完了していない状態でクリックすると文字が最後まで表示される機能を実装したいのですが、やり方がわかりません。

    その時に流れている文字列の最後の一文字を表示する。
    と書きたい場合どうなりますか?

    1. >kazuさん
      コメントありがとうございます。
      ご質問についてですが、私も試してみましたので、記事の一番最後に方法を追記いたしました。

      ただこのとき、もともとの記事のソースで、使われていない変数を宣言してしまっていることに気がつきまして、ほんの少しだけ修正させていただいたので、ご了承ください。すみません。

      お役に立てれば幸いです。

  10. とても分かりやすく解説ありがとうございます!
    個人でノベルゲームを作るときに参考にしているのですが、
    選択肢でストーリーを分岐する方法がどうしてもわかりません。
    もしご機会があれば、選択肢でストーリーが分岐する方法も解説していただければ幸いです。

    1. >harumakiさん
      コメントありがとうございます。
      選択肢によって、ストーリーが変化していくと、ゲームも面白くなりますよね。

      ただ、私自身、選択肢でストーリーを分岐させる機能をまだ作ったことがないので、私からなにかアドバイスをすることはできないです。
      私も、選択肢による分岐の機能を作ることに興味はあるのですが、なかなか時間がとれず、検証することができません。

      お力になれず、申し訳ありません。

        1. >harumakiさん
          お役にたてず、申し訳ありませんでした。
          解決方法が見つかりますようお祈りしています。

  11. 解説ありがとうございます。
    ご教授いただいたところで大変恐縮ですが、質問をさせて下さい。背景を切り抜き、背景が透明となったキャラ単体の画像(png)をイベントCGに設定し、背景が透明なまま表示させる方法を教えてください。切り抜いたはず画像(イベントCGに設定されたキャラ単体の画像)の背景が真っ暗になってしまいます…。

    1. >facing the windさん
      コメントありがとうございます。
      背景が透明のキャラ単体の画像、というのは、立ち絵のことかと思いますので、背景画像の上に立ち絵を表示させたい、ということでよろしいしょうか。

      私も、後ろを透過させた立ち絵(png)を表示させてみたのですが、背景が黒くはなりませんでした。

      ただ、今回作ったノベルゲームは、ゲーム自体の背景を黒に設定しているので、後ろを透過させた立ち絵を1枚だけ表示させた場合、背景は黒くなってしまいます。

      ですのでもしかすると、立ち絵が原因ではなく、背景画像の方がうまく表示されていないのかもしれません。
      ゲームを再生したあと、ヒエラルキーから、背景の画像が存在しているかをご確認ください。

      お役に立てれば嬉しいです。

  12. 丁寧な解説ありがとうございます。とても参考になっています。
    UserScriptManagerをInspectorから指定することができずに困っています。
    (一覧にも表示されないです)
    何度も見直してサイト通りになっていると思うのですが…

    解決方法などがあれば教えてもらいたいです!

    1. >ノベルゲームを作りたい!さん
      コメントありがとうございます。
      まず、記事にひとつミスがありました。
      UserScriptManagerをInspectorから指定する部分の「GameManager.cs」ファイルのソースコードで、変更していない行(閉じ括弧の部分)をハイライトしていました。(ソースコード自体の間違いではありません)
      この部分が原因でのミスでしたら、申し訳ございません。記事は修正させていただきました。

      Inspectorの一覧にUserScriptManagerの項目が表示されない原因として、まず、HierarchyのGameManagerオブジェクトが選択されているかご確認ください。

      次に、GameManagerオブジェクトにGameManagerファイルがアタッチされているかご確認ください。
      もしそれが原因でしたら、HierarchyのGameManagerオブジェクトに、ProjectからGameManagerファイルをドラッグ&ドロップしてください。

      それが原因でなければ、「GameManager.cs」のソースコードにミスがあるのかもしれません。
      エラーが表示されていないか、ソースコードに「public UserScriptManager userScriptManager;」の記述があるか、ご確認ください。

      参考になれば幸いです。

deep へ返信する コメントをキャンセル

メールアドレスが公開されることはありません。 が付いている欄は必須項目です




オリジナルゲーム.com