🤖

マインクラフトプラグイン開発のオブジェクト管理が難しい件

2024/01/28に公開

はじめに

マインクラフトのプラグイン開発で設計に躓いたため記録

なにで躓いているか

現在開発しているマインクラフトのプラグインはゲーム性を追加するプラグインなのですが、これのオブジェクト管理やパッケージ管理(この記事ではあまり触れません)が難しい。

作るオブジェクトの想定

プラグインの実装によりゲーム性追加を実装します。
作成するプラグインには次のようオブジェクトを用意することが考えられます。

  • プラグイン本体
    • コマンドに関するもの
    • ゲーム性の追加に関するもの(特にこの部分の管理が鬼門)
      • ゲームの設定を管理するクラス
      • UIを描画するクラス(BossBar、Scoreboard等)
      • ゲームのロジックを実行するクラス

実際の実行フロー

  1. 必要であれば設定コマンドで設定を行う
  2. スタートコマンドを実行してゲームをスタートする
  3. UIを表示させ、ゲームをスタートさせる
  4. ゲームが終了する

どこが難しいの

ゲームロジックとUI表示などが密結合になりがちだし、パッケージをどのように分ければいいかわからない

パッケージの管理

現在は次のように管理しています

  • Pluginクラス
  • command
    • ComandHandlerクラス
    • CommandListクラス
    • TabCompleteクラス
    • gamecommand
      • Startクラス
      • Stopクラス
      • Configクラス
      • GameCommandインターフェース
  • game
    • ui
      • BossBarクラス
      • Scoreboardクラス
    • logic
      • Logicクラス
      • LogicListenerクラス
        正直、変数の命名方法などを学習してもいまだにどのように管理すればよいかわかりません

どの辺が密結合になるのか

特にUIとLogicパッケージで密結合になりがちです。
基本的にUIはLogicクラスがグリップしているゲーム進行に合わせて表示を変えることが多いです。
しかし、例としてBossBarなどに残り時間を一定間隔で表示するメソッドなどを実装してしまうと、ゲームの進行を一部BossBarに委譲してしまうことになります。

class BossBar () : UI {

    // 依存ロジックインスタンス
    private lateinit var timeManager: TimeManager

    // 依存プラグインオブジェクト
    private var bossBar = Bukkit.createBossbar("init", BarColor.GREEN, BarStyle.SOLID)
    private lateinit var bossBarUpdater: BukkitRunnable

    // 残り時間管理
    private var remainingTime: Long = 0

    // 中略

    // TimeManagerから呼び出されることを前提としたメソッド
    fun execBossBar() {
        remainingTime = config.totalTime.toLong()
        bossbar.color = BarColor.BLUE
        updateBossBarProgress({ (reminaingTime.toDouble() / config.totalTime.toLong()) }, "残り時間")
    }

    fun updateBossBarProgress(calculateProgress: () -> Double, title: String) {
        bossBar,isVisible = true
        bossBarUpdater = object : BukkitRunnable() {
            if (remainingTime == 0L) {
                bossBar.isVisible = false
                this.cancel()
                // Logicクラスのメソッドを呼び出して、ロジックを進行させる
                timeManager.runNextLogic()
            } else {
                val progress = calculateProgress()
                bossBar.progress = progress
                val minutes = String.format("%02d", remainingTime / 60)
                val seconds = String.format("%02d", remainingTime % 60)
                bossBar.setTitle("$title $minutes:$seconds")
                remainingTime--
            }
        }

    }
}

このことから、BossBarは常にLogicのインスタンスを保持していなければならないし、LogicクラスもUIを変更するために、BossBarのインスタンスを保持しなければならないという関係性になってしまいます。

また、先ほど残り時間をBossBarに実行させていましたが、これをLogic側のクラスにプロパティとして実装していたとしてもBossBarクラスは引き続きLogic側のクラスを保持し、特定のプロパティなどを監視して自身のメソッドを実行する必要がありますし、Logic側のクラスはUIを更新する必要があるため、お互いにインスタンスを持ち合うような関係は避けられない状況です。

どのようにして解決すればよいか

わからん。
2024/01/30とりあえず終着点を見つけました。

今回の件では、プラグインが提供するオブジェクトを自身で管理しようとしていたことが問題だと分かりました。

プラグインのオブジェクトは当然ですがプラグイン側で保持します。
なので、あくまでプラグイン側のオブジェクトをラップして操作しやすくするためのトップレベル関数なり、静的クラスを用意し、値を保持せずに操作のみ行います。
実際のコード
こうすることで、ロジック側で相手のオブジェクトを保持せずに操作のみすることができます。

なぜ躓いたのか

今回のプラグイン作成において初めてKotlinを使用し、パラダイムや設計を学びながら作成しようとしました。

が、主にオブジェクト指向に関する設計やベストプラクティスを調べていくと、やれシングルトンは悪だのstaticおじさんなどの情報をインプットしてしまったため。シングルトンと静的クラスがごっちゃになってました。

シングルトンや静的クラスで問題視されているのは、自身に公開されたプロパティを持ち、グローバル変数のようにどこからでもアクセスされる環境であるパターンがほとんどです。

結果的にテストが組みにくかったり、依存関係が整理しづらい。問題が起きた時に追えないパターンがよく示されますが、操作のみ行うクラスに関しては言及されている例は少ないのかなと感じます。

先ほどの変更により、どこからでもUIなどの操作ができてしまうことにはなりますが、DBや、ファイル操作などと同様に、プラグイン側で整理されている以上、不可抗力なのかなと感じます。

Discussion