画面作成のためのKotlin入門~Kotlinの基本を学習する~

イントロダクション

Kotlinで画面の作製、GUIを作り、ゲームを作成しようと思っています。
KorGEというゲームエンジンをインストールして、サンプルプロジェクトを動かしたところで「基本がわからん!」となり、学習することにしました。

学習したいポイントは以下の通りです。

  1. 基本文法
  2. ライブラリなどのインポート方法
  3. GUIの作り方

まだまだ、基本の学習中でコードの解析が終わらないので引き続きこの記事は更新いたします。

問題1

Kotlinは開発環境にすごーく依存するため、ライブラリなどGradleでの設定が必要になるので。。。

解決策

サンプルプロジェクトをカスタムする方向でプログラムを学習&作成していくことにしました。

参考サイト

  1. korlibs.kogeパッケージのAPIドキュメント
  2. KorGEのサイト
  3. KorGEのドキュメントサイト

今回は、GUIアプリを作成したいので、サンプルに「2048」というゲームをカスタムします。
2048ゲームの画面

Kotlinの基本

まずは、サンプルコードを見てみます。結構長いです。。。
そのため、上記のリンク先にコードがあります。

基本を学習するにあたり、サンプルコードを見ながら「ここは、こーなっている。。。」という形で学習していきます。

import文について

サンプルコードの下のようなコードがこれに当たります。

import korlibs.event.*
import korlibs.korge.*
import korlibs.korge.animate.*
 ・
 ・
 ・

各ライブラリのクラスをインポートしています。Javaと同じです。
そして、自分で作成したクラスなどもインポートできます。

ちなみにJava言語での「static import」が使えるかについては「NO」通常のimport文でインポートできるようです。使えるようにできるということです。
stackoverfllow参照

変数の扱い

下のように定義できます。

// 定数
val x: Int = 5
// 変更可能な普通の変数
var y: Int = 5
// インクリメント
y += 1

省略した書き方

val x = 5

関数と変数の定義

val PI = 3.14
var x = 0

fun incrementX() {
    x += 1
}

varとvalの違い

サンプルコードには下のようなコードがあります。「var」と「val」、そして「fun」で始まる行があります。
結論から言うとJava言語でいうなら

  • 「val」は「final」修飾子つきの変数です。
    つまり定数です。クラスの場合は、インスタンスが変更されなければエラーになりません。
  • 「var」は通常の変数です。
    つまり、値の書き換えなど変更ができます。
  • 「fun」は関数を示す。
    fun numberFor(blockId: Int) = blocks[blockId]!!.number

    上の関数は、blocks配列のblockId番目のnumberを返します。

ちなみに、サンプルコードの一部を抜粋、読んで理解しているところです。

var cellSize: Float = 0f
var fieldSize: Float = 0f
var leftIndent: Float = 0f
var topIndent: Float = 0f
var font: BitmapFont by Delegates.notNull()

fun columnX(number: Int) = leftIndent + 10 + (cellSize + 10) * number
fun rowY(number: Int) = topIndent + 10 + (cellSize + 10) * number

var map = PositionMap()
val blocks = mutableMapOf<Int, Block>()
var history: History by Delegates.notNull()

fun numberFor(blockId: Int) = blocks[blockId]!!.number
fun deleteBlock(blockId: Int) = blocks.remove(blockId)!!.removeFromParent()

val score = ObservableProperty(0)
val best = ObservableProperty(0)

var freeId = 0
var isAnimationRunning = false
var isGameOver = false

文字列の扱い

変数と文字列を同時に使用する場合、Java言語で書くと右のような書き方「println("XXX" + a)」の処理を行いたい場合

var a = 1
// simple name in template:
val s1 = "a is $a" 

a = 2
// arbitrary expression in template:
val s2 = "${s1.replace("is", "was")}, but now is $a"

「$」を使用するようです。どこかのフレームワークで見たような。。。感じです。
注意点として、Stringクラスのメソッドを使用したい時などは「{}」で囲う必要があるみたいです。

クラスの作り方

コンストラクタで作成する感じ?

class Rectangle(val height: Double, val length: Double) {
    val perimeter = (height + length) * 2 
}
fun main() {
    val rectangle = Rectangle(5.0, 2.0)
    println("The perimeter is ${rectangle.perimeter}")
}

