(Unity3D)ダイナミックボタンアクション

あるゲームのUIを作っている間にボタンに関する問題に遭遇しました。選択されたエンティティによって、表示されているボタンが変わるようにしたいと思いました。

もちろん一つずつのエンティティのGameObjectを準備して場合によってSetActive()trueかfalseにしてもいいですが、スケーラビリティは?エンティティを追加しようとする毎に新しいGameObjectを準備しますか?そんな面倒なことは勘弁してほしいですよね。

ということでスケーラビリティのため、エンティティを選択する時にボタンが編集され、ちょっとしたアルゴリズムを書きました。 

こういうシステムが入っているUIを作る人がいて参考になればと思い、分かりやすいバージョンを準備して下記にそのケースを紹介します。 

想定ケース: 

1.エンティティによる、ボタンの数が異なる場合でもOK。 

2.ボタンの見た目を編集可能にする。 

3.ボタンのアクションがstringの引数を受け取れる。 

4.簡単にするため、ボタンの最大数は10にします。 

先ずはボタンの情報をまとめるクラスActivityButtonをつくります 

public class ActionButton {
    public UnityAction<string> theAction;
    public string argument;
    public Sprite sprite;
    public ActionButton(UnityAction<string> act, Sprite sprite, string arg=""){
        theAction = act;
        argument=arg;
        this.sprite = sprite;
    }
}

UIマネージャーにはButton、Image、GameObjectの配列を必要です。パブリックなGameObjectの配列を作ってシーンからボタンをドラッグして、Start()でButtonとImageの配列を作りました 

public class MyEpicUIManager : MonoBehaviour {
    Button[] actionBtns;
    Image[] actionImgs;
    public GameObject[] actionBtnObjs;

    void Start () { 
        // Action Panel Setup
        actionBtns = new Button[actionBtnObjs.Length];
        actionImgs = new Image[actionBtnObjs.Length];
        for(int i=0; i<actionBtnObjs.Length; i++){
            actionBtns[i]=actionBtnObjs[i].GetComponent<Button>();
            actionImgs[i]=actionBtnObjs[i].GetComponent<Image>();
        } 
    }
}

ボタンをゲットしたので、次はActionButtonのリストを取得しボタンを編集するメソッドを作りました。そしてボタンをクリアするメソッドも必要です。 

public void SetActions(List<ActionButton> btnActions){
    ClearActions();
    for(int i=0; i<btnActions.Count; i++){
        actionBtnObjs[i].SetActive(true); 
        UnityAction<string> action = btnActions[i].theAction;
        string arg = btnActions[i].argument;
        actionBtns[i].onClick.AddListener(()=>action(arg)); 
        actionImgs[i].sprite = btnActions[i].sprite; 
    } 
}

public void ClearActions(){
    for(int i = 0; i<actionBtnObjs.Length; i++){
        actionBtns[i].onClick.RemoveAllListeners();
        actionBtnObjs[i].SetActive(false);
    }
}

これでUIマネージャーが完了です。Start()でClearActionsを呼べば、ゲームを起動する際にボタンクリアできます 

エンティティにはそれそれに自分のActionButtonリストが必要です。UIマネージャーのSetActionsを呼べるようにする必要があります。そしてアクション自体も必要です。

public class MyEntity1 : MonoBehaviour {
 
    public MyEpicUIManager ui;
    List<ActionButton> uiActions = new List<ActionButton>();

    void Start(){
        uiActions.Add(new ActionButton(Action1,Resources.Load<Sprite>("Sprites/E1A1"))); 
        uiActions.Add(new ActionButton(Action2,Resources.Load<Sprite>("Sprites/E1A2"), "Test argument 1")); 
        uiActions.Add(new ActionButton(Action2,Resources.Load<Sprite>("Sprites/E1A2"), "Test argument 2")); 
        uiActions.Add(new ActionButton(Action3,Resources.Load<Sprite>("Sprites/E1A3"), "3"));
        uiActions.Add(new ActionButton(Action3,Resources.Load<Sprite>("Sprites/E1A3"), "5")); 
    }

    // An action that doesn't really use the string provided 
    public void Action1(string s =""){
        print("Entity 1 Action 1");
    }

    // An action that uses the string
    public void Action2(string s =""){
        print("Entity 1 Action 2 Argument: " + s);
    }

    // An action that changes the string to an int
    public void Action3(string s =""){
        string msg = "Entity 1 Action 3 ->"+s+" hashes: ";
        int n = 0;
        int.TryParse(s, out n);
        for(int i=0; i <n; i++) msg+="#";
        print(msg);
    }

    public void OnSelect(){
        ui.SetActions(uiActions);
    }
} 
public class MyEntity2 : MonoBehaviour {
 
    public MyEpicUIManager ui;
    List<ActionButton> uiActions = new List<ActionButton>();

    void Start(){
        uiActions.Add(new ActionButton(Action1,Resources.Load<Sprite>("Sprites/E2A1"))); 
        uiActions.Add(new ActionButton(Action2,Resources.Load<Sprite>("Sprites/E2A2"), "Test argument 1")); 
        uiActions.Add(new ActionButton(Action2,Resources.Load<Sprite>("Sprites/E2A2"), "Test argument 2")); 
        uiActions.Add(new ActionButton(Action2,Resources.Load<Sprite>("Sprites/E2A2"), "Test argument 3"));
        uiActions.Add(new ActionButton(Action3,Resources.Load<Sprite>("Sprites/E2A3"), "3"));
        uiActions.Add(new ActionButton(Action3,Resources.Load<Sprite>("Sprites/E2A3"), "5")); 
        uiActions.Add(new ActionButton(Action3,Resources.Load<Sprite>("Sprites/E2A3"), "7")); 
    }

    // An action that doesn't really use the string provided 
    public void Action1(string s =""){
        print("Entity 2 Action 1");
    }

    // An action that uses the string
    public void Action2(string s =""){
        print("Entity 2 Action 2 Argument: " + s);
    }

    // An action that changes the string to an int
    public void Action3(string s =""){
        string msg = "Entity 2 Action 3 ->"+s+" asteriscs: ";
        int n = 0;
        int.TryParse(s, out n);
        for(int i=0; i <n; i++) msg+="*";
        print(msg);
    }

    public void OnSelect(){
        ui.SetActions(uiActions);
    }
}

こちらはエンティティのサンプルです。OnSelectはキーボードかシーン上のボタンで試してみてください。このサンプルを動かすため “Assets/Resources/Sprites”に、正しい名前のスプライトを入れてください。そしてスプライトのImport SettingsのTexture Typeは “sprite (2D and UI)”にする必要があります。 

シーンに必要なものはCanvasと7つ以上のボタンです。UIマネージャーをシーンに追加して、インスペクターでボタンをドラッグします。エンティティもシーンに追加してインスペクターでUIマネージャーをドラッグしてください。 

それで完了です。短くて簡単な例となりますが、これでエンティティを追加ときにいちいち新しいパネルを準備する必要がなくなります。そしてアクションを追加したり削除する場合でも、リストから追加したり削除することで実現できます。