This document written: 2014-05-31 .. 2022-07-09

『Android ゲームプログラミング A to Z』フレームワークの実装作業(後編)

メインループ(ゲームエンジン)

本家(BAG)の実装は AndroidFastRenderView となっている。この部分については、プラットフォーム毎に大きく異なるものなので、インターフェースは用意されておらず、Game クラスの補助クラスとしての位置付けであり、ゲーム開発者は意識する必要のない部分となる。

よって、僕の側で何ら独自に工夫する余地は基本的にはない(コードの掲載は割愛するので BAG を参照のこと)。プラットフォーム毎に実装を分ける必要性のなさから、名前だけ、FastRenderView に昇格させた形となる。

技術的には Android API の SurfaceView を使ったものであり、Java 標準 API の Thread によって生成した別スレッドで無限ループを回す形となっている。解説は 実装に使う Android API 群(第 4 章)中編で紹介した通りであり、そこのサンプルプログラムの内部クラスとして定義した FastRenderView ともほとんど同じである。

ゲームエンジン本体(ゲームエンジン)

BAG はインターフェースが Game、実装が AndroidGameとなっている。

この実装は、ここまでで部品として実装した各種ドライバー的オブジェクトを集成する存在である。またそれだけでなく、Android アプリのメインプログラムである Activity として実装するので、onCreate / onResume / onPause という Activity のライフサイクルの中に、プログラムを組み込むことにもなる。例えば、スクリーンのフルスクリーン化などといった作業はこのクラスの onCreate の中で行うことになる。

ここでも、インターフェースを省いて直接実装してしまう作業が基本になるので、implements Game を除去して AndroidGames を Game クラスそのものとして扱う形になる。しかし、フルスクリーン化処理、画面ロック、ディスプレイのサイズの算出方法などは、Android API の変遷のため、BAG とはかなり異なっている。

また、メインループが update()present() に分けてあったのを libGDX 同様、render() メソッド一つにしたため、テキストのフレームワークとは相違している。


abstract class Game : Activity() {

    // default 16:9
    protected var horizontalSize = 640
    protected var verticalSize = 360

    protected lateinit var fastRenderView: FastRenderView
    lateinit var touchHandler: TouchHandler
        protected set

    lateinit var graphics: Graphics
        private set
    lateinit var fileIO: FileIO
        private set
    lateinit var audio: Audio
        private set
    lateinit var currentScreen: Screen
        private set

    // フレームワークの利用者(プログラマー)が設定するアプリの初期スクリーンオブジェクト
    abstract val startScreen: Screen

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        // WakeLock に対する代替手法
        window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)

    		// グラフィックス関係のセットアップ

        val isLandscape =
            (resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE)

        val framebufferWidth = if (isLandscape) horizontalSize else verticalSize
        val framebufferHeight = if (isLandscape) verticalSize else horizontalSize
        val framebuffer = Bitmap.createBitmap(
            framebufferWidth, framebufferHeight, Bitmap.Config.RGB_565
        )
 
        val displayWidth = AppWindowUtil.getScreenWidth(this, true)
        val displayHeight = AppWindowUtil.getScreenHeight(this, true)
        // タッチ座標をゲームの論理座標に変換するための縮尺
        val scaleX = framebufferWidth.toFloat() / displayWidth
        val scaleY = framebufferHeight.toFloat() / displayHeight

        fastRenderView = FastRenderView(this)
        setContentView(fastRenderView)
        fastRenderView.setup(this, framebuffer)
        graphics = Graphics(assets, framebuffer)

        // マルチタッチ関係のセットアップ
        touchHandler = TouchHandler(fastRenderView, scaleX, scaleY)

        // ファイル入出力ドライバーの初期化
        fileIO = FileIO(this)
        // オーディオドライバーの初期化
        audio = Audio(this)

        currentScreen = startScreen
    }

    override fun onWindowFocusChanged(hasFocus: Boolean) {
        super.onWindowFocusChanged(hasFocus)

        if (hasFocus) {
            val decorView = window.decorView

            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
                val windowInsetsController = decorView.windowInsetsController
                windowInsetsController!!.systemBarsBehavior =
                    WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
                windowInsetsController.hide(
                    WindowInsets.Type.statusBars() // status bar の on/off
                            or WindowInsets.Type.navigationBars() // navigation bar の on/off
                )

                // status/navigation bar の on/off に依らないレイアウティング
                window.setDecorFitsSystemWindows(false)
            } else {
                decorView.systemUiVisibility = (
                        View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
                                or View.SYSTEM_UI_FLAG_FULLSCREEN // status bar の on/off
                                or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
                        )
            }
        }
    }

    override fun onResume() {
        super.onResume()

        // onResume では Screen が先、RenderView が後
        currentScreen.resume()
        fastRenderView.resume()
    }

    override fun onPause() {
        // onPause では RenderView が先、Screen が後
        fastRenderView.pause()

        if (isFinishing) {
            currentScreen.dispose()
        } else {
            currentScreen.pause()
        }

        super.onPause()
    }

    fun setScreen(screen: Screen) {
        currentScreen.pause()
        currentScreen.dispose()
        screen.resume()
        screen.render(0f)
        currentScreen = screen
    }
}

この Game クラスはあくまでも抽象(abstract)クラスである。このフレームワークを利用して個々のゲームを開発する時に、この Game クラスを継承した GameActivity クラスを実装して、StartScreen となる Screen オブジェクト名を指定する作業だけは必要である。

また、ADT (Eclipse) ではいくつかの警告が表示されるかもしれない。複数バージョンの API 用に if ブロックで対応してあるのが原因である。気になるのであれば、API バージョンに関する警告を無視するようにアノテーションを付せばいい。

論理スクリーン(ゲームエンジン)

BAG のインターフェースは Screen となっている。

このインターフェースは、フレームワークを利用するゲーム開発者が、ゲームのプログラムを記述するための雛形であり、プラットフォームに依存した実装とは無縁なものなので、フレームワーク側としてはインターフェースのみとなる。

ほとんど変更する余地はないが、update()present() に分けてあったのを render() に統一したため、前 2 者の定義を削除して、代わりに次の render の定義を追加する作業が必要となる:


abstract class Screen(protected val game: Game) {
    abstract fun dispose()
    abstract fun resume()
    abstract fun pause()
    abstract fun render(deltaTime: Float)
}

最後に

以上で、自家版の Android 専用ゲーム開発フレームワークは完成した。

フレームワークの利用にあたっては、Game クラスを継承した GameActivity を作成し、その中で開始画面となる Screen オブジェクトのクラス名を指定する。具体的には次のような感じである。開始画面のクラス名が StartScreen.kt ならば:


class GameActivity : Game() {
    override val startScreen: Screen
        get() = StartScreen(this)
}

あとは色々な Screen オブジェクトを用意して、それらの Screen を入れ替えたりしながら、ゲームプログラムが成立することになる。Screen オブジェクトの実装内容は、ゲーム開発者次第である。


読解『A to Z』