継承関係の作り方「:」を使用する。Shapeクラスを継承したRectangleクラス

open class Shape

class Rectangle(val height: Double, val length: Double): Shape() {
    val perimeter = (height + length) * 2
}

メインメソッド

引き続き読み進めていきます。次はメインメソッドを読んでいきます。ちょっと長いので一部抜粋しながら読み進めます。

まずは、下のコードです。メインメソッドの書き始め部分です。通常のメインメソッドは下のように書くのですが。。。

fun main(args:Array<String>){
    println("Hello Kotlin")
}

もしくは、下のように書きます。

fun main() {
    println("Hello world!")
}

標準入出力

標準入力は、ユーザーの入力、出力はコンソールなどに出力することを言います。
Kotlinの場合は簡単に記述できます。

println("入力してください: ")

// ユーザーの入力を受け付けます
val yourWord = readln()

print("標準出力に入力した文字を出力: ")
print(yourWord)

関数(Function)の定義方法

Kotlinで定義する場合の書き方です。それぞれ引数あり、なし、返り値あり、なしの書き方を記載します。

引数と返り値ありの関数

引数あり、返り値ありの関数。引数も返り値もInt型です。

fun sum(a: Int, b: Int): Int {
    return a + b
}

簡単な課書き方をすると下のようになります。

fun sum(a: Int, b: Int) = a + b

引数あり、返り値なしの関数

引数あり、返り値なしの場合です。

fun printSum(a: Int, b: Int): Unit {
    println("sum of $a and $b is ${a + b}")
}

引数なし、返り値なしの関数

fun printSum() {
    println("sum of $a and $b is " + (1 + 2))
}

KorGEの場合

KorGEでは違うようです。これもひとつずつ分解指定解析します。1回解析してしまえばあとは、見ただけでわかります(笑)

初めの疑問点

メインメソッドの「suspend fun main()」の「suspend」の部分です。どんな意味があるのでしょうか?

suspend fun main() = Korge(
    virtualSize = Size(480, 640),
    title = "2048",
    bgcolor = RGBA(253, 247, 240),
    /**
        `gameId` is associated with the location of storage, which contains `history` and `best`.
        see [Views.realSettingsFolder]
     */
    gameId = "io.github.rezmike.game2048",
    forceRenderEveryFrame = false, // Optimization to reduce battery usage!
) { ... }

ここでの注意点は「suspend」キーワードです。このキーワードはこちらのページを参考に学習しました。またこちらのページでは「Coroutineとsuspendの違い」について学習しました。

結論を書くと下のようになります。ChatGPT先生に聞いたら簡潔に答えてくれました。

  • Coroutineは非同期処理全体を管理するためのメカニズム
  • suspend関数(修飾子?)はCoroutine内で一時停止可能な関数であり、非同期処理の具体的な一部を担います。

つまりsuspendは

「非同期処理とか、一時停止可能な関数(function)ですよ」という意味でした。
具体的には、現在のアプリが動いているデバイス以外へリクエストなどを送信しても、結果が返ってくるのを待たずに処理が動くとか
レスポンスを待って処理を行うとかが可能という意味でス。

「どうやって?」という部分に関しては今後調査していく必要があります。しかし、現段階ではここでSTOPしておきます。

次の疑問点

「main() = XXX」の部分です。具体的には、下の部分です。

fun main() = Korge(
    virtualSize = Size(480, 640),
    title = "2048",
    bgcolor = RGBA(253, 247, 240),
    /**
        `gameId` is associated with the location of storage, which contains `history` and `best`.
        see [Views.realSettingsFolder]
     */
    gameId = "io.github.rezmike.game2048",
    forceRenderEveryFrame = false, // Optimization to reduce battery usage!
)

こちらのサイトを参考にすると「インライン関数」として書いてあるようですが、ちょっと違うような気がします。

次いで調べたのがKotlinのドキュメントページです、英語ですが、日本語に変換して読みます。
Kotlin-Flllowを使用してデバックする」という記事にありました。
コードとしては、下のようなコードで、「メソッドの内容を書き換える」書き方です。。。

fun main() = runBlocking {
    simple()
        .collect { value ->
            delay(300)
            println(value)
        }
}

上記のコードは、下のコードをデバックするためのコードのようです。
つまり、メインメソッドにブロックを使用しrunBlocking()メソッドでコルーチンをラップします。
事前に定義されているのが「runBlocking()」でコルーチンの仕組みの一つです。
このメソッドをオーバーライドしてデバックを行っているというわけですね。なるほど!

