お疲れ様です、poppyです。本日もこちらのブログを見に来てくださりありがとうございます。
今回もUnityでアセットを使わずにノベルゲームを作るところをお見せいたします。
第4回は「ウェイト・フェードイン・フェードアウト」など、文章以外の部分を中心にやっていきます。
制作環境
制作環境は第1~3回と同様です。また、変数についても第1~3回のものを使い回すのでご注意ください。
- バージョン:2022.3.24f1 (日本語化パッチ適用済み)
- テンプレート:Universal 2D
- スクリプト:Visual Studio 2022 (Windows)
今回は「宴」などのアセットは用いませんのでご了承ください。(多分使ったほうが早くできるとは思うけど)
当ブログの記載内容や情報の信頼性については可能な限り十分注意をしておりますが、その完全性、正確性、妥当性及び公正性について保証するものではありません。
情報の誤りや不適切な表現があった場合には予告なしに記事の編集・削除を行うこともございます。あくまでもご自身の判断にてご覧頂くようにお願い致します。
当ブログの記載内容によって被った損害・損失については一切の責任を負いかねます。ご了承ください。
あくまでも実装当時のコードを掲載しております。現在はバグなどを潰すために修正を行っているため本記事とは異なるコードになっておりますが、
こちらに反映する予定はございません。大変申し訳ありません。あくまでも参考程度に見ていただければ幸いです。
今回の目標
第4回では以下のことができるようにします。
・画像の表示
・画像の配置場所、拡大率の設定
・画像レイヤーの設定
Update()の分離
第3回まではMainTextControllerにあったUpdate()でしたが、今回から文章表示以外のこともこなすことになりますので、Update専用のスクリプトを用意してそちらから呼び出すようにします。第3回までと同様、「GameUpdateManager」の「スクリプト実効化」を行います(スクリプト実効化については第2回参照)。
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;
public ImageManager imageManager;
public GameUpdateManager gameUpdateManager;
// ユーザスクリプトの、今の行の数値。クリック(タップ)のたびに1ずつ増える。
[System.NonSerialized] public int lineNumber;
void Awake()
{
// これで、別のクラスからGameManagerの変数などを使えるようになる。
Instance = this;
lineNumber = 0;
}
}
}
そして、GameUpdateManagerを以下のように記述します。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace NovelGame
{
public class GameUpdateManager : MonoBehaviour
{
//ゲームがどのような状態なのかを表現する変数stateMode
string stateMode;
// Start is called before the first frame update
void Start()
{
stateMode = "novel";
}
public string getStateMode()
{
return stateMode;
}
// Update is called once per frame
void Update()
{
if (stateMode == "novel")
{
//novelモードのとき、mainTextControllerのnovelUpdateを呼び出す
GameManager.Instance.mainTextController.novelUpdate();
}
else
{ //waitモードなど他のモードのときは何もしない
return;
}
}
//ノベルモードにするメソッド
public void ToggleToNovelMode()
{
stateMode = "novel";
}
//ウェイトモードにするメソッド
public void ToggleToWaitMode()
{
stateMode = "wait";
}
}
}
MainTextControllerの方も変更します。ちなみに前回からちょくちょくいじっていたのでそれも反映させます。
MainTextController.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using TMPro;
namespace NovelGame
{
public class MainTextController : MonoBehaviour
{
[SerializeField] TextMeshProUGUI _mainTextObject;
[SerializeField] TextMeshProUGUI _nameTextObject;
int _displayedSentenceLength;
int _sentenceLength;
float _time;
float _feedTime;
// Start is called before the first frame update
void Start()
{
_time = 0f;
_feedTime = 0.05f;
// 最初の行のテキストを表示、または命令を実行
string sentence = GameManager.Instance.userScriptManager.GetCurrentSentence();
bool isStatement = GameManager.Instance.userScriptManager.IsStatement(sentence);
DisplayText(sentence, isStatement);
if (isStatement)
{
GameManager.Instance.userScriptManager.ExecuteStatement(sentence);
}
//現在の文章が文章なのにノベルモードじゃなければ
else if (GameManager.Instance.gameUpdateManager.getStateMode() != "novel")
{
GameManager.Instance.gameUpdateManager.ToggleToNovelMode();
}
}
// ノベルモード時のupdate
public void novelUpdate()
{
// 文章を1文字ずつ表示する
_time += Time.deltaTime;
if (_time >= _feedTime)
{
_time -= _feedTime;
if (!CanGoToTheNextLine())
{
_displayedSentenceLength++;
_mainTextObject.maxVisibleCharacters = _displayedSentenceLength;
}
}
// クリックされたとき、次の行へ移動
if (Input.GetMouseButtonUp(0))
{
if (CanGoToTheNextLine())
{
GoToTheNextLine();
}
else
{
_displayedSentenceLength = _sentenceLength;
}
}
}
// その行の、すべての文字が表示されていなければ、まだ次の行へ進むことはできない
public bool CanGoToTheNextLine()
{
string sentence = GameManager.Instance.userScriptManager.GetCurrentSentence();
string[] words = sentence.Split(',');
string textsentence = words[2];
_sentenceLength = textsentence.Length;
return (_displayedSentenceLength > textsentence.Length);
}
// 次の行へ移動
public void GoToTheNextLine()
{
_displayedSentenceLength = 0;
_time = 0f;
_mainTextObject.maxVisibleCharacters = 0;
GameManager.Instance.lineNumber++;
string sentence = GameManager.Instance.userScriptManager.GetCurrentSentence();
bool isStatement = GameManager.Instance.userScriptManager.IsStatement(sentence);
DisplayText(sentence,isStatement);
if(isStatement)
{
GameManager.Instance.userScriptManager.ExecuteStatement(sentence);
}
//現在の文章が文章なのにノベルモードじゃなければ
else if(GameManager.Instance.gameUpdateManager.getStateMode() != "novel")
{
GameManager.Instance.gameUpdateManager.ToggleToNovelMode();
}
}
// テキストを表示
public void DisplayText(string sentence, bool isStatement)
{
string[] words = sentence.Split(',');
string namesentence = words[1];
string textsentence = words[2];
if(isStatement)
{
_mainTextObject.text = null;
_nameTextObject.text = null;
}
else
{
_mainTextObject.text = textsentence;
_nameTextObject.text = namesentence;
}
}
}
}
ウェイトの実装
続いてウェイトを実装します。ここではコルーチンを使用します。
コルーチンとは、実行を停止して Unity へ制御を戻し、その次のフレームで停止したところから続行することができる関数です。(Unity公式ドキュメントより)
別に使わなくても問題ないのですが、コルーチンにはyield return new WaitForSeconds();と指定した時間止めることができる文があり、スマートに記述できるためこちらを採用します。
ウェイトについてはUserScriptManager上で実現します。
UserScriptManager.cs
// ステートメントを実行するメソッド
public void ExecuteStatement(string sentence)
{
string[] words = sentence.Split(','); // 文章を単語に分割する
// 単語によって処理を分岐する
switch (words[1].ToLower())
{
case "putimage":
int layerOrder = ConvertToInt(words[4],10000);
int img_x = ConvertToInt(words[5]);
int img_y = ConvertToInt(words[6]);
int scale_percent = ConvertToInt(words[7],100);
GameManager.Instance.imageManager.PutImage(words[2], words[3],layerOrder,img_x,img_y,scale_percent); // 画像を表示する
GameManager.Instance.mainTextController.GoToTheNextLine();
break;
case "removeimage":
GameManager.Instance.imageManager.RemoveImage(words[2]); // 画像を削除する
GameManager.Instance.mainTextController.GoToTheNextLine();
break;
case "wait":
float waitTime = ConvertToFloat(words[2], 1f);
GameManager.Instance.gameUpdateManager.ToggleToWaitMode();
StartCoroutine(WaitCoroutine(waitTime));
break;
default:
break;
}
}
private IEnumerator WaitCoroutine(float waitTime)
{
yield return new WaitForSeconds(waitTime);
// 待機後、次の行に進む処理を実行
GameManager.Instance.mainTextController.GoToTheNextLine();
}
StartCoroutineでコルーチンをスタートさせて、yield return new WaitForSeconds(waitTime)でwaitTime秒待ち、その後次の行を読み込む形になります。
適当にウェイトを挟んでみます。
実行すると「いま何時・・・・・・」の後ウェイトを挟んで「うん、いそいでね」が表示されたかと思います。
フェードイン・フェードアウト
続いて、フェードイン・フェードアウトをやっていきます。こちらもコルーチンを使って解決します。スクリーンに関するスクリプトScreenFadeControllerを作ります。ScreenFadeControllerの「スクリプト実効化」を行います。
そして、以下のようにフェードイン・フェードアウトを実装します。
ScreenFadeController.cs
using System.Collections;
using UnityEngine;
namespace NovelGame
{
public class ScreenFadeController : MonoBehaviour
{
[SerializeField] Sprite fadeSprite; // フェードに使用するスプライト
float fadeSpeed; // フェードの速度
private SpriteRenderer spriteRenderer; // スプライトレンダラーの参照
bool isFading = false; // フェード中かどうかのフラグ
// 初期化
void Start()
{
// スプライトレンダラーを取得
spriteRenderer = GetComponent<SpriteRenderer>();
// スプライトレンダラーが存在しない場合は追加する
if (spriteRenderer == null)
{
spriteRenderer = gameObject.AddComponent<SpriteRenderer>();
}
// フェードに使用するスプライトを設定
spriteRenderer.sprite = fadeSprite;
// フェードスプライトが設定されている場合は非表示にする
if (fadeSprite != null)
{
spriteRenderer.enabled = false;
}
}
// フェードインを実行する関数
public void ExecuteScreenFadeIn(float t = 1f)
{
if (!isFading) // フェード中でない場合のみ処理を行う
{
fadeSpeed = t;
StartCoroutine(FadeIn());
}
}
// フェードアウトを実行する関数
public void ExecuteScreenFadeOut(float t = 1f)
{
if (!isFading) // フェード中でない場合のみ処理を行う
{
fadeSpeed = t;
StartCoroutine(FadeOut());
}
}
// フェードインのコルーチン
private IEnumerator FadeIn()
{
isFading = true; // フェード中フラグを立てる
//ウェイトモードに移行
GameManager.Instance.gameUpdateManager.ToggleToWaitMode();
// フェードスプライトを表示する
spriteRenderer.enabled = true;
Color color = spriteRenderer.color;
if (color.a != 1)
{
color.a = 1;
spriteRenderer.color = color;
}
// アルファ値を下げてフェードインする
while (spriteRenderer.color.a > 0)
{
color.a -= fadeSpeed * Time.deltaTime;
spriteRenderer.color = color;
yield return null; // フレームの終了まで待機
}
isFading = false; // フェード中フラグを解除
GameManager.Instance.mainTextController.GoToTheNextLine();
}
// フェードアウトのコルーチン
private IEnumerator FadeOut()
{
isFading = true; // フェード中フラグを立てる
// フェードスプライトを表示する
spriteRenderer.enabled = true;
//ウェイトモードに移行
GameManager.Instance.gameUpdateManager.ToggleToWaitMode();
Color color = spriteRenderer.color;
//アルファ値が0以外なら一旦0に戻す
if (color.a != 0)
{
color.a = 0;
spriteRenderer.color = color;
}
// アルファ値を上げてフェードアウトする
while (spriteRenderer.color.a < 1)
{
color.a += fadeSpeed * Time.deltaTime;
spriteRenderer.color = color;
yield return null; // フレームの終了まで待機
}
isFading = false; // フェード中フラグを解除
GameManager.Instance.mainTextController.GoToTheNextLine();
}
// フェード中かどうかを返す関数
public bool IsFading()
{
return isFading;
}
}
}
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;
public ImageManager imageManager;
public ScreenFadeController ScreenFadeController;
public GameUpdateManager gameUpdateManager;
// ユーザスクリプトの、今の行の数値。クリック(タップ)のたびに1ずつ増える。
[System.NonSerialized] public int lineNumber;
void Awake()
{
// これで、別のクラスからGameManagerの変数などを使えるようになる。
Instance = this;
lineNumber = 0;
}
}
}
UserScriptManager.cs
// ステートメントを実行するメソッド
public void ExecuteStatement(string sentence)
{
string[] words = sentence.Split(','); // 文章を単語に分割する
float _fadetime = 1f;
// 単語によって処理を分岐する
switch (words[1].ToLower())
{
case "putimage":
int layerOrder = ConvertToInt(words[4], 10000);
int img_x = ConvertToInt(words[5]);
int img_y = ConvertToInt(words[6]);
int scale_percent = ConvertToInt(words[7], 100);
GameManager.Instance.imageManager.PutImage(words[2], words[3], layerOrder, img_x, img_y, scale_percent); // 画像を表示する
GameManager.Instance.mainTextController.GoToTheNextLine();
break;
case "removeimage":
GameManager.Instance.imageManager.RemoveImage(words[2]); // 画像を削除する
GameManager.Instance.mainTextController.GoToTheNextLine();
break;
case "screenfadeout":
_fadetime = ConvertToFloat(words[2], 3f);
GameManager.Instance.ScreenFadeController.ExecuteScreenFadeOut(_fadetime); // フェードアウト
break;
case "screenfadein":
_fadetime = ConvertToFloat(words[2], 3f);
GameManager.Instance.ScreenFadeController.ExecuteScreenFadeIn(_fadetime); //フェードイン
break;
case "wait":
float waitTime = ConvertToFloat(words[2], 1f);
GameManager.Instance.gameUpdateManager.ToggleToWaitMode();
StartCoroutine(WaitCoroutine(waitTime));
break;
default:
break;
}
}
private IEnumerator WaitCoroutine(float waitTime)
{
yield return new WaitForSeconds(waitTime);
// 待機後、次の行に進む処理を実行
GameManager.Instance.mainTextController.GoToTheNextLine();
}
お気づきになった方もいらっしゃるかもしれませんが、ScreenFadeControllerにはFade Spriteというスプライト(画像)を用意する必要があります。これは真っ黒の画像を用意してください。パワポでも画像編集ソフトでもなんでも良いです。やっていることは真っ黒の画像の透明度を操作して真っ黒にしたり透明にしたりするだけです。ある種ちょっと強引な策な感じがします。
それはともかく、フェードイン・フェードアウトをウェイトを挟むようにいれてみましょう。
これで、「いま何時・・・」のあとフェードアウトして、少し時間が立つとフェードインすることがわかると思います。これでフェードイン・フェードアウトが表現できました。
画像の移動
最後に画像の移動をやって今回は終わりにします。本当は前回やる予定でしたが、画像の移動はウェイトが伴うためこちらに持ってきた次第です。
今回は線形移動と初めと終わりが遅く、中程が速くなるような移動(easeinout)の2種類を用意しました。他にも必要な場合はEvaluateEasingFunctionに追加することによっていくらでも増やすことができます。
ImageManager.cs
// 画像を指定された座標に移動させる関数
public void MoveImage(string imageName, float targetX, float targetY, float animationTime, string easingType = "linear")
{
//ウェイトモードに移行
GameManager.Instance.gameUpdateManager.ToggleToWaitMode();
GameObject image = null;
//画像名と合う画像を検索
foreach (var tuple in _textToSpriteObject)
{
if (tuple.Item1 == imageName)
{
image = tuple.Item2;
break;
}
}
// 引数のimageがnullでないことを確認
if (image == null)
{
Debug.LogError("Error: Image GameObject is null.");
GameManager.Instance.gameUpdateManager.ToggleToNovelMode();
return;
}
StartCoroutine(MoveImageCoroutine(image, new Vector2(targetX, targetY), animationTime,easingType));
}
// コルーチン: 画像を指定された座標に指定された時間で移動させる
private IEnumerator MoveImageCoroutine(GameObject image, Vector2 targetPosition, float animationTime, string easingType)
{
// 画像のRectTransformを取得
RectTransform rectTransform = image.GetComponent<RectTransform>();
// 移動開始時の位置を記録
Vector2 initialPosition = rectTransform.anchoredPosition;
// 経過時間
float elapsedTime = 0.0f;
// アニメーションの時間内に移動
while (elapsedTime < animationTime)
{
// 正規化された経過時間を計算(0から1)
float t = elapsedTime / animationTime;
// 指定されたイージングタイプに基づいてイージング関数を適用
float easeT = EvaluateEasingFunction(t, easingType);
// 移動量を計算
Vector2 movement = Vector2.Lerp(initialPosition, targetPosition, easeT) - rectTransform.anchoredPosition;
// 移動量を適用し、アニメーションを実行
rectTransform.anchoredPosition += movement;
// 経過時間を更新
elapsedTime += Time.deltaTime;
// 次のフレームまで待機
yield return null;
}
// アニメーション終了時に位置を調整し、目標位置に配置する
rectTransform.anchoredPosition = targetPosition;
//次の行へ進む
GameManager.Instance.mainTextController.GoToTheNextLine();
}
// 指定されたイージングタイプに応じてイージング関数を評価
private float EvaluateEasingFunction(float t, string easingType)
{
switch (easingType.ToLower())
{
// 線形イージング
case "linear":
return t;
// EaseInOut イージング
case "easeinout":
return EaseInOut(t);
// サポートされていない場合は線形イージングを使用
default:
Debug.LogWarning("Unsupported easing type. Using linear easing.");
return t;
}
}
// EaseInOut イージング関数
private float EaseInOut(float t)
{
return t < 0.5f ? 2 * t * t : -1 + (4 - 2 * t) * t;
}
UserScriptManager.cs
// ステートメントを実行するメソッド
public void ExecuteStatement(string sentence)
{
string[] words = sentence.Split(','); // 文章を単語に分割する
float _fadetime = 1f;
// 単語によって処理を分岐する
switch (words[1].ToLower())
{
case "putimage":
int layerOrder = ConvertToInt(words[4], 10000);
int img_x = ConvertToInt(words[5]);
int img_y = ConvertToInt(words[6]);
int scale_percent = ConvertToInt(words[7],100);
GameManager.Instance.imageManager.PutImage(words[2], words[3],layerOrder,img_x,img_y,scale_percent); // 画像を表示する
GameManager.Instance.mainTextController.GoToTheNextLine();
break;
case "removeimage":
GameManager.Instance.imageManager.RemoveImage(words[2]); // 画像を削除する
GameManager.Instance.mainTextController.GoToTheNextLine();
break;
case "screenfadeout":
_fadetime = ConvertToFloat(words[2],3f) ;
GameManager.Instance.ScreenFadeController.ExecuteScreenFadeOut(_fadetime); // フェードアウト
break;
case "screenfadein":
_fadetime = ConvertToFloat(words[2], 3f);
GameManager.Instance.ScreenFadeController.ExecuteScreenFadeIn(_fadetime); //フェードイン
break;
case "wait":
float waitTime = ConvertToFloat(words[2], 1f);
GameManager.Instance.gameUpdateManager.ToggleToWaitMode();
StartCoroutine(WaitCoroutine(waitTime));
break;
case "moveimage":
float targetX = ConvertToFloat(words[3]);
float targetY = ConvertToFloat(words[4]);
float animetionTime = ConvertToFloat(words[5],1f);
GameManager.Instance.imageManager.MoveImage(words[2], targetX, targetY, animetionTime, words[6]); //画像を移動する
break;
default:
break;
}
}
ここではやすひめの画像を移動させることにします。
このようにやすひめが右に移動していれば成功です!
終わりに
ということで今回はノベルゲーム開発の第4回ということでウェイトやそれに関連したフェードアウト・フェードインなどができました!
それではまた!