【2022年】AndroidStduio + Kotlinでゲームの土台作ってみる

# 前置き

AndroidStudio＋KotlinでAndroidで動くゲームの土台作る（2D限定）

# iOS版も作りたいよ

UnityとかUnrealEngine4使えばいいと思うよ
あれならAndroidやiOSの他にも、理論上NintendoSwitchとかPS4とかに吐き出せるっぽいし
（Nintendoは、Nintendoに開発者登録して専用のUnityを落とす必要があった気がする）

iOSは開発者登録とか色々めんどくさいから
正直個人で楽しみながら作るにはおすすめできない

# 対象読者

Androidだけで軽くゲームっぽいのを作りたい人や
当たり判定とかを自前で実装してみたい人向け
ただゲーム作りたいだけならUnityとかUnrealEngine4使（略）

# ゲームとして成立するのに必要な条件

以下の４つを満たす必要がある

### ループ

ゲームはユーザーが止めるまで処理を実行し続ける必要がある

### 入力処理

ユーザーからの入力を受け取る必要がある

### 逐次処理

入力処理や状態変化に基づいて、各オブジェクトの情報を変更する処理

### 描画処理

逐次処理の情報を元にオブジェクトを描画する処理

# とりあえずプロジェクトを作る

「Empty Activity」でプロジェクトを作る

# View に描画を行う

最初に悪い例として、Viewクラスをそのまま使い土台を作ろうとしたが
理想通りに動かなかったため、割愛する

# SurfaceView に描画を行う

~~~ main_activity.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <SurfaceView
        android:id="@+id/surfaceView"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.5"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintVertical_bias="1.0" />
    
</androidx.constraintlayout.widget.ConstraintLayout>
~~~

SurfaceView を 画面全体を覆うように定義する

~~~ MainActivity.kt
package com.game.gameapplicaton

import android.content.Context
import android.graphics.*
import android.os.Bundle
import android.view.SurfaceHolder
import android.view.SurfaceView
import androidx.appcompat.app.AppCompatActivity
import kotlin.concurrent.thread

class MainActivity : AppCompatActivity()  {

    class CustomSurfaceView : SurfaceView , SurfaceHolder.Callback
    {
        private var xPos = 0.0f
        private var surfaceHolder: SurfaceHolder? = null
        var color: Int? = null
        private var canvas: Canvas? = null

        constructor(context: Context, surfaceView: SurfaceView) : super(context) {
            surfaceHolder = surfaceView.holder

            /// 背景を透過させ、一番上に表示
            surfaceHolder!!.setFormat(PixelFormat.TRANSPARENT)
            surfaceView.setZOrderOnTop(true)
            /// コールバック
            surfaceHolder!!.addCallback(this)

        }

        // surfaceViewが作られたとき
        override fun surfaceCreated(holder: SurfaceHolder) {

            val fps : Long = 30
            thread {
                while(true)
                {
                    xPos += 1.0f
                    draw()
                    Thread.sleep( 1000 / fps )
                }
            }.start()

        }

        override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) {

        }

        override fun surfaceDestroyed(holder: SurfaceHolder) {
        }


        private fun draw() {
            canvas = Canvas()

            /// ロックしてキャンバスを取得
            canvas = surfaceHolder!!.lockCanvas()

            //// キャンバスのクリア
            canvas!!.drawColor(0, PorterDuff.Mode.CLEAR)

            // とりあえず円を描く
            val paint = Paint()
            color = Color.BLACK
            canvas!!.drawCircle( 200.0f + xPos  , 200.0f , 30.0f , paint)

            /// ロックを解除
            surfaceHolder!!.unlockCanvasAndPost(canvas)
        }

    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        val surfaceView : SurfaceView = findViewById(R.id.surfaceView)
        /// CustomSurfaceViewのインスタンスを生成
        CustomSurfaceView(this, surfaceView)
    }
    
}
~~~

基本コピペでいいが、重要な部分だけ説明

~~~ MainActitiby.kt
// surfaceViewが作られたとき
        override fun surfaceCreated(holder: SurfaceHolder) {

            val fps : Long = 30
            thread {
                while(true)
                {
                    xPos += 1.0f
                    draw()
                    Thread.sleep( 1000 / fps )
                }
            }.start()

        }
~~~

while(true)により、ゲームとして成立する条件である「ループ」を行う
何も考えずにwhile(true)をすると、アプリそのものが固まってしまうため、
Threadでアプリそのものの動作と同時並行で実行することで、アプリのフリーズを回避する

処理としては、draw関数で、単純な円を描画しており、描画前に円のｘ座標を加算することで
毎ループ時に円が移動する

その後、(１秒 ÷ 30 ) 秒待つ事で、xPosが高速で加算されて一瞬で円が移動するのを防いでいる
一応、いろんなゲームの基準であるfps30を基準にしているが、
この待ち処理は簡略なものであり、完璧なものではない 

# ビルドしてみよう

円が右方向に移動するだけのActivityが起動するはずだ

# 今後の展望

この処理は、必要最小の実装しかしてないため、
次の記事でほんの少し実用を想定した土台へ近づいていく（予定）

# 参考

https://qiita.com/ymshun/items/a1447bdfcea8ef24d765

# おまけ

ゴリゴリの３Dでやりたい狂人は「GLSurfaceView」使うといいかもしれないね

https://www.scaredeer.com/2021/03/android-opengl-es-surfaceview-glsurfaceview.html