ゴマちゃんフロンティア

アザラシが大好きなエンジニアの開発日記です

【開発日記】キャラクターのカラーチェンジ機能の実装

time 2018/06/28

というわけで、今回は「アザラシを守るゲーム」に新しい機能を追加するお話です。
所謂「カラーチェンジ」が欲しかったので、タイトル画面でいくつかのパターンから切り替えられるようにしてみます。割とさくっと実装できるかと思いきや、かなり苦戦してしまったので、備忘も兼ねて紹介します。

マネージャクラスの紹介

タイトル画面の動作を管理するTitleSceneManagerというクラスが既にあるので、そこに処理を追加していきます。Unity的にはシーン上にある空オブジェクトにTitleSceneManagerを付けています。このマネージャクラスをベースとしてあれこれするので、先にソースコードを記載します。

using System;
using System.Linq;
using UnityEngine;
using UnityEngine.SceneManagement;
using UnityEngine.UI;

/// <summary>
/// タイトル画面のマネージャクラス
/// </summary>
public class TitleSceneManager : MonoBehaviour
{
    // 現在のタイトル画面のモード
    protected TitleSceneMode currentMode;

    // カメラ位置
    public Vector3 defaultCameraPosition;
    public Vector3 shilrissCameraPosition;
    public Vector3 gomaCameraPosition;

    // トップ画面UI系の親オブジェクト
    public GameObject topUIParent;

    // カラーチェンジ系UIの親オブジェクト
    public GameObject colorChangeUIParent;

    // シルリスのカラーパターンUIの親オブジェクト
    public GameObject shilrissColorPatternUIParent;

    // アザラシのカラーパターンUIの親オブジェクト
    public GameObject gomaColorPatternUIParent;

    // 現在のカラー変更対象のキャラクター
    public ColorChangeCharacter currentColorChangeCharacter;

    // カラー変更対象のキャラクター名表示用ラベル
    public Text colorChangeCharacterLabel;

    // 設定系UIの親オブジェクト
    public GameObject settingUIParent;

    // カラーマネージャ
    protected ColorManager colorManager;

    void Start()
    {
        colorManager = GameObject.FindObjectOfType<ColorManager>();

        // カメラ位置の設定
        defaultCameraPosition = transform.Find("DefaultPosition").position;
        shilrissCameraPosition = transform.Find("ShilrissPosition").position;
        gomaCameraPosition = transform.Find("GomaPosition").position;

        // ロード時はトップに設定
        currentMode = TitleSceneMode.Top;

        topUIParent.SetActive(true);
        colorChangeUIParent.SetActive(false);
        settingUIParent.SetActive(true);

        // キャラクターにカラー設定を反映
        OnLoadSetCharacterColor();

        // ロード時のカラー選択対象の初期値設定
        currentColorChangeCharacter = ColorChangeCharacter.Shilriss;
        colorChangeCharacterLabel.text = currentColorChangeCharacter.DisplayName();
        SwitchActiveColorPattern(currentColorChangeCharacter, false);
    }

    void Update()
    {
        colorChangeCharacterLabel.text = currentColorChangeCharacter.DisplayName();
    }

    /// <summary>
    /// 画面ロード時にキャラクターの色情報を反映する
    /// </summary>
    protected void OnLoadSetCharacterColor()
    {
        colorManager.SetShilrissCurrentColor();
        colorManager.SetGomaCurrentColor();
    }

    /// <summary>
    /// シルリスのカラーパターンを設定する
    /// </summary>
    /// <param name="colorPattern"></param>
    protected void SetShilrissColor(ShilrissColorPattern colorPattern)
    {
        ColorManager.currentShilrissColor = colorPattern;
        colorManager.SetShilrissColor(colorPattern);
    }

    /// <summary>
    /// アザラシのカラーパターンを設定する
    /// </summary>
    /// <param name="colorPattern"></param>
    protected void SetGomaColor(GomaColorPattern colorPattern)
    {
        ColorManager.currentGomaColor = colorPattern;
        colorManager.SetGomaColor(colorPattern);
    }

