diff --git a/chapter2/README.md b/chapter2/README.md new file mode 100644 index 0000000..9be265b --- /dev/null +++ b/chapter2/README.md @@ -0,0 +1,23 @@ +# Chapter2 + +## 前提条件 + +`Homebrew` などで SDL2 をインストール + +``` +brew install sdl2 sdl2_image +``` + +## ビルド・実行 + +Assets ディレクトリに [画像ファイル](https://github.com/gameprogcpp/code/tree/master/Chapter02/Assets)を用意 + +``` +./gradlew assemble +./gradlew runDebugExecutableChapter2 +``` + +と gradle を実行 + +## 実行結果 +![](chapter2.png) \ No newline at end of file diff --git a/chapter2/build.gradle.kts b/chapter2/build.gradle.kts new file mode 100644 index 0000000..c059ef0 --- /dev/null +++ b/chapter2/build.gradle.kts @@ -0,0 +1,63 @@ +plugins { + kotlin("multiplatform") version "1.5.10" +} + +repositories { + mavenCentral() +} + +kotlin { + macosX64("chapter2") { // on macOS + // linuxX64("native") // on Linux + // mingwX64("native") // on Windows + compilations["main"].cinterops { + val sdl by creating { + if (file("/usr/local/include/SDL").exists()) { + includeDirs("/usr/local/include/SDL") + } else { + includeDirs("/usr/local/include/SDL2") + } + includeDirs.headerFilterOnly("/usr/local/include") + } + } + compilations["main"].enableEndorsedLibs = true + binaries { + executable { + entryPoint = "chapter2.main" + + val distTaskName = linkTaskName.replaceFirst("link", "dist") + val distTask = tasks.register(distTaskName) { + from("src/chapter2Main/resources") + into(linkTask.outputFile.get().parentFile) + dependsOn(linkTask) + } + tasks["assemble"].dependsOn(distTask) + + runTask?.workingDir(project.provider { outputDirectory }) + } + } + } + sourceSets { + commonMain { + dependencies { + implementation("org.jetbrains.kotlinx:kotlinx-cli:0.3.2") + } + } + } +} + +buildscript { + dependencies { + classpath("org.jlleitschuh.gradle:ktlint-gradle:10.1.0") + } +} + +tasks.withType { + gradleVersion = "6.7.1" + distributionType = Wrapper.DistributionType.BIN +} + +apply { + plugin("org.jlleitschuh.gradle.ktlint") + from("ktlint.gradle.kt") +} diff --git a/chapter2/chapter2.png b/chapter2/chapter2.png new file mode 100644 index 0000000..b790ebf Binary files /dev/null and b/chapter2/chapter2.png differ diff --git a/chapter2/gradle.properties b/chapter2/gradle.properties new file mode 100644 index 0000000..5dade1f --- /dev/null +++ b/chapter2/gradle.properties @@ -0,0 +1,50 @@ +# +# Copyright 2010-2019 JetBrains s.r.o. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# A version of the Kotlin compiler that is used to build Kotlin/Native. +buildKotlinVersion=1.5.0-RC-556 +buildKotlinCompilerRepo=https://cache-redirector.jetbrains.com/maven.pkg.jetbrains.space/kotlin/p/kotlin/bootstrap +remoteRoot=konan_tests +kotlinCompilerRepo=https://teamcity.jetbrains.com/guestAuth/app/rest/builds/buildType:(id:Kotlin_KotlinPublic_Compiler),number:1.5.10-871,branch:default:any,pinned:true/artifacts/content/maven +kotlinVersion=1.5.10-871 +kotlinStdlibRepo=https://teamcity.jetbrains.com/guestAuth/app/rest/builds/buildType:(id:Kotlin_KotlinPublic_Compiler),number:1.5.10-871,branch:default:any,pinned:true/artifacts/content/maven +kotlinStdlibVersion=1.5.10-871 +kotlinStdlibTestsVersion=1.5.10-871 +testKotlinCompilerVersion=1.5.10-871 +konanVersion=1.5.10 + +# A version of Xcode required to build the Kotlin/Native compiler. +xcodeMajorVersion=12 + +# A GTest revision used to test the runtime. +# The latest release GTest (1.10.0) doesn't properly register skipped tests in an XML-report. +# Therefore we use a fixed commit form the master branch where this problem is already fixed. +# https://github.com/google/googletest/commit/07f4869221012b16b7f9ee685d94856e1fc9f361 +gtestRevision=07f4869221012b16b7f9ee685d94856e1fc9f361 + +org.gradle.jvmargs='-Dfile.encoding=UTF-8' +org.gradle.workers.max=4 +slackApiVersion=1.2.0 +ktorVersion=1.2.1 +shadowVersion=5.1.0 +metadataVersion=0.0.1-dev-10 + +# Uncomment to compile Kotlin/Native backend modules with JVM IR backend. +# kotlin.build.useIR=true + +# Uncomment to enable composite build +#kotlinProjectPath= +kotlin.native.cacheKind.macosX64=none \ No newline at end of file diff --git a/chapter2/gradle/kotlinGradlePlugin.gradle b/chapter2/gradle/kotlinGradlePlugin.gradle new file mode 100644 index 0000000..2a53ac3 --- /dev/null +++ b/chapter2/gradle/kotlinGradlePlugin.gradle @@ -0,0 +1,37 @@ +def properties = ['buildKotlinVersion', 'buildKotlinCompilerRepo', 'kotlinVersion', 'kotlinCompilerRepo'] + +for (prop in properties) { + if (!hasProperty(prop)) { + throw new GradleException("Please ensure the '$prop' property is defined before applying this script.") + } +} + +project.buildscript.repositories { + maven { + url buildKotlinCompilerRepo + } + maven { + url kotlinCompilerRepo + } + maven { + url 'https://cache-redirector.jetbrains.com/maven-central' + } + mavenCentral() +} + +project.buildscript.dependencies { + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion" +} +configurations { + kotlinCompilerClasspath +} +project.repositories { + maven { url buildKotlinCompilerRepo } + maven { + url kotlinCompilerRepo + } +} + +project.dependencies { + kotlinCompilerClasspath("org.jetbrains.kotlin:kotlin-compiler-embeddable:$buildKotlinVersion") +} diff --git a/chapter2/gradle/loadRootProperties.gradle b/chapter2/gradle/loadRootProperties.gradle new file mode 100644 index 0000000..25edb22 --- /dev/null +++ b/chapter2/gradle/loadRootProperties.gradle @@ -0,0 +1,16 @@ +if (!project.hasProperty("rootBuildDirectory")) { + throw new GradleException('Please ensure the "rootBuildDirectory" property is defined before applying this script.') +} + +ext.distDir = file("$rootBuildDirectory/dist") + +def rootProperties = new Properties() +def rootDir = file(rootBuildDirectory).absolutePath +file("$rootDir/gradle.properties").withReader { + rootProperties.load(it) +} +rootProperties.each { k, v -> + if (!project.hasProperty(k)) { + ext.set(k, v) + } +} diff --git a/chapter2/gradle/wrapper/gradle-wrapper.jar b/chapter2/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..e708b1c Binary files /dev/null and b/chapter2/gradle/wrapper/gradle-wrapper.jar differ diff --git a/chapter2/gradle/wrapper/gradle-wrapper.properties b/chapter2/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..14e30f7 --- /dev/null +++ b/chapter2/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-all.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/chapter2/gradlew b/chapter2/gradlew new file mode 100755 index 0000000..4f906e0 --- /dev/null +++ b/chapter2/gradlew @@ -0,0 +1,185 @@ +#!/usr/bin/env sh + +# +# Copyright 2015 the original author or authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=`expr $i + 1` + done + case $i in + 0) set -- ;; + 1) set -- "$args0" ;; + 2) set -- "$args0" "$args1" ;; + 3) set -- "$args0" "$args1" "$args2" ;; + 4) set -- "$args0" "$args1" "$args2" "$args3" ;; + 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=`save "$@"` + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +exec "$JAVACMD" "$@" diff --git a/chapter2/gradlew.bat b/chapter2/gradlew.bat new file mode 100644 index 0000000..ac1b06f --- /dev/null +++ b/chapter2/gradlew.bat @@ -0,0 +1,89 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/chapter2/ktlint.gradle.kt b/chapter2/ktlint.gradle.kt new file mode 100644 index 0000000..c00b5ca --- /dev/null +++ b/chapter2/ktlint.gradle.kt @@ -0,0 +1,3 @@ +ktlint { + disabledRules = ["no-wildcard-imports"] +} \ No newline at end of file diff --git a/chapter2/settings.gradle.kts b/chapter2/settings.gradle.kts new file mode 100644 index 0000000..e69de29 diff --git a/chapter2/src/chapter2Main/kotlin/Actor.kt b/chapter2/src/chapter2Main/kotlin/Actor.kt new file mode 100644 index 0000000..e3739c2 --- /dev/null +++ b/chapter2/src/chapter2Main/kotlin/Actor.kt @@ -0,0 +1,54 @@ +package chapter2 + +import kotlin.collections.* + +open class Actor(val game: Game) { + + enum class State { + Active, + Paused, + Dead + } + + var position: Vector2 = Vector2.zero + var scale: Float = 1.0f + var rotation: Float = 0.0f + var state: State = State.Active + private var components = mutableListOf() + + init { + game.addActor(this) + } + + fun update(deltaTime: Float) { + if (state == State.Active) { + updateComponents(deltaTime) + updateActor(deltaTime) + } + } + + private fun updateComponents(deltaTime: Float) { + for (component in components) { + component.update(deltaTime) + } + } + + open fun updateActor(deltaTime: Float) { + } + + fun addComponent(component: Component) { + components.add(component) + components.sortBy { it.updateOrder } + } + + fun removeComponent(component: Component) { + components.remove(component) + } + + fun dispose() { + game.removeActor(this) + for (component in components) { + component.dispose() + } + } +} diff --git a/chapter2/src/chapter2Main/kotlin/AnimSpriteComponent.kt b/chapter2/src/chapter2Main/kotlin/AnimSpriteComponent.kt new file mode 100644 index 0000000..d7e8cfb --- /dev/null +++ b/chapter2/src/chapter2Main/kotlin/AnimSpriteComponent.kt @@ -0,0 +1,38 @@ +package chapter2 + +import kotlinx.cinterop.* +import sdl.* + +class AnimSpriteComponent(owner: Actor, drawOrder: Int = 100) : SpriteComponent(owner, drawOrder) { + + private var animationTextures: List?> = listOf() + private var currentFrame = 0.0f + private var animationFps = 2.4f + + override fun update(deltaTime: Float) { + super.update(deltaTime) + + if (animationTextures.isNotEmpty()) { + // Update the current frame based on frame rate + // and delta time + currentFrame += animationFps * deltaTime + + // Wrap current frame if needed + while (currentFrame >= animationTextures.size) { + currentFrame -= animationTextures.size + } + + // Set the current texture + setTexture(animationTextures[currentFrame.toInt()]) + } + } + + fun setAnimationTextures(textures: List?>) { + animationTextures = textures + + if (animationTextures.isNotEmpty()) { + currentFrame = 0.0f + setTexture(animationTextures[0]) + } + } +} diff --git a/chapter2/src/chapter2Main/kotlin/BGSpriteComponent.kt b/chapter2/src/chapter2Main/kotlin/BGSpriteComponent.kt new file mode 100644 index 0000000..3f10a0b --- /dev/null +++ b/chapter2/src/chapter2Main/kotlin/BGSpriteComponent.kt @@ -0,0 +1,52 @@ +package chapter2 + +import kotlinx.cinterop.* +import sdl.* + +class BGSpriteComponent(owner: Actor, drawOrder: Int = 10) : SpriteComponent(owner, drawOrder) { + + data class BGTexture(val texture: CPointer, val offset: Vector2) + + val bgTextureList = mutableListOf() + var screenSize: Vector2 = Vector2.zero + var scrollSpeed: Float = 0.0f + + override fun update(deltaTime: Float) { + super.update(deltaTime) + for (bg in bgTextureList) { + // Update the x offset + bg.offset.x += scrollSpeed * deltaTime + // If this is completely off the screen, reset offset to + // the right of the last bg texture + if (bg.offset.x < -screenSize.x) { + bg.offset.x = (bgTextureList.size - 1) * screenSize.x - 1 + } + } + } + + override fun draw(renderer: CPointer?) { + for (bg in bgTextureList) { + memScoped { + val r = alloc().apply { + // Assume screen size dimensions + w = screenSize.x.toInt() + h = screenSize.y.toInt() + // Center the rectangle around the position of the owner + x = (owner.position.x - w / 2 + bg.offset.x).toInt() + y = (owner.position.y - h / 2 + bg.offset.y).toInt() + } + // Draw this background + SDL_RenderCopy(renderer, bg.texture, null, r.ptr.reinterpret()) + } + } + } + + fun setBGTextures(textures: List?>) { + for ((count, tex) in textures.withIndex()) { + tex ?: continue + val offset = Vector2(x = count * screenSize.x, 0f) + val temp = BGTexture(texture = tex, offset = offset) + bgTextureList.add(temp) + } + } +} diff --git a/chapter2/src/chapter2Main/kotlin/Component.kt b/chapter2/src/chapter2Main/kotlin/Component.kt new file mode 100644 index 0000000..4f42b39 --- /dev/null +++ b/chapter2/src/chapter2Main/kotlin/Component.kt @@ -0,0 +1,15 @@ +package chapter2 + +open class Component constructor(private val owner: Actor, var updateOrder: Int = 100) { + + init { + owner.addComponent(this) + } + + open fun update(deltaTime: Float) { + } + + open fun dispose() { + owner.removeComponent(this) + } +} diff --git a/chapter2/src/chapter2Main/kotlin/Game.kt b/chapter2/src/chapter2Main/kotlin/Game.kt new file mode 100644 index 0000000..f654b3a --- /dev/null +++ b/chapter2/src/chapter2Main/kotlin/Game.kt @@ -0,0 +1,226 @@ +package chapter2 + +import kotlinx.cinterop.* +import sdl.* + +class Game { + + private var isRunning: Boolean = true + private var ticksCount: UInt = 0u + private var window: CPointer? = null + private var renderer: CPointer? = null + private var ship: Ship? = null + private var actorList = mutableListOf() + private var pendingActorList = mutableListOf() + private var spriteList = mutableListOf() + private var textureMap = mutableMapOf>() + private var updateActors = false + + fun initialize(): Boolean { + val result = SDL_Init(SDL_INIT_VIDEO.toUInt() or SDL_INIT_AUDIO.toUInt()) + if (result != 0) { + throw Error("Unable to initialize SDL: ${SDL_GetError()}") + } + + window = SDL_CreateWindow( + "Game Programming in Kotlin/Native (Chapter 2)", // Window title + 100, // Top left x-coordinate of window + 100, // Top left y-coordinate of window + 1024, // Width of window + 768, // Height of window + SDL_WINDOW_SHOWN // Flags (0 for no flags set) + ) + + if (window == null) { + throw Error("Failed to create window: ${SDL_GetError()}") + } + + renderer = SDL_CreateRenderer( + window, // Window to create renderer for + -1, // Usually -1 + SDL_RENDERER_ACCELERATED or SDL_RENDERER_PRESENTVSYNC + ) + + if (renderer == null) { + throw Error("Failed to create renderer: ${SDL_GetError()}") + } + + if (IMG_Init(IMG_INIT_PNG.toInt()) == 0) { + throw Error("Unable to initialize SDL_image: ${SDL_GetError()}") + } + + loadData() + + ticksCount = SDL_GetTicks() + + return true + } + + fun runloop() { + while (isRunning) { + processInput() + updateGame() + generateOutput() + } + } + + private fun processInput() { + memScoped { + val event = alloc() + while (SDL_PollEvent(event.ptr.reinterpret()) != 0) { + when (event.type) { + SDL_QUIT -> isRunning = false + } + } + } + + val state = SDL_GetKeyboardState(null) + state ?: return + if (state[SDL_SCANCODE_ESCAPE.toInt()].toInt() != 0) { + isRunning = false + } + + ship?.processKeyboard(state) + } + + private fun updateGame() { + // Wait until 16ms has elapsed since last frame + while (!SDL_TICKS_PASSED(SDL_GetTicks().toInt(), ticksCount.toInt() + 16)) Unit + + // Delta time is the difference in ticks from last frame + // (converted to seconds) + var deltaTime = (SDL_GetTicks() - ticksCount).toFloat() / 1000.0f + + // Clamp maximum delta time value + if (deltaTime > 0.05f) { + deltaTime = 0.05f + } + + // Update tick counts (for next frame) + ticksCount = SDL_GetTicks() + + updateActors = true + + for (actor in actorList) { + actor.update(deltaTime) + } + + updateActors = false + + actorList.addAll(pendingActorList) + pendingActorList.clear() + + val deadActor = mutableListOf() + + for (actor in actorList) { + if (actor.state == Actor.State.Dead) { + deadActor.add(actor) + } + } + + deadActor.forEach { + it.dispose() + } + } + + private fun generateOutput() { + SDL_SetRenderDrawColor(renderer, 0, 0, 0, 255) + SDL_RenderClear(renderer) + + spriteList.forEach { it.draw(renderer) } + + // Swap front buffer and back buffer + SDL_RenderPresent(renderer) + } + + fun shutdown() { + unloadData() + IMG_Quit() + SDL_DestroyRenderer(renderer) + SDL_DestroyWindow(window) + SDL_Quit() + } + + private fun loadData() { + ship = Ship(this).apply { + position = Vector2(100f, 384f) + scale = 1.5f + } + + val actor = Actor(this).apply { + position = Vector2(512f, 384f) + } + + BGSpriteComponent(actor).apply { + screenSize = Vector2(1024f, 768f) + setBGTextures( + listOf( + getTexture("Assets/Farback01.png"), + getTexture("Assets/Farback02.png") + ) + ) + scrollSpeed = -100f + } + BGSpriteComponent(actor, 50).apply { + screenSize = Vector2(1024f, 768f) + setBGTextures( + listOf( + getTexture("Assets/Stars.png"), + getTexture("Assets/Stars.png") + ) + ) + scrollSpeed = -200f + } + } + + private fun unloadData() { + actorList.forEach { + it.dispose() + } + textureMap.forEach { + SDL_DestroyTexture(it.value) + } + textureMap.clear() + } + + fun addActor(actor: Actor) { + if (updateActors) { + pendingActorList + } else { + actorList + }.add(actor) + } + + fun removeActor(actor: Actor) { + pendingActorList.remove(actor) + actorList.remove(actor) + } + + fun addSprite(sprite: SpriteComponent) { + spriteList.add(sprite) + spriteList.sortBy { it.drawOrder } + } + + fun removeSprite(sprite: SpriteComponent) { + spriteList.remove(sprite) + } + + fun getTexture(path: String): CPointer { + var texture = textureMap[path] + if (texture != null) { + return texture + } else { + val surface = IMG_Load(path) ?: throw Error("Failed to load texture file $path") + + texture = SDL_CreateTextureFromSurface(renderer, surface) + SDL_FreeSurface(surface) + if (texture == null) { + throw Error("Failed to convert surface to texture for $path") + } + textureMap[path] = texture + } + return texture + } + + private inline fun SDL_TICKS_PASSED(a: Int, b: Int): Boolean = (b - a) <= 0 +} diff --git a/chapter2/src/chapter2Main/kotlin/Math.kt b/chapter2/src/chapter2Main/kotlin/Math.kt new file mode 100644 index 0000000..6ac729b --- /dev/null +++ b/chapter2/src/chapter2Main/kotlin/Math.kt @@ -0,0 +1,6 @@ +package chapter2 + +const val pi = 3.1415926535f + +fun toDegree(radian: Float): Float = radian * 180f / pi +fun Float.toDegree(): Float = toDegree(this) diff --git a/chapter2/src/chapter2Main/kotlin/Ship.kt b/chapter2/src/chapter2Main/kotlin/Ship.kt new file mode 100644 index 0000000..709e78d --- /dev/null +++ b/chapter2/src/chapter2Main/kotlin/Ship.kt @@ -0,0 +1,62 @@ +package chapter2 + +import kotlinx.cinterop.* +import sdl.* + +class Ship(game: Game) : Actor(game) { + + private var rightSpeed = 0.0f + private var downSpeed = 0.0f + + init { + val component = AnimSpriteComponent(this) + component.setAnimationTextures( + listOf( + game.getTexture("Assets/Ship01.png"), + game.getTexture("Assets/Ship02.png"), + game.getTexture("Assets/Ship03.png"), + game.getTexture("Assets/Ship04.png") + ) + ) + } + + override fun updateActor(deltaTime: Float) { + super.updateActor(deltaTime) + val pos: Vector2 = position + pos.x += rightSpeed * deltaTime + pos.y += downSpeed * deltaTime + // Restrict position to left half of screen + // Restrict position to left half of screen + if (pos.x < 25.0f) { + pos.x = 25.0f + } else if (pos.x > 500.0f) { + pos.x = 500.0f + } + if (pos.y < 25.0f) { + pos.y = 25.0f + } else if (pos.y > 743.0f) { + pos.y = 743.0f + } + position = pos + } + + fun processKeyboard(state: CPointer?) { + state ?: return + rightSpeed = 0.0f + downSpeed = 0.0f + // right/left + if (state[SDL_SCANCODE_D.toInt()].toInt() != 0) { + rightSpeed += 250.0f + } + if (state[SDL_SCANCODE_A.toInt()].toInt() != 0) { + rightSpeed -= 250.0f + } + // up/down + if (state[SDL_SCANCODE_S.toInt()].toInt() != 0) { + downSpeed += 300.0f + } + if (state[SDL_SCANCODE_W.toInt()].toInt() != 0) { + downSpeed -= 300.0f + } + } +} diff --git a/chapter2/src/chapter2Main/kotlin/SpriteComponent.kt b/chapter2/src/chapter2Main/kotlin/SpriteComponent.kt new file mode 100644 index 0000000..caef51f --- /dev/null +++ b/chapter2/src/chapter2Main/kotlin/SpriteComponent.kt @@ -0,0 +1,44 @@ +package chapter2 + +import kotlinx.cinterop.* +import sdl.* + +open class SpriteComponent(val owner: Actor, var drawOrder: Int = 100) : Component(owner) { + + private var texture: CPointer? = null + private var textureWidth: Int = 0 + private var textureHeight: Int = 0 + + init { + owner.game.addSprite(this) + } + + open fun draw(renderer: CPointer?) { + memScoped { + val r = alloc().apply { + w = (textureWidth.toFloat() * owner.scale).toInt() + h = (textureHeight.toFloat() * owner.scale).toInt() + x = (owner.position.x - w / 2).toInt() + y = (owner.position.y - h / 2).toInt() + } + + SDL_RenderCopyEx(renderer, texture, null, r.ptr.reinterpret(), -owner.rotation.toDegree().toDouble(), null, SDL_FLIP_NONE) + } + } + + fun setTexture(tex: CPointer?) { + texture = tex + memScoped { + val width = alloc() + val height = alloc() + SDL_QueryTexture(texture, null, null, width.ptr.reinterpret(), height.ptr.reinterpret()) + textureWidth = width.value + textureHeight = height.value + } + } + + override fun dispose() { + super.dispose() + owner.game.removeSprite(this) + } +} diff --git a/chapter2/src/chapter2Main/kotlin/Vector2.kt b/chapter2/src/chapter2Main/kotlin/Vector2.kt new file mode 100644 index 0000000..3924329 --- /dev/null +++ b/chapter2/src/chapter2Main/kotlin/Vector2.kt @@ -0,0 +1,7 @@ +package chapter2 + +data class Vector2(var x: Float, var y: Float) { + companion object { + val zero = Vector2(0f, 0f) + } +} diff --git a/chapter2/src/chapter2Main/kotlin/main.kt b/chapter2/src/chapter2Main/kotlin/main.kt new file mode 100644 index 0000000..6dcdbe6 --- /dev/null +++ b/chapter2/src/chapter2Main/kotlin/main.kt @@ -0,0 +1,14 @@ +package chapter2 + +fun main() { + val game = Game() + val result = game.initialize() + + if (result) { + game.runloop() + } + + game.shutdown() + + return +} diff --git a/chapter2/src/chapter2Main/resources/Assets/.gitkeep b/chapter2/src/chapter2Main/resources/Assets/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/chapter2/src/nativeInterop/cinterop/sdl.def b/chapter2/src/nativeInterop/cinterop/sdl.def new file mode 100644 index 0000000..34500e3 --- /dev/null +++ b/chapter2/src/nativeInterop/cinterop/sdl.def @@ -0,0 +1,7 @@ +headers = SDL.h SDL_Image.h stdlib.h time.h +entryPoint = SDL_main + +headerFilter = SDL* stdlib.h time.h + +compilerOpts.osx = -D_POSIX_SOURCE +linkerOpts.osx = -L/usr/local/lib -lSDL2 -lSDL2_image \ No newline at end of file