ゴマちゃんフロンティア

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

【開発日記】溜め攻撃をコンパクトに実装する方法を考えてみたお話

time 2018/12/30

というわけで、今回はUnityにおけるスクリプトの作成方法に関するお話です。
所謂「溜め攻撃」とか「チャージ攻撃」とかをアクションゲームでよく見掛けますよね。

私がUnityでそれを実装する場合、「ボタン押している間はカウントを加算し、離したタイミングでカウントが一定以上なら攻撃」という処理をC#でガリガリ書いています。
ただ「カウント用の変数」「溜め攻撃発動までのカウント値」を変数として持たなければならず、ボタン押下中のカウント処理も書かなければなりません。
そこで今回は使う側のクラスのなるべく汚さずに実装する方法を考えてみました!

溜め攻撃用クラスの作成

まずは溜め攻撃の制御と実行を行うためのクラスを作成します。
Update()メソッドを使うので、通常のUnityスクリプト通りMonoBehaviourを継承して作ります。

using System;
using UnityEngine;

/// <summary>
/// チャージ攻撃を制御するGameObject用コントローラクラス
/// </summary>
public class ChargeAttackController : MonoBehaviour
{
    /// <summary>
    /// チャージ攻撃発動に必要なカウント
    /// </summary>
    protected int invoke_require_count;

    /// <summary>
    /// 現在のチャージカウント
    /// </summary>
    protected int current_count = 0;

    /// <summary>
    /// チャージ攻撃の溜めと発動を行うボタン
    /// </summary>
    protected string input_button_name;

    /// <summary>
    /// チャージ攻撃発動時に実行するアクション
    /// </summary>
    protected Action chargeAction;

    void Update()
    {
        // ボタンを離した際に一定値以上カウントが溜まっていればアクション実行
        if (Input.GetButtonUp(input_button_name)) {
            if (invoke_require_count <= current_count) {
                current_count = 0;
                chargeAction();
            }
        }

        // ボタン押下中はカウント加算
        if (Input.GetButton(input_button_name)) {
            current_count++;
        } else {
            // ボタンを離した場合はリセット
            current_count = 0;
        }
    }

    /// <summary>
    /// 初期化処理
    /// </summary>
    /// <param name="invoke_count"></param>
    /// <param name="input_button_name"></param>
    /// <param name="chargeAction"></param>
    public void Init(int invoke_count, string input_button_name, Action chargeAction)
    {
        this.invoke_require_count = invoke_count;
        this.input_button_name = input_button_name;
        this.chargeAction = chargeAction;
    }

    /// <summary>
    /// 現在のチャージカウントの溜め具合を0~1の間で返す
    /// </summary>
    /// <returns></returns>
    public float PropChargeCount()
    {
        // 既に溜まっている場合は1を返す
        if (invoke_require_count <= current_count) {
            return 1f;
        }
        // float型にキャストして割合を計算
        return (float)current_count / (float)invoke_require_count;
    }
}

必要なパラメータとして「溜める時間(フレーム単位)」「溜めと発動に使用するボタン名」「発動時に実行するアクション」をフィールドに定義します。
それらのパラメータを設定するためのメソッドも作っておきます。

ポイントは初期化時にAction型でチャージ後に実行する処理を受け取っておくことです。
ここで生成元から匿名メソッドを受け取っておくことで、「ボタンを押してチャージ→離してアクション実行」の流れをこのクラスに任せることができます。「生成元のインスタンスを受け取って特定のメソッドを実行」でも出来なくはないですが、「特定の型に依存してしまうこと」と「生成元に溜め攻撃実行時の専用のメソッドが必要になること」を避けたかったのでこの形にしました。

溜め攻撃を制御するゲームオブジェクトの追加

溜め攻撃を行いたいクラスで前述のスクリプトを設定したゲームオブジェクトを生成します。
生成したゲームオブジェクトは念のため生成元となるオブジェクトの子として設定しておきましょう。

private void Start()
{
    // 溜め攻撃用オブジェクト追加
    GameObject chargeAttackControllerObject = new GameObject("ChargeAttackController");
    chargeAttackControllerObject.transform.SetParent(transform);

    ChargeAttackController chargeAttackController = chargeAttackControllerObject.AddComponent<ChargeAttackController>();
    chargeAttackController.Init(120, "Attack", () => {
        // 溜め攻撃の処理を記述
        animator.Play("ChargeAttack1");
    });
}

動的に生成してすぐに初期化用メソッドを呼ぶのがポイントです。
実際にChargeAttackControllerUpdate()が動き始めるのは生成された次のフレームからなので、その前に溜め攻撃実行用の匿名メソッドを渡してしまいます。