    /// <summary>
    /// キャラクターカラーパターンUIの有効/無効を切り替える
    /// </summary>
    /// <param name="colorCharacter"></param>
    protected void SwitchActiveColorPattern(ColorChangeCharacter colorCharacter, bool moveCamera = true)
    {
        Vector3 cameraPosition = defaultCameraPosition;

        switch (colorCharacter) {
            case ColorChangeCharacter.Shilriss:
                shilrissColorPatternUIParent.SetActive(true);
                gomaColorPatternUIParent.SetActive(false);
                cameraPosition = shilrissCameraPosition;
                break;
            case ColorChangeCharacter.Goma:
                shilrissColorPatternUIParent.SetActive(false);
                gomaColorPatternUIParent.SetActive(true);
                cameraPosition = gomaCameraPosition;
                break;
        }

        if (moveCamera) {
            MoveMainCameraPosition(cameraPosition);
        }
    }

    /// <summary>
    /// メインカメラの位置を移動させる
    /// </summary>
    /// <param name="position"></param>
    protected void MoveMainCameraPosition(Vector3 position)
    {
        iTween.MoveTo(Camera.main.gameObject, iTween.Hash(
            "amount", 1f,
            "position", position
        ));
    }

    #region UIイベント系関数
    /// <summary>
    /// カラーチェンジボタン押下時のイベント
    /// </summary>
    public void OnClickColorChange()
    {
        topUIParent.SetActive(false);
        colorChangeUIParent.SetActive(true);
        settingUIParent.SetActive(false);

        currentMode = TitleSceneMode.ColorChange;

        SwitchActiveColorPattern(currentColorChangeCharacter);
    }

    /// <summary>
    /// カラーチェンジキャラクター左送りボタン押下時のイベント
    /// </summary>
    public void OnClickColorChangeLeft()
    {
        currentColorChangeCharacter = currentColorChangeCharacter.GetPrev();
        SwitchActiveColorPattern(currentColorChangeCharacter);
    }

    /// <summary>
    /// カラーチェンジキャラクター右送りボタン押下時のイベント
    /// </summary>
    public void OnClickColorChangeRight()
    {
        currentColorChangeCharacter = currentColorChangeCharacter.GetNext();
        SwitchActiveColorPattern(currentColorChangeCharacter);
    }

    /// <summary>
    /// シルリスのカラーボタン押下時のイベント
    /// </summary>
    public void OnClickShilrissColor(int patternIndex)
    {
        SetShilrissColor((ShilrissColorPattern)patternIndex);
    }

    /// <summary>
    /// アザラシのカラーボタン押下時のイベント
    /// </summary>
    public void OnClickGomaColor(int patternIndex)
    {
        SetGomaColor((GomaColorPattern)patternIndex);
    }

    /// <summary>
    /// カラーチェンジ確定ボタン押下時のイベント
    /// </summary>
    public void OnClickColorChangeApply()
    {
        topUIParent.SetActive(true);
        colorChangeUIParent.SetActive(false);
        settingUIParent.SetActive(true);

        MoveMainCameraPosition(defaultCameraPosition);

        currentMode = TitleSceneMode.Top;
    }
}

public enum TitleSceneMode
{
    Top = 1,
    ColorChange = 2
}

これ以外にも音量設定やスコア・バージョン表示を行っていますが、長くなるため省略しています。各関数の意味やColorManagerクラスについては後ほど解説します。

カラーチェンジ用メニューの追加

まずは現在のタイトル画面にカラーチェンジ用のメニューボタンを追加します。ただし今の画面的には音量設定のUIが邪魔です。行く行くは設定系のボタンも追加したいのですが、今回は右側に寄せておきます。

カラーチェンジモード中は関係するUI以外を非表示にします。そのために「トップ画面系」「オプション系」「カラーチェンジ系」に分類し、空オブジェクトの子オブジェクトに設定することでまとめておきます。

親オブジェクトの参照はTitleSceneManagertopUIParentcolorChangeUIParentsettingUIParentにUnity上からそれぞれ割り当てておきましょう。各イベントに応じてGameObject.SetActive()で有効・無効を切り替えます。

カラーチェンジモードにしたときはこんな感じです。左側を空けているのは、後述するカメラ移動時にキャラクターが見えやすくなるようにするためです。

キャラクター名左右の矢印やカラー一覧はすべてButtonを使用しています。このButtonにイベントとしてTitleSceneManagerの各関数を割り当てています。

また「タイトル画面でどのようなUI表示パターンがあるか」と「現在の表示パターンは何か」をプログラム上で把握しておくと、今後機能を拡張する上でやりやすそうだったので、列挙型で定義しておきました。クラス下部のTitleSceneModeがそれに該当します。
今回は使っていませんが、例えば「カラーチェンジ中にESCキーでキャンセル」等の処理を追加する場合に役立つかと思います。

カラー変更の実装

カラーチェンジ用メニューを作ったところで、実際のカラーチェンジ処理を考えていきます。

カラー変更対象の選択

今回カラー変更の対象とするのは、味方側であるリスとアザラシです。まずこの2匹のどちらのカラーを変えるのかを選択させる必要があります。なので「カラーチェンジで選択できるキャラクター」を列挙型で定義しておきます。

public enum ColorChangeCharacter
{
    Shilriss = 1,
    Goma = 2
}

合わせて列挙型のヘルパークラス「ColorChangeCharacterHelper」を作ります。列挙型の拡張メソッドとなるように引数を設定するのがポイントで、冗長な記述を少なくすることができます。

using System;

/// <summary>
/// ColorChangeCharacter列挙型のヘルパークラス
/// </summary>
public static class ColorChangeCharacterHelper
{
    /// <summary>
    /// 画面上に表示する名称を取得する
    /// </summary>
    /// <param name="colorCharacter"></param>
    /// <returns></returns>
    public static string DisplayName(this ColorChangeCharacter colorCharacter)
    {
        string[] names = { "Shilriss", "Goma" };
        return names[(int)colorCharacter - 1];
    }

    /// <summary>
    /// 列挙型の次の要素を取得する
    /// </summary>
    /// <param name="colorCharacter"></param>
    /// <returns></returns>
    public static ColorChangeCharacter GetPrev(this ColorChangeCharacter colorCharacter)
    {
        // 1つ前の要素が定義されているか確認
        if (Enum.IsDefined(typeof(ColorChangeCharacter), colorCharacter - 1)) {
            return colorCharacter - 1;
        }

        // ない場合は末尾の要素を返す
        var values = Enum.GetValues(typeof(ColorChangeCharacter));
        return (ColorChangeCharacter)values.GetValue(values.Length - 1);
    }

    /// <summary>
    /// 列挙型の前の要素を取得する
    /// </summary>
    /// <param name="colorCharacter"></param>
    /// <returns></returns>
    public static ColorChangeCharacter GetNext(this ColorChangeCharacter colorCharacter)
    {
        // 次の要素が定義されているか確認
        if (Enum.IsDefined(typeof(ColorChangeCharacter), colorCharacter + 1)) {
            return colorCharacter + 1;
        }

        // ない場合は先頭の要素を返す
        var values = Enum.GetValues(typeof(ColorChangeCharacter));
        return (ColorChangeCharacter)values.GetValue(0);
    }
}

Buttonコンポーネントのインスペクターから左ボタン右ボタンそれぞれにイベントを設定します。イベント実行時にHelperGetPrev()GetNext()を呼び出すことで、現在の前・次の列挙型を取得できるようにします。同時に選択中のキャラクターとカラーパターンの切り替えを行います。
このあたりのお話はOnClickColorChangeLeft()OnClickColorChangeRight()をご参照ください。

カラーチェンジ時のカメラワーク

SwitchActiveColorPattern()実行時にメインカメラの移動を行い、キャラクターが見えやすいようにします。カメラ移動処理はiTweenでさくっと書いてしまいます。
移動するカメラ位置の3つはVector3型でフィールドに定義されており、各地点に空オブジェクトを配置して移動先として使用します。そのままUnity上から参照を設定しても良いのですが、マネージャ用オブジェクトの子オブジェクトにしてTransform.Find()を使うことも取得することができます。

今回はStart()内の「カメラ位置の設定」というコメントが書かれているあたりでやっています。もちろんUnity上から設定しても問題ないので、お好みの方法で良いと思います。

ちなみに位置が重要な空オブジェクトを扱う場合、デバッグ用にオブジェクト位置を可視化するクラスを作ると便利です。Sceneビューからクリックで選択できるようになるので微調整がやりやすくなります。
過去の記事で紹介しているので、興味があればご参照ください。

【Unity】キャラクターをスクリプトで自動的に移動させる方法

キャラクター毎のカラーパターンの定義

キャラクター毎に選択可能パターンをボタンとして配置し、ボタンの色はそのパターンのメインとなる1色を表示するようにしました。
スクリプト上では「切り替え可能なパターン」と「現在選択しているパターン」を設定できるようにパラメータを追加します。またクリックされた際のイベントをTitleSceneManagerに実装します。
イベント関数は引数でint型を受け取るようにすることで、押下されたカラーボタンに応じたカラーパターンを設定することができます。

次にカラーパターンを管理するオブジェクトとして「ColorManager」クラスを作成します。リスとアザラシのカラーパターン用マテリアルの保持や、カラーパターンの適用を行えるようにします。

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

/// <summary>
/// キャラクターのカラーパターンの管理を行うマネージャクラス
/// </summary>
public class ColorManager : MonoBehaviour
{
    // 現在のシルリスのカラーパターン
    public static ShilrissColorPattern currentShilrissColor;

    // シルリスの変更対象のRenderer
    protected Renderer shilrissCloth;
    protected Renderer shilrissStole;
    protected Renderer shilrissHood;
    protected Renderer shilrissRibbon;

    // シルリスのカラー定義
    [SerializeField]
    protected List<ShilrissColor> shilrissColors;

    // 現在のアザラシのカラーパターン
    public static GomaColorPattern currentGomaColor;

    // アザラシの変更対象のRenderer
    protected Renderer gomaBody;

    // アザラシのカラー定義
    [SerializeField]
    protected List<GomaColor> gomaColors;

    // 変更対象オブジェクトの名前
    private const string RISS_CLOTH_NAME = "shilriss_cloth";
    private const string RISS_STOLE_NAME = "shilriss_stole";
    private const string RISS_HOOD_NAME = "shilriss_hood";
    private const string RISS_RIBBON_NAME = "shilriss_ribon";
    private const string GOMA_BODY_NAME = "azarashi01_body";

    void Start()
    {
        shilrissCloth = GameObject.Find(RISS_CLOTH_NAME).GetComponent<Renderer>();
        shilrissStole = GameObject.Find(RISS_STOLE_NAME).GetComponent<Renderer>();
        shilrissHood = GameObject.Find(RISS_HOOD_NAME).GetComponent<Renderer>();
        shilrissRibbon = GameObject.Find(RISS_RIBBON_NAME).GetComponent<Renderer>();

        gomaBody = GameObject.Find(GOMA_BODY_NAME).GetComponent<Renderer>();

        // パターンが設定されていない場合はデフォルト値を設定
        if (!Enum.IsDefined(typeof(ShilrissColorPattern), currentShilrissColor)) {
            currentShilrissColor = ShilrissColorPattern.Pattern1;
        }

        if (!Enum.IsDefined(typeof(GomaColorPattern), currentGomaColor)) {
            currentGomaColor = GomaColorPattern.Pattern1;
        }
    }

    /// <summary>
    /// 現在のシーンのシルリスにカラーパターンを適用する
    /// </summary>
    /// <param name="colorPattern"></param>
    public void SetShilrissColor(ShilrissColorPattern colorPattern)
    {
        var colors = GetShilrissColor(colorPattern);

        // Cloth
        var clothMaterials = shilrissCloth.materials;
        clothMaterials[0] = colors.subColor;
        clothMaterials[1] = colors.mainColor;
        shilrissCloth.materials = clothMaterials;

        // Stole
        var stoleMaterials = shilrissStole.materials;
        stoleMaterials[0] = colors.subColor;
        stoleMaterials[1] = colors.stoleColor;
        shilrissStole.materials = stoleMaterials;

        // Hood
        var hoodMaterials = shilrissHood.materials;
        hoodMaterials[0] = colors.subColor;
        hoodMaterials[1] = colors.mainColor;
        shilrissHood.materials = hoodMaterials;

        // Ribbon
        shilrissRibbon.material = colors.ribbonColor;
    }

    /// <summary>
    /// 現在のカラーパターンをシルリスに適用する
    /// </summary>
    public void SetShilrissCurrentColor()
    {
        SetShilrissColor(currentShilrissColor);
    }

    /// <summary>
    /// シルリスのカラーパターンを取得する
    /// </summary>
    /// <param name="colorPattern"></param>
    /// <returns></returns>
    public ShilrissColor GetShilrissColor(ShilrissColorPattern colorPattern)
    {
        return shilrissColors[(int)colorPattern - 1];
    }

    /// <summary>
    /// 現在のシーンのアザラシにカラーパターンを適用する
    /// </summary>
    /// <param name="colorPattern"></param>
    public void SetGomaColor(GomaColorPattern colorPattern)
    {
        var colors = GetGomaColor(colorPattern);

        // Body
        var bodyMaterials = gomaBody.materials;
        bodyMaterials[0] = colors.mainColor;
        gomaBody.materials = bodyMaterials;
    }