import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
import kotlin.system.*

fun simple(): Flow<Int> = flow {
    for (i in 1..3) {
        delay(100)
        emit(i)
    }
}

結局は。。。

問題の「main() = KorGE( ... )の部分はKorGEフレームワークでメインメソッドを書き換えているということで、納得しました。

つまりは、上のrunBlocking()関数で書き換えたときと同じように、KorGEクラスで書き換えた、オーバーライドしてメインの処理を実行するようにした
ということです。
そのため、下のようにコンストラクタ部分に、画面の情報をセットしています。

Korge(
    virtualSize = Size(480, 640),
    title = "2048",
    bgcolor = RGBA(253, 247, 240),
    ・
    ・
    ・

そして、処理(main)部分がそのあとに続きます。

suspend main() = KorgGE(
    ... // 画面サイズなどのプロパティを設定
) {
    メインの処理。。。
}

サンプルコード

Kotlinの基本に戻る

import korlibs.event.*
import korlibs.korge.*
import korlibs.korge.animate.*
import korlibs.korge.input.*
import korlibs.korge.service.storage.*
import korlibs.korge.tween.*
import korlibs.korge.ui.*
import korlibs.korge.view.*
import korlibs.image.color.*
import korlibs.image.font.*
import korlibs.image.format.*
import korlibs.image.text.TextAlignment
import korlibs.io.async.*
import korlibs.io.async.ObservableProperty
import korlibs.io.file.std.*
import korlibs.korge.style.styles
import korlibs.korge.style.textColor
import korlibs.korge.style.textFont
import korlibs.korge.style.textSize
import korlibs.korge.view.align.*
import korlibs.math.geom.*
import korlibs.math.interpolation.*
import korlibs.time.seconds
import kotlin.collections.set
import kotlin.properties.*
import kotlin.random.*

var cellSize: Float = 0f
var fieldSize: Float = 0f
var leftIndent: Float = 0f
var topIndent: Float = 0f
var font: BitmapFont by Delegates.notNull()

fun columnX(number: Int) = leftIndent + 10 + (cellSize + 10) * number
fun rowY(number: Int) = topIndent + 10 + (cellSize + 10) * number

var map = PositionMap()
val blocks = mutableMapOf<Int, Block>()
var history: History by Delegates.notNull()

fun numberFor(blockId: Int) = blocks[blockId]!!.number
fun deleteBlock(blockId: Int) = blocks.remove(blockId)!!.removeFromParent()

val score = ObservableProperty(0)
val best = ObservableProperty(0)

var freeId = 0
var isAnimationRunning = false
var isGameOver = false

suspend fun main() = Korge(
    virtualSize = Size(480, 640),
    title = "2048",
    bgcolor = RGBA(253, 247, 240),
    /**
        `gameId` is associated with the location of storage, which contains `history` and `best`.
        see [Views.realSettingsFolder]
     */
    gameId = "io.github.rezmike.game2048",
    forceRenderEveryFrame = false, // Optimization to reduce battery usage!
) {
    font = resourcesVfs["clear_sans.fnt"].readBitmapFont()

    val storage = views.storage
    history = History(storage.getOrNull("history")) {
        storage["history"] = it.toString()
    }
    best.update(storage.getOrNull("best")?.toInt() ?: 0)

    score.observe {
        if (it > best.value) best.update(it)
    }
    best.observe {
        storage["best"] = it.toString()
    }

    cellSize = views.virtualWidth / 5f
    fieldSize = 50 + 4 * cellSize
    leftIndent = (views.virtualWidth - fieldSize) / 2
    topIndent = 150f

    val bgField = roundRect(Size(fieldSize, fieldSize), RectCorners(5), fill = Colors["#b9aea0"]) {
        position(leftIndent, topIndent)
    }
    graphics {
        fill(Colors["#cec0b2"]) {
            for (i in 0..3) {
                for (j in 0..3) {
                    roundRect(
                        10 + (10 + cellSize) * i, 10 + (10 + cellSize) * j,
                        cellSize, cellSize,
                        5f
                    )
                }
            }
        }
    }.position(leftIndent, topIndent)

    val bgLogo = roundRect(Size(cellSize, cellSize), RectCorners(5), fill = RGBA(237, 196, 3)) {
        position(leftIndent, 30f)
    }
    text("2048", cellSize * 0.5f, Colors.WHITE, font).centerOn(bgLogo)

    val bgBest = roundRect(Size(cellSize * 1.5, cellSize * 0.8), RectCorners(5f), fill = Colors["#bbae9e"]) {
        alignRightToRightOf(bgField)
        alignTopToTopOf(bgLogo)
    }
    text("BEST", cellSize * 0.25f, RGBA(239, 226, 210), font) {
        centerXOn(bgBest)
        alignTopToTopOf(bgBest, 5.0)
    }
    text(best.value.toString(), cellSize * 0.5f, Colors.WHITE, font) {
        setTextBounds(Rectangle(0f, 0f, bgBest.width, cellSize - 24f))
        alignment = TextAlignment.MIDDLE_CENTER
        alignTopToTopOf(bgBest, 12.0)
        centerXOn(bgBest)
        best.observe {
            text = it.toString()
        }
    }

    val bgScore = roundRect(Size(cellSize * 1.5f, cellSize * 0.8f), RectCorners(5.0f), fill = Colors["#bbae9e"]) {
        alignRightToLeftOf(bgBest, 24.0)
        alignTopToTopOf(bgBest)
    }
    text("SCORE", cellSize * 0.25f, RGBA(239, 226, 210), font) {
        centerXOn(bgScore)
        alignTopToTopOf(bgScore, 5.0)
    }
    text(score.value.toString(), cellSize * 0.5f, Colors.WHITE, font) {
        setTextBounds(Rectangle(0f, 0f, bgScore.width, cellSize - 24f))
        alignment = TextAlignment.MIDDLE_CENTER
        centerXOn(bgScore)
        alignTopToTopOf(bgScore, 12.0)
        score.observe {
            text = it.toString()
        }
    }

    val btnSize = cellSize * 0.3
    val restartImg = resourcesVfs["restart.png"].readBitmap()
    val undoImg = resourcesVfs["undo.png"].readBitmap()
    val restartBlock = container {
        val background = roundRect(Size(btnSize, btnSize), RectCorners(5f), fill = RGBA(185, 174, 160))
        image(restartImg) {
            size(btnSize * 0.8, btnSize * 0.8)
            centerOn(background)
        }
        alignTopToBottomOf(bgBest, 5.0)
        alignRightToRightOf(bgField)
        onClick {
            this@Korge.restart()
        }
    }
    val undoBlock = container {
        val background = roundRect(Size(btnSize, btnSize), RectCorners(5f), fill = RGBA(185, 174, 160))
        image(undoImg) {
            size(btnSize * 0.6, btnSize * 0.6)
            centerOn(background)
        }
        alignTopToTopOf(restartBlock)
        alignRightToLeftOf(restartBlock, 5.0)
        onClick {
            this@Korge.restoreField(history.undo())
        }
    }

    if (!history.isEmpty()) {
        restoreField(history.currentElement)
    } else {
        generateBlockAndSave()
    }

    root.keys.down {
        when (it.key) {
            Key.LEFT -> moveBlocksTo(Direction.LEFT)
            Key.RIGHT -> moveBlocksTo(Direction.RIGHT)
            Key.UP -> moveBlocksTo(Direction.TOP)
            Key.DOWN -> moveBlocksTo(Direction.BOTTOM)
            else -> Unit
        }
    }

    onSwipe(20.0) {
        when (it.direction) {
            SwipeDirection.LEFT -> moveBlocksTo(Direction.LEFT)
            SwipeDirection.RIGHT -> moveBlocksTo(Direction.RIGHT)
            SwipeDirection.TOP -> moveBlocksTo(Direction.TOP)
            SwipeDirection.BOTTOM -> moveBlocksTo(Direction.BOTTOM)
        }
    }
}

投稿者:

takunoji

音響、イベント会場設営業界からIT業界へ転身。現在はJava屋としてサラリーマンをやっている。自称ガテン系プログラマー(笑) Javaプログラミングを布教したい、ラスパイとJavaの相性が良いことに気が付く。 Spring framework, Struts, Seaser, Hibernate, Playframework, JavaEE6, JavaEE7などの現場経験あり。 SQL, VBA, PL/SQL, コマンドプロント, Shellなどもやります。