溜め段階に応じた処理の実装

ここまでで「一定時間以上押して離すと発動」は実装できました。
ただ実際のゲームを考えると、溜めている間何も見た目に変化がないのは寂しい上に分かりにくいです。「半分まで溜まったら小さめのエフェクト」「最大まで溜まったら大きめのエフェクト」とかで視覚的に溜め具合が分かるようにしたいですね。

ということで、先ほどの溜め攻撃制御クラスのUpdate()メソッド内を修正します。溜め攻撃発動と同様にAction型を受け取っておき、溜まり具合に応じて呼び出します。
各アクションは1回だけ実行するため、フィールドにフラグを用意して制御します。

/// <summary>
/// 50%チャージアクション実行制御用フラグ
/// </summary>
private bool half_charge_action_flg = false;

/// <summary>
/// チャージ完了時のアクション実行制御用フラグ
/// </summary>
private bool complete_charge_action_flg = false;

/// <summary>
/// 50%チャージ完了時に実行するアクション
/// </summary>
protected Action halfChargeAction;

/// <summary>
/// チャージ完了時に実行するアクション
/// </summary>
protected Action chargeCompleteAction;

void Update()
{
    // ボタンを離した際に一定値以上カウントが溜まっていればアクション実行
    if (Input.GetButtonUp(input_button_name)) {
        if (invoke_require_count <= current_count) {
            current_count = 0;
            half_charge_action_flg = false;
            complete_charge_action_flg = false;

            chargeAction();
        }
    }

    // ボタン押下中はカウント加算
    if (Input.GetButton(input_button_name)) {
        current_count++;
    } else {
        // ボタンを離した場合はリセット
        current_count = 0;
        half_charge_action_flg = false;
        complete_charge_action_flg = false;
    }

    // チャージ中のアクション実行制御
    if (Input.GetButton(input_button_name)) {
        // 50%チャージ時のアクション実行
        if (halfChargeAction != null) {
            if (0.5f <= PropChargeCount() && !half_charge_action_flg) {
                half_charge_action_flg = true;
                halfChargeAction();
            }
        }

        // チャージ完了時のアクション実行
        if (chargeCompleteAction != null) {
            if (1f <= PropChargeCount() && !complete_charge_action_flg) {
                complete_charge_action_flg = true;
                chargeCompleteAction();
            }
        }
    }
}

/// <summary>
/// 50%チャージ時のアクションを設定
/// </summary>
/// <param name="halfChargeAction"></param>
public void SetHalfChargeAction(Action halfChargeAction)
{
    this.halfChargeAction = halfChargeAction;
}

/// <summary>
/// チャージ完了時のアクションを設定
/// </summary>
/// <param name="chargeCompleteAction"></param>
public void SetChargeCompleteAction(Action chargeCompleteAction)
{
    this.chargeCompleteAction = chargeCompleteAction;
}

次に使用する側のクラスで呼び出すメソッドを設定します。こちらも溜め攻撃時の設定と同様に匿名メソッドを渡します。
チャージ時のエフェクトはクラスのフィールドに定義し、あらかじめインスペクターから設定しておきます。

/// <summary>
/// 半分チャージ時のエフェクト
/// </summary>
[SerializeField]
protected GameObject halfChargeEffect;

// チャージ完了時のエフェクト
[SerializeField]
protected GameObject chargeCompleteEffect;

private void Start()
{
    // 溜め攻撃用オブジェクト追加
    GameObject chargeAttackControllerObject = new GameObject("ChargeAttackController");
    chargeAttackControllerObject.transform.SetParent(transform);

    ChargeAttackController chargeAttackController = chargeAttackControllerObject.AddComponent<ChargeAttackController>();
    chargeAttackController.Init(120, "Attack", () => {
        // 溜め攻撃の処理を記述
        animator.Play("ChargeAttack1");
    });

    // 溜め段階に応じたエフェクト生成アクションの設定
    chargeAttackController.SetHalfChargeAction(() =>  Instantiate(halfChargeEffect, transform));
    chargeAttackController.SetChargeCompleteAction(() =>  Instantiate(chargeCompleteEffect, transform.position, Quaternion.identity));
}

ここではエフェクトを生成していますが、ゲームに応じて他の処理を入れることもできます。
というかそのためのAction型ですね。

実際の動き

先日公開したゲーム「ばくれつうさぴょん」に実際に組み込んでみました。

まあ今回はロジック上の話なので動きだけを見ても実感は沸きませんが…中身的にはスッキリ書けています。
この形式なら今後キャラクターが増えたとしても安心ですね。

down

コメントする