    /// <summary>
    /// 現在のカラーパターンをアザラシに適用する
    /// </summary>
    public void SetGomaCurrentColor()
    {
        SetGomaColor(currentGomaColor);
    }

    /// <summary>
    /// アザラシのカラーパターンを取得する
    /// </summary>
    /// <param name="colorPattern"></param>
    /// <returns></returns>
    public GomaColor GetGomaColor(GomaColorPattern colorPattern)
    {
        return gomaColors[(int)colorPattern - 1];
    }
}

リスとアザラシでは扱うマテリアルの数が違うので、それぞれのデータ構造をクラスで作成します。

using System;
using UnityEngine;

[Serializable]
public class ShilrissColor
{
    public Material mainColor;
    public Material subColor;
    public Material stoleColor;
    public Material ribbonColor;
}
using System;
using UnityEngine;

[Serializable]
public class GomaColor
{
    public Material mainColor;
}

キモはSerializable属性で、これを設定しないとインスペクターに表示されません。このクラスをList型で定義すれば、インスペクター上には以下のように表示されます。後はカラーパターンのインデックスに合わせてマテリアルを設定していきましょう。

ポイントその2はColorManagerの「現在選択されているカラーパターン」把握用変数をstaticで宣言していることです。ColorManager自体はStart()時に実行中のシーンにいるリス&アザラシのRendererを取ってくるのでシングルトンにできません。といってもこのままシーンを跨ぐと設定値が消えてしまいます。
シーンを跨いで値を保持する場合、静的フィールドに持つかDontDestroyOnLoad()でシーン遷移時に消えないオブジェクトに持たせるかですが、他に管理したいパラメータがないのであれば静的に持ってしまったほうが手っ取り早いでしょう。
ゲーム画面遷移後はそのシーン内のColorManagerの参照を取得し、Set~CurrentColor()を呼び出すことでカラーパターンを適用できるようにしました。

カラー変更の反映

キャラクターの色を動的に変える方法でパッと思いつくのは以下の2つです。

  1. Rendererの参照するマテリアルを入れ替える
  2. Rendererが参照しているマテリアルのプロパティを変える

最初は2の方法で行こうと思いましたが、実行時に変更したカラーが実行後も維持されてしまうのがネックです。あらかじめマテリアルを用意しておいたほうが、今後マテリアルのテクスチャやシェーダを切り替える場合にも対応できると感じたので、1の方法で行くことにしました。
変更先のマテリアルはカラーパターンとして用意されているため、ColorManagerStart()で取得しておいたRenderermaterialsを入れ替えます。このあたりの処理はSetShilrissColor()SetGomaColor()で行っています。

マテリアルが1つだけのRendererはそのまま突っ込めばOKですが、複数ある場合はちょっと面倒です。「materialsをいったん全て取得し代入し直す」のがポイントで、Renderer.materials[1]のように要素に直接代入しても適用されません。配列として取ってから必要な要素のみ入れ替えて入れ直します。
入れ替えるマテリアルはモデルの作り方によって異なるので、使用するモデルに合わせて書き換えてください。リスとアザラシで関数を分けたのもそのためです。

実行時の動作

これまで紹介したことを組み合わせると、以下のような挙動が実現できます。

概ね自分の想定通りの挙動ができました。仕組みさえ作ってしまえばカラーパターンを足していくだけです。

用意したカラーパターンの紹介

リス

体色を変えるのはアレなので、服とリボンの色を何パターンか作ってみました。

色の組み合わせはかなり適当です。今見ると黒だけやたら浮いていますね…。
作っている人のセンスがかなり残念なので、「こんな組み合わせがいい!」というアドバイスがあればご指摘いただけると助かります!

アザラシ

こちらは服やアクセサリが何もないので体色を変えるしかなく、あまりバリエーションが用意できませんでした。ただ前々から「灰色」が欲しかったので、そのパターンを作っておきました。

パターンといっても本体色のマテリアルは1つなので1色変わっているだけです。将来的には「ゴマ模様」とか欲しいところですね。

あとがき

そんなわけで、キャラクターのカラーチェンジを実装してみました。
今回作った機能は次回バージョンアップ時に導入する予定です。実際はどこかに保存しておかないと次回プレイ時に消えてしまうので、そのあたりを整備してからになるかと思います。

down

コメントする