commit 05466725af340b9b4a28657e310666e7e646518d Author: Attila Uygun Date: Mon Apr 13 13:24:53 2020 +0200 Initial commit. diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9add192 --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +.vscode +build/android/.gradle +build/android/app/.cxx +build/android/app/build +build/android/build +build/linux/gltest_x86_64_debug +build/linux/gltest_x86_64_release +build/linux/obj diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..e1f425d --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020 Attila Uygun + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..10ab2d9 --- /dev/null +++ b/README.md @@ -0,0 +1,17 @@ +A simple, cross-platform, multi-threaded 2D game engine with OpenGL renderer written in modern +C++. Supports Linux and Android platforms. + +Build for Linux (gcc or clang): + +cd build/linux +make + +Build for Android: + +cd build/android +./gradlew :app:assembleRelease + +Build for Android and install (debug): + +cd build/android +./gradlew :app:installDebug diff --git a/assets/PixelCaps!.ttf b/assets/PixelCaps!.ttf new file mode 100644 index 0000000..298733d Binary files /dev/null and b/assets/PixelCaps!.ttf differ diff --git a/assets/enemy_anims_01_frames_ok.png b/assets/enemy_anims_01_frames_ok.png new file mode 100644 index 0000000..cc6a560 Binary files /dev/null and b/assets/enemy_anims_01_frames_ok.png differ diff --git a/assets/enemy_anims_02_frames_ok.png b/assets/enemy_anims_02_frames_ok.png new file mode 100644 index 0000000..5a88700 Binary files /dev/null and b/assets/enemy_anims_02_frames_ok.png differ diff --git a/assets/enemy_anims_blast_ok.png b/assets/enemy_anims_blast_ok.png new file mode 100644 index 0000000..2dfcc8f Binary files /dev/null and b/assets/enemy_anims_blast_ok.png differ diff --git a/assets/enemy_anims_flare_ok.png b/assets/enemy_anims_flare_ok.png new file mode 100644 index 0000000..74b648e Binary files /dev/null and b/assets/enemy_anims_flare_ok.png differ diff --git a/assets/enemy_ray_ok.png b/assets/enemy_ray_ok.png new file mode 100644 index 0000000..fbd1042 Binary files /dev/null and b/assets/enemy_ray_ok.png differ diff --git a/assets/enemy_target_multi_ok.png b/assets/enemy_target_multi_ok.png new file mode 100644 index 0000000..acc40ff Binary files /dev/null and b/assets/enemy_target_multi_ok.png differ diff --git a/assets/enemy_target_single_ok.png b/assets/enemy_target_single_ok.png new file mode 100644 index 0000000..2ed17ac Binary files /dev/null and b/assets/enemy_target_single_ok.png differ diff --git a/assets/engine/RobotoMono-Regular.ttf b/assets/engine/RobotoMono-Regular.ttf new file mode 100755 index 0000000..495a82c Binary files /dev/null and b/assets/engine/RobotoMono-Regular.ttf differ diff --git a/assets/engine/pass_through.glsl_fragment b/assets/engine/pass_through.glsl_fragment new file mode 100644 index 0000000..29f1730 --- /dev/null +++ b/assets/engine/pass_through.glsl_fragment @@ -0,0 +1,12 @@ +#ifdef GL_ES +precision mediump float; +#endif + +uniform vec4 color; +uniform sampler2D texture; + +varying vec2 tex_coord_0; + +void main() { + gl_FragColor = texture2D(texture, tex_coord_0) * color; +} diff --git a/assets/engine/pass_through.glsl_vertex b/assets/engine/pass_through.glsl_vertex new file mode 100644 index 0000000..a9b3956 --- /dev/null +++ b/assets/engine/pass_through.glsl_vertex @@ -0,0 +1,26 @@ +attribute vec2 in_position; +attribute vec2 in_tex_coord_0; + +uniform vec2 scale; +uniform vec2 offset; +uniform vec2 pivot; +uniform vec2 rotation; +uniform vec2 tex_offset; +uniform vec2 tex_scale; +uniform mat4 projection; + +varying vec2 tex_coord_0; + +void main() { + // Simple 2d transform. + vec2 position = in_position; + position *= scale; + position += pivot; + position = vec2(position.x * rotation.y + position.y * rotation.x, + position.y * rotation.y - position.x * rotation.x); + position += offset - pivot; + + tex_coord_0 = (in_tex_coord_0 + tex_offset) * tex_scale; + + gl_Position = projection * vec4(position, 0.0, 1.0); +} diff --git a/assets/engine/quad.mesh b/assets/engine/quad.mesh new file mode 100644 index 0000000..53bb937 --- /dev/null +++ b/assets/engine/quad.mesh @@ -0,0 +1,10 @@ +// This creates a normalized unit sized quad. +{ + "primitive": "TriangleStrip", + "vertex_description": "p2f;t2f", + "num_vertices": 4, + "vertices": [-0.5, -0.5, 0.0, 1.0, + 0.5, -0.5, 1.0, 1.0, + -0.5, 0.5, 0.0, 0.0, + 0.5, 0.5, 1.0, 0.0] +} diff --git a/assets/engine/solid.glsl_fragment b/assets/engine/solid.glsl_fragment new file mode 100644 index 0000000..e8fb622 --- /dev/null +++ b/assets/engine/solid.glsl_fragment @@ -0,0 +1,9 @@ +#ifdef GL_ES +precision mediump float; +#endif + +uniform vec4 color; + +void main() { + gl_FragColor = color; +} diff --git a/assets/engine/solid.glsl_vertex b/assets/engine/solid.glsl_vertex new file mode 100644 index 0000000..f1ac061 --- /dev/null +++ b/assets/engine/solid.glsl_vertex @@ -0,0 +1,20 @@ +attribute vec2 in_position; +attribute vec2 in_tex_coord_0; + +uniform vec2 scale; +uniform vec2 offset; +uniform vec2 pivot; +uniform vec2 rotation; +uniform mat4 projection; + +void main() { + // Simple 2d transform. + vec2 position = in_position; + position *= scale; + position += pivot; + position = vec2(position.x * rotation.y + position.y * rotation.x, + position.y * rotation.y - position.x * rotation.x); + position += offset - pivot; + + gl_Position = projection * vec4(position, 0.0, 1.0); +} diff --git a/assets/explosion.mp3 b/assets/explosion.mp3 new file mode 100644 index 0000000..067f4b2 Binary files /dev/null and b/assets/explosion.mp3 differ diff --git a/assets/sky.glsl_fragment b/assets/sky.glsl_fragment new file mode 100644 index 0000000..8a66224 --- /dev/null +++ b/assets/sky.glsl_fragment @@ -0,0 +1,61 @@ +#ifdef GL_ES +precision mediump float; +#else +#define lowp +#define mediump +#define highp +#endif + +uniform highp vec2 sky_offset; +uniform vec3 nebula_color; + +varying highp vec2 tex_coord_0; + +float random(highp vec2 p) { + highp float sd = sin(dot(p, vec2(54.90898, 18.233))); + return fract(sd * 2671.6182); +} + +float nebula(in highp vec2 p) { + highp vec2 i = floor(p); + highp vec2 f = fract(p); + + float a = random(i); + float b = random(i + vec2(1.0, 0.0)); + float c = random(i + vec2(0.0, 1.0)); + float d = random(i + vec2(1.0, 1.0)); + + vec2 u = smoothstep(0.0, 1.0, f); + + return mix(a, b, u.x) + + (c - a)* u.y * (1.0 - u.x) + + (d - b) * u.x * u.y; +} + +float stars(in highp vec2 p, float num_cells, float size) { + highp vec2 n = p * num_cells; + highp vec2 i = floor(n); + + vec2 a = n - i - random(i); + a /= num_cells * size; + float e = dot(a, a); + + return smoothstep(0.95, 1.0, (1.0 - e * 35.0)); +} + +void main() { + highp vec2 layer1_coord = tex_coord_0 + sky_offset; + highp vec2 layer2_coord = tex_coord_0 + sky_offset * 0.7; + vec3 result = vec3(0.); + + float c = nebula(layer2_coord * 3.0) * 0.35 - 0.05; + result += nebula_color * floor(c * 60.0) / 60.0; + + c = stars(layer1_coord, 8.0, 0.05); + result += vec3(0.97, 0.74, 0.74) * c; + + c = stars(layer2_coord, 16.0, 0.025) * 0.5; + result += vec3(0.9, 0.9, 0.95) * c; + + gl_FragColor = vec4(result, 1.0); +} diff --git a/assets/sky.glsl_vertex b/assets/sky.glsl_vertex new file mode 100644 index 0000000..143a112 --- /dev/null +++ b/assets/sky.glsl_vertex @@ -0,0 +1,17 @@ +attribute vec2 in_position; +attribute vec2 in_tex_coord_0; + +uniform vec2 scale; +uniform mat4 projection; + +varying vec2 tex_coord_0; + +void main() { + // Simple 2d transform. + vec2 position = in_position; + position *= scale; + + tex_coord_0 = in_tex_coord_0; + + gl_Position = projection * vec4(position, 0.0, 1.0); +} diff --git a/build/android/app/CMakeLists.txt b/build/android/app/CMakeLists.txt new file mode 100644 index 0000000..0560709 --- /dev/null +++ b/build/android/app/CMakeLists.txt @@ -0,0 +1,130 @@ +# +# Copyright (C) The Android Open Source Project +# +# 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. +# + +cmake_minimum_required(VERSION 3.4.1) + +# OBOE Library +set (OBOE_DIR ../../../src/third_party/oboe) +add_subdirectory(${OBOE_DIR} ./oboe-bin) + + +include(AndroidNdkModules) +android_ndk_import_module_cpufeatures() + +# build native_app_glue as a static lib +if (CMAKE_BUILD_TYPE MATCHES Debug) + set(${CMAKE_C_FLAGS}, "${CMAKE_C_FLAGS} -D_DEBUG") +else () + set(${CMAKE_C_FLAGS}, "${CMAKE_C_FLAGS}") +endif () +add_library(native_app_glue STATIC + ${ANDROID_NDK}/sources/android/native_app_glue/android_native_app_glue.c) + +# now build app's shared lib +if (CMAKE_BUILD_TYPE MATCHES Debug) + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++17 -Wall -Werror -D_DEBUG") +else () + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++17 -Wall -Werror") +endif () + + +# Export ANativeActivity_onCreate(), +# Refer to: https://github.com/android-ndk/ndk/issues/381. +set(CMAKE_SHARED_LINKER_FLAGS + "${CMAKE_SHARED_LINKER_FLAGS} -u ANativeActivity_onCreate") + +add_library(native-activity SHARED + ../../../src/base/collusion_test.cc + ../../../src/base/log.cc + ../../../src/base/random.cc + ../../../src/base/task_runner.cc + ../../../src/base/timer.cc + ../../../src/base/vecmath.cc + ../../../src/base/worker.cc + ../../../src/demo/credits.cc + ../../../src/demo/demo.cc + ../../../src/demo/enemy.cc + ../../../src/demo/hud.cc + ../../../src/demo/menu.cc + ../../../src/demo/player.cc + ../../../src/demo/sky_quad.cc + ../../../src/engine/animatable.cc + ../../../src/engine/animator.cc + ../../../src/engine/audio/audio_base.cc + ../../../src/engine/audio/audio_oboe.cc + ../../../src/engine/audio/audio_resource.cc + ../../../src/engine/engine.cc + ../../../src/engine/font.cc + ../../../src/engine/image_quad.cc + ../../../src/engine/image.cc + ../../../src/engine/mesh.cc + ../../../src/engine/platform/asset_file_android.cc + ../../../src/engine/platform/asset_file.cc + ../../../src/engine/platform/platform_android.cc + ../../../src/engine/platform/platform.cc + ../../../src/engine/renderer/geometry.cc + ../../../src/engine/renderer/render_command.cc + ../../../src/engine/renderer/render_resource.cc + ../../../src/engine/renderer/renderer_android.cc + ../../../src/engine/renderer/renderer_types.cc + ../../../src/engine/renderer/renderer.cc + ../../../src/engine/renderer/shader.cc + ../../../src/engine/renderer/texture.cc + ../../../src/engine/shader_source.cc + ../../../src/engine/solid_quad.cc + ../../../src/engine/sound_player.cc + ../../../src/engine/sound.cc + ../../../src/third_party/android/gestureDetector.cpp + ../../../src/third_party/android/gl3stub.c + ../../../src/third_party/android/GLContext.cpp + ../../../src/third_party/jsoncpp/jsoncpp.cc + ../../../src/third_party/minizip/ioapi.c + ../../../src/third_party/minizip/unzip.c + ../../../src/third_party/r8b/pffft.cpp + ../../../src/third_party/r8b/r8bbase.cpp + ../../../src/third_party/texture_compressor/dxt_encoder_internals.cc + ../../../src/third_party/texture_compressor/dxt_encoder.cc + ../../../src/third_party/texture_compressor/texture_compressor_etc1.cc + ../../../src/third_party/texture_compressor/texture_compressor.cc +) + +if (ANDROID_ABI STREQUAL armeabi-v7a) + target_sources(native-activity PRIVATE ../../../src/third_party/texture_compressor/dxt_encoder_neon.cc) + target_sources(native-activity PRIVATE ../../../src/third_party/texture_compressor/texture_compressor_etc1_neon.cc) + set_source_files_properties(../../../src/third_party/r8b/pffft.cpp PROPERTIES COMPILE_FLAGS -mfpu=neon) + set_source_files_properties(../../../src/third_party/texture_compressor/dxt_encoder_neon.cc PROPERTIES COMPILE_FLAGS -mfpu=neon) + set_source_files_properties(../../../src/third_party/texture_compressor/texture_compressor_etc1_neon.cc PROPERTIES COMPILE_FLAGS -mfpu=neon) +endif() + +if (ANDROID_ABI STREQUAL arm64-v8a) + target_sources(native-activity PRIVATE ../../../src/third_party/texture_compressor/dxt_encoder_neon.cc) + target_sources(native-activity PRIVATE ../../../src/third_party/texture_compressor/texture_compressor_etc1_neon.cc) +endif() + +target_include_directories(native-activity PRIVATE + ${ANDROID_NDK}/sources/android/native_app_glue + ) + +# add lib dependencies +target_link_libraries(native-activity + android + native_app_glue + oboe + cpufeatures + EGL + GLESv2 + log + z) diff --git a/build/android/app/build.gradle b/build/android/app/build.gradle new file mode 100644 index 0000000..aee51b9 --- /dev/null +++ b/build/android/app/build.gradle @@ -0,0 +1,40 @@ +apply plugin: 'com.android.application' + +android { + compileSdkVersion 29 + + defaultConfig { + applicationId = 'com.example.native_activity' + minSdkVersion 14 + targetSdkVersion 28 + externalNativeBuild { + cmake { + arguments '-DANDROID_STL=c++_static' + } + } + } + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), + 'proguard-rules.pro' + } + } + externalNativeBuild { + cmake { + version '3.10.2' + path 'CMakeLists.txt' + } + } + sourceSets { + main { + assets.srcDirs = ['../../../assets'] + } + } +} + +dependencies { + implementation fileTree(dir: 'libs', include: ['*.jar']) + implementation 'androidx.appcompat:appcompat:1.0.2' + implementation 'androidx.constraintlayout:constraintlayout:1.1.3' +} diff --git a/build/android/app/src/main/AndroidManifest.xml b/build/android/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..4f7f4c1 --- /dev/null +++ b/build/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/build/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/build/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000..cde69bc Binary files /dev/null and b/build/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/build/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/build/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000..c133a0c Binary files /dev/null and b/build/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/build/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/build/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000..bfa42f0 Binary files /dev/null and b/build/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/build/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/build/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000..324e72c Binary files /dev/null and b/build/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/build/android/app/src/main/res/values/strings.xml b/build/android/app/src/main/res/values/strings.xml new file mode 100644 index 0000000..d8f5513 --- /dev/null +++ b/build/android/app/src/main/res/values/strings.xml @@ -0,0 +1,4 @@ + + + NativeActivity + diff --git a/build/android/build.gradle b/build/android/build.gradle new file mode 100644 index 0000000..b4f5ce0 --- /dev/null +++ b/build/android/build.gradle @@ -0,0 +1,21 @@ +// Top-level build file where you can add configuration options common to all sub-projects/modules. +buildscript { + repositories { + google() + jcenter() + } + dependencies { + classpath 'com.android.tools.build:gradle:3.5.2' + } +} + +allprojects { + repositories { + google() + jcenter() + } +} + +task clean(type: Delete) { + delete rootProject.buildDir +} diff --git a/build/android/gradle.properties b/build/android/gradle.properties new file mode 100644 index 0000000..7bef3c2 --- /dev/null +++ b/build/android/gradle.properties @@ -0,0 +1,20 @@ +# Project-wide Gradle settings. + +# IDE (e.g. Android Studio) users: +# Gradle settings configured through the IDE *will override* +# any settings specified in this file. + +# For more details on how to configure your build environment visit +# http://www.gradle.org/docs/current/userguide/build_environment.html + +# Specifies the JVM arguments used for the daemon process. +# The setting is particularly useful for tweaking memory settings. +android.enableJetifier=true +android.useAndroidX=true +org.gradle.jvmargs=-Xmx1536m + +# When configured, Gradle will run in incubating parallel mode. +# This option should only be used with decoupled projects. More details, visit +# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects +# org.gradle.parallel=true + diff --git a/build/android/gradle/wrapper/gradle-wrapper.jar b/build/android/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..8c0fb64 Binary files /dev/null and b/build/android/gradle/wrapper/gradle-wrapper.jar differ diff --git a/build/android/gradle/wrapper/gradle-wrapper.properties b/build/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..48c4576 --- /dev/null +++ b/build/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Sun Feb 05 19:39:12 IST 2017 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-5.4.1-all.zip diff --git a/build/android/gradlew b/build/android/gradlew new file mode 100755 index 0000000..91a7e26 --- /dev/null +++ b/build/android/gradlew @@ -0,0 +1,164 @@ +#!/usr/bin/env bash + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# 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 +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; +esac + +# For Cygwin, ensure paths are in UNIX format before anything is touched. +if $cygwin ; then + [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` +fi + +# 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\"`/" >&- +APP_HOME="`pwd -P`" +cd "$SAVED" >&- + +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" ] ; 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, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + + # 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=$((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 + +# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules +function splitJvmOpts() { + JVM_OPTS=("$@") +} +eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS +JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" + +exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" diff --git a/build/android/local.properties b/build/android/local.properties new file mode 100644 index 0000000..b03a2b4 --- /dev/null +++ b/build/android/local.properties @@ -0,0 +1,8 @@ +## This file must *NOT* be checked into Version Control Systems, +# as it contains information specific to your local configuration. +# +# Location of the SDK. This is only used by Gradle. +# For customization when using a Version Control System, please read the +# header note. +#Thu Apr 09 18:23:32 CEST 2020 +sdk.dir=/home/auygun/Android/Sdk diff --git a/build/android/settings.gradle b/build/android/settings.gradle new file mode 100644 index 0000000..573abcb --- /dev/null +++ b/build/android/settings.gradle @@ -0,0 +1,2 @@ +include ':app' + diff --git a/build/linux/Makefile b/build/linux/Makefile new file mode 100644 index 0000000..6e1dfe3 --- /dev/null +++ b/build/linux/Makefile @@ -0,0 +1,174 @@ +.DEFAULT_GOAL := all + +# --- Input variables --- +BUILD ?= release +ifeq ($(findstring $(BUILD),debug release),) + $(error BUILD must be set to debug or release) +endif + +# Build all executables by default. +APPS ?= gltest + +# If the VERBOSE flag isn't set, then mute superfluous output. +ifeq ($(VERBOSE),) + HUSH_COMPILE = @echo "Compiling $<"; + HUSH_LINK = @echo "Linking $@"; + HUSH_GENERATE = @echo "Generating $@"; + HUSH_CLEAN = @ +endif + +# --- Internal variables --- +ARCH := $(shell uname -p) +SRC_ROOT := $(abspath ../../src) +OUTPUT_DIR := $(abspath .) +INTERMEDIATE_DIR := $(OUTPUT_DIR)/obj +BUILD_DIR := $(INTERMEDIATE_DIR)/$(BUILD) + +ARFLAGS = r +LDFLAGS = -lX11 -lGL -lz -pthread -lasound + +# Always enable debug information. +CFLAGS += -g + +# Flags to generate dependency information. +CFLAGS += -MD -MP -MT $@ + +# Predefined flags. +ifeq ($(BUILD), debug) + CFLAGS += -D_DEBUG +endif + +# Enable compiler optimizations for everything except debug. +# Note that a very aggresssive optimization level is used and it may not be +# valid for all standard compliant programs. Reduce this level on individual +# files or modules as needed. +ifneq ($(BUILD), debug) + CFLAGS += -Ofast +endif + +# Flag to turn on extended instruction sets for the compiler. +CFLAGS += -msse2 + +# Let C++ inherit all C flags. +CXXFLAGS = $(CFLAGS) + +# Enable C++17 +CXXFLAGS += -std=c++17 + +# --- Internal functions --- +app_exe = $(OUTPUT_DIR)/$(1)_$(ARCH)_$(BUILD) +objs_from_src = $(patsubst $(SRC_ROOT)/%, $(BUILD_DIR)/%.o, $(basename $(1))) +objs_from_src_in = $(call objs_from_src, $(shell find $(1) -name "*.cc" -o -name "*.cpp" -o -name "*.c")) + +# --- gltest application --- +ifneq ($(filter gltest,$(APPS)),) + +GLTEST_SRC := \ + $(SRC_ROOT)/base/collusion_test.cc \ + $(SRC_ROOT)/base/log.cc \ + $(SRC_ROOT)/base/random.cc \ + $(SRC_ROOT)/base/task_runner.cc \ + $(SRC_ROOT)/base/timer.cc \ + $(SRC_ROOT)/base/vecmath.cc \ + $(SRC_ROOT)/base/worker.cc \ + $(SRC_ROOT)/demo/credits.cc \ + $(SRC_ROOT)/demo/demo.cc \ + $(SRC_ROOT)/demo/enemy.cc \ + $(SRC_ROOT)/demo/hud.cc \ + $(SRC_ROOT)/demo/menu.cc \ + $(SRC_ROOT)/demo/player.cc \ + $(SRC_ROOT)/demo/sky_quad.cc \ + $(SRC_ROOT)/engine/animatable.cc \ + $(SRC_ROOT)/engine/animator.cc \ + $(SRC_ROOT)/engine/audio/audio_alsa.cc \ + $(SRC_ROOT)/engine/audio/audio_base.cc \ + $(SRC_ROOT)/engine/audio/audio_resource.cc \ + $(SRC_ROOT)/engine/engine.cc \ + $(SRC_ROOT)/engine/font.cc \ + $(SRC_ROOT)/engine/image_quad.cc \ + $(SRC_ROOT)/engine/image.cc \ + $(SRC_ROOT)/engine/mesh.cc \ + $(SRC_ROOT)/engine/platform/asset_file_linux.cc \ + $(SRC_ROOT)/engine/platform/asset_file.cc \ + $(SRC_ROOT)/engine/platform/platform_linux.cc \ + $(SRC_ROOT)/engine/platform/platform.cc \ + $(SRC_ROOT)/engine/renderer/geometry.cc \ + $(SRC_ROOT)/engine/renderer/render_command.cc \ + $(SRC_ROOT)/engine/renderer/render_resource.cc \ + $(SRC_ROOT)/engine/renderer/renderer_linux.cc \ + $(SRC_ROOT)/engine/renderer/renderer_types.cc \ + $(SRC_ROOT)/engine/renderer/renderer.cc \ + $(SRC_ROOT)/engine/renderer/shader.cc \ + $(SRC_ROOT)/engine/renderer/texture.cc \ + $(SRC_ROOT)/engine/shader_source.cc \ + $(SRC_ROOT)/engine/solid_quad.cc \ + $(SRC_ROOT)/engine/sound_player.cc \ + $(SRC_ROOT)/engine/sound.cc \ + $(SRC_ROOT)/third_party/glew/glew.c \ + $(SRC_ROOT)/third_party/jsoncpp/jsoncpp.cc \ + $(SRC_ROOT)/third_party/minizip/ioapi.c \ + $(SRC_ROOT)/third_party/minizip/unzip.c \ + $(SRC_ROOT)/third_party/r8b/pffft.cpp \ + $(SRC_ROOT)/third_party/r8b/r8bbase.cpp \ + $(SRC_ROOT)/third_party/texture_compressor/dxt_encoder_internals.cc \ + $(SRC_ROOT)/third_party/texture_compressor/dxt_encoder.cc \ + $(SRC_ROOT)/third_party/texture_compressor/texture_compressor_etc1.cc \ + $(SRC_ROOT)/third_party/texture_compressor/texture_compressor.cc + +GLTEST_EXE := $(call app_exe,gltest) +GLTEST_OBJS := $(call objs_from_src, $(GLTEST_SRC)) +EXES += $(GLTEST_EXE) +OBJS += $(GLTEST_OBJS) + +$(GLTEST_EXE): $(GLTEST_OBJS) $(LIBS) + +endif + + +# --- Build rules --- + +# Dependencies. +DEPS = $(OBJS:.o=.d) +-include $(DEPS) + +.PHONY: all clean cleanall help + +all: $(EXES) + +clean: + @echo "Cleaning..." + $(HUSH_CLEAN) $(RM) -r $(BUILD_DIR) + +cleanall: + @echo "Cleaning all..." + $(HUSH_CLEAN) $(RM) -r $(INTERMEDIATE_DIR) + +help: + @echo "BUILD = Build mode. One of:" + @echo " debug (no optimizations)" + @echo " release (optimizations, the default)" + @echo "APPS = Applications to build. Defaults to all." + @echo "VERBOSE = Full output from commands if set." + +# It's important that libraries are specified last as Ubuntu uses "ld --as-needed" by default. +# Only the static libraries referenced by the object files will be linked into the executable. +# Beware that circular dependencies doesn't work with this flag. +$(EXES): + @mkdir -p $(@D) + $(HUSH_LINK) $(CXX) -o $@ $^ $(LDFLAGS) + +$(BUILD_DIR)/%.a: + @mkdir -p $(@D) + $(HUSH_GENERATE) $(AR) $(ARFLAGS) $@ $^ + +$(BUILD_DIR)/%.o: $(SRC_ROOT)/%.c + @mkdir -p $(@D) + $(HUSH_COMPILE) $(CC) -c $(CFLAGS) -o $@ $< + +$(BUILD_DIR)/%.o: $(SRC_ROOT)/%.cc + @mkdir -p $(@D) + $(HUSH_COMPILE) $(CXX) -c $(CXXFLAGS) -o $@ $< + +$(BUILD_DIR)/%.o: $(SRC_ROOT)/%.cpp + @mkdir -p $(@D) + $(HUSH_COMPILE) $(CXX) -c $(CXXFLAGS) -o $@ $< diff --git a/src/LICENSE b/src/LICENSE new file mode 100644 index 0000000..e1f425d --- /dev/null +++ b/src/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020 Attila Uygun + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/src/base/closure.h b/src/base/closure.h new file mode 100644 index 0000000..0c4036d --- /dev/null +++ b/src/base/closure.h @@ -0,0 +1,12 @@ +#ifndef CLOSURE_H +#define CLOSURE_H + +#include + +namespace base { + +using Closure = std::function; + +} // namespace base + +#endif // CLOSURE_H diff --git a/src/base/collusion_test.cc b/src/base/collusion_test.cc new file mode 100644 index 0000000..f783913 --- /dev/null +++ b/src/base/collusion_test.cc @@ -0,0 +1,51 @@ +#include "collusion_test.h" + +#include +#include +#include + +namespace base { + +bool Intersection(const Vector2& center, + const Vector2& size, + const Vector2& point) { + float dx = point.x - center.x; + float px = size.x / 2 - fabs(dx); + if (px <= 0) + return false; + + float dy = point.y - center.y; + float py = size.y / 2 - fabs(dy); + return py > 0; +} + +bool Intersection(const Vector2& center, + const Vector2& size, + const Vector2& origin, + const Vector2& dir) { + Vector2 min = center - size / 2; + Vector2 max = center + size / 2; + + float tmin = std::numeric_limits::min(); + float tmax = std::numeric_limits::max(); + + if (dir.x != 0.0) { + float tx1 = (min.x - origin.x) / dir.x; + float tx2 = (max.x - origin.x) / dir.x; + + tmin = std::max(tmin, std::min(tx1, tx2)); + tmax = std::min(tmax, std::max(tx1, tx2)); + } + + if (dir.y != 0.0) { + float ty1 = (min.y - origin.y) / dir.y; + float ty2 = (max.y - origin.y) / dir.y; + + tmin = std::max(tmin, std::min(ty1, ty2)); + tmax = std::min(tmax, std::max(ty1, ty2)); + } + + return tmax >= tmin; +} + +} // namespace base diff --git a/src/base/collusion_test.h b/src/base/collusion_test.h new file mode 100644 index 0000000..a62b896 --- /dev/null +++ b/src/base/collusion_test.h @@ -0,0 +1,23 @@ +#ifndef COLLUSION_TEST_H +#define COLLUSION_TEST_H + +#include "vecmath.h" + +namespace base { + +// AABB vs point. +bool Intersection(const Vector2& center, + const Vector2& size, + const Vector2& point); + +// Ray-AABB intersection test. +// center, size: Center and size of the box. +// origin, dir: Origin and direction of the ray. +bool Intersection(const Vector2& center, + const Vector2& size, + const Vector2& origin, + const Vector2& dir); + +} // namespace base + +#endif // COLLUSION_TEST_H diff --git a/src/base/file.h b/src/base/file.h new file mode 100644 index 0000000..154c499 --- /dev/null +++ b/src/base/file.h @@ -0,0 +1,25 @@ +#ifndef FILE_H +#define FILE_H + +#include +#include + +namespace internal { + +struct ScopedFILECloser { + inline void operator()(FILE* x) const { + if (x) + fclose(x); + } +}; + +} // namespace internal + +namespace base { + +// Automatically closes file. +using ScopedFILE = std::unique_ptr; + +} // namespace base + +#endif // FILE_H diff --git a/src/base/hash.h b/src/base/hash.h new file mode 100644 index 0000000..06b7020 --- /dev/null +++ b/src/base/hash.h @@ -0,0 +1,21 @@ +#ifndef HASH_H +#define HASH_H + +#include + +#define HHASH(x) base::HornerHash(31, x) + +namespace base { + +// Compile time string hashing function. +template +constexpr inline size_t HornerHash(size_t prime, + const char (&str)[N], + size_t Len = N - 1) { + return (Len <= 1) ? str[0] + : (prime * HornerHash(prime, str, Len - 1) + str[Len - 1]); +} + +} // namespace base + +#endif // HASH_H diff --git a/src/base/interpolation.h b/src/base/interpolation.h new file mode 100644 index 0000000..bfe7f44 --- /dev/null +++ b/src/base/interpolation.h @@ -0,0 +1,43 @@ +#ifndef INTERPOLATION_H +#define INTERPOLATION_H + +namespace base { + +// Round a float to int. +inline int Round(float f) { + return int(f + 0.5f); +} + +// Linearly interpolate between a and b, by fraction t. +template +inline T Lerp(const T& a, const T& b, float t) { + return a + (b - a) * t; +} + +template <> +inline int Lerp(const int& a, const int& b, float t) { + return Round(a + (b - a) * t); +} + +inline float SmoothStep(float t) { + return t * t * (3 - 2 * t); +} + +inline float SmootherStep(float t) { + return t * t * t * (t * (t * 6 - 15) + 10); +} + +// Interpolating spline defined by four control points with the curve drawn only +// from 0 to 1 which are p1 and p2 respectively. +inline float CatmullRom(float t, float p0, float p3) { + return 0.5f * ((-p0 + 1) * t + (2 * p0 + 4 * 1 - p3) * t * t + + (-p0 - 3 * 1 + p3) * t * t * t); +} + +inline float Acceleration(float t, float w) { + return w * t * t + (1 - w) * t; +} + +} // namespace base + +#endif // INTERPOLATION_H diff --git a/src/base/log.cc b/src/base/log.cc new file mode 100644 index 0000000..8eeeba6 --- /dev/null +++ b/src/base/log.cc @@ -0,0 +1,52 @@ +#include "log.h" + +#if defined(__ANDROID__) +#include +#else +#include +#endif + +namespace base { + +// This is never instantiated, it's just used for EAT_STREAM_PARAMETERS to have +// an object of the correct type on the LHS of the unused part of the ternary +// operator. +Log* Log::swallow_stream; + +Log::Log(const char* file, int line) : file_(file), line_(line) {} + +Log::~Log() { + stream_ << std::endl; + std::string text(stream_.str()); + std::string filename(file_); + size_t last_slash_pos = filename.find_last_of("\\/"); + if (last_slash_pos != std::string::npos) + filename = filename.substr(last_slash_pos + 1); +#if defined(__ANDROID__) + __android_log_print(ANDROID_LOG_ERROR, "kaliber", "[%s:%d] %s", + filename.c_str(), line_, text.c_str()); +#else + printf("[%s:%d] %s", filename.c_str(), line_, text.c_str()); +#endif +} + +template <> +Log& Log::operator<<(const bool& arg) { + stream_ << (arg ? "true" : "false"); + return *this; +} + +template <> +Log& Log::operator<<(const Vector2& arg) { + stream_ << "(" << arg.x << ", " << arg.y << ")"; + return *this; +} + +template <> +Log& Log::operator<<(const Vector4& arg) { + stream_ << "(" << arg.x << ", " << arg.y << ", " << arg.z << ", " << arg.w + << ")"; + return *this; +} + +} // namespace base diff --git a/src/base/log.h b/src/base/log.h new file mode 100644 index 0000000..a83505f --- /dev/null +++ b/src/base/log.h @@ -0,0 +1,49 @@ +#ifndef LOG_H +#define LOG_H + +#include +#include "vecmath.h" + +#define EAT_STREAM_PARAMETERS \ + true ? (void)0 : base::Log::Voidify() & (*base::Log::swallow_stream) + +#define LOG base::Log(__FILE__, __LINE__) + +#ifdef _DEBUG +#define DLOG base::Log(__FILE__, __LINE__) +#else +#define DLOG EAT_STREAM_PARAMETERS +#endif + +namespace base { + +class Log { + public: + class Voidify { + public: + Voidify() = default; + // This has to be an operator with a precedence lower than << but + // higher than ?: + void operator&(Log&) {} + }; + + Log(const char* file, int line); + ~Log(); + + template + Log& operator<<(const T& arg) { + stream_ << arg; + return *this; + } + + static Log* swallow_stream; + + private: + const char* file_; + const int line_; + std::ostringstream stream_; +}; + +} // namespace base + +#endif // LOG_H diff --git a/src/base/mem.h b/src/base/mem.h new file mode 100644 index 0000000..81b5c58 --- /dev/null +++ b/src/base/mem.h @@ -0,0 +1,52 @@ +#ifndef MEM_H +#define MEM_H + +#include +#include +#include + +#if defined(__ANDROID__) +#include +#endif + +#define ALIGN_MEM(alignment) __attribute__((aligned(alignment))) + +namespace internal { + +struct ScopedAlignedFree { + inline void operator()(void* x) const { + if (x) + free(x); + } +}; + +} // namespace internal + +namespace base { + +template +struct AlignedMem { + using ScoppedPtr = std::unique_ptr; +}; + +template +inline void* AlignedAlloc(size_t size) { + void* ptr = NULL; +#if defined(__ANDROID__) + ptr = memalign(kAlignment, size); +#else + if (posix_memalign(&ptr, kAlignment, size)) + ptr = NULL; +#endif + assert(ptr); + // assert(((unsigned)ptr & (kAlignment - 1)) == 0); + return ptr; +} + +inline void AlignedFree(void* mem) { + free(mem); +} + +} // namespace base + +#endif // MEM_H diff --git a/src/base/misc.h b/src/base/misc.h new file mode 100644 index 0000000..13fc0ef --- /dev/null +++ b/src/base/misc.h @@ -0,0 +1,34 @@ +#ifndef MISC_H +#define MISC_H + +#define CRASH *((int*)nullptr) = 0; + +namespace base { + +// ToDo: x86 has the bsr instruction. +inline int GetHighestBitPos(int value) { + return (0xFFFF0000 & value ? value &= 0xFFFF0000, 1 : 0) * 0x10 + + (0xFF00FF00 & value ? value &= 0xFF00FF00, 1 : 0) * 0x08 + + (0xF0F0F0F0 & value ? value &= 0xF0F0F0F0, 1 : 0) * 0x04 + + (0xCCCCCCCC & value ? value &= 0xCCCCCCCC, 1 : 0) * 0x02 + + (0xAAAAAAAA & value ? 1 : 0) * 0x01; +} + +// Get the highest set bit in an integer number +inline int GetHighestBit(int value) { + return 0x1 << GetHighestBitPos(value); +} + +// Check if the given integer is a power of two, ie if only one bit is set. +inline bool IsPow2(int value) { + return GetHighestBit(value) == value; +} + +inline int RoundUpToPow2(int val) { + int i = GetHighestBit(val); + return val == i ? val : i << 1; +} + +} // namespace base + +#endif // MISC_H diff --git a/src/base/random.cc b/src/base/random.cc new file mode 100644 index 0000000..c9c7066 --- /dev/null +++ b/src/base/random.cc @@ -0,0 +1,26 @@ +#include "random.h" + +#include + +#include "interpolation.h" + +namespace base { + +Random::Random() { + std::random_device rd; + generator_ = std::mt19937(rd()); + real_distribution_ = std::uniform_real_distribution(0, 1); +} + +Random::Random(unsigned seed) { + generator_ = std::mt19937(seed); + real_distribution_ = std::uniform_real_distribution(0, 1); +} + +Random::~Random() = default; + +int Random::Roll(int sides) { + return Lerp(1, sides, GetFloat()); +} + +} // namespace base diff --git a/src/base/random.h b/src/base/random.h new file mode 100644 index 0000000..aab1f10 --- /dev/null +++ b/src/base/random.h @@ -0,0 +1,27 @@ +#ifndef RANDOM_GENERATOR_H +#define RANDOM_GENERATOR_H + +#include + +namespace base { + +class Random { + public: + Random(); + Random(unsigned seed); + ~Random(); + + // Returns a random float between 0 and 1. + float GetFloat() { return real_distribution_(generator_); } + + // Roll dice with the given number of sides. + int Roll(int sides); + + private: + std::mt19937 generator_; + std::uniform_real_distribution real_distribution_; +}; + +} // namespace base + +#endif // RANDOM_GENERATOR_H diff --git a/src/base/task_runner.cc b/src/base/task_runner.cc new file mode 100644 index 0000000..30261de --- /dev/null +++ b/src/base/task_runner.cc @@ -0,0 +1,30 @@ +#include "task_runner.h" + +namespace base { + +void TaskRunner::Enqueue(base::Closure task) { + std::unique_lock scoped_lock(mutex_); + thread_tasks_.emplace_back(std::move(task)); +} + +void TaskRunner::Run() { + for (;;) { + base::Closure task; + { + std::unique_lock scoped_lock(mutex_); + if (!thread_tasks_.empty()) { + task.swap(thread_tasks_.front()); + thread_tasks_.pop_front(); + } + } + if (!task) + break; + task(); + } +} + +bool TaskRunner::IsBoundToCurrentThread() { + return thread_id_ == std::this_thread::get_id(); +} + +} // namespace base diff --git a/src/base/task_runner.h b/src/base/task_runner.h new file mode 100644 index 0000000..943f0ed --- /dev/null +++ b/src/base/task_runner.h @@ -0,0 +1,32 @@ +#ifndef TASK_RUNNER_H +#define TASK_RUNNER_H + +#include +#include +#include +#include "closure.h" + +namespace base { + +class TaskRunner { + public: + TaskRunner() = default; + ~TaskRunner() = default; + + void Enqueue(base::Closure cb); + void Run(); + + bool IsBoundToCurrentThread(); + + private: + std::thread::id thread_id_ = std::this_thread::get_id(); + std::mutex mutex_; + std::deque thread_tasks_; + + TaskRunner(TaskRunner const&) = delete; + TaskRunner& operator=(TaskRunner const&) = delete; +}; + +} // namespace base + +#endif // TASK_RUNNER_H diff --git a/src/base/timer.cc b/src/base/timer.cc new file mode 100644 index 0000000..e30b30f --- /dev/null +++ b/src/base/timer.cc @@ -0,0 +1,28 @@ +#include "timer.h" + +namespace base { + +Timer::Timer() { + Reset(); +} + +void Timer::Reset() { + gettimeofday(&last_time_, nullptr); + + seconds_passed_ = 0.0f; + seconds_accumulated_ = 0.0f; +} + +void Timer::Update() { + timeval currentTime; + gettimeofday(¤tTime, nullptr); + seconds_passed_ = + (float)(currentTime.tv_sec - last_time_.tv_sec) + + 0.000001f * (float)(currentTime.tv_usec - last_time_.tv_usec); + + last_time_ = currentTime; + + seconds_accumulated_ += seconds_passed_; +} + +} // namespace base diff --git a/src/base/timer.h b/src/base/timer.h new file mode 100644 index 0000000..24d222f --- /dev/null +++ b/src/base/timer.h @@ -0,0 +1,29 @@ +#ifndef TIMER_H +#define TIMER_H + +#include + +namespace base { + +class Timer { + public: + Timer(); + ~Timer() = default; + + void Reset(); + + void Update(); + + float GetSecondsPassed() const { return seconds_passed_; } + float GetSecondsAccumulated() const { return seconds_accumulated_; } + + private: + float seconds_passed_ = 0.0f; + float seconds_accumulated_ = 0.0f; + + timeval last_time_; +}; + +} // namespace base + +#endif // TIMER_H diff --git a/src/base/vecmath.cc b/src/base/vecmath.cc new file mode 100644 index 0000000..126106b --- /dev/null +++ b/src/base/vecmath.cc @@ -0,0 +1,15 @@ +#include "vecmath.h" + +namespace base { + +Matrix4x4 Ortho(float left, float right, float bottom, float top) { + Matrix4x4 m(1); + m.c[0].x = 2.0f / (right - left); + m.c[1].y = 2.0f / (top - bottom); + m.c[2].z = -1.0f; + m.c[3].x = -(right + left) / (right - left); + m.c[3].y = -(top + bottom) / (top - bottom); + return m; +} + +} // namespace base diff --git a/src/base/vecmath.h b/src/base/vecmath.h new file mode 100644 index 0000000..6649a3d --- /dev/null +++ b/src/base/vecmath.h @@ -0,0 +1,163 @@ +#ifndef VEC_MATH_H +#define VEC_MATH_H + +#include +#include + +namespace base { + +struct Vector2 { + float x, y; + + Vector2() {} + Vector2(float _x, float _y) : x(_x), y(_y) {} + + float Magnitude() { return sqrt(x * x + y * y); } + + Vector2 Normalize() { + float m = Magnitude(); + x /= m; + y /= m; + return *this; + } + + float DotProduct(const Vector2& v) { return x * v.x + y * v.y; } + + float CrossProduct(const Vector2& v) { return x * v.y - y * v.x; } + + Vector2 operator-() { return Vector2(x * -1.0f, y * -1.0f); } + + Vector2 operator+=(const Vector2& v) { + x += v.x; + y += v.y; + return *this; + } + + Vector2 operator-=(const Vector2& v) { + x -= v.x; + y -= v.y; + return *this; + } + + Vector2 operator*=(const Vector2& v) { + x *= v.x; + y *= v.y; + return *this; + } + + Vector2 operator*=(float s) { + x *= s; + y *= s; + return *this; + } + + Vector2 operator/=(const Vector2& v) { + x /= v.x; + y /= v.y; + return *this; + } + + Vector2 operator/=(float s) { + x /= s; + y /= s; + return *this; + } + + const float* GetData() const { return &x; } +}; + +inline Vector2 operator+(const Vector2& v1, const Vector2& v2) { + return Vector2(v1.x + v2.x, v1.y + v2.y); +} + +inline Vector2 operator-(const Vector2& v1, const Vector2& v2) { + return Vector2(v1.x - v2.x, v1.y - v2.y); +} + +inline Vector2 operator*(const Vector2& v1, const Vector2& v2) { + return Vector2(v1.x * v2.x, v1.y * v2.y); +} + +inline Vector2 operator/(const Vector2& v1, const Vector2& v2) { + return Vector2(v1.x / v2.x, v1.y / v2.y); +} + +inline Vector2 operator*(const Vector2& v, float s) { + return Vector2(v.x * s, v.y * s); +} + +inline Vector2 operator/(const Vector2& v, float s) { + return Vector2(v.x / s, v.y / s); +} + +inline bool operator==(const Vector2& v1, const Vector2& v2) { + return v1.x == v2.x && v1.y == v2.y; +} + +inline bool operator!=(const Vector2& v1, const Vector2& v2) { + return v1.x != v2.x || v1.y != v2.y; +} + +struct Vector3 { + float x, y, z; + + Vector3() {} + Vector3(float _x, float _y, float _z) : x(_x), y(_y), z(_z) {} + + const float* GetData() const { return &x; } +}; + +inline Vector3 operator+(const Vector3& v1, const Vector3& v2) { + return Vector3(v1.x + v2.x, v1.y + v2.y, v1.z + v2.z); +} + +struct Vector4 { + float x, y, z, w; + + Vector4() {} + Vector4(float _x, float _y, float _z, float _w) + : x(_x), y(_y), z(_z), w(_w) {} + + Vector4 operator+=(const Vector4& v) { + x += v.x; + y += v.y; + z += v.z; + w += v.w; + return *this; + } + + const float* GetData() const { return &x; } +}; + +inline Vector4 operator*(const Vector4& v1, const Vector4& v2) { + return Vector4(v1.x * v2.x, v2.y * v2.y, v1.z * v2.z, v1.w * v2.w); +} + +inline Vector4 operator*(const Vector4& v, float s) { + return Vector4(v.x * s, v.y * s, v.z * s, v.w * s); +} + +inline Vector4 operator+(const Vector4& v1, const Vector4& v2) { + return Vector4(v1.x + v2.x, v1.y + v2.y, v1.z + v2.z, v1.w + v2.w); +} + +inline Vector4 operator-(const Vector4& v1, const Vector4& v2) { + return Vector4(v1.x - v2.x, v1.y - v2.y, v1.z - v2.z, v1.w - v2.w); +} + +struct Matrix4x4 { + Vector4 c[4]; + + Matrix4x4() {} + Matrix4x4(float s) + : c{Vector4(s, 0, 0, 0), Vector4(0, s, 0, 0), Vector4(0, 0, s, 0), + Vector4(0, 0, 0, s)} {} + + const float* GetData() const { return &c[0].x; } +}; + +Matrix4x4 Ortho(float left, float right, float bottom, float top); + +} // namespace base + +#endif // VEC_MATH_H diff --git a/src/base/worker.cc b/src/base/worker.cc new file mode 100644 index 0000000..e0ae406 --- /dev/null +++ b/src/base/worker.cc @@ -0,0 +1,68 @@ +#include "worker.h" +#include "log.h" + +namespace base { + +Worker::Worker(unsigned max_concurrency) : max_concurrency_(max_concurrency) { + if (max_concurrency_ > std::thread::hardware_concurrency() || + max_concurrency_ == 0) { + max_concurrency_ = std::thread::hardware_concurrency(); + if (max_concurrency_ == 0) + max_concurrency_ = 1; + } +} + +Worker::~Worker() = default; + +void Worker::Enqueue(base::Closure task) { + if (!active_) { + unsigned concurrency = max_concurrency_; + while (concurrency--) + threads_.emplace_back(&Worker::WorkerMain, this); + active_ = true; + } + + bool notify; + { + std::unique_lock scoped_lock(mutex_); + notify = tasks_.empty(); + tasks_.emplace_back(std::move(task)); + } + if (notify) + cv_.notify_all(); +} + +void Worker::Join() { + if (!active_) + return; + + { + std::unique_lock scoped_lock(mutex_); + quit_when_idle_ = true; + } + cv_.notify_all(); + for (auto& thread : threads_) + thread.join(); + threads_.clear(); + active_ = false; +} + +void Worker::WorkerMain() { + for (;;) { + base::Closure task; + { + std::unique_lock scoped_lock(mutex_); + while (tasks_.empty()) { + if (quit_when_idle_) + return; + cv_.wait(scoped_lock); + } + task.swap(tasks_.front()); + tasks_.pop_front(); + } + + task(); + } +} + +} // namespace base diff --git a/src/base/worker.h b/src/base/worker.h new file mode 100644 index 0000000..7059941 --- /dev/null +++ b/src/base/worker.h @@ -0,0 +1,40 @@ +#ifndef WORKER_H +#define WORKER_H + +#include +#include +#include +#include +#include +#include "closure.h" + +namespace base { + +// Feed the worker tasks and they will be called on a thread from the pool. +class Worker { + public: + Worker(unsigned max_concurrency = 0); + ~Worker(); + + void Enqueue(base::Closure task); + void Join(); + + private: + bool active_ = false; + unsigned max_concurrency_ = 0; + + std::condition_variable cv_; + std::mutex mutex_; + std::vector threads_; + std::deque tasks_; + bool quit_when_idle_ = false; + + void WorkerMain(); + + Worker(Worker const&) = delete; + Worker& operator=(Worker const&) = delete; +}; + +} // namespace base + +#endif // WORKER_H diff --git a/src/demo/credits.cc b/src/demo/credits.cc new file mode 100644 index 0000000..095f584 --- /dev/null +++ b/src/demo/credits.cc @@ -0,0 +1,138 @@ +#include "credits.h" + +#include "../base/log.h" +#include "../base/vecmath.h" +#include "../base/worker.h" +#include "../engine/engine.h" +#include "../engine/font.h" +#include "../engine/image.h" +#include "../engine/input_event.h" +#include "../engine/renderer/texture.h" +#include "demo.h" + +using namespace base; +using namespace eng; + +namespace { + +constexpr char kCreditsLines[Credits::kNumLines][15] = { + "Credits", "Code:", "Attila Uygun", "Graphics:", "Erkan Erturk"}; + +constexpr float kLineSpaces[Credits::kNumLines - 1] = {1.5f, 0.5f, 1.5f, 0.5f}; + +const Vector4 kTextColor = {0.3f, 0.55f, 1.0f, 1}; +constexpr float kFadeSpeed = 0.2f; + +} // namespace + +Credits::Credits() = default; + +Credits::~Credits() = default; + +bool Credits::Initialize() { + const Font& font = static_cast(Engine::Get().GetGame())->GetFont(); + + max_text_width_ = -1; + for (int i = 0; i < kNumLines; ++i) { + int width, height; + font.CalculateBoundingBox(kCreditsLines[i], width, height); + if (width > max_text_width_) + max_text_width_ = width; + } + + for (int i = 0; i < kNumLines; ++i) + text_animator_.Attach(&text_[i]); + + return true; +} + +void Credits::Update(float delta_time) { + text_animator_.Update(delta_time); +} + +void Credits::OnInputEvent(std::unique_ptr event) { + if ((event->GetType() == InputEvent::kTap || + event->GetType() == InputEvent::kDragEnd || + event->GetType() == InputEvent::kNavigateBack) && + !text_animator_.IsPlaying(Animator::kBlending)) { + Hide(); + Engine& engine = Engine::Get(); + static_cast(engine.GetGame())->EnterMenuState(); + } +} + +void Credits::Draw() { + for (int i = 0; i < kNumLines; ++i) + text_[i].Draw(); +} + +void Credits::ContextLost() { + if (tex_) + tex_->Update(CreateImage()); +} + +void Credits::Show() { + tex_ = Engine::Get().CreateRenderResource(); + tex_->Update(CreateImage()); + + for (int i = 0; i < kNumLines; ++i) { + text_[i].Create(tex_, {1, kNumLines}); + text_[i].SetOffset({0, 0}); + text_[i].SetScale({1, 1}); + text_[i].AutoScale(); + text_[i].SetColor(kTextColor * Vector4(1, 1, 1, 0)); + text_[i].SetFrame(i); + + if (i > 0) { + text_[i].PlaceToBottomOf(text_[i - 1]); + text_[i].Translate(text_[i - 1].GetOffset() * Vector2(0, 1)); + text_[i].Translate({0, text_[i - 1].GetScale().y * -kLineSpaces[i - 1]}); + } + } + + float center_offset_y = + (text_[0].GetOffset().y - text_[kNumLines - 1].GetOffset().y) / 2; + for (int i = 0; i < kNumLines; ++i) + text_[i].Translate({0, center_offset_y}); + + text_animator_.SetEndCallback(Animator::kBlending, [&]() -> void { + text_animator_.SetEndCallback(Animator::kBlending, nullptr); + }); + text_animator_.SetBlending(kTextColor, kFadeSpeed); + text_animator_.Play(Animator::kBlending, false); + text_animator_.SetVisible(true); +} + +void Credits::Hide() { + text_animator_.SetEndCallback(Animator::kBlending, [&]() -> void { + for (int i = 0; i < kNumLines; ++i) + text_[i].Destory(); + tex_.reset(); + text_animator_.SetEndCallback(Animator::kBlending, nullptr); + text_animator_.SetVisible(false); + }); + text_animator_.SetBlending(kTextColor * Vector4(1, 1, 1, 0), kFadeSpeed); + text_animator_.Play(Animator::kBlending, false); +} + +std::unique_ptr Credits::CreateImage() { + const Font& font = static_cast(Engine::Get().GetGame())->GetFont(); + + int line_height = font.GetLineHeight() + 1; + auto image = std::make_unique(); + image->Create(max_text_width_, line_height * kNumLines); + image->Clear({1, 1, 1, 0}); + + Worker worker(kNumLines); + for (int i = 0; i < kNumLines; ++i) { + int w, h; + font.CalculateBoundingBox(kCreditsLines[i], w, h); + float x = (image->GetWidth() - w) / 2; + float y = line_height * i; + worker.Enqueue(std::bind(&Font::Print, &font, x, y, kCreditsLines[i], + image->GetBuffer(), image->GetWidth())); + } + worker.Join(); + + return image; +} diff --git a/src/demo/credits.h b/src/demo/credits.h new file mode 100644 index 0000000..d5a3447 --- /dev/null +++ b/src/demo/credits.h @@ -0,0 +1,47 @@ +#ifndef CREDITS_H +#define CREDITS_H + +#include +#include + +#include "../engine/animator.h" +#include "../engine/image_quad.h" + +namespace eng { +class Image; +class InputEvent; +class Texture; +} // namespace eng + +class Credits { + public: + static constexpr int kNumLines = 5; + + Credits(); + ~Credits(); + + bool Initialize(); + + void Update(float delta_time); + + void OnInputEvent(std::unique_ptr event); + + void Draw(); + + void ContextLost(); + + void Show(); + void Hide(); + + private: + std::shared_ptr tex_; + + eng::ImageQuad text_[kNumLines]; + eng::Animator text_animator_; + + int max_text_width_ = 0; + + std::unique_ptr CreateImage(); +}; + +#endif // CREDITS_H diff --git a/src/demo/damage_type.h b/src/demo/damage_type.h new file mode 100644 index 0000000..6757427 --- /dev/null +++ b/src/demo/damage_type.h @@ -0,0 +1,20 @@ +#ifndef DAMAGE_TYPE_H +#define DAMAGE_TYPE_H + +enum DamageType { + kDamageType_Invalid = -1, + kDamageType_Green, + kDamageType_Blue, + kDamageType_Any, + kDamageType_Max +}; + +enum EnemyType { + kEnemyType_Invalid = -1, + kEnemyType_Skull, + kEnemyType_Bug, + kEnemyType_Tank, + kEnemyType_Max +}; + +#endif // DAMAGE_TYPE_H diff --git a/src/demo/demo.cc b/src/demo/demo.cc new file mode 100644 index 0000000..7ea118d --- /dev/null +++ b/src/demo/demo.cc @@ -0,0 +1,251 @@ +#include "demo.h" + +#include +#include + +#include "../base/interpolation.h" +#include "../base/log.h" +#include "../base/random.h" +#include "../engine/engine.h" +#include "../engine/game_factory.h" +#include "../engine/input_event.h" + +DECLARE_GAME_BEGIN +DECLARE_GAME(Demo) +DECLARE_GAME_END + +using namespace base; +using namespace eng; + +bool Demo::Initialize() { + if (!font_.Load("PixelCaps!.ttf")) + return false; + + if (!sky_.Create()) { + LOG << "Could not create the sky."; + return false; + } + + if (!enemy_.Initialize()) { + LOG << "Failed to create the enemy."; + return false; + } + + if (!player_.Initialize()) { + LOG << "Failed to create the enemy."; + return false; + } + + if (!hud_.Initialize()) { + LOG << "Failed to create the hud."; + return false; + } + + if (!menu_.Initialize()) { + LOG << "Failed to create the menu."; + return false; + } + + if (!credits_.Initialize()) { + LOG << "Failed to create the credits."; + return false; + } + + EnterMenuState(); + + return true; +} + +void Demo::Update(float delta_time) { + Engine& engine = Engine::Get(); + + while (std::unique_ptr event = engine.GetNextInputEvent()) { + if (state_ == kMenu) + menu_.OnInputEvent(std::move(event)); + else if (state_ == kCredits) + credits_.OnInputEvent(std::move(event)); + else + player_.OnInputEvent(std::move(event)); + } + + if (delayed_work_timer_ > 0) { + delayed_work_timer_ -= delta_time; + if (delayed_work_timer_ <= 0) { + base::Closure cb = std::move(delayed_work_cb_); + delayed_work_cb_ = nullptr; + cb(); + } + } + + if (add_score_ > 0) { + score_ += add_score_; + add_score_ = 0; + hud_.PrintScore(score_, true); + } + + hud_.Update(delta_time); + menu_.Update(delta_time); + credits_.Update(delta_time); + + if (state_ == kMenu) + UpdateMenuState(delta_time); + else if (state_ == kGame) + UpdateGameState(delta_time); +} + +void Demo::Draw(float frame_frac) { + sky_.Draw(frame_frac); + player_.Draw(frame_frac); + enemy_.Draw(frame_frac); + hud_.Draw(); + menu_.Draw(); + credits_.Draw(); +} + +void Demo::ContextLost() { + enemy_.ContextLost(); + player_.ContextLost(); + hud_.ContextLost(); + menu_.ContextLost(); + credits_.ContextLost(); + sky_.ContextLost(); +} + +void Demo::LostFocus() { + if (state_ == kGame) + EnterMenuState(); +} + +void Demo::GainedFocus() {} + +void Demo::AddScore(int score) { + add_score_ += score; +} + +void Demo::EnterMenuState() { + if (state_ == kMenu) + return; + if (wave_ == 0) { + menu_.SetOptionEnabled(Menu::kContinue, false); + } else { + menu_.SetOptionEnabled(Menu::kContinue, true); + menu_.SetOptionEnabled(Menu::kNewGame, false); + } + menu_.Show(); + state_ = kMenu; +} + +void Demo::EnterCreditsState() { + if (state_ == kCredits) + return; + credits_.Show(); + state_ = kCredits; +} + +void Demo::EnterGameState() { + if (state_ == kGame) + return; + hud_.Show(); + state_ = kGame; +} + +void Demo::UpdateMenuState(float delta_time) { + switch (menu_.selected_option()) { + case Menu::kOption_Invalid: + break; + case Menu::kContinue: + menu_.Hide(); + Continue(); + break; + case Menu::kNewGame: + menu_.Hide(); + StartNewGame(); + break; + case Menu::kCredits: + menu_.Hide(); + EnterCreditsState(); + break; + case Menu::kExit: + Engine::Get().Exit(); + break; + default: + assert(false); + } +} + +void Demo::UpdateGameState(float delta_time) { + sky_.Update(delta_time); + player_.Update(delta_time); + enemy_.Update(delta_time); + + if (waiting_for_next_wave_) + return; + + if (enemy_.num_enemies_killed_in_current_wave() != last_num_enemies_killed_) { + last_num_enemies_killed_ = enemy_.num_enemies_killed_in_current_wave(); + int enemies_remaining = total_enemies_ - last_num_enemies_killed_; + + if (enemies_remaining <= 0) { + waiting_for_next_wave_ = true; + hud_.SetProgress(wave_ > 0 ? 0 : 1); + + enemy_.OnWaveFinished(); + + SetDelayedWork(1, [&]() -> void { + Random& rnd = Engine::Get().GetRandomGenerator(); + int dominant_channel = rnd.Roll(3) - 1; + if (dominant_channel == last_dominant_channel_) + dominant_channel = (dominant_channel + 1) % 3; + last_dominant_channel_ = dominant_channel; + + float weights[3] = {0, 0, 0}; + weights[dominant_channel] = 1; + Vector4 c = {Lerp(0.75f, 0.95f, rnd.GetFloat()) * weights[0], + Lerp(0.75f, 0.95f, rnd.GetFloat()) * weights[1], + Lerp(0.75f, 0.95f, rnd.GetFloat()) * weights[2], 1}; + c += {Lerp(0.1f, 0.5f, rnd.GetFloat()) * (1 - weights[0]), + Lerp(0.1f, 0.5f, rnd.GetFloat()) * (1 - weights[1]), + Lerp(0.1f, 0.5f, rnd.GetFloat()) * (1 - weights[2]), 1}; + sky_.SwitchColor(c); + + ++wave_; + hud_.PrintScore(score_, true); + hud_.PrintWave(wave_, true); + hud_.SetProgress(1); + + float factor = 3 * (log10(5 * (float)wave_) / log10(1.2f)) - 25; + total_enemies_ = (int)(6 * factor); + last_num_enemies_killed_ = 0; + DLOG << "wave: " << wave_ << " total_enemies_: " << total_enemies_; + + enemy_.OnWaveStarted(wave_); + + waiting_for_next_wave_ = false; + }); + } else { + hud_.SetProgress((float)enemies_remaining / (float)total_enemies_); + } + } +} + +void Demo::Continue() { + EnterGameState(); +} + +void Demo::StartNewGame() { + score_ = 0; + add_score_ = 0; + wave_ = 0; + last_num_enemies_killed_ = -1; + total_enemies_ = 0; + waiting_for_next_wave_ = false; + delayed_work_timer_ = 0; + delayed_work_cb_ = nullptr; + EnterGameState(); +} + +void Demo::SetDelayedWork(float seconds, base::Closure cb) { + assert(delayed_work_cb_ == nullptr); + delayed_work_cb_ = std::move(cb); + delayed_work_timer_ = seconds; +} diff --git a/src/demo/demo.h b/src/demo/demo.h new file mode 100644 index 0000000..6c06aab --- /dev/null +++ b/src/demo/demo.h @@ -0,0 +1,82 @@ +#ifndef DEMO_H +#define DEMO_H + +#include "../base/closure.h" +#include "../engine/font.h" +#include "../engine/game.h" +#include "credits.h" +#include "enemy.h" +#include "hud.h" +#include "menu.h" +#include "player.h" +#include "sky_quad.h" + +class Demo : public eng::Game { + public: + Demo() = default; + ~Demo() override = default; + + bool Initialize() override; + + void Update(float delta_time) override; + + void Draw(float frame_frac) override; + + void ContextLost() override; + + void LostFocus() override; + + void GainedFocus() override; + + void AddScore(int score); + + void EnterMenuState(); + void EnterCreditsState(); + void EnterGameState(); + + const eng::Font& GetFont() { return font_; } + + Player& GetPlayer() { return player_; } + Enemy& GetEnemy() { return enemy_; } + + int wave() { return wave_; } + + private: + enum State { kState_Invalid = -1, kMenu, kGame, kCredits, kState_Max }; + + State state_ = kState_Invalid; + + Player player_; + Enemy enemy_; + Hud hud_; + Menu menu_; + Credits credits_; + + SkyQuad sky_; + int last_dominant_channel_ = -1; + + eng::Font font_; + + int score_ = 0; + int add_score_ = 0; + + int wave_ = 0; + + int last_num_enemies_killed_ = -1; + int total_enemies_ = 0; + + int waiting_for_next_wave_ = false; + + float delayed_work_timer_ = 0; + base::Closure delayed_work_cb_; + + void UpdateMenuState(float delta_time); + void UpdateGameState(float delta_time); + + void Continue(); + void StartNewGame(); + + void SetDelayedWork(float seconds, base::Closure cb); +}; + +#endif // DEMO_H diff --git a/src/demo/enemy.cc b/src/demo/enemy.cc new file mode 100644 index 0000000..3584f92 --- /dev/null +++ b/src/demo/enemy.cc @@ -0,0 +1,476 @@ +#include "enemy.h" + +#include +#include +#include + +#include "../base/collusion_test.h" +#include "../base/interpolation.h" +#include "../base/log.h" +#include "../engine/engine.h" +#include "../engine/font.h" +#include "../engine/image.h" +#include "../engine/renderer/texture.h" +#include "../engine/sound.h" +#include "demo.h" + +using namespace base; +using namespace eng; + +namespace { + +constexpr int enemy_frame_start[][3] = {{0, 50, -1}, + {13, 33, -1}, + {-1, -1, 100}}; +constexpr int enemy_frame_count[][3] = {{7, 7, -1}, {6, 6, -1}, {-1, -1, 7}}; +constexpr int enemy_frame_speed = 12; + +constexpr int enemy_scores[] = {100, 150, 300}; + +constexpr float kSpawnPeriod[kEnemyType_Max][2] = {{2, 5}, + {15, 25}, + {110, 130}}; + +void SetupFadeOutAnim(Animator& animator, float delay) { + animator.SetEndCallback(Animator::kTimer, [&]() -> void { + animator.SetBlending({1, 1, 1, 0}, 0.5f, + std::bind(Acceleration, std::placeholders::_1, -1)); + animator.Play(Animator::kBlending, false); + }); + animator.SetEndCallback(Animator::kBlending, + [&]() -> void { animator.SetVisible(false); }); + animator.SetTimer(delay); +} + +} // namespace + +Enemy::Enemy() + : skull_tex_(Engine::Get().CreateRenderResource()), + bug_tex_(Engine::Get().CreateRenderResource()), + target_tex_(Engine::Get().CreateRenderResource()), + blast_tex_(Engine::Get().CreateRenderResource()), + score_tex_{Engine::Get().CreateRenderResource(), + Engine::Get().CreateRenderResource(), + Engine::Get().CreateRenderResource()} {} + +Enemy::~Enemy() = default; + +bool Enemy::Initialize() { + explosion_sound_ = std::make_shared(); + if (!explosion_sound_->Load("explosion.mp3")) + return false; + + return CreateRenderResources(); +} + +void Enemy::ContextLost() { + CreateRenderResources(); +} + +void Enemy::Update(float delta_time) { + if (!waiting_for_next_wave_) { + if (spawn_factor_interpolator_ < 1) { + spawn_factor_interpolator_ += delta_time * 0.1f; + if (spawn_factor_interpolator_ > 1) + spawn_factor_interpolator_ = 1; + } + + for (int i = 0; i < kEnemyType_Max; ++i) + seconds_since_last_spawn_[i] += delta_time; + + SpawnNextEnemy(); + } + + for (auto it = enemies_.begin(); it != enemies_.end(); ++it) { + if (it->marked_for_removal) { + it = enemies_.erase(it); + continue; + } + it->sprite_animator.Update(delta_time); + it->target_animator.Update(delta_time); + it->blast_animator.Update(delta_time); + it->health_animator.Update(delta_time); + it->score_animator.Update(delta_time); + it->movement_animator.Update(delta_time); + } +} + +void Enemy::Draw(float frame_frac) { + for (auto& e : enemies_) { + e.sprite.Draw(); + e.target.Draw(); + e.blast.Draw(); + e.health_base.Draw(); + e.health_bar.Draw(); + e.score.Draw(); + } +} + +bool Enemy::HasTarget(DamageType damage_type) { + assert(damage_type > kDamageType_Invalid && damage_type < kDamageType_Any); + + return GetTarget(damage_type) ? true : false; +} + +Vector2 Enemy::GetTargetPos(DamageType damage_type) { + assert(damage_type > kDamageType_Invalid && damage_type < kDamageType_Any); + + EnemyUnit* target = GetTarget(damage_type); + if (target) + return target->sprite.GetOffset() - + Vector2(0, target->sprite.GetScale().y / 2.5f); + return {0, 0}; +} + +void Enemy::SelectTarget(DamageType damage_type, + const Vector2& origin, + const Vector2& dir, + float snap_factor) { + assert(damage_type > kDamageType_Invalid && damage_type < kDamageType_Any); + + if (waiting_for_next_wave_) + return; + + EnemyUnit* best_enemy = nullptr; + + float closest_dist = std::numeric_limits::max(); + for (auto& e : enemies_) { + if (e.hit_points <= 0 || e.marked_for_removal) + continue; + + if (e.targetted_by_weapon_ == damage_type) { + e.targetted_by_weapon_ = kDamageType_Invalid; + e.target.SetVisible(false); + e.target_animator.Stop(Animator::kAllAnimations); + } + + if (!base::Intersection(e.sprite.GetOffset(), + e.sprite.GetScale() * snap_factor, origin, dir)) + continue; + + Vector2 weapon_enemy_dir = e.sprite.GetOffset() - origin; + float enemy_weapon_dist = weapon_enemy_dir.Magnitude(); + if (closest_dist > enemy_weapon_dist) { + closest_dist = enemy_weapon_dist; + best_enemy = &e; + } + } + + if (best_enemy) { + best_enemy->targetted_by_weapon_ = damage_type; + best_enemy->target.SetVisible(true); + if (damage_type == kDamageType_Green) { + best_enemy->target.SetFrame(0); + best_enemy->target_animator.SetFrames(6, 28); + } else { + best_enemy->target.SetFrame(6); + best_enemy->target_animator.SetFrames(6, 28); + } + best_enemy->target_animator.Play(Animator::kFrames, false); + } +} + +void Enemy::DeselectTarget(DamageType damage_type) { + assert(damage_type > kDamageType_Invalid && damage_type < kDamageType_Any); + + EnemyUnit* target = GetTarget(damage_type); + if (target) { + target->targetted_by_weapon_ = kDamageType_Invalid; + target->target.SetVisible(false); + target->target_animator.Stop(Animator::kAllAnimations); + } +} + +void Enemy::HitTarget(DamageType damage_type) { + assert(damage_type > kDamageType_Invalid && damage_type < kDamageType_Any); + + if (waiting_for_next_wave_) + return; + + EnemyUnit* target = GetTarget(damage_type); + + if (target) { + target->target.SetVisible(false); + target->target_animator.Stop(Animator::kAllAnimations); + } + + if (!target || (target->damage_type != kDamageType_Any && + target->damage_type != damage_type)) + return; + + TakeDamage(target, 1); +} + +void Enemy::OnWaveFinished() { + for (auto& e : enemies_) { + if (!e.marked_for_removal && e.hit_points > 0) + e.movement_animator.Pause(Animator::kMovement); + } + waiting_for_next_wave_ = true; +} + +void Enemy::OnWaveStarted(int wave) { + for (auto& e : enemies_) { + if (!e.marked_for_removal && e.hit_points > 0) { + if (wave == 1) + e.marked_for_removal = true; + else + TakeDamage(&e, 100); + } + } + num_enemies_killed_in_current_wave_ = 0; + seconds_since_last_spawn_ = {0, 0, 0}; + seconds_to_next_spawn_ = {0, 0, 0}; + spawn_factor_ = 1 / (log10(0.25f * (wave + 4) + 1.468f) * 6); + spawn_factor_interpolator_ = 0; + waiting_for_next_wave_ = false; +} + +void Enemy::TakeDamage(EnemyUnit* target, int damage) { + assert(!target->marked_for_removal); + assert(target->hit_points > 0); + + target->blast.SetVisible(true); + target->blast_animator.Play(Animator::kFrames, false); + + target->hit_points -= damage; + if (target->hit_points <= 0) { + if (!waiting_for_next_wave_) + ++num_enemies_killed_in_current_wave_; + + target->sprite.SetVisible(false); + target->health_base.SetVisible(false); + target->health_bar.SetVisible(false); + target->score.SetVisible(true); + + target->score_animator.Play(Animator::kTimer | Animator::kMovement, false); + target->movement_animator.Pause(Animator::kMovement); + + target->explosion_.Play(false); + + Engine& engine = Engine::Get(); + Demo* game = static_cast(engine.GetGame()); + game->AddScore(GetScore(target->enemy_type)); + } else { + target->targetted_by_weapon_ = kDamageType_Invalid; + + Vector2 s = target->sprite.GetScale() * Vector2(0.6f, 0.01f); + s.x *= (float)target->hit_points / (float)target->total_health; + float t = (s.x - target->health_bar.GetScale().x) / 2; + target->health_bar.SetScale(s); + target->health_bar.Translate({t, 0}); + + target->health_base.SetVisible(true); + target->health_bar.SetVisible(true); + + target->health_animator.Stop(Animator::kTimer | Animator::kBlending); + target->health_animator.Play(Animator::kTimer, false); + } +} + +void Enemy::SpawnNextEnemy() { + Engine& engine = Engine::Get(); + Random& rnd = engine.GetRandomGenerator(); + + float factor = Lerp(1.0f, spawn_factor_, spawn_factor_interpolator_); + EnemyType enemy_type = kEnemyType_Invalid; + + for (int i = 0; i < kEnemyType_Max; ++i) { + if (seconds_since_last_spawn_[i] >= seconds_to_next_spawn_[i]) { + if (seconds_to_next_spawn_[i] > 0) + enemy_type = (EnemyType)i; + + seconds_since_last_spawn_[i] = 0; + seconds_to_next_spawn_[i] = + Lerp(kSpawnPeriod[i][0] * factor, kSpawnPeriod[i][1] * factor, + rnd.GetFloat()); + break; + } + } + + if (enemy_type == kEnemyType_Invalid) + return; + + DamageType damage_type = enemy_type == kEnemyType_Tank + ? kDamageType_Any + : (DamageType)(rnd.Roll(2) - 1); + + Vector2 s = engine.GetScreenSize(); + int col; + col = rnd.Roll(4) - 1; + if (col == last_spawn_col_) + col = (col + 1) % 4; + last_spawn_col_ = col; + float x = (s.x / 4) / 2 + (s.x / 4) * col - s.x / 2; + Vector2 pos = {x, s.y / 2}; + float speed = + enemy_type == kEnemyType_Tank ? 36.0f : (rnd.Roll(4) == 4 ? 6.0f : 10.0f); + + Spawn(enemy_type, damage_type, pos, speed); +} + +void Enemy::Spawn(EnemyType enemy_type, + DamageType damage_type, + const Vector2& pos, + float speed) { + assert(enemy_type > kEnemyType_Invalid && enemy_type < kEnemyType_Max); + assert(damage_type > kDamageType_Invalid && damage_type < kDamageType_Max); + + Engine& engine = Engine::Get(); + Demo* game = static_cast(engine.GetGame()); + + auto& e = enemies_.emplace_back(); + e.enemy_type = enemy_type; + e.damage_type = damage_type; + if (enemy_type == kEnemyType_Skull) { + e.total_health = e.hit_points = 1; + e.sprite.Create(skull_tex_, {10, 13}, 100, 100); + } else if (enemy_type == kEnemyType_Bug) { + e.total_health = e.hit_points = 2; + e.sprite.Create(bug_tex_, {10, 4}); + } else { // kEnemyType_Tank + e.total_health = e.hit_points = 6; + e.sprite.Create(skull_tex_, {10, 13}, 100, 100); + } + e.sprite.AutoScale(); + e.sprite.SetVisible(true); + Vector2 spawn_pos = pos + Vector2(0, e.sprite.GetScale().y / 2); + e.sprite.SetOffset(spawn_pos); + + e.sprite.SetFrame(enemy_frame_start[enemy_type][damage_type]); + e.sprite_animator.SetFrames(enemy_frame_count[enemy_type][damage_type], + enemy_frame_speed); + + e.sprite_animator.Attach(&e.sprite); + e.sprite_animator.Play(Animator::kFrames, true); + + e.target.Create(target_tex_, {6, 2}); + e.target.AutoScale(); + e.target.SetOffset(spawn_pos); + + e.blast.Create(blast_tex_, {6, 2}); + e.blast.AutoScale(); + e.blast.SetOffset(spawn_pos); + + e.health_base.Scale(e.sprite.GetScale() * Vector2(0.6f, 0.01f)); + e.health_base.SetOffset(spawn_pos); + e.health_base.PlaceToBottomOf(e.sprite); + e.health_base.SetColor({0.5f, 0.5f, 0.5f, 1}); + + e.health_bar.Scale(e.sprite.GetScale() * Vector2(0.6f, 0.01f)); + e.health_bar.SetOffset(spawn_pos); + e.health_bar.PlaceToBottomOf(e.sprite); + e.health_bar.SetColor({0.161f, 0.89f, 0.322f, 1}); + + e.score.Create(score_tex_[e.enemy_type]); + e.score.AutoScale(); + e.score.SetColor({1, 1, 1, 1}); + e.score.SetOffset(spawn_pos); + + e.target_animator.Attach(&e.target); + + e.blast_animator.SetEndCallback(Animator::kFrames, + [&]() -> void { e.blast.SetVisible(false); }); + if (damage_type == kDamageType_Green) { + e.blast.SetFrame(0); + e.blast_animator.SetFrames(6, 28); + } else { + e.blast.SetFrame(6); + e.blast_animator.SetFrames(6, 28); + } + e.blast_animator.Attach(&e.blast); + + SetupFadeOutAnim(e.health_animator, 1); + e.health_animator.Attach(&e.health_base); + e.health_animator.Attach(&e.health_bar); + + SetupFadeOutAnim(e.score_animator, 0.2f); + e.score_animator.SetMovement({0, engine.GetScreenSize().y / 2}, 2.0f); + e.score_animator.SetEndCallback( + Animator::kMovement, [&]() -> void { e.marked_for_removal = true; }); + e.score_animator.Attach(&e.score); + + float max_distance = + engine.GetScreenSize().y - game->GetPlayer().GetWeaponScale().y / 2; + + e.movement_animator.SetMovement( + {0, -max_distance}, speed, + std::bind(Acceleration, std::placeholders::_1, -0.15f)); + e.movement_animator.SetEndCallback(Animator::kMovement, [&]() -> void { + e.sprite.SetVisible(false); + e.target.SetVisible(false); + e.blast.SetVisible(false); + e.marked_for_removal = true; + }); + e.movement_animator.Attach(&e.sprite); + e.movement_animator.Attach(&e.target); + e.movement_animator.Attach(&e.blast); + e.movement_animator.Attach(&e.health_base); + e.movement_animator.Attach(&e.health_bar); + e.movement_animator.Attach(&e.score); + e.movement_animator.Play(Animator::kMovement, false); + + e.explosion_.SetSound(explosion_sound_); + e.explosion_.SetVariate(true); + e.explosion_.SetSimulateStereo(true); +} + +Enemy::EnemyUnit* Enemy::GetTarget(DamageType damage_type) { + for (auto& e : enemies_) { + if (e.targetted_by_weapon_ == damage_type && e.hit_points > 0 && + !e.marked_for_removal) + return &e; + } + return nullptr; +} + +int Enemy::GetScore(EnemyType enemy_type) { + assert(enemy_type > kEnemyType_Invalid && enemy_type < kEnemyType_Max); + return enemy_scores[enemy_type]; +} + +std::unique_ptr Enemy::GetScoreImage(int score) { + const Font& font = static_cast(Engine::Get().GetGame())->GetFont(); + + std::string text = std::to_string(score); + int width, height; + font.CalculateBoundingBox(text.c_str(), width, height); + + auto image = std::make_unique(); + image->Create(width, height); + image->Clear({1, 1, 1, 0}); + + font.Print(0, 0, text.c_str(), image->GetBuffer(), image->GetWidth()); + + return image; +} + +bool Enemy::CreateRenderResources() { + auto skull_image = std::make_unique(); + if (!skull_image->Load("enemy_anims_01_frames_ok.png")) + return false; + auto bug_image = std::make_unique(); + if (!bug_image->Load("enemy_anims_02_frames_ok.png")) + return false; + auto target_image = std::make_unique(); + if (!target_image->Load("enemy_target_single_ok.png")) + return false; + auto blast_image = std::make_unique(); + if (!blast_image->Load("enemy_anims_blast_ok.png")) + return false; + + skull_image->Compress(); + bug_image->Compress(); + target_image->Compress(); + blast_image->Compress(); + + skull_tex_->Update(std::move(skull_image)); + bug_tex_->Update(std::move(bug_image)); + target_tex_->Update(std::move(target_image)); + blast_tex_->Update(std::move(blast_image)); + + for (int i = 0; i < kEnemyType_Max; ++i) + score_tex_[i]->Update(GetScoreImage(GetScore((EnemyType)i))); + + return true; +} diff --git a/src/demo/enemy.h b/src/demo/enemy.h new file mode 100644 index 0000000..0b578de --- /dev/null +++ b/src/demo/enemy.h @@ -0,0 +1,119 @@ +#ifndef ENEMY_H +#define ENEMY_H + +#include +#include +#include + +#include "../base/vecmath.h" +#include "../engine/animator.h" +#include "../engine/image_quad.h" +#include "../engine/solid_quad.h" +#include "../engine/sound_player.h" +#include "damage_type.h" + +namespace eng { +class Image; +class Sound; +class Texture; +} // namespace eng + +class Enemy { + public: + Enemy(); + ~Enemy(); + + bool Initialize(); + + void ContextLost(); + + void Update(float delta_time); + + void Draw(float frame_frac); + + bool HasTarget(DamageType damage_type); + base::Vector2 GetTargetPos(DamageType damage_type); + + void SelectTarget(DamageType damage_type, + const base::Vector2& origin, + const base::Vector2& dir, + float snap_factor); + void DeselectTarget(DamageType damage_type); + + void HitTarget(DamageType damage_type); + + void OnWaveFinished(); + void OnWaveStarted(int wave); + + int num_enemies_killed_in_current_wave() { + return num_enemies_killed_in_current_wave_; + } + + private: + struct EnemyUnit { + EnemyType enemy_type = kEnemyType_Invalid; + DamageType damage_type = kDamageType_Invalid; + + bool marked_for_removal = false; + DamageType targetted_by_weapon_ = kDamageType_Invalid; + int total_health = 0; + int hit_points = 0; + + eng::ImageQuad sprite; + eng::ImageQuad target; + eng::ImageQuad blast; + eng::ImageQuad score; + eng::SolidQuad health_base; + eng::SolidQuad health_bar; + + eng::Animator movement_animator; + eng::Animator sprite_animator; + eng::Animator target_animator; + eng::Animator blast_animator; + eng::Animator health_animator; + eng::Animator score_animator; + + eng::SoundPlayer explosion_; + }; + + std::shared_ptr skull_tex_; + std::shared_ptr bug_tex_; + std::shared_ptr target_tex_; + std::shared_ptr blast_tex_; + std::shared_ptr score_tex_[kEnemyType_Max]; + + std::shared_ptr explosion_sound_; + + std::list enemies_; + + int num_enemies_killed_in_current_wave_ = 0; + + std::array seconds_since_last_spawn_ = {0, 0, 0}; + std::array seconds_to_next_spawn_ = {0, 0, 0}; + + float spawn_factor_ = 0; + float spawn_factor_interpolator_ = 0; + + bool waiting_for_next_wave_ = false; + + int last_spawn_col_ = 0; + + void TakeDamage(EnemyUnit* target, int damage); + + void SpawnNextEnemy(); + + void Spawn(EnemyType enemy_type, + DamageType damage_type, + const base::Vector2& pos, + float speed); + + EnemyUnit* GetTarget(DamageType damage_type); + + int GetScore(EnemyType enemy_type); + + std::unique_ptr GetScoreImage(int score); + + bool CreateRenderResources(); +}; + +#endif // ENEMY_H diff --git a/src/demo/hud.cc b/src/demo/hud.cc new file mode 100644 index 0000000..bda90ab --- /dev/null +++ b/src/demo/hud.cc @@ -0,0 +1,164 @@ +#include "hud.h" + +#include "../base/interpolation.h" +#include "../base/log.h" +#include "../base/vecmath.h" +#include "../engine/engine.h" +#include "../engine/font.h" +#include "../engine/image.h" +#include "../engine/renderer/texture.h" +#include "demo.h" + +using namespace base; +using namespace eng; + +namespace { + +constexpr float kHorizontalMargin = 0.07f; +constexpr float kVerticalMargin = 0.025f; + +const Vector4 kPprogressBarColor[2] = {{0.256f, 0.434f, 0.72f, 1}, + {0.905f, 0.493f, 0.194f, 1}}; +const Vector4 kTextColor = {0.895f, 0.692f, 0.24f, 1}; + +} // namespace + +Hud::Hud() { + text_[0].Create(Engine::Get().CreateRenderResource()); + text_[1].Create(Engine::Get().CreateRenderResource()); +} + +Hud::~Hud() = default; + +bool Hud::Initialize() { + Engine& engine = Engine::Get(); + const Font& font = static_cast(engine.GetGame())->GetFont(); + + int tmp; + font.CalculateBoundingBox("big_enough_text", max_text_width_, tmp); + + for (int i = 0; i < 2; ++i) { + auto image = CreateImage(); + + text_[i].GetTexture()->Update(std::move(image)); + text_[i].AutoScale(); + text_[i].SetColor(kTextColor); + + Vector2 pos = (engine.GetScreenSize() / 2 - text_[i].GetScale() / 2); + pos -= engine.GetScreenSize() * Vector2(kHorizontalMargin, kVerticalMargin); + + Vector2 scale = engine.GetScreenSize() * Vector2(1, 0); + scale -= engine.GetScreenSize() * Vector2(kHorizontalMargin * 4, 0); + scale += text_[0].GetScale() * Vector2(0, 0.3f); + + progress_bar_[i].Scale(scale); + progress_bar_[i].Translate(pos * Vector2(0, 1)); + progress_bar_[i].SetColor(kPprogressBarColor[i] * Vector4(1, 1, 1, 0)); + + pos -= progress_bar_[i].GetScale() * Vector2(0, 4); + text_[i].Translate(pos * Vector2(i ? 1 : -1, 1)); + + progress_bar_animator_[i].Attach(&progress_bar_[i]); + + text_animator_cb_[i] = [&, i]() -> void { + text_animator_[i].SetEndCallback(Animator::kBlending, nullptr); + text_animator_[i].SetBlending( + kTextColor, 2, std::bind(Acceleration, std::placeholders::_1, -1)); + text_animator_[i].Play(Animator::kBlending, false); + }; + text_animator_[i].Attach(&text_[i]); + } + + return true; +} + +void Hud::Update(float delta_time) { + for (int i = 0; i < 2; ++i) { + text_animator_[i].Update(delta_time); + progress_bar_animator_[i].Update(delta_time); + } +} + +void Hud::Draw() { + for (int i = 0; i < 2; ++i) { + progress_bar_[i].Draw(); + text_[i].Draw(); + } +} + +void Hud::ContextLost() { + PrintScore(last_score_, false); + PrintWave(last_wave_, false); +} + +void Hud::Show() { + if (text_[0].IsVisible()) + return; + + for (int i = 0; i < 2; ++i) { + progress_bar_[i].SetVisible(true); + text_[i].SetVisible(true); + progress_bar_animator_[i].SetBlending(kPprogressBarColor[i], 0.3f); + progress_bar_animator_[i].Play(Animator::kBlending, false); + } +} + +void Hud::PrintScore(int score, bool flash) { + last_score_ = score; + Print(0, std::to_string(score)); + + if (flash) { + text_animator_[0].SetEndCallback(Animator::kBlending, text_animator_cb_[0]); + text_animator_[0].SetBlending( + {1, 1, 1, 1}, 0.1f, std::bind(Acceleration, std::placeholders::_1, 1)); + text_animator_[0].Play(Animator::kBlending, false); + } +} + +void Hud::PrintWave(int wave, bool flash) { + last_wave_ = wave; + std::string text = "wave "; + text += std::to_string(wave); + Print(1, text.c_str()); + + if (flash) { + text_animator_[1].SetEndCallback(Animator::kBlending, text_animator_cb_[1]); + text_animator_[1].SetBlending({1, 1, 1, 1}, 0.08f); + text_animator_[1].Play(Animator::kBlending, false); + } +} + +void Hud::SetProgress(float progress) { + progress = std::min(std::max(0.0f, progress), 1.0f); + last_progress_ = progress; + Vector2 s = progress_bar_[0].GetScale() * Vector2(progress, 1); + float t = (s.x - progress_bar_[1].GetScale().x) / 2; + progress_bar_[1].SetScale(s); + progress_bar_[1].Translate({t, 0}); +} + +void Hud::Print(int i, const std::string& text) { + const Font& font = static_cast(Engine::Get().GetGame())->GetFont(); + + auto image = CreateImage(); + + float x = 0; + if (i == 1) { + int w, h; + font.CalculateBoundingBox(text.c_str(), w, h); + x = image->GetWidth() - w; + } + + font.Print(x, 0, text.c_str(), image->GetBuffer(), image->GetWidth()); + + text_[i].GetTexture()->Update(std::move(image)); +} + +std::unique_ptr Hud::CreateImage() { + const Font& font = static_cast(Engine::Get().GetGame())->GetFont(); + + auto image = std::make_unique(); + image->Create(max_text_width_, font.GetLineHeight()); + image->Clear({1, 1, 1, 0}); + return image; +} diff --git a/src/demo/hud.h b/src/demo/hud.h new file mode 100644 index 0000000..fea2edb --- /dev/null +++ b/src/demo/hud.h @@ -0,0 +1,54 @@ +#ifndef HUD_H +#define HUD_H + +#include +#include + +#include "../base/closure.h" +#include "../engine/animator.h" +#include "../engine/image_quad.h" +#include "../engine/solid_quad.h" + +namespace eng { +class Image; +} // namespace eng + +class Hud { + public: + Hud(); + ~Hud(); + + bool Initialize(); + + void Update(float delta_time); + + void Draw(); + + void ContextLost(); + + void Show(); + + void PrintScore(int score, bool flash); + void PrintWave(int wave, bool flash); + void SetProgress(float progress); + + private: + eng::SolidQuad progress_bar_[2]; + eng::ImageQuad text_[2]; + + eng::Animator progress_bar_animator_[2]; + eng::Animator text_animator_[2]; + base::Closure text_animator_cb_[2]; + + int max_text_width_ = 0; + + int last_score_ = 0; + int last_wave_ = 0; + float last_progress_ = 0; + + void Print(int i, const std::string& text); + + std::unique_ptr CreateImage(); +}; + +#endif // HUD_H diff --git a/src/demo/menu.cc b/src/demo/menu.cc new file mode 100644 index 0000000..c19fff6 --- /dev/null +++ b/src/demo/menu.cc @@ -0,0 +1,214 @@ +#include "menu.h" + +#include +#include +#include + +#include "../base/collusion_test.h" +#include "../base/interpolation.h" +#include "../base/log.h" +#include "../base/worker.h" +#include "../engine/engine.h" +#include "../engine/font.h" +#include "../engine/image.h" +#include "../engine/input_event.h" +#include "../engine/renderer/texture.h" +#include "demo.h" + +using namespace base; +using namespace eng; + +namespace { + +constexpr char kMenuOption[Menu::kOption_Max][10] = {"continue", "start", + "credits", "exit"}; + +constexpr float kMenuOptionSpace = 1.5f; + +const Vector4 kColorNormal = {1, 1, 1, 1}; +const Vector4 kColorHighlight = {5, 5, 5, 1}; +constexpr float kBlendingSpeed = 0.12f; + +const Vector4 kColorFadeOut = {1, 1, 1, 0}; +constexpr float kFadeSpeed = 0.2f; + +} // namespace + +Menu::Menu() : tex_(Engine::Get().CreateRenderResource()) {} + +Menu::~Menu() = default; + +bool Menu::Initialize() { + const Font& font = static_cast(Engine::Get().GetGame())->GetFont(); + + max_text_width_ = -1; + for (int i = 0; i < kOption_Max; ++i) { + int width, height; + font.CalculateBoundingBox(kMenuOption[i], width, height); + if (width > max_text_width_) + max_text_width_ = width; + } + + tex_->Update(CreateImage()); + + for (int i = 0; i < kOption_Max; ++i) { + items_[i].text.Create(tex_, {1, 4}); + items_[i].text.AutoScale(); + items_[i].text.Scale(1.5f); + items_[i].text.SetColor(kColorFadeOut); + items_[i].text.SetVisible(false); + items_[i].text.SetFrame(i); + + items_[i].select_item_cb_ = [&, i]() -> void { + items_[i].text_animator.SetEndCallback( + Animator::kBlending, [&, i]() -> void { + items_[i].text_animator.SetEndCallback(Animator::kBlending, + nullptr); + selected_option_ = (Option)i; + }); + items_[i].text_animator.SetBlending(kColorNormal, kBlendingSpeed); + items_[i].text_animator.Play(Animator::kBlending, false); + }; + items_[i].text_animator.Attach(&items_[i].text); + } + // Get the item positions calculated. + SetOptionEnabled(kContinue, true); + + return true; +} + +void Menu::Update(float delta_time) { + for (int i = 0; i < kOption_Max; ++i) { + if (items_[i].hide) + continue; + items_[i].text_animator.Update(delta_time); + } +} + +void Menu::OnInputEvent(std::unique_ptr event) { + if (event->GetType() == InputEvent::kTap || + event->GetType() == InputEvent::kDragStart) + tap_pos_[0] = tap_pos_[1] = event->GetVector(0); + else if (event->GetType() == InputEvent::kDrag) + tap_pos_[1] = event->GetVector(0); + + if ((event->GetType() != InputEvent::kTap && + event->GetType() != InputEvent::kDragEnd) || + IsAnimating()) + return; + + for (int i = 0; i < kOption_Max; ++i) { + if (items_[i].hide) + continue; + if (!Intersection(items_[i].text.GetOffset(), + items_[i].text.GetScale() * Vector2(1.2f, 2), + tap_pos_[0])) + continue; + if (!Intersection(items_[i].text.GetOffset(), + items_[i].text.GetScale() * Vector2(1.2f, 2), + tap_pos_[1])) + continue; + + items_[i].text_animator.SetEndCallback(Animator::kBlending, + items_[i].select_item_cb_); + items_[i].text_animator.SetBlending(kColorHighlight, kBlendingSpeed); + items_[i].text_animator.Play(Animator::kBlending, false); + } +} + +void Menu::Draw() { + for (int i = 0; i < kOption_Max; ++i) + items_[i].text.Draw(); +} + +void Menu::ContextLost() { + tex_->Update(CreateImage()); +} + +void Menu::SetOptionEnabled(Option o, bool enable) { + int first = -1, last = -1; + for (int i = 0; i < kOption_Max; ++i) { + if (i == o) + items_[i].hide = !enable; + if (!items_[i].hide) { + items_[i].text.SetOffset({0, 0}); + if (last >= 0) { + items_[i].text.PlaceToBottomOf(items_[last].text); + items_[i].text.Translate(items_[last].text.GetOffset() * Vector2(0, 1)); + items_[i].text.Translate( + {0, items_[last].text.GetScale().y * -kMenuOptionSpace}); + } + if (first < 0) + first = i; + last = i; + } + } + + float center_offset_y = + (items_[first].text.GetOffset().y - items_[last].text.GetOffset().y) / 2; + for (int i = 0; i < kOption_Max; ++i) { + if (!items_[i].hide) + items_[i].text.Translate({0, center_offset_y}); + } +} + +void Menu::Show() { + for (int i = 0; i < kOption_Max; ++i) { + if (items_[i].hide) + continue; + items_[i].text_animator.SetEndCallback( + Animator::kBlending, [&, i]() -> void { + items_[i].text_animator.SetEndCallback(Animator::kBlending, nullptr); + }); + items_[i].text_animator.SetBlending(kColorNormal, kFadeSpeed); + items_[i].text_animator.Play(Animator::kBlending, false); + items_[i].text.SetVisible(true); + } +} + +void Menu::Hide() { + selected_option_ = kOption_Invalid; + for (int i = 0; i < kOption_Max; ++i) { + if (items_[i].hide) + continue; + items_[i].text_animator.SetEndCallback( + Animator::kBlending, [&, i]() -> void { + items_[i].text_animator.SetEndCallback(Animator::kBlending, nullptr); + items_[i].text.SetVisible(false); + }); + items_[i].text_animator.SetBlending(kColorFadeOut, kFadeSpeed); + items_[i].text_animator.Play(Animator::kBlending, false); + } +} + +std::unique_ptr Menu::CreateImage() { + const Font& font = static_cast(Engine::Get().GetGame())->GetFont(); + + int line_height = font.GetLineHeight() + 1; + auto image = std::make_unique(); + image->Create(max_text_width_, line_height * kOption_Max); + + // Fill the area of each menu item with gradient. + image->GradientV({1.0f, 1.0f, 1.0f, 0}, {.0f, .0f, 1.0f, 0}, line_height); + + base::Worker worker(kOption_Max); + for (int i = 0; i < kOption_Max; ++i) { + int w, h; + font.CalculateBoundingBox(kMenuOption[i], w, h); + float x = (image->GetWidth() - w) / 2; + float y = line_height * i; + worker.Enqueue(std::bind(&Font::Print, &font, x, y, kMenuOption[i], + image->GetBuffer(), image->GetWidth())); + } + worker.Join(); + + return image; +} + +bool Menu::IsAnimating() { + for (int i = 0; i < kOption_Max; ++i) { + if (items_[i].text_animator.IsPlaying(Animator::kBlending)) + return true; + } + return false; +} diff --git a/src/demo/menu.h b/src/demo/menu.h new file mode 100644 index 0000000..d0b3027 --- /dev/null +++ b/src/demo/menu.h @@ -0,0 +1,72 @@ +#ifndef MENU_H +#define MENU_H + +#include +#include + +#include "../base/closure.h" +#include "../base/vecmath.h" +#include "../engine/animator.h" +#include "../engine/image_quad.h" + +namespace eng { +class Image; +class InputEvent; +class Texture; +} // namespace eng + +class Menu { + public: + enum Option { + kOption_Invalid = -1, + kContinue, + kNewGame, + kCredits, + kExit, + kOption_Max, + }; + + Menu(); + ~Menu(); + + bool Initialize(); + + void Update(float delta_time); + + void OnInputEvent(std::unique_ptr event); + + void Draw(); + + void ContextLost(); + + void SetOptionEnabled(Option o, bool enable); + + void Show(); + void Hide(); + + Option selected_option() const { return selected_option_; } + + private: + struct Item { + eng::ImageQuad text; + eng::Animator text_animator; + base::Closure select_item_cb_; + bool hide = false; + }; + + std::shared_ptr tex_; + + Item items_[kOption_Max]; + + int max_text_width_ = 0; + + Option selected_option_ = kOption_Invalid; + + base::Vector2 tap_pos_[2] = {{0, 0}, {0, 0}}; + + std::unique_ptr CreateImage(); + + bool IsAnimating(); +}; + +#endif // MENU_H diff --git a/src/demo/player.cc b/src/demo/player.cc new file mode 100644 index 0000000..5f0bbbd --- /dev/null +++ b/src/demo/player.cc @@ -0,0 +1,354 @@ +#include "player.h" + +#include + +#include "../base/log.h" +#include "../engine/engine.h" +#include "../engine/image.h" +#include "../engine/input_event.h" +#include "demo.h" + +using namespace base; +using namespace eng; + +namespace { + +constexpr int wepon_warmup_frame[] = {1, 9}; +constexpr int wepon_warmup_frame_count = 4; +constexpr int wepon_cooldown_frame[] = {5, 13}; +constexpr int wepon_cooldown_frame_count = 3; +constexpr int wepon_anim_speed = 48; + +} // namespace + +Player::Player() + : weapon_tex_(Engine::Get().CreateRenderResource()), + beam_tex_(Engine::Get().CreateRenderResource()) {} + +Player::~Player() = default; + +bool Player::Initialize() { + if (!CreateRenderResources()) + return false; + SetupWeapons(); + return true; +} + +void Player::ContextLost() { + CreateRenderResources(); +} + +void Player::Update(float delta_time) { + for (int i = 0; i < 2; ++i) { + warmup_animator_[i].Update(delta_time); + cooldown_animator_[i].Update(delta_time); + beam_animator_[i].Update(delta_time); + spark_animator_[i].Update(delta_time); + } + + if (active_weapon_ != kDamageType_Invalid) + UpdateTarget(); +} + +void Player::OnInputEvent(std::unique_ptr event) { + if (event->GetType() == InputEvent::kNavigateBack) + NavigateBack(); + else if (event->GetType() == InputEvent::kDragStart) + DragStart(event->GetVector(0)); + else if (event->GetType() == InputEvent::kDrag) + Drag(event->GetVector(0)); + else if (event->GetType() == InputEvent::kDragEnd) + DragEnd(); + else if (event->GetType() == InputEvent::kDragCancel) + DragCancel(); +} + +void Player::Draw(float frame_frac) { + for (int i = 0; i < 2; ++i) { + drag_sign_[i].Draw(); + beam_[i].Draw(); + beam_spark_[i].Draw(); + weapon_[i].Draw(); + } +} + +Vector2 Player::GetWeaponPos(DamageType type) const { + return Engine::Get().GetScreenSize() / + Vector2(type == kDamageType_Green ? 3.5f : -3.5f, -2) + + Vector2(0, weapon_[type].GetScale().y * 0.7f); +} + +Vector2 Player::GetWeaponScale() const { + return weapon_[0].GetScale(); +} + +DamageType Player::GetWeaponType(const Vector2& pos) { + DamageType closest_weapon = kDamageType_Invalid; + float closest_dist = std::numeric_limits::max(); + for (int i = 0; i < 2; ++i) { + float dist = (pos - weapon_[i].GetOffset()).Magnitude(); + if (dist < closest_dist) { + closest_dist = dist; + closest_weapon = (DamageType)i; + } + } + + assert(closest_weapon != kDamageType_Invalid); + if (closest_dist < weapon_[closest_weapon].GetScale().x * 0.9f) + return closest_weapon; + return kDamageType_Invalid; +} + +void Player::SetBeamLength(DamageType type, float len) { + beam_[type].SetOffset({0, 0}); + beam_[type].SetScale({len, beam_[type].GetScale().y}); + beam_[type].PlaceToRightOf(weapon_[type]); + beam_[type].Translate(weapon_[type].GetScale() * Vector2(-0.5f, 0)); + beam_[type].SetPivot(beam_[type].GetOffset()); + beam_[type].Translate(weapon_[type].GetOffset()); +} + +void Player::WarmupWeapon(DamageType type) { + cooldown_animator_[type].Stop(Animator::kFrames); + warmup_animator_[type].Play(Animator::kFrames, false); +} + +void Player::CooldownWeapon(DamageType type) { + warmup_animator_[type].Stop(Animator::kFrames); + cooldown_animator_[type].Play(Animator::kFrames, false); +} + +void Player::Fire(DamageType type, Vector2 dir) { + Engine& engine = Engine::Get(); + Enemy& enemy = static_cast(engine.GetGame())->GetEnemy(); + + if (enemy.HasTarget(type)) + dir = weapon_[type].GetOffset() - enemy.GetTargetPos(type); + else + dir *= engine.GetScreenSize().y * 1.3f; + + float len = dir.Magnitude(); + SetBeamLength(type, len); + + dir.Normalize(); + float cos_theta = dir.DotProduct(Vector2(1, 0)); + float theta = acos(cos_theta) + M_PI; + beam_[type].SetTheta(theta); + beam_spark_[type].SetTheta(theta); + + beam_[type].SetColor({1, 1, 1, 1}); + beam_[type].SetVisible(true); + beam_spark_[type].SetVisible(true); + + spark_animator_[type].Stop(Animator::kMovement); + float length = beam_[type].GetScale().x * 0.85f; + Vector2 movement = dir * -length; + // Convert from units per second to duration. + float speed = 1.0f / (18.0f / length); + spark_animator_[type].SetMovement(movement, speed); + spark_animator_[type].Play(Animator::kMovement, false); +} + +bool Player::IsFiring(DamageType type) { + return beam_animator_[type].IsPlaying(Animator::kBlending) || + spark_animator_[type].IsPlaying(Animator::kMovement); +} + +void Player::SetupWeapons() { + for (int i = 0; i < 2; ++i) { + // Setup draw sign. + drag_sign_[i].Create(weapon_tex_, {8, 2}); + drag_sign_[i].AutoScale(); + drag_sign_[i].SetFrame(i * 8); + + // Setup weapon. + weapon_[i].Create(weapon_tex_, {8, 2}); + weapon_[i].AutoScale(); + weapon_[i].SetVisible(true); + weapon_[i].SetFrame(wepon_warmup_frame[i]); + + // Setup beam. + beam_[i].Create(beam_tex_, {1, 2}); + beam_[i].AutoScale(); + beam_[i].SetFrame(i); + beam_[i].PlaceToRightOf(weapon_[i]); + beam_[i].Translate(weapon_[i].GetScale() * Vector2(-0.5f, 0)); + beam_[i].SetPivot(beam_[i].GetOffset()); + + // Setup beam spark. + beam_spark_[i].Create(weapon_tex_, {8, 2}); + beam_spark_[i].AutoScale(); + beam_spark_[i].SetFrame(i * 8 + 1); + beam_spark_[i].PlaceToRightOf(weapon_[i]); + beam_spark_[i].Translate(weapon_[i].GetScale() * Vector2(-0.5f, 0)); + beam_spark_[i].SetPivot(beam_spark_[i].GetOffset()); + + // Place parts on the screen. + Vector2 offset = GetWeaponPos((DamageType)i); + beam_[i].Translate(offset); + beam_spark_[i].Translate(offset); + weapon_[i].Translate(offset); + + // Setup animators. + weapon_[i].SetFrame(wepon_cooldown_frame[i]); + cooldown_animator_[i].SetFrames(wepon_cooldown_frame_count, + wepon_anim_speed); + cooldown_animator_[i].SetEndCallback(Animator::kFrames, [&, i]() -> void { + weapon_[i].SetFrame(wepon_warmup_frame[i]); + }); + cooldown_animator_[i].Attach(&weapon_[i]); + + weapon_[i].SetFrame(wepon_warmup_frame[i]); + warmup_animator_[i].SetFrames(wepon_warmup_frame_count, wepon_anim_speed); + warmup_animator_[i].SetRotation(M_PI * 2, 8.0f); + warmup_animator_[i].Attach(&weapon_[i]); + warmup_animator_[i].Play(Animator::kRotation, true); + + spark_animator_[i].SetEndCallback(Animator::kMovement, [&, i]() -> void { + beam_spark_[i].SetVisible(false); + beam_animator_[i].Play(Animator::kBlending, false); + static_cast(Engine::Get().GetGame()) + ->GetEnemy() + .HitTarget((DamageType)i); + }); + spark_animator_[i].Attach(&beam_spark_[i]); + + beam_animator_[i].SetEndCallback( + Animator::kBlending, [&, i]() -> void { beam_[i].SetVisible(false); }); + beam_animator_[i].SetBlending({1, 1, 1, 0}, 0.16f); + beam_animator_[i].Attach(&beam_[i]); + } +} + +void Player::UpdateTarget() { + if (IsFiring(active_weapon_)) + return; + + Engine& engine = Engine::Get(); + Demo* game = static_cast(engine.GetGame()); + + if (drag_valid_) { + Vector2 dir = (drag_end_ - drag_start_).Normalize(); + game->GetEnemy().SelectTarget(active_weapon_, drag_start_, dir, 1.2f); + if (!game->GetEnemy().HasTarget(active_weapon_)) + game->GetEnemy().SelectTarget(active_weapon_, drag_start_, dir, 2); + } else { + game->GetEnemy().DeselectTarget(active_weapon_); + } +} + +void Player::DragStart(const Vector2& pos) { + active_weapon_ = GetWeaponType(pos); + if (active_weapon_ == kDamageType_Invalid) + return; + + drag_start_ = drag_end_ = pos; + + drag_sign_[active_weapon_].SetOffset(drag_start_); + drag_sign_[active_weapon_].SetVisible(true); +} + +void Player::Drag(const Vector2& pos) { + if (active_weapon_ == kDamageType_Invalid) + return; + + drag_end_ = pos; + drag_sign_[active_weapon_].SetOffset(drag_end_); + + if (ValidateDrag()) { + if (!drag_valid_ && !IsFiring(active_weapon_)) + WarmupWeapon(active_weapon_); + drag_valid_ = true; + } else { + if (drag_valid_ && !IsFiring(active_weapon_)) + CooldownWeapon(active_weapon_); + drag_valid_ = false; + } +} + +void Player::DragEnd() { + if (active_weapon_ == kDamageType_Invalid) + return; + + UpdateTarget(); + + DamageType type = active_weapon_; + active_weapon_ = kDamageType_Invalid; + drag_sign_[type].SetVisible(false); + + Vector2 fire_dir = (drag_start_ - drag_end_).Normalize(); + + if (drag_valid_ && !IsFiring(type)) { + if (warmup_animator_[type].IsPlaying(Animator::kFrames)) { + warmup_animator_[type].SetEndCallback( + Animator::kFrames, [&, type, fire_dir]() -> void { + warmup_animator_[type].SetEndCallback(Animator::kFrames, nullptr); + CooldownWeapon(type); + Fire(type, fire_dir); + }); + } else { + CooldownWeapon(type); + Fire(type, fire_dir); + } + } + + drag_valid_ = false; + drag_start_ = drag_end_ = {0, 0}; +} + +void Player::DragCancel() { + if (active_weapon_ == kDamageType_Invalid) + return; + + DamageType type = active_weapon_; + active_weapon_ = kDamageType_Invalid; + drag_sign_[type].SetVisible(false); + + if (drag_valid_ && !IsFiring(type)) { + if (warmup_animator_[type].IsPlaying(Animator::kFrames)) { + warmup_animator_[type].SetEndCallback( + Animator::kFrames, [&, type]() -> void { + warmup_animator_[type].SetEndCallback(Animator::kFrames, nullptr); + CooldownWeapon(type); + }); + } else { + CooldownWeapon(type); + } + } + + drag_valid_ = false; + drag_start_ = drag_end_ = {0, 0}; +} + +bool Player::ValidateDrag() { + Vector2 dir = drag_end_ - drag_start_; + float len = dir.Magnitude(); + dir.Normalize(); + if (len < weapon_[active_weapon_].GetScale().y / 4) + return false; + if (dir.DotProduct(Vector2(0, 1)) < 0) + return false; + return true; +} + +void Player::NavigateBack() { + DragCancel(); + Engine& engine = Engine::Get(); + static_cast(engine.GetGame())->EnterMenuState(); +} + +bool Player::CreateRenderResources() { + auto weapon_image = std::make_unique(); + if (!weapon_image->Load("enemy_anims_flare_ok.png")) + return false; + auto beam_image = std::make_unique(); + if (!beam_image->Load("enemy_ray_ok.png")) + return false; + + weapon_image->Compress(); + beam_image->Compress(); + + weapon_tex_->Update(std::move(weapon_image)); + beam_tex_->Update(std::move(beam_image)); + return true; +} diff --git a/src/demo/player.h b/src/demo/player.h new file mode 100644 index 0000000..76d1822 --- /dev/null +++ b/src/demo/player.h @@ -0,0 +1,79 @@ +#ifndef PLAYER_H +#define PLAYER_H + +#include + +#include "../base/vecmath.h" +#include "../engine/animator.h" +#include "../engine/image_quad.h" +#include "../engine/renderer/texture.h" +#include "damage_type.h" + +namespace eng { +class InputEvent; +} // namespace eng + +class Player { + public: + Player(); + ~Player(); + + bool Initialize(); + + void ContextLost(); + + void Update(float delta_time); + + void OnInputEvent(std::unique_ptr event); + + void Draw(float frame_frac); + + base::Vector2 GetWeaponPos(DamageType type) const; + base::Vector2 GetWeaponScale() const; + + private: + std::shared_ptr weapon_tex_; + std::shared_ptr beam_tex_; + + eng::ImageQuad drag_sign_[2]; + eng::ImageQuad weapon_[2]; + eng::ImageQuad beam_[2]; + eng::ImageQuad beam_spark_[2]; + + eng::Animator warmup_animator_[2]; + eng::Animator cooldown_animator_[2]; + eng::Animator beam_animator_[2]; + eng::Animator spark_animator_[2]; + + DamageType active_weapon_ = kDamageType_Invalid; + + base::Vector2 drag_start_ = {0, 0}; + base::Vector2 drag_end_ = {0, 0}; + bool drag_valid_ = false; + + DamageType GetWeaponType(const base::Vector2& pos); + + void SetBeamLength(DamageType type, float len); + + void WarmupWeapon(DamageType type); + void CooldownWeapon(DamageType type); + + void Fire(DamageType type, base::Vector2 dir); + bool IsFiring(DamageType type); + + void SetupWeapons(); + + void UpdateTarget(); + + void DragStart(const base::Vector2& pos); + void Drag(const base::Vector2& pos); + void DragEnd(); + void DragCancel(); + bool ValidateDrag(); + + void NavigateBack(); + + bool CreateRenderResources(); +}; + +#endif // PLAYER_H diff --git a/src/demo/sky_quad.cc b/src/demo/sky_quad.cc new file mode 100644 index 0000000..b9bab2e --- /dev/null +++ b/src/demo/sky_quad.cc @@ -0,0 +1,64 @@ +#include "sky_quad.h" + +#include "../base/interpolation.h" +#include "../base/log.h" +#include "../base/random.h" +#include "../engine/engine.h" +#include "../engine/renderer/geometry.h" +#include "../engine/renderer/shader.h" +#include "../engine/shader_source.h" + +using namespace base; +using namespace eng; + +SkyQuad::SkyQuad() + : shader_(Engine::Get().CreateRenderResource()), + sky_offset_{ + 0, Lerp(0.0f, 10.0f, Engine::Get().GetRandomGenerator().GetFloat())} { +} + +SkyQuad::~SkyQuad() = default; + +bool SkyQuad::Create() { + Engine& engine = Engine::Get(); + + auto source = std::make_unique(); + if (!source->Load("sky.glsl")) + return false; + shader_->Create(std::move(source), engine.GetQuad()->vertex_description()); + + scale_ = engine.GetScreenSize(); + + color_animator_.Attach(this); + + return true; +} + +void SkyQuad::Update(float delta_time) { + sky_offset_ += {0, delta_time * 0.04f}; + color_animator_.Update(delta_time); +} + +void SkyQuad::Draw(float frame_frac) { + Vector2 sky_offset = Lerp(last_sky_offset_, sky_offset_, frame_frac); + + shader_->Activate(); + shader_->SetUniform("scale", scale_); + shader_->SetUniform("projection", Engine::Get().GetProjectionMarix()); + shader_->SetUniform("sky_offset", sky_offset); + shader_->SetUniform("nebula_color", + {nebula_color_.x, nebula_color_.y, nebula_color_.z}); + + Engine::Get().GetQuad()->Draw(); + last_sky_offset_ = sky_offset_; +} + +void SkyQuad::ContextLost() { + Create(); +} + +void SkyQuad::SwitchColor(const Vector4& color) { + color_animator_.SetBlending(color, 5, + std::bind(SmoothStep, std::placeholders::_1)); + color_animator_.Play(Animator::kBlending, false); +} diff --git a/src/demo/sky_quad.h b/src/demo/sky_quad.h new file mode 100644 index 0000000..271c7bf --- /dev/null +++ b/src/demo/sky_quad.h @@ -0,0 +1,52 @@ +#ifndef SKY_QUAD_H +#define SKY_QUAD_H + +#include "../base/vecmath.h" +#include "../engine/animatable.h" +#include "../engine/animator.h" + +#include +#include +#include +#include + +namespace eng { +class Shader; +} // namespace eng + +class SkyQuad : public eng::Animatable { + public: + SkyQuad(); + ~SkyQuad(); + + SkyQuad(const SkyQuad&) = delete; + SkyQuad& operator=(const SkyQuad&) = delete; + + bool Create(); + + void Update(float delta_time); + + // Animatable interface. + void SetFrame(size_t frame) override {} + size_t GetFrame() const override { return 0; } + size_t GetNumFrames() const override { return 0; } + void SetColor(const base::Vector4& color) override { nebula_color_ = color; } + base::Vector4 GetColor() const override { return nebula_color_; } + + void Draw(float frame_frac); + void ContextLost(); + + void SwitchColor(const base::Vector4& color); + + private: + std::shared_ptr shader_; + + base::Vector2 sky_offset_ = {0, 0}; + base::Vector2 last_sky_offset_ = {0, 0}; + base::Vector4 nebula_color_ = {0, 0, 0, 1}; + base::Vector2 scale_ = {1, 1}; + + eng::Animator color_animator_; +}; + +#endif // SKY_QUAD_H diff --git a/src/engine/animatable.cc b/src/engine/animatable.cc new file mode 100644 index 0000000..4f25921 --- /dev/null +++ b/src/engine/animatable.cc @@ -0,0 +1,33 @@ +#include "animatable.h" + +#include + +using namespace base; + +namespace eng { + +void Animatable::Translate(const Vector2& offset) { + offset_ += offset; +} + +void Animatable::Scale(const Vector2& scale) { + scale_ *= scale; +} + +void Animatable::Scale(float scale) { + scale_ *= scale; +} + +void Animatable::Rotate(float angle) { + theta_ += angle; + rotation_.x = sin(theta_); + rotation_.y = cos(theta_); +} + +void Animatable::SetTheta(float theta) { + theta_ = theta; + rotation_.x = sin(theta_); + rotation_.y = cos(theta_); +} + +} // namespace eng diff --git a/src/engine/animatable.h b/src/engine/animatable.h new file mode 100644 index 0000000..446e3d0 --- /dev/null +++ b/src/engine/animatable.h @@ -0,0 +1,66 @@ +#ifndef SHAPE_H +#define SHAPE_H + +#include "../base/vecmath.h" + +namespace eng { + +class Animatable { + public: + Animatable() = default; + virtual ~Animatable() = default; + + void Translate(const base::Vector2& offset); + void Scale(const base::Vector2& scale); + void Scale(float scale); + void Rotate(float angle); + + void SetOffset(const base::Vector2& offset) { offset_ = offset; } + void SetScale(const base::Vector2& scale) { scale_ = scale; } + void SetPivot(const base::Vector2& pivot) { pivot_ = pivot; } + void SetTheta(float theta); + + base::Vector2 GetOffset() const { return offset_; } + base::Vector2 GetScale() const { return scale_; } + base::Vector2 GetPivot() const { return pivot_; } + float GetTheta() const { return theta_; } + + // Pure virtuals for frame animation support. + virtual void SetFrame(size_t frame) = 0; + virtual size_t GetFrame() const = 0; + virtual size_t GetNumFrames() const = 0; + + virtual void SetColor(const base::Vector4& color) = 0; + virtual base::Vector4 GetColor() const = 0; + + void SetVisible(bool visible) { visible_ = visible; } + bool IsVisible() const { return visible_; } + + void PlaceToLeftOf(const Animatable& s) { + Translate({s.GetScale().x / -2.0f + GetScale().x / -2.0f, 0}); + } + + void PlaceToRightOf(const Animatable& s) { + Translate({s.GetScale().x / 2.0f + GetScale().x / 2.0f, 0}); + } + + void PlaceToTopOf(const Animatable& s) { + Translate({0, s.GetScale().y / 2.0f + GetScale().y / 2.0f}); + } + + void PlaceToBottomOf(const Animatable& s) { + Translate({0, s.GetScale().y / -2.0f + GetScale().y / -2.0f}); + } + + protected: + base::Vector2 offset_ = {0, 0}; + base::Vector2 scale_ = {1, 1}; + base::Vector2 pivot_ = {0, 0}; + base::Vector2 rotation_ = {0, 1}; + float theta_ = 0; + bool visible_ = false; +}; + +} // namespace eng + +#endif // SHAPE_H diff --git a/src/engine/animator.cc b/src/engine/animator.cc new file mode 100644 index 0000000..2a34669 --- /dev/null +++ b/src/engine/animator.cc @@ -0,0 +1,276 @@ +#include "animator.h" + +#include "../base/interpolation.h" +#include "../base/log.h" +#include "animatable.h" + +using namespace base; + +namespace eng { + +void Animator::Attach(Animatable* animatable) { + elements_.push_back({animatable, + {0, 0}, + 0, + animatable->GetColor(), + (int)animatable->GetFrame()}); +} + +void Animator::Play(int animation, bool loop) { + play_flags_ |= animation; + loop_flags_ |= loop ? animation : 0; +} + +void Animator::Pause(int animation) { + play_flags_ &= ~animation; +} + +void Animator::Stop(int animation) { + if ((animation & kMovement) != 0) + movement_time_ = 0; + if ((animation & kRotation) != 0) + rotation_time_ = 0; + if ((animation & kBlending) != 0) + blending_time_ = 0; + if ((animation & kFrames) != 0) + frame_time_ = 0; + if ((animation & kTimer) != 0) + timer_time_ = 0; + + play_flags_ |= animation; + Update(0); + play_flags_ &= ~animation; + loop_flags_ &= ~animation; +} + +void Animator::SetEndCallback(int animation, base::Closure cb) { + if ((inside_cb_ & animation) != 0) { + has_pending_cb_ = true; + pending_cb_ = std::move(cb); + } + if ((animation & kMovement) != 0 && inside_cb_ != kMovement) + movement_cb_ = std::move(cb); + if ((animation & kRotation) != 0 && inside_cb_ != kRotation) + rotation_cb_ = std::move(cb); + if ((animation & kBlending) != 0 && inside_cb_ != kBlending) + blending_cb_ = std::move(cb); + if ((animation & kFrames) != 0 && inside_cb_ != kFrames) + frame_cb_ = std::move(cb); + if ((animation & kTimer) != 0 && inside_cb_ != kTimer) + timer_cb_ = std::move(cb); +} + +void Animator::SetMovement(Vector2 direction, + float duration, + Interpolator interpolator) { + movement_direction_ = direction; + movement_speed_ = 1.0f / duration; + movement_interpolator_ = std::move(interpolator); + + for (auto& a : elements_) + a.movement_last_offset = {0, 0}; +} + +void Animator::SetRotation(float trget, + float duration, + Interpolator interpolator) { + rotation_target_ = trget; + rotation_speed_ = 1.0f / duration; + rotation_interpolator_ = std::move(interpolator); + + for (auto& a : elements_) + a.rotation_last_theta = 0; +} + +void Animator::SetBlending(Vector4 target, + float duration, + Interpolator interpolator) { + blending_target_ = target; + blending_speed_ = 1.0f / duration; + for (auto& a : elements_) + a.blending_start = a.animatable->GetColor(); + blending_interpolator_ = std::move(interpolator); +} + +void Animator::SetFrames(int count, + int frames_per_second, + Interpolator interpolator) { + frame_count_ = count; + frame_speed_ = (float)frames_per_second / (float)count; + for (auto& a : elements_) + a.frame_start_ = a.animatable->GetFrame(); + frame_interpolator_ = std::move(interpolator); +} + +void Animator::SetTimer(float duration) { + timer_speed_ = 1.0f / duration; +} + +void Animator::SetVisible(bool visible) { + for (auto& a : elements_) + a.animatable->SetVisible(visible); +} + +void Animator::Update(float delta_time) { + if (play_flags_ & kMovement) + UpdateMovement(delta_time); + if (play_flags_ & kRotation) + UpdateRotation(delta_time); + if (play_flags_ & kBlending) + UpdateBlending(delta_time); + if (play_flags_ & kFrames) + UpdateFrame(delta_time); + if (play_flags_ & kTimer) + UpdateTimer(delta_time); + + for (auto& a : elements_) { + if (play_flags_ & kMovement) { + float interpolated_time = movement_interpolator_ + ? movement_interpolator_(movement_time_) + : movement_time_; + Vector2 offset = + base::Lerp({0, 0}, movement_direction_, interpolated_time); + a.animatable->Translate(offset - a.movement_last_offset); + a.movement_last_offset = offset; + } + + if (play_flags_ & kRotation) { + float interpolated_time = rotation_interpolator_ + ? rotation_interpolator_(rotation_time_) + : rotation_time_; + float theta = base::Lerp(0.0f, rotation_target_, interpolated_time); + a.animatable->Rotate(theta - a.rotation_last_theta); + a.rotation_last_theta = theta; + } + + if (play_flags_ & kBlending) { + float interpolated_time = blending_interpolator_ + ? blending_interpolator_(blending_time_) + : blending_time_; + Vector4 r = + base::Lerp(a.blending_start, blending_target_, interpolated_time); + a.animatable->SetColor(r); + } + + if (play_flags_ & kFrames) { + float interpolated_time = + frame_interpolator_ ? frame_interpolator_(frame_time_) : frame_time_; + int target = a.frame_start_ + frame_count_; + int r = base::Lerp(a.frame_start_, target, interpolated_time); + if (r < target) + a.animatable->SetFrame(r); + } + } +} + +void Animator::UpdateMovement(float delta_time) { + if ((loop_flags_ & kMovement) == 0 && movement_time_ == 1.0f) { + movement_time_ = 0; + play_flags_ &= ~kMovement; + if (movement_cb_) { + inside_cb_ = kMovement; + movement_cb_(); + inside_cb_ = kNone; + if (has_pending_cb_) { + has_pending_cb_ = false; + movement_cb_ = std::move(pending_cb_); + } + } + return; + } + + movement_time_ += movement_speed_ * delta_time; + if (movement_time_ > 1) + movement_time_ = + (loop_flags_ & kMovement) == 0 ? 1 : fmod(movement_time_, 1.0f); +} + +void Animator::UpdateRotation(float delta_time) { + if ((loop_flags_ & kRotation) == 0 && rotation_time_ == 1.0f) { + rotation_time_ = 0; + play_flags_ &= ~kRotation; + if (rotation_cb_) { + inside_cb_ = kRotation; + rotation_cb_(); + inside_cb_ = kNone; + if (has_pending_cb_) { + has_pending_cb_ = false; + rotation_cb_ = std::move(pending_cb_); + } + } + return; + } + + rotation_time_ += rotation_speed_ * delta_time; + if (rotation_time_ > 1) + rotation_time_ = + (loop_flags_ & kRotation) == 0 ? 1 : fmod(rotation_time_, 1.0f); +} + +void Animator::UpdateBlending(float delta_time) { + if ((loop_flags_ & kBlending) == 0 && blending_time_ == 1.0f) { + blending_time_ = 0; + play_flags_ &= ~kBlending; + if (blending_cb_) { + inside_cb_ = kBlending; + blending_cb_(); + inside_cb_ = kNone; + if (has_pending_cb_) { + has_pending_cb_ = false; + blending_cb_ = std::move(pending_cb_); + } + } + return; + } + + blending_time_ += blending_speed_ * delta_time; + if (blending_time_ > 1) + blending_time_ = + (loop_flags_ & kBlending) == 0 ? 1 : fmod(blending_time_, 1.0f); +} + +void Animator::UpdateFrame(float delta_time) { + if ((loop_flags_ & kFrames) == 0 && frame_time_ == 1.0f) { + frame_time_ = 0; + play_flags_ &= ~kFrames; + if (frame_cb_) { + inside_cb_ = kFrames; + frame_cb_(); + inside_cb_ = kNone; + if (has_pending_cb_) { + has_pending_cb_ = false; + frame_cb_ = std::move(pending_cb_); + } + } + return; + } else if ((loop_flags_ & kFrames) != 0 && frame_time_ == 1.0f) { + frame_time_ = 0; + } + + frame_time_ += frame_speed_ * delta_time; + if (frame_time_ > 1) + frame_time_ = 1; +} + +void Animator::UpdateTimer(float delta_time) { + if (timer_time_ == 1.0f) { + timer_time_ = 0; + play_flags_ &= ~kTimer; + if (timer_cb_) { + inside_cb_ = kTimer; + timer_cb_(); + inside_cb_ = kNone; + if (has_pending_cb_) { + has_pending_cb_ = false; + timer_cb_ = std::move(pending_cb_); + } + } + return; + } + + timer_time_ += timer_speed_ * delta_time; + if (timer_time_ > 1) + timer_time_ = 1; +} + +} // namespace eng diff --git a/src/engine/animator.h b/src/engine/animator.h new file mode 100644 index 0000000..4c2d952 --- /dev/null +++ b/src/engine/animator.h @@ -0,0 +1,136 @@ +#ifndef ANIMATOR_H +#define ANIMATOR_H + +#include + +#include "../base/closure.h" +#include "../base/vecmath.h" + +namespace eng { + +class Animatable; + +class Animator { + public: + // Animation type flags. + enum Flags { + kNone = 0, + kMovement = 1, + kRotation = 2, + kBlending = 4, + kFrames = 8, + kTimer = 16, + kAllAnimations = kMovement | kRotation | kBlending | kFrames + }; + + using Interpolator = std::function; + + Animator() = default; + ~Animator() = default; + + // Attached the given animatable to this animator and sets the start values. + void Attach(Animatable* animatable); + + void Play(int animation, bool loop); + void Pause(int animation); + void Stop(int animation); + + // Set callback for the given animations. It's called for each animation once + // it ends. Not that it's not called for looping animations. + void SetEndCallback(int animation, base::Closure cb); + + // Set movement animation parameters. Movement is relative to the attached + // animatable's current position. Distance is calculated from the magnitude of + // direction vector. Duration is in seconds. + void SetMovement(base::Vector2 direction, + float duration, + Interpolator interpolator = nullptr); + + // Set rotation animation parameters. Rotation is relative to the attached + // animatable's current rotation. Duration is in seconds. + void SetRotation(float target, + float duration, + Interpolator interpolator = nullptr); + + // Set color blending animation parameters. Color blending animation is + // absolute. The absolute start colors are obtained from the attached + // animatables. Duration is in seconds. + void SetBlending(base::Vector4 target, + float duration, + Interpolator interpolator = nullptr); + + // Set frame playback animation parameters. Frame animation is absolute. The + // absolute start frames are obtained from the attached animatables. Plays + // count number of frames. + void SetFrames(int count, + int frames_per_second, + Interpolator interpolator = nullptr); + + // Set Timer parameters. Timer doesn't play any animation. Usefull for + // triggering a callback after the given seconds passed. Loop parameter is + // ignored when played. + void SetTimer(float duration); + + // Set visibility of all attached animatables. + void SetVisible(bool visible); + + void Update(float delta_time); + + bool IsPlaying(int animation) const { return play_flags_ & animation; } + + private: + struct Element { + Animatable* animatable; + base::Vector2 movement_last_offset = {0, 0}; + float rotation_last_theta = 0; + base::Vector4 blending_start = {0, 0, 0, 0}; + int frame_start_ = 0; + }; + + unsigned int play_flags_ = 0; + unsigned int loop_flags_ = 0; + std::vector elements_; + + base::Vector2 movement_direction_ = {0, 0}; + float movement_speed_ = 0; + float movement_time_ = 0; + Interpolator movement_interpolator_; + base::Closure movement_cb_; + + float rotation_target_ = 0; + float rotation_speed_ = 0; + float rotation_time_ = 0; + Interpolator rotation_interpolator_; + base::Closure rotation_cb_; + + base::Vector4 blending_target_ = {0, 0, 0, 0}; + float blending_speed_ = 0; + float blending_time_ = 0; + Interpolator blending_interpolator_; + base::Closure blending_cb_; + + int frame_count_ = 0; + float frame_speed_ = 0; + float frame_time_ = 0; + Interpolator frame_interpolator_; + base::Closure frame_cb_; + + float timer_speed_ = 0; + float timer_time_ = 0; + base::Closure timer_cb_; + + // State used to set new callback during a callback. + bool has_pending_cb_ = false; + base::Closure pending_cb_; + Flags inside_cb_ = kNone; + + void UpdateMovement(float delta_time); + void UpdateRotation(float delta_time); + void UpdateBlending(float delta_time); + void UpdateFrame(float delta_time); + void UpdateTimer(float delta_time); +}; + +} // namespace eng + +#endif // ANIMATOR_H diff --git a/src/engine/audio/audio.h b/src/engine/audio/audio.h new file mode 100644 index 0000000..ee1989a --- /dev/null +++ b/src/engine/audio/audio.h @@ -0,0 +1,20 @@ +#ifndef AUDIO_H +#define AUDIO_H + +#if defined(__ANDROID__) +#include "audio_oboe.h" +#elif defined(__linux__) +#include "audio_alsa.h" +#endif + +namespace eng { + +#if defined(__ANDROID__) +using Audio = AudioOboe; +#elif defined(__linux__) +using Audio = AudioAlsa; +#endif + +} // namespace eng + +#endif // AUDIO_H diff --git a/src/engine/audio/audio_alsa.cc b/src/engine/audio/audio_alsa.cc new file mode 100644 index 0000000..cc39e96 --- /dev/null +++ b/src/engine/audio/audio_alsa.cc @@ -0,0 +1,187 @@ +#include "audio_alsa.h" + +#include + +#include "../../base/log.h" +#include "audio_resource.h" + +using namespace base; + +namespace eng { + +AudioAlsa::AudioAlsa() = default; + +AudioAlsa::~AudioAlsa() = default; + +bool AudioAlsa::Initialize() { + LOG << "Initializing audio system."; + + int err; + + // Contains information about the hardware. + snd_pcm_hw_params_t* hw_params; + + // "default" is usualy PulseAudio. Use "plughw:CARD=PCH" instead for direct + // hardware device with software format conversion. + if ((err = snd_pcm_open(&pcm_handle_, "plughw:CARD=PCH", + SND_PCM_STREAM_PLAYBACK, 0)) < 0) { + LOG << "Cannot open audio device. Error: " << snd_strerror(err); + return false; + } + + do { + // Allocate the snd_pcm_hw_params_t structure on the stack. + snd_pcm_hw_params_alloca(&hw_params); + + // Init hw_params with full configuration space. + if ((err = snd_pcm_hw_params_any(pcm_handle_, hw_params)) < 0) { + LOG << "Cannot initialize hardware parameter structure. Error: " + << snd_strerror(err); + break; + } + + if ((err = snd_pcm_hw_params_set_access( + pcm_handle_, hw_params, SND_PCM_ACCESS_RW_INTERLEAVED)) < 0) { + LOG << "Cannot set access type. Error: " << snd_strerror(err); + break; + } + + if ((err = snd_pcm_hw_params_set_format(pcm_handle_, hw_params, + SND_PCM_FORMAT_FLOAT)) < 0) { + LOG << "Cannot set sample format. Error: " << snd_strerror(err); + break; + } + + // Disable software resampler. + if ((err = snd_pcm_hw_params_set_rate_resample(pcm_handle_, hw_params, 0)) < + 0) { + LOG << "Cannot disbale software resampler. Error: " << snd_strerror(err); + break; + } + + unsigned sample_rate = 48000; + if ((err = snd_pcm_hw_params_set_rate_near(pcm_handle_, hw_params, + &sample_rate, 0)) < 0) { + LOG << "Cannot set sample rate. Error: " << snd_strerror(err); + break; + } + + if ((err = snd_pcm_hw_params_set_channels(pcm_handle_, hw_params, 2)) < 0) { + LOG << "Cannot set channel count. Error: " << snd_strerror(err); + break; + } + + // Set period time to 4 ms. The latency will be 12 ms for 3 perods. + unsigned period_time = 4000; + if ((err = snd_pcm_hw_params_set_period_time_near(pcm_handle_, hw_params, + &period_time, 0)) < 0) { + LOG << "Cannot set periods. Error: " << snd_strerror(err); + break; + } + + unsigned periods = 3; + if ((err = snd_pcm_hw_params_set_periods_near(pcm_handle_, hw_params, + &periods, 0)) < 0) { + LOG << "Cannot set periods. Error: " << snd_strerror(err); + break; + } + + // Apply HW parameter settings to PCM device and prepare device. + if ((err = snd_pcm_hw_params(pcm_handle_, hw_params)) < 0) { + LOG << "Cannot set parameters. Error: " << snd_strerror(err); + break; + } + + if ((err = snd_pcm_prepare(pcm_handle_)) < 0) { + LOG << "Cannot prepare audio interface for use. Error: " + << snd_strerror(err); + break; + } + + snd_pcm_access_t access; + unsigned num_channels; + snd_pcm_format_t format; + snd_pcm_uframes_t period_size; + snd_pcm_uframes_t buffer_size; + + snd_pcm_hw_params_get_access(hw_params, &access); + snd_pcm_hw_params_get_channels(hw_params, &num_channels); + snd_pcm_hw_params_get_format(hw_params, &format); + snd_pcm_hw_params_get_period_size(hw_params, &period_size, nullptr); + snd_pcm_hw_params_get_period_time(hw_params, &period_time, nullptr); + snd_pcm_hw_params_get_periods(hw_params, &periods, nullptr); + snd_pcm_hw_params_get_buffer_size(hw_params, &buffer_size); + + LOG << "Alsa Audio:"; + LOG << " access: " << snd_pcm_access_name(access); + LOG << " format: " << snd_pcm_format_name(format); + LOG << " channel count: " << num_channels; + LOG << " sample rate: " << sample_rate; + LOG << " period size: " << period_size; + LOG << " period time: " << period_time; + LOG << " periods: " << periods; + LOG << " buffer_size: " << buffer_size; + + num_channels_ = num_channels; + sample_rate_ = sample_rate; + period_size_ = period_size; + + StartWorker(); + + return true; + } while (false); + + snd_pcm_close(pcm_handle_); + return false; +} + +void AudioAlsa::Shutdown() { + LOG << "Shutting down audio system."; + TerminateWorker(); + snd_pcm_drop(pcm_handle_); + snd_pcm_close(pcm_handle_); +} + +size_t AudioAlsa::GetSampleRate() { + return sample_rate_; +} + +bool AudioAlsa::StartWorker() { + LOG << "Starting audio thread."; + + std::promise promise; + std::future future = promise.get_future(); + worker_thread_ = + std::thread(&AudioAlsa::WorkerMain, this, std::move(promise)); + return future.get(); +} + +void AudioAlsa::TerminateWorker() { + // Notify worker thread and wait for it to terminate. + if (terminate_worker_) + return; + terminate_worker_ = true; + LOG << "Terminating audio thread"; + worker_thread_.join(); +} + +void AudioAlsa::WorkerMain(std::promise promise) { + promise.set_value(true); + + size_t num_frames = period_size_ / (num_channels_ * sizeof(float)); + auto buffer = std::make_unique(num_frames * 2); + + for (;;) { + if (terminate_worker_) + return; + + RenderAudio(buffer.get(), num_frames); + + while (snd_pcm_writei(pcm_handle_, buffer.get(), num_frames) < 0) { + snd_pcm_prepare(pcm_handle_); + LOG << "Audio buffer underrun!"; + } + } +} + +} // namespace eng diff --git a/src/engine/audio/audio_alsa.h b/src/engine/audio/audio_alsa.h new file mode 100644 index 0000000..fd54f2f --- /dev/null +++ b/src/engine/audio/audio_alsa.h @@ -0,0 +1,46 @@ +#ifndef AUDIO_ALSA_H +#define AUDIO_ALSA_H + +#include +#include +#include + +#include "audio_base.h" + +typedef struct _snd_pcm snd_pcm_t; + +namespace eng { + +class AudioResource; + +class AudioAlsa : public AudioBase { + public: + AudioAlsa(); + ~AudioAlsa(); + + bool Initialize(); + + void Shutdown(); + + size_t GetSampleRate(); + + private: + // Handle for the PCM device. + snd_pcm_t* pcm_handle_; + + std::thread worker_thread_; + bool terminate_worker_ = false; + + size_t num_channels_ = 0; + size_t sample_rate_ = 0; + size_t period_size_ = 0; + + bool StartWorker(); + void TerminateWorker(); + + void WorkerMain(std::promise promise); +}; + +} // namespace eng + +#endif // AUDIO_ALSA_H diff --git a/src/engine/audio/audio_base.cc b/src/engine/audio/audio_base.cc new file mode 100644 index 0000000..e6ea592 --- /dev/null +++ b/src/engine/audio/audio_base.cc @@ -0,0 +1,138 @@ +#include "audio_base.h" + +#include + +#include "../../base/log.h" +#include "../sound.h" + +using namespace base; + +namespace eng { + +AudioBase::AudioBase() = default; + +AudioBase::~AudioBase() { + worker_.Join(); +} + +void AudioBase::Play(std::shared_ptr sample) { + std::unique_lock scoped_lock(mutex_); + samples_[0].push_back(sample); +} + +void AudioBase::Update() { + task_runner_.Run(); +} + +void AudioBase::RenderAudio(float* output_buffer, size_t num_frames) { + { + std::unique_lock scoped_lock(mutex_); + samples_[1].splice(samples_[1].end(), samples_[0]); + } + + memset(output_buffer, 0, sizeof(float) * num_frames * kChannelCount); + + for (auto it = samples_[1].begin(); it != samples_[1].end();) { + AudioSample* sample = it->get(); + + unsigned flags = sample->flags; + bool remove = false; + + if (flags & AudioSample::kStopped) { + remove = true; + } else { + auto sound = sample->sound.get(); + + const float* src[2] = {const_cast(sound)->GetBuffer(0), + const_cast(sound)->GetBuffer(1)}; + if (!src[1]) + src[1] = src[0]; // mono. + + size_t num_samples = sound->GetNumSamples(); + size_t num_channels = sound->num_channels(); + size_t src_index = sample->src_index; + size_t step = sample->step; + size_t accumulator = sample->accumulator; + float amplitude = sample->amplitude; + float amplitude_inc = sample->amplitude_inc; + float max_amplitude = sample->max_amplitude; + + size_t channel_offset = + (flags & AudioSample::kSimulateStereo) && num_channels == 1 + ? sound->hz() / 10 + : 0; + + for (size_t i = 0; i < num_frames * kChannelCount;) { + // Mix the 1st channel. + output_buffer[i++] += src[0][src_index] * amplitude; + + // Mix the 2nd channel. Offset the source index for stereo simulation. + size_t ind = channel_offset + src_index; + if (ind < num_samples) + output_buffer[i++] += src[1][ind] * amplitude; + else if (flags & AudioSample::kLoop) + output_buffer[i++] += src[1][ind % num_samples] * amplitude; + else + i++; + + // Apply amplitude modification. + amplitude += amplitude_inc; + if (amplitude <= 0) { + remove = true; + break; + } else if (amplitude > max_amplitude) { + amplitude = max_amplitude; + } + + // Basic resampling for variations. + accumulator += step; + src_index += accumulator / 10; + accumulator %= 10; + + // Advance source index. + if (src_index >= num_samples) { + if (!sound->is_streaming_sound()) { + if (flags & AudioSample::kLoop) { + src_index %= num_samples; + } else { + remove = true; + break; + } + } else if (!sound->IsStreamingInProgress()) { + if (sound->eof()) { + remove = true; + break; + } + + src_index = 0; + + // Swap buffers and start streaming in background. + sound->SwapBuffers(); + src[0] = const_cast(sound)->GetBuffer(0); + src[1] = const_cast(sound)->GetBuffer(1); + + worker_.Enqueue(std::bind(&Sound::Stream, sample->sound, + flags & AudioSample::kLoop)); + } else { + LOG << "Buffer underrun!"; + src_index = 0; + } + } + } + + sample->src_index = src_index; + sample->accumulator = accumulator; + sample->amplitude = amplitude; + } + + if (remove) { + task_runner_.Enqueue(sample->end_cb); + sample->active = false; + it = samples_[1].erase(it); + } else { + ++it; + } + } +} + +} // namespace eng diff --git a/src/engine/audio/audio_base.h b/src/engine/audio/audio_base.h new file mode 100644 index 0000000..217bd84 --- /dev/null +++ b/src/engine/audio/audio_base.h @@ -0,0 +1,41 @@ +#ifndef AUDIO_BASE_H +#define AUDIO_BASE_H + +#include +#include +#include + +#include "../../base/closure.h" +#include "../../base/task_runner.h" +#include "../../base/worker.h" +#include "audio_sample.h" + +namespace eng { + +class Sound; + +class AudioBase { + public: + void Play(std::shared_ptr impl_data); + + void Update(); + + protected: + static constexpr int kChannelCount = 2; + + std::list> samples_[2]; + std::mutex mutex_; + + base::Worker worker_{1}; + + base::TaskRunner task_runner_; + + AudioBase(); + ~AudioBase(); + + void RenderAudio(float* output_buffer, size_t num_frames); +}; + +} // namespace eng + +#endif // AUDIO_BASE_H diff --git a/src/engine/audio/audio_forward.h b/src/engine/audio/audio_forward.h new file mode 100644 index 0000000..d3ec438 --- /dev/null +++ b/src/engine/audio/audio_forward.h @@ -0,0 +1,16 @@ +#ifndef AUDIO_FORWARD_H +#define AUDIO_FORWARD_H + +namespace eng { + +#if defined(__ANDROID__) +class AudioOboe; +using Audio = AudioOboe; +#elif defined(__linux__) +class AudioAlsa; +using Audio = AudioAlsa; +#endif + +} // namespace eng + +#endif // AUDIO_FORWARD_H diff --git a/src/engine/audio/audio_oboe.cc b/src/engine/audio/audio_oboe.cc new file mode 100644 index 0000000..fc626d8 --- /dev/null +++ b/src/engine/audio/audio_oboe.cc @@ -0,0 +1,78 @@ +#include "audio_oboe.h" + +#include "../../base/log.h" +#include "../../third_party/oboe/include/oboe/Oboe.h" +#include "audio_resource.h" + +using namespace base; + +namespace eng { + +AudioOboe::AudioOboe() : callback_(std::make_unique(this)) {} + +AudioOboe::~AudioOboe() = default; + +bool AudioOboe::Initialize() { + LOG << "Initializing audio system."; + + return RestartStream(); +} + +void AudioOboe::Shutdown() { + LOG << "Shutting down audio system."; +} + +size_t AudioOboe::GetSampleRate() { + return stream_->getSampleRate(); +} + +AudioOboe::StreamCallback::StreamCallback(AudioOboe* audio) : audio_(audio) {} + +AudioOboe::StreamCallback::~StreamCallback() = default; + +oboe::DataCallbackResult AudioOboe::StreamCallback::onAudioReady( + oboe::AudioStream* oboe_stream, + void* audio_data, + int32_t num_frames) { + float* output_buffer = static_cast(audio_data); + audio_->RenderAudio(output_buffer, num_frames); + return oboe::DataCallbackResult::Continue; +} + +void AudioOboe::StreamCallback::onErrorAfterClose( + oboe::AudioStream* oboe_stream, + oboe::Result error) { + LOG << "Error after close. Error: " << oboe::convertToText(error); + + audio_->RestartStream(); +} + +bool AudioOboe::RestartStream() { + oboe::AudioStreamBuilder builder; + oboe::Result result = + builder.setSharingMode(oboe::SharingMode::Exclusive) + ->setPerformanceMode(oboe::PerformanceMode::LowLatency) + ->setFormat(oboe::AudioFormat::Float) + ->setChannelCount(kChannelCount) + ->setDirection(oboe::Direction::Output) + ->setUsage(oboe::Usage::Game) + ->setCallback(callback_.get()) + ->openManagedStream(stream_); + + LOG << "Oboe Audio Stream:"; + LOG << " performance mode: " << (int)stream_->getPerformanceMode(); + LOG << " format: " << (int)stream_->getFormat(); + LOG << " channel count: " << stream_->getChannelCount(); + LOG << " sample rate: " << stream_->getSampleRate(); + + if (result != oboe::Result::OK) { + LOG << "Failed to create the playback stream. Error: " + << oboe::convertToText(result); + return false; + } + + stream_->start(); + return true; +} + +} // namespace eng diff --git a/src/engine/audio/audio_oboe.h b/src/engine/audio/audio_oboe.h new file mode 100644 index 0000000..ae91a35 --- /dev/null +++ b/src/engine/audio/audio_oboe.h @@ -0,0 +1,50 @@ +#ifndef AUDIO_OBOE_H +#define AUDIO_OBOE_H + +#include + +#include "../../third_party/oboe/include/oboe/AudioStream.h" +#include "../../third_party/oboe/include/oboe/AudioStreamCallback.h" +#include "audio_base.h" + +namespace eng { + +class AudioResource; + +class AudioOboe : public AudioBase { + public: + AudioOboe(); + ~AudioOboe(); + + bool Initialize(); + + void Shutdown(); + + size_t GetSampleRate(); + + private: + class StreamCallback : public oboe::AudioStreamCallback { + public: + StreamCallback(AudioOboe* audio); + ~StreamCallback() override; + + oboe::DataCallbackResult onAudioReady(oboe::AudioStream* oboe_stream, + void* audio_data, + int32_t num_frames) override; + + void onErrorAfterClose(oboe::AudioStream* oboe_stream, + oboe::Result error) override; + + private: + AudioOboe* audio_; + }; + + oboe::ManagedStream stream_; + std::unique_ptr callback_; + + bool RestartStream(); +}; + +} // namespace eng + +#endif // AUDIO_OBOE_H diff --git a/src/engine/audio/audio_resource.cc b/src/engine/audio/audio_resource.cc new file mode 100644 index 0000000..abdcda6 --- /dev/null +++ b/src/engine/audio/audio_resource.cc @@ -0,0 +1,72 @@ +#include "audio_resource.h" + +#include "../../base/log.h" +#include "../sound.h" +#include "audio.h" +#include "audio_sample.h" + +namespace eng { + +AudioResource::AudioResource(Audio* audio) + : sample_(std::make_shared()), audio_(audio) {} + +AudioResource::~AudioResource() { + sample_->flags |= AudioSample::kStopped; +} + +void AudioResource::Play(std::shared_ptr sound, + float amplitude, + bool reset_pos) { + if (sample_->active) + return; + + if (reset_pos) { + sample_->src_index = 0; + sample_->accumulator = 0; + } + sample_->flags &= ~AudioSample::kStopped; + sample_->sound = sound; + sample_->amplitude = amplitude; + sample_->active = true; + + audio_->Play(sample_); +} + +void AudioResource::Stop() { + if (!sample_->active) + return; + + sample_->flags |= AudioSample::kStopped; +} + +void AudioResource::SetLoop(bool loop) { + if (loop) + sample_->flags |= AudioSample::kLoop; + else + sample_->flags &= ~AudioSample::kLoop; +} + +void AudioResource::SetSimulateStereo(bool simulate) { + if (simulate) + sample_->flags |= AudioSample::kSimulateStereo; + else + sample_->flags &= ~AudioSample::kSimulateStereo; +} + +void AudioResource::SetResampleStep(size_t step) { + sample_->step = step + 10; +} + +void AudioResource::SetMaxAmplitude(float max_amplitude) { + sample_->max_amplitude = max_amplitude; +} + +void AudioResource::SetAmplitudeInc(float amplitude_inc) { + sample_->amplitude_inc = amplitude_inc; +} + +void AudioResource::SetEndCallback(base::Closure cb) { + sample_->end_cb = cb; +} + +} // namespace eng diff --git a/src/engine/audio/audio_resource.h b/src/engine/audio/audio_resource.h new file mode 100644 index 0000000..b791227 --- /dev/null +++ b/src/engine/audio/audio_resource.h @@ -0,0 +1,41 @@ +#ifndef AUDIO_RESOURCE_H +#define AUDIO_RESOURCE_H + +#include + +#include "../../base/closure.h" +#include "audio_forward.h" + +namespace eng { + +struct AudioSample; +class Sound; + +class AudioResource { + public: + AudioResource(Audio* audio); + ~AudioResource(); + + void Play(std::shared_ptr sound, float amplitude, bool reset_pos); + + void Stop(); + + void SetLoop(bool loop); + void SetSimulateStereo(bool simulate); + void SetResampleStep(size_t step); + void SetMaxAmplitude(float max_amplitude); + void SetAmplitudeInc(float amplitude_inc); + void SetEndCallback(base::Closure cb); + + private: + std::shared_ptr sample_; + + Audio* audio_ = nullptr; + + AudioResource(const AudioResource&) = delete; + AudioResource& operator=(const AudioResource&) = delete; +}; + +} // namespace eng + +#endif // AUDIO_RESOURCE_H diff --git a/src/engine/audio/audio_sample.h b/src/engine/audio/audio_sample.h new file mode 100644 index 0000000..5d11d7a --- /dev/null +++ b/src/engine/audio/audio_sample.h @@ -0,0 +1,32 @@ +#ifndef AUDIO_SAMPLE_H +#define AUDIO_SAMPLE_H + +#include + +#include "../../base/closure.h" + +namespace eng { + +class Sound; + +struct AudioSample { + enum SampleFlags { kLoop = 1, kStopped = 2, kSimulateStereo = 4 }; + + // Read-only accessed by the audio thread. + std::shared_ptr sound; + unsigned flags = 0; + size_t step = 10; + float amplitude_inc = 0; + float max_amplitude = 1.0f; + base::Closure end_cb; + + // Write accessed by the audio thread. + size_t src_index = 0; + size_t accumulator = 0; + float amplitude = 1.0f; + bool active = false; +}; + +} // namespace eng + +#endif // AUDIO_SAMPLE_H diff --git a/src/engine/engine.cc b/src/engine/engine.cc new file mode 100644 index 0000000..f41846d --- /dev/null +++ b/src/engine/engine.cc @@ -0,0 +1,314 @@ +#include "engine.h" + +#include "../base/log.h" +#include "../base/worker.h" +#include "../third_party/texture_compressor/texture_compressor.h" +#include "audio/audio.h" +#include "audio/audio_resource.h" +#include "font.h" +#include "game.h" +#include "game_factory.h" +#include "image.h" +#include "input_event.h" +#include "mesh.h" +#include "platform/platform.h" +#include "renderer/geometry.h" +#include "renderer/render_command.h" +#include "renderer/renderer.h" +#include "renderer/shader.h" +#include "renderer/texture.h" +#include "shader_source.h" + +using namespace base; + +namespace eng { + +Engine* Engine::singleton = nullptr; + +Engine::Engine(Platform* platform, Renderer* renderer, Audio* audio) + : platform_(platform), renderer_(renderer), audio_(audio) { + assert(!singleton); + singleton = this; + + renderer_->SetContextLostCB(std::bind(&Engine::ContextLost, this)); + + quad_ = CreateRenderResource(); + pass_through_shader_ = CreateRenderResource(); + solid_shader_ = CreateRenderResource(); +} + +Engine::~Engine() { + singleton = nullptr; +} + +Engine& Engine::Get() { + return *singleton; +} + +bool Engine::Initialize() { + // The orthogonal viewport is (-1.0 .. 1.0) for the short edge of the screen. + // For the long endge, it's calculated from aspect ratio. + if (GetScreenWidth() > GetScreenHeight()) { + float aspect_ratio = (float)GetScreenWidth() / (float)GetScreenHeight(); + LOG << "aspect ratio: " << aspect_ratio; + screen_size_ = {aspect_ratio * 2.0f, 2.0f}; + projection_ = base::Ortho(-aspect_ratio, aspect_ratio, -1.0f, 1.0f); + } else { + float aspect_ratio = (float)GetScreenHeight() / (float)GetScreenWidth(); + LOG << "aspect_ratio: " << aspect_ratio; + screen_size_ = {2.0f, aspect_ratio * 2.0f}; + projection_ = base::Ortho(-1.0, 1.0, -aspect_ratio, aspect_ratio); + } + + if (renderer_->SupportsDXT5()) { + tex_comp_alpha_ = TextureCompressor::Create(TextureCompressor::kFormatDXT5); + } else if (renderer_->SupportsATC()) { + tex_comp_alpha_ = + TextureCompressor::Create(TextureCompressor::kFormatATCIA); + } + + if (renderer_->SupportsDXT1()) { + tex_comp_opaque_ = + TextureCompressor::Create(TextureCompressor::kFormatDXT1); + } else if (renderer_->SupportsATC()) { + tex_comp_opaque_ = TextureCompressor::Create(TextureCompressor::kFormatATC); + } else if (renderer_->SupportsETC1()) { + tex_comp_opaque_ = + TextureCompressor::Create(TextureCompressor::kFormatETC1); + } + + system_font_ = std::make_unique(); + system_font_->Load("engine/RobotoMono-Regular.ttf"); + + if (!CreateRenderResources()) + return false; + + game_ = GameFactoryBase::CreateGame(""); + if (!game_) { + printf("No game found to run.\n"); + return false; + } + + if (!game_->Initialize()) { + LOG << "Failed to initialize the game."; + return false; + } + + return true; +} + +void Engine::Shutdown() { + LOG << "Shutting down engine."; +} + +void Engine::Update(float delta_time) { + seconds_accumulated_ += delta_time; + + audio_->Update(); + renderer_->Update(); + + game_->Update(delta_time); + + fps_seconds_ += delta_time; + if (fps_seconds_ >= 1) { + fps_ = renderer_->GetAndResetFPS(); + fps_seconds_ = 0; + } + + if (stats_.IsVisible()) + PrintStats(); +} + +void Engine::Draw(float frame_frac) { + auto cmd = std::make_unique(); + cmd->rgba = {0, 0, 0, 1}; + renderer_->EnqueueCommand(std::move(cmd)); + renderer_->EnqueueCommand(std::make_unique()); + + game_->Draw(frame_frac); + + if (stats_.IsVisible()) + stats_.Draw(); + + renderer_->EnqueueCommand(std::make_unique()); +} + +void Engine::LostFocus() { + if (game_) + game_->LostFocus(); +} + +void Engine::GainedFocus() { + if (game_) + game_->GainedFocus(); +} + +void Engine::Exit() { + platform_->Exit(); +} + +Vector2 Engine::ToScale(const Vector2& vec) { + return GetScreenSize() * vec / + Vector2((float)GetScreenWidth(), (float)GetScreenHeight()); +} + +Vector2 Engine::ToPosition(const Vector2& vec) { + return ToScale(vec) - GetScreenSize() / 2.0f; +} + +std::shared_ptr Engine::CreateAudioResource() { + return std::make_shared(audio_); +} + +void Engine::AddInputEvent(std::unique_ptr event) { + switch (event->GetType()) { + case InputEvent::kTap: + if (((GetScreenSize() / 2) * 0.9f - event->GetVector(0)).Magnitude() <= + 0.25f) { + SetSatsVisible(!stats_.IsVisible()); + // Consume event. + return; + } + break; + case InputEvent::kKeyPress: + if (event->GetKeyPress() == 's') { + SetSatsVisible(!stats_.IsVisible()); + // Consume event. + return; + } + break; + case InputEvent::kDrag: + if (stats_.IsVisible()) { + if ((stats_.GetOffset() - event->GetVector(0)).Magnitude() <= + stats_.GetScale().y) + stats_.SetOffset(event->GetVector(0)); + // TODO: Enqueue DragCancel so we can consume this event. + } + break; + default: + break; + } + + input_queue_.push_back(std::move(event)); +} + +std::unique_ptr Engine::GetNextInputEvent() { + std::unique_ptr event; + if (!input_queue_.empty()) { + event.swap(input_queue_.front()); + input_queue_.pop_front(); + } + return event; +} + +TextureCompressor* Engine::GetTextureCompressor(bool opacity) { + return opacity ? tex_comp_alpha_.get() : tex_comp_opaque_.get(); +} + +int Engine::GetScreenWidth() const { + return renderer_->screen_width(); +} + +int Engine::GetScreenHeight() const { + return renderer_->screen_height(); +} + +int Engine::GetDeviceDpi() const { + return platform_->GetDeviceDpi(); +} + +const std::string& Engine::GetRootPath() const { + return platform_->GetRootPath(); +} + +size_t Engine::GetAudioSampleRate() { + return audio_->GetSampleRate(); +} + +bool Engine::IsMobile() const { + return platform_->mobile_device(); +} + +std::shared_ptr Engine::CreateRenderResourceInternal( + RenderResourceFactoryBase& factory) { + return renderer_->CreateResource(factory); +} + +void Engine::ContextLost() { + CreateRenderResources(); + + game_->ContextLost(); +} + +bool Engine::CreateRenderResources() { + // Create the quad geometry we can reuse for all sprites. + auto quad_mesh = std::make_unique(); + if (!quad_mesh->Load("engine/quad.mesh")) { + LOG << "Could not create quad mesh."; + return false; + } + quad_->Create(std::move(quad_mesh)); + + // Create the shader we can reuse for texture rendering. + auto source = std::make_unique(); + if (!source->Load("engine/pass_through.glsl")) { + LOG << "Could not create pass through shader."; + return false; + } + pass_through_shader_->Create(std::move(source), quad_->vertex_description()); + + // Create the shader we can reuse for solid rendering. + source = std::make_unique(); + if (!source->Load("engine/solid.glsl")) { + LOG << "Could not create solid shader."; + return false; + } + solid_shader_->Create(std::move(source), quad_->vertex_description()); + + return true; +} + +void Engine::SetSatsVisible(bool visible) { + stats_.SetVisible(visible); + if (visible) + stats_.Create(CreateRenderResource()); + else + stats_.Destory(); +} + +void Engine::PrintStats() { + constexpr int width = 200; + std::vector lines; + std::string line; + line = "fps: "; + line += std::to_string(fps_); + lines.push_back(line); + line = "cmd: "; + line += std::to_string(renderer_->global_queue_size() + + renderer_->render_queue_size()); + lines.push_back(line); + + constexpr int margin = 5; + int line_height = system_font_->GetLineHeight(); + int image_width = width + margin * 2; + int image_height = (line_height + margin) * lines.size() + margin; + + auto image = std::make_unique(); + image->Create(image_width, image_height); + image->Clear({1, 1, 1, 0.08f}); + + Worker worker(2); + int y = margin; + for (auto& text : lines) { + worker.Enqueue(std::bind(&Font::Print, system_font_.get(), margin, y, + text.c_str(), image->GetBuffer(), + image->GetWidth())); + y += line_height + margin; + } + worker.Join(); + + stats_.GetTexture()->Update(std::move(image)); + stats_.AutoScale(); +} + +} // namespace eng diff --git a/src/engine/engine.h b/src/engine/engine.h new file mode 100644 index 0000000..1c40181 --- /dev/null +++ b/src/engine/engine.h @@ -0,0 +1,148 @@ +#ifndef ENGINE_H +#define ENGINE_H + +#include +#include +#include + +#include "../base/random.h" +#include "../base/vecmath.h" +#include "audio/audio_forward.h" +#include "image_quad.h" +#include "renderer/render_resource.h" + +class TextureCompressor; + +namespace eng { + +class AudioResource; +class Font; +class Game; +class InputEvent; +class Renderer; +struct RenderCommand; +class Platform; +class Geometry; +class Shader; + +class Engine { + public: + Engine(Platform* platform, Renderer* renderer, Audio* audio); + ~Engine(); + + static Engine& Get(); + + bool Initialize(); + + void Shutdown(); + + void Update(float delta_time); + void Draw(float frame_frac); + + void LostFocus(); + void GainedFocus(); + + void Exit(); + + // Convert size from pixels to viewport scale. + base::Vector2 ToScale(const base::Vector2& vec); + + // Convert position form pixels to viewport coordinates. + base::Vector2 ToPosition(const base::Vector2& vec); + + template + std::shared_ptr CreateRenderResource() { + RenderResourceFactory factory; + return std::dynamic_pointer_cast(CreateRenderResourceInternal(factory)); + } + + std::shared_ptr CreateAudioResource(); + + void AddInputEvent(std::unique_ptr event); + std::unique_ptr GetNextInputEvent(); + + // Access to the render resources. + std::shared_ptr GetQuad() { return quad_; } + std::shared_ptr GetPassThroughShader() { + return pass_through_shader_; + } + std::shared_ptr GetSolidShader() { return solid_shader_; } + + const Font* GetSystemFont() { return system_font_.get(); } + + base::Random& GetRandomGenerator() { return random_; } + + TextureCompressor* GetTextureCompressor(bool opacity); + + Game* GetGame() { return game_.get(); } + + // Return screen width/height in pixels. + int GetScreenWidth() const; + int GetScreenHeight() const; + + // Return screen size in viewport scale. + base::Vector2 GetScreenSize() const { return screen_size_; } + + const base::Matrix4x4& GetProjectionMarix() const { return projection_; } + + int GetDeviceDpi() const; + + const std::string& GetRootPath() const; + + size_t GetAudioSampleRate(); + + bool IsMobile() const; + + float seconds_accumulated() const { return seconds_accumulated_; } + + private: + static Engine* singleton; + + Platform* platform_ = nullptr; + + Renderer* renderer_ = nullptr; + + Audio* audio_; + + std::unique_ptr game_; + + std::shared_ptr quad_; + std::shared_ptr pass_through_shader_; + std::shared_ptr solid_shader_; + + base::Vector2 screen_size_ = {0, 0}; + base::Matrix4x4 projection_; + + std::unique_ptr system_font_; + + std::unique_ptr tex_comp_opaque_; + std::unique_ptr tex_comp_alpha_; + + ImageQuad stats_; + + float fps_seconds_ = 0; + int fps_ = 0; + + float seconds_accumulated_ = 0.0f; + + std::deque> input_queue_; + + base::Random random_; + + std::shared_ptr CreateRenderResourceInternal( + RenderResourceFactoryBase& factory); + + void ContextLost(); + + bool CreateRenderResources(); + + void SetSatsVisible(bool visible); + void PrintStats(); + + Engine(const Engine&) = delete; + Engine& operator=(const Engine&) = delete; +}; + +} // namespace eng + +#endif // ENGINE_H diff --git a/src/engine/font.cc b/src/engine/font.cc new file mode 100644 index 0000000..057a8a6 --- /dev/null +++ b/src/engine/font.cc @@ -0,0 +1,193 @@ +#include "font.h" + +#include "../base/log.h" +#include "engine.h" +#include "platform/asset_file.h" + +#define STB_TRUETYPE_IMPLEMENTATION +#include "../third_party/stb/stb_truetype.h" + +namespace eng { + +bool Font::Load(const std::string& file_name) { + // Read the font file. + size_t buffer_size = 0; + auto buffer = AssetFile::ReadWholeFile( + file_name.c_str(), Engine::Get().GetRootPath().c_str(), &buffer_size); + if (!buffer) { + LOG << "Failed to read font file."; + return false; + } + + do { + // Allocate a cache bitmap for the glyphs. + // This is one 8 bit channel intensity data. + // It's tighly packed. + glyph_cache_ = std::make_unique(kGlyphSize * kGlyphSize); + if (!glyph_cache_) { + LOG << "Failed to allocate glyph cache."; + break; + } + + // Rasterize glyphs and pack them into the cache. + const float kFontHeight = 32.0f; + if (stbtt_BakeFontBitmap((unsigned char*)buffer.get(), 0, kFontHeight, + glyph_cache_.get(), kGlyphSize, kGlyphSize, + kFirstChar, kNumChars, glyph_info_) <= 0) { + LOG << "Failed to bake the glyph cache: "; + glyph_cache_.reset(); + break; + } + + int x0, y0, x1, y1; + CalculateBoundingBox("`IlfKgjy_{)", x0, y0, x1, y1); + line_height_ = y1 - y0; + yoff_ = -y0; + + return true; + } while (false); + + glyph_cache_.reset(); + + return false; +} + +static void StretchBlit_I8_to_RGBA32(int dst_x0, + int dst_y0, + int dst_x1, + int dst_y1, + int src_x0, + int src_y0, + int src_x1, + int src_y1, + uint8_t* dst_rgba, + int dst_pitch, + const uint8_t* src_i, + int src_pitch) { + // LOG << "-- StretchBlit: --"; + // LOG << "dst: rect(" << dst_x0 << ", " << dst_y0 << ")..(" + // << dst_x1 << ".." << dst_y1 << "), pitch(" << dst_pitch << ")"; + // LOG << "src: rect(" << src_x0 << ", " << src_y0 << ")..(" + // << src_x1 << ".." << src_y1 << "), pitch(" << src_pitch << ")"; + + int dst_width = dst_x1 - dst_x0, dst_height = dst_y1 - dst_y0, + src_width = src_x1 - src_x0, src_height = src_y1 - src_y0; + + // int dst_dx = dst_width > 0 ? 1 : -1, + // dst_dy = dst_height > 0 ? 1 : -1; + + // LOG << "dst_width = " << dst_width << ", dst_height = " << dst_height; + // LOG << "src_width = " << src_width << ", src_height = " << src_height; + + uint8_t* dst = dst_rgba + (dst_x0 + dst_y0 * dst_pitch) * 4; + const uint8_t* src = src_i + (src_x0 + src_y0 * src_pitch) * 1; + + // First check if we have to stretch at all. + if ((dst_width == src_width) && (dst_height == src_height)) { + // No, straight blit then. + for (int y = 0; y < dst_height; ++y) { + for (int x = 0; x < dst_width; ++x) { + // Alpha test, no blending for now. + if (src[x]) { +#if 0 + dst[x * 4 + 0] = src[x]; + dst[x * 4 + 1] = src[x]; + dst[x * 4 + 2] = src[x]; + dst[x * 4 + 3] = 255; +#else + dst[x * 4 + 3] = src[x]; +#endif + } + } + + dst += dst_pitch * 4; + src += src_pitch * 1; + } + } else { + // ToDo + } +} + +void Font::CalculateBoundingBox(const std::string& text, + int& x0, + int& y0, + int& x1, + int& y1) const { + x0 = 0; + y0 = 0; + x1 = 0; + y1 = 0; + + if (!glyph_cache_) + return; + + float x = 0, y = 0; + + const char* ptr = text.c_str(); + while (*ptr) { + if (*ptr >= kFirstChar /*&& *ptr < (kFirstChar + kNumChars)*/) { + stbtt_aligned_quad q; + stbtt_GetBakedQuad(glyph_info_, kGlyphSize, kGlyphSize, *ptr - kFirstChar, + &x, &y, &q, 1); + + int ix0 = (int)q.x0, iy0 = (int)q.y0, ix1 = (int)q.x1, iy1 = (int)q.y1; + + if (ix0 < x0) + x0 = ix0; + if (iy0 < y0) + y0 = iy0; + if (ix1 > x1) + x1 = ix1; + if (iy1 > y1) + y1 = iy1; + + ++ptr; + } + } +} + +void Font::CalculateBoundingBox(const std::string& text, + int& width, + int& height) const { + int x0, y0, x1, y1; + CalculateBoundingBox(text, x0, y0, x1, y1); + width = x1 - x0; + height = y1 - y0; + // LOG << "width = " << width << ", height = " << height; +} + +void Font::Print(int x, + int y, + const std::string& text, + uint8_t* buffer, + int width) const { + // LOG("Font::Print() = %s\n", text); + + if (!glyph_cache_) + return; + + float fx = (float)x, fy = (float)y + (float)yoff_; + + const char* ptr = text.c_str(); + while (*ptr) { + if (*ptr >= kFirstChar /*&& *ptr < (kFirstChar + kNumChars)*/) { + stbtt_aligned_quad q; + stbtt_GetBakedQuad(glyph_info_, kGlyphSize, kGlyphSize, *ptr - kFirstChar, + &fx, &fy, &q, 1); + + // LOG("-- glyph --\nxy = (%f %f) .. (%f %f)\nuv = (%f %f) .. (%f %f)\n", + // q.x0, q.y0, q.x1, q.y1, q.s0, q.t0, q.s1, q.t1); + + int ix0 = (int)q.x0, iy0 = (int)q.y0, ix1 = (int)q.x1, iy1 = (int)q.y1, + iu0 = (int)(q.s0 * kGlyphSize), iv0 = (int)(q.t0 * kGlyphSize), + iu1 = (int)(q.s1 * kGlyphSize), iv1 = (int)(q.t1 * kGlyphSize); + + StretchBlit_I8_to_RGBA32(ix0, iy0, ix1, iy1, iu0, iv0, iu1, iv1, buffer, + width, glyph_cache_.get(), kGlyphSize); + + ++ptr; + } + } +} + +} // namespace eng diff --git a/src/engine/font.h b/src/engine/font.h new file mode 100644 index 0000000..7f5f604 --- /dev/null +++ b/src/engine/font.h @@ -0,0 +1,54 @@ +#ifndef FONT_H +#define FONT_H + +#include +#include +#include + +#include "../third_party/stb/stb_truetype.h" + +namespace eng { + +class Font { + public: + Font() = default; + ~Font() = default; + + bool Load(const std::string& file_name); + + void CalculateBoundingBox(const std::string& text, + int& width, + int& height) const; + void CalculateBoundingBox(const std::string& text, + int& x0, + int& y0, + int& x1, + int& y1) const; + + void Print(int x, + int y, + const std::string& text, + uint8_t* buffer, + int width) const; + + int GetLineHeight() const { return line_height_; } + + bool IsValid() const { return !!glyph_cache_; } + + private: + enum Constants { + kGlyphSize = 512, + kFirstChar = 32, // ' ' (space) + kNumChars = 96 // Covers almost all ASCII chars. + }; + + std::unique_ptr glyph_cache_; // Image data. + stbtt_bakedchar glyph_info_[kNumChars]; // Coordinates and advance. + + int line_height_ = 0; + int yoff_ = 0; +}; + +} // namespace eng + +#endif // FONT_H diff --git a/src/engine/game.h b/src/engine/game.h new file mode 100644 index 0000000..5ae6525 --- /dev/null +++ b/src/engine/game.h @@ -0,0 +1,30 @@ +#ifndef GAME_H +#define GAME_H + +namespace eng { + +class Game { + public: + Game() = default; + virtual ~Game() = default; + + virtual bool Initialize() = 0; + + virtual void Update(float delta_time) = 0; + + virtual void Draw(float frame_frac) = 0; + + virtual void ContextLost() = 0; + + virtual void LostFocus() = 0; + + virtual void GainedFocus() = 0; + + private: + Game(const Game&) = delete; + Game& operator=(const Game&) = delete; +}; + +} // namespace eng + +#endif // GAME_H diff --git a/src/engine/game_factory.h b/src/engine/game_factory.h new file mode 100644 index 0000000..e982621 --- /dev/null +++ b/src/engine/game_factory.h @@ -0,0 +1,55 @@ +#ifndef GAME_FACTORY_H +#define GAME_FACTORY_H + +#include +#include +#include + +#define DECLARE_GAME_BEGIN \ + std::vector> \ + eng::GameFactoryBase::game_classes = { +#define DECLARE_GAME(CLASS) {#CLASS, new eng::GameFactory()}, +#define DECLARE_GAME_END }; + +namespace eng { + +class Game; + +class GameFactoryBase { + public: + virtual ~GameFactoryBase() = default; + + static std::unique_ptr CreateGame(const std::string& name) { + if (name.empty()) + return game_classes.size() > 0 + ? game_classes.begin()->second->CreateGame() + : nullptr; + for (auto& element : game_classes) { + if (element.first == name) + return element.second->CreateGame(); + } + return nullptr; + } + + private: + virtual std::unique_ptr CreateGame() { return nullptr; } + + static std::vector> game_classes; +}; + +template +class GameFactory : public GameFactoryBase { + public: + ~GameFactory() override = default; + + private: + using GameType = Type; + + std::unique_ptr CreateGame() override { + return std::make_unique(); + } +}; + +} // namespace eng + +#endif // GAME_FACTORY_H diff --git a/src/engine/image.cc b/src/engine/image.cc new file mode 100644 index 0000000..ee4aa7b --- /dev/null +++ b/src/engine/image.cc @@ -0,0 +1,359 @@ +#include "image.h" + +#include +#include + +#include "../base/interpolation.h" +#include "../base/log.h" +#include "../base/misc.h" +#include "../third_party/texture_compressor/texture_compressor.h" +#include "engine.h" +#include "platform/asset_file.h" + +// This 3rd party library is written in C and uses malloc, which means that we +// have to do the same. +#define STBI_NO_STDIO +#include "../third_party/stb/stb_image.h" + +using namespace base; + +namespace { + +// Blend between two colors with equal weights. +uint32_t Mix2(uint32_t p0, uint32_t p1) { + uint32_t r = (((p0 >> 0) & 0xff) + ((p1 >> 0) & 0xff)) / 2; + uint32_t g = (((p0 >> 8) & 0xff) + ((p1 >> 8) & 0xff)) / 2; + uint32_t b = (((p0 >> 16) & 0xff) + ((p1 >> 16) & 0xff)) / 2; + uint32_t a = (((p0 >> 24) & 0xff) + ((p1 >> 24) & 0xff)) / 2; + + return (r << 0) | (g << 8) | (b << 16) | (a << 24); +} + +// Blend between four colors with equal weights. +uint32_t Mix4(uint32_t p0, uint32_t p1, uint32_t p2, uint32_t p3) { + uint32_t r = (((p0 >> 0) & 0xff) + ((p1 >> 0) & 0xff) + ((p2 >> 0) & 0xff) + + ((p3 >> 0) & 0xff)) / + 4; + uint32_t g = (((p0 >> 8) & 0xff) + ((p1 >> 8) & 0xff) + ((p2 >> 8) & 0xff) + + ((p3 >> 8) & 0xff)) / + 4; + uint32_t b = (((p0 >> 16) & 0xff) + ((p1 >> 16) & 0xff) + + ((p2 >> 16) & 0xff) + ((p3 >> 16) & 0xff)) / + 4; + uint32_t a = (((p0 >> 24) & 0xff) + ((p1 >> 24) & 0xff) + + ((p2 >> 24) & 0xff) + ((p3 >> 24) & 0xff)) / + 4; + + return (r << 0) | (g << 8) | (b << 16) | (a << 24); +} + +// Anisotropic blending of colors. +void MipNonUniform(void* dst, const void* src, size_t length) { + const uint32_t* s = reinterpret_cast(src); + uint32_t* d = reinterpret_cast(dst); + for (size_t y = 0; y < length; ++y) { + *d++ = Mix2(s[0], s[1]); + s += 2; + } +} + +} // namespace + +namespace eng { + +Image::Image() = default; + +Image::Image(const Image& other) { + Copy(other); +} + +Image::~Image() = default; + +Image& Image::operator=(const Image& other) { + Copy(other); + return *this; +} + +bool Image::Create(int w, int h) { + width_ = w; + height_ = h; + + buffer_.reset((uint8_t*)AlignedAlloc<16>(w * h * 4 * sizeof(uint8_t))); + + return true; +} + +void Image::Copy(const Image& other) { + if (other.buffer_) { + int size = other.GetSize(); + buffer_.reset((uint8_t*)AlignedAlloc<16>(size)); + memcpy(buffer_.get(), other.buffer_.get(), size); + } + width_ = other.width_; + height_ = other.height_; + format_ = other.format_; +} + +bool Image::CreateMip(const Image& other) { + if (other.width_ <= 1 || other.height_ <= 1 || other.GetFormat() != kRGBA32) + return false; + + // Reduce the dimensions. + width_ = std::max(other.width_ >> 1, 1); + height_ = std::max(other.height_ >> 1, 1); + format_ = kRGBA32; + buffer_.reset((uint8_t*)AlignedAlloc<16>(GetSize())); + + // If the width isn't perfectly divisable with two, then we end up skewing + // the image because the source offset isn't updated properly. + bool unaligned_width = other.width_ & 1; + + // Special case the non-uniform/anisotropic cases, eg 4:1 or 1:4 textures. + // This is only an issue once we reach the highest mip levels where one + // dimension is one pixel. + if (other.width_ == 1) { + // Interestingly the horizontal and vertical case becomes the same code, + // it's only about which value to use as the run length that differs. + MipNonUniform(buffer_.get(), other.buffer_.get(), height_); + } else if (other.height_ == 1) { + MipNonUniform(buffer_.get(), other.buffer_.get(), width_); + } else { + const uint32_t* s = reinterpret_cast(other.buffer_.get()); + uint32_t* d = reinterpret_cast(buffer_.get()); + for (size_t y = 0; y < height_; ++y) { + for (size_t x = 0; x < width_; ++x) { + *d++ = Mix4(s[0], s[1], s[other.width_], s[other.width_ + 1]); + s += 2; + } + if (unaligned_width) + ++s; + s += other.width_; + } + } + + return true; +} + +bool Image::Load(const std::string& file_name) { + size_t buffer_size = 0; + auto file_buffer = AssetFile::ReadWholeFile( + file_name.c_str(), Engine::Get().GetRootPath().c_str(), &buffer_size); + if (!file_buffer) { + LOG << "Failed to read file: " << file_name; + return false; + } + + int w, h, c; + buffer_.reset((uint8_t*)stbi_load_from_memory( + (const stbi_uc*)file_buffer.get(), buffer_size, &w, &h, &c, 0)); + if (!buffer_) { + LOG << "Failed to load image file: " << file_name; + return false; + } + + uint8_t* converted_buffer = NULL; + switch (c) { + case 1: + // LOG("Converting image from 1 to 4 channels.\n"); + // Assume it's an intensity, duplicate it to RGB and fill A with opaque. + converted_buffer = + (uint8_t*)AlignedAlloc<16>(w * h * 4 * sizeof(uint8_t)); + for (int i = 0; i < w * h; ++i) { + converted_buffer[i * 4 + 0] = buffer_[i]; + converted_buffer[i * 4 + 1] = buffer_[i]; + converted_buffer[i * 4 + 2] = buffer_[i]; + converted_buffer[i * 4 + 3] = 255; + } + break; + + case 3: + // LOG("Converting image from 3 to 4 channels.\n"); + // Add an opaque channel. + converted_buffer = + (uint8_t*)AlignedAlloc<16>(w * h * 4 * sizeof(uint8_t)); + for (int i = 0; i < w * h; ++i) { + converted_buffer[i * 4 + 0] = buffer_[i * 3 + 0]; + converted_buffer[i * 4 + 1] = buffer_[i * 3 + 1]; + converted_buffer[i * 4 + 2] = buffer_[i * 3 + 2]; + converted_buffer[i * 4 + 3] = 255; + } + break; + + case 4: + break; // This is the wanted format. + + case 2: + default: + LOG << "Image had unsuitable number of color components: " << c << " " + << file_name; + buffer_.reset(); + return false; + } + + if (converted_buffer) + buffer_.reset(converted_buffer); + + width_ = w; + height_ = h; + +#if 0 // Fill the alpha channel with transparent gradient alpha for testing + uint8_t* modifyBuf = buffer; + for (int j = 0; j < height; ++j, modifyBuf += width * 4) + { + for (int i = 0; i < width; ++i) + { + float dist = sqrt(float(i*i + j*j)); + float alpha = (((dist > 0.0f ? dist : 0.0f) / sqrt((float)(width * width + height * height))) * 255.0f); + modifyBuf[i * 4 + 3] = (unsigned char)alpha; + } + } +#endif + + return !!buffer_; +} + +size_t Image::GetSize() const { + switch (format_) { + case kRGBA32: + return width_ * height_ * 4; + case kDXT1: + case kATC: + return ((width_ + 3) / 4) * ((height_ + 3) / 4) * 8; + case kDXT5: + case kATCIA: + return ((width_ + 3) / 4) * ((height_ + 3) / 4) * 16; + case kETC1: + return (width_ * height_ * 4) / 8; + default: + return 0; + } +} + +void Image::ConvertToPow2() { + int new_width = RoundUpToPow2(width_); + int new_height = RoundUpToPow2(height_); + if ((new_width != width_) || (new_height != height_)) { + LOG << "Converting image from (" << width_ << ", " << height_ << ") to (" + << new_width << ", " << new_height << ")"; + + int bigger_size = new_width * new_height * 4 * sizeof(uint8_t); + uint8_t* bigger_buffer = (uint8_t*)AlignedAlloc<16>(bigger_size); + + // Fill it with black. + memset(bigger_buffer, 0, bigger_size); + + // Copy over the old bitmap. +#if 0 + // Centered in the new bitmap. + int offset_x = (new_width - width_) / 2; + int offset_y = (new_height - height_) / 2; + for (int y = 0; y < height_; ++y) + memcpy(bigger_buffer + (offset_x + (y + offset_y) * new_width) * 4, + buffer_.get() + y * width_ * 4, width_ * 4); +#else + for (int y = 0; y < height_; ++y) + memcpy(bigger_buffer + (y * new_width) * 4, + buffer_.get() + y * width_ * 4, width_ * 4); +#endif + + // Swap the buffers and dimensions. + buffer_.reset(bigger_buffer); + width_ = new_width; + height_ = new_height; + } +} + +bool Image::Compress() { + if (IsCompressed()) + return true; + + TextureCompressor* tc = Engine::Get().GetTextureCompressor(true); + if (!tc) + return false; + + switch (tc->format()) { + case TextureCompressor::kFormatATC: + format_ = kATC; + break; + case TextureCompressor::kFormatATCIA: + format_ = kATCIA; + break; + case TextureCompressor::kFormatDXT1: + format_ = kDXT1; + break; + case TextureCompressor::kFormatDXT5: + format_ = kDXT5; + break; + case TextureCompressor::kFormatETC1: + format_ = kETC1; + break; + default: + return false; + } + + LOG << "Compressing image. Format: " << format_; + + unsigned compressedSize = GetSize(); + uint8_t* compressedBuffer = + (uint8_t*)AlignedAlloc<16>(compressedSize * sizeof(uint8_t)); + + const uint8_t* src = buffer_.get(); + uint8_t* dst = compressedBuffer; + + tc->Compress(src, dst, width_, height_, TextureCompressor::kQualityHigh); + + buffer_.reset(compressedBuffer); + return true; +} + +uint8_t* Image::GetBuffer() { + return buffer_.get(); +} + +void Image::Clear(Vector4 rgba) { + // Quantize the color to target resolution. + uint8_t r = (uint8_t)(rgba.x * 255.0f), g = (uint8_t)(rgba.y * 255.0f), + b = (uint8_t)(rgba.z * 255.0f), a = (uint8_t)(rgba.w * 255.0f); + + // Fill out the first line manually. + for (int w = 0; w < width_; ++w) { + buffer_.get()[w * 4 + 0] = r; + buffer_.get()[w * 4 + 1] = g; + buffer_.get()[w * 4 + 2] = b; + buffer_.get()[w * 4 + 3] = a; + } + + // Copy the first line to the rest of them. + for (int h = 1; h < height_; ++h) + memcpy(buffer_.get() + h * width_ * 4, buffer_.get(), width_ * 4); +} + +void Image::GradientH() { + // Fill out the first line manually. + for (int x = 0; x < width_; ++x) { + uint8_t intensity = x > 255 ? 255 : x; + buffer_.get()[x * 4 + 0] = intensity; + buffer_.get()[x * 4 + 1] = intensity; + buffer_.get()[x * 4 + 2] = intensity; + buffer_.get()[x * 4 + 3] = 255; + } + + // Copy the first line to the rest of them. + for (int h = 1; h < height_; ++h) + memcpy(buffer_.get() + h * width_ * 4, buffer_.get(), width_ * 4); +} + +void Image::GradientV(const Vector4& c1, const Vector4& c2, int height) { + // Fill each section with gradient. + for (int h = 0; h < height_; ++h) { + Vector4 c = Lerp(c1, c2, fmod(h, height) / (float)height); + for (int x = 0; x < width_; ++x) { + buffer_.get()[h * width_ * 4 + x * 4 + 0] = c.x * 255; + buffer_.get()[h * width_ * 4 + x * 4 + 1] = c.y * 255; + buffer_.get()[h * width_ * 4 + x * 4 + 2] = c.z * 255; + buffer_.get()[h * width_ * 4 + x * 4 + 3] = 0; + } + } +} + +} // namespace eng diff --git a/src/engine/image.h b/src/engine/image.h new file mode 100644 index 0000000..5835d7b --- /dev/null +++ b/src/engine/image.h @@ -0,0 +1,59 @@ +#ifndef IMAGE_H +#define IMAGE_H + +#include +#include + +#include "../base/mem.h" +#include "../base/vecmath.h" + +namespace eng { + +class Image { + public: + enum Format { kRGBA32, kDXT1, kDXT5, kETC1, kATC, kATCIA }; + + Image(); + Image(const Image& other); + ~Image(); + + Image& operator=(const Image& other); + + bool Create(int width, int height); + void Copy(const Image& other); + bool CreateMip(const Image& other); + bool Load(const std::string& file_name); + + bool Compress(); + + void ConvertToPow2(); + + int GetWidth() const { return width_; } + int GetHeight() const { return height_; } + + Format GetFormat() const { return format_; } + bool IsCompressed() const { return format_ > kRGBA32; } + + size_t GetSize() const; + + const uint8_t* GetBuffer() const { return buffer_.get(); } + uint8_t* GetBuffer(); + + bool IsValid() const { return !!buffer_; } + + void Clear(base::Vector4 rgba); + void GradientH(); + void GradientV(const base::Vector4& c1, const base::Vector4& c2, int height); + + private: + base::AlignedMem::ScoppedPtr buffer_; + int width_ = 0; + int height_ = 0; + Format format_ = kRGBA32; + + std::string name_; +}; + +} // namespace eng + +#endif // IMAGE_H diff --git a/src/engine/image_quad.cc b/src/engine/image_quad.cc new file mode 100644 index 0000000..038cdf0 --- /dev/null +++ b/src/engine/image_quad.cc @@ -0,0 +1,87 @@ +#include "image_quad.h" + +#include + +#include "engine.h" +#include "renderer/geometry.h" +#include "renderer/shader.h" +#include "renderer/texture.h" + +using namespace base; + +namespace eng { + +void ImageQuad::Create(std::shared_ptr texture, + std::array num_frames, + int frame_width, + int frame_height) { + texture_ = texture; + num_frames_ = std::move(num_frames); + frame_width_ = frame_width; + frame_height_ = frame_height; +} + +void ImageQuad::Destory() { + texture_.reset(); +} + +void ImageQuad::AutoScale() { + Vector2 dimensions = {GetFrameWidth(), GetFrameHeight()}; + SetScale(Engine::Get().ToScale(dimensions)); + Scale((float)Engine::Get().GetDeviceDpi() / 200.0f); +} + +void ImageQuad::SetFrame(size_t frame) { + assert(frame < GetNumFrames()); + current_frame_ = frame; +} + +size_t ImageQuad::GetNumFrames() const { + return num_frames_[0] * num_frames_[1]; +} + +void ImageQuad::Draw() { + if (!IsVisible() || !texture_ || !texture_->IsValid()) + return; + + texture_->Activate(); + + Vector2 tex_scale = {GetFrameWidth() / texture_->GetWidth(), + GetFrameHeight() / texture_->GetHeight()}; + + std::shared_ptr quad = Engine::Get().GetQuad(); + std::shared_ptr shader = Engine::Get().GetPassThroughShader(); + + shader->Activate(); + shader->SetUniform("offset", offset_); + shader->SetUniform("scale", scale_); + shader->SetUniform("pivot", pivot_); + shader->SetUniform("rotation", rotation_); + shader->SetUniform("tex_offset", GetUVOffset(current_frame_)); + shader->SetUniform("tex_scale", tex_scale); + shader->SetUniform("projection", Engine::Get().GetProjectionMarix()); + shader->SetUniform("color", color_); + shader->SetUniform("texture", 0); + + quad->Draw(); +} + +float ImageQuad::GetFrameWidth() const { + return frame_width_ > 0 ? (float)frame_width_ + : texture_->GetWidth() / (float)num_frames_[0]; +} + +float ImageQuad::GetFrameHeight() const { + return frame_height_ > 0 ? (float)frame_height_ + : texture_->GetHeight() / (float)num_frames_[1]; +} + +// Return the uv offset for the given frame. +Vector2 ImageQuad::GetUVOffset(int frame) const { + assert(frame < num_frames_[0] * num_frames_[1]); + if (num_frames_[0] == 1 && num_frames_[1] == 1) + return {0, 0}; + return {(float)(frame % num_frames_[0]), (float)(frame / num_frames_[0])}; +} + +} // namespace eng diff --git a/src/engine/image_quad.h b/src/engine/image_quad.h new file mode 100644 index 0000000..e09a2a8 --- /dev/null +++ b/src/engine/image_quad.h @@ -0,0 +1,57 @@ +#ifndef IMAGE_QUAD_H +#define IMAGE_QUAD_H + +#include "../base/vecmath.h" +#include "animatable.h" + +#include +#include + +namespace eng { + +class Texture; + +class ImageQuad : public Animatable { + public: + ImageQuad() = default; + ~ImageQuad() override = default; + + void Create(std::shared_ptr texture, + std::array num_frames = {1, 1}, + int frame_width = 0, + int frame_height = 0); + + void Destory(); + + void AutoScale(); + + // Animatable interface. + void SetFrame(size_t frame) override; + size_t GetFrame() const override { return current_frame_; } + size_t GetNumFrames() const override; + void SetColor(const base::Vector4& color) override { color_ = color; } + base::Vector4 GetColor() const override { return color_; } + + void Draw(); + + std::shared_ptr GetTexture() { return texture_; } + + private: + std::shared_ptr texture_; + + size_t current_frame_ = 0; + std::array num_frames_ = {1, 1}; // horizontal, vertical + int frame_width_ = 0; + int frame_height_ = 0; + + base::Vector4 color_ = {1, 1, 1, 1}; + + float GetFrameWidth() const; + float GetFrameHeight() const; + + base::Vector2 GetUVOffset(int frame) const; +}; + +} // namespace eng + +#endif // IMAGE_QUAD_H diff --git a/src/engine/input_event.h b/src/engine/input_event.h new file mode 100644 index 0000000..c022eb8 --- /dev/null +++ b/src/engine/input_event.h @@ -0,0 +1,49 @@ +#ifndef INPUT_EVENT_H +#define INPUT_EVENT_H + +#include +#include "../base/vecmath.h" + +namespace eng { + +class InputEvent { + public: + enum Type { + kInvalid, + kTap, + kDoubleTap, + kDragStart, + kDrag, + kDragEnd, + kDragCancel, + kPinchStart, + kPinch, + kNavigateBack, + kKeyPress, + kType_Max // Not a type. + }; + + InputEvent(Type type) : type_(type) {} + InputEvent(Type type, const base::Vector2& vec1) + : type_(type), vec_{vec1, {0, 0}} {} + InputEvent(Type type, const base::Vector2& vec1, const base::Vector2& vec2) + : type_(type), vec_{vec1, vec2} {} + InputEvent(Type type, char key) : type_(type), key_(key) {} + ~InputEvent() = default; + + Type GetType() { return type_; } + base::Vector2 GetVector(size_t i) { + assert(i < 2); + return vec_[i]; + } + char GetKeyPress() { return key_; } + + private: + Type type_ = kInvalid; + base::Vector2 vec_[2] = {{0, 0}, {0, 0}}; + char key_ = 0; +}; + +} // namespace eng + +#endif // INPUT_EVENT_H diff --git a/src/engine/mesh.cc b/src/engine/mesh.cc new file mode 100644 index 0000000..6ed9dea --- /dev/null +++ b/src/engine/mesh.cc @@ -0,0 +1,169 @@ +#include "mesh.h" + +#include +#include + +#include "../base/log.h" +#include "../third_party/jsoncpp/json.h" +#include "engine.h" +#include "platform/asset_file.h" + +namespace eng { + +// Used to parse the vertex layout, +// e.g. "p3f;c4b" for "position 3 floats, color 4 bytes". +const char Mesh::kLayoutDelimiter[] = ";/ \t"; + +bool Mesh::Create(Primitive primitive, + const std::string& vertex_description, + size_t num_vertices, + const void* vertices, + DataType index_description, + size_t num_indices, + const void* indices) { + primitive_ = primitive; + num_vertices_ = num_vertices; + index_description_ = index_description; + num_indices_ = num_indices; + + if (!ParseVertexDescription(vertex_description, vertex_description_)) { + LOG << "Failed to parse vertex description."; + return false; + } + + int vertex_buffer_size = GetVertexSize() * num_vertices_; + if (vertex_buffer_size > 0) { + vertices_ = std::make_unique(vertex_buffer_size); + memcpy(vertices_.get(), vertices, vertex_buffer_size); + } + + if (!indices) + return true; + + int index_buffer_size = GetIndexSize() * num_indices_; + if (index_buffer_size > 0) { + indices_ = std::make_unique(index_buffer_size); + memcpy(indices_.get(), indices, index_buffer_size); + } + + return true; +} + +bool Mesh::Load(const std::string& file_name) { + size_t buffer_size = 0; + auto json_mesh = AssetFile::ReadWholeFile(file_name.c_str(), + Engine::Get().GetRootPath().c_str(), + &buffer_size, true); + if (!json_mesh) { + LOG << "Failed to read file: " << file_name; + return false; + } + + std::string err; + Json::Value root; + Json::CharReaderBuilder builder; + const std::unique_ptr reader(builder.newCharReader()); + if (!reader->parse(json_mesh.get(), json_mesh.get() + buffer_size, &root, + &err)) { + LOG << "Failed to load mesh. Json parser error: " << err; + return false; + } + + const std::string& primitive_str = root["primitive"].asString(); + if (primitive_str == "Triangles") { + primitive_ = kPrimitive_Triangles; + } else if (primitive_str == "TriangleStrip") { + primitive_ = kPrimitive_TriangleStrip; + } else { + LOG << "Failed to load mesh. Invalid primitive: " << primitive_str; + return false; + } + + num_vertices_ = root["num_vertices"].asUInt(); + + if (!ParseVertexDescription(root["vertex_description"].asString(), + vertex_description_)) { + LOG << "Failed to parse vertex description."; + return false; + } + + size_t array_size = 0; + for (auto& attr : vertex_description_) { + array_size += std::get<2>(attr); + } + array_size *= num_vertices_; + + const Json::Value vertices = root["vertices"]; + if (vertices.size() != array_size) { + LOG << "Failed to load mesh. Vertex array size: " << vertices.size() + << ", expected " << array_size; + return false; + } + + int vertex_buffer_size = GetVertexSize() * num_vertices_; + if (vertex_buffer_size <= 0) { + LOG << "Failed to load mesh. Invalid vertex size."; + return false; + } + + vertices_ = std::make_unique(vertex_buffer_size); + + char* dst = vertices_.get(); + int i = 0; + while (i < vertices.size()) { + for (auto& attr : vertex_description_) { + auto [attrib_type, data_type, num_elements, type_size] = attr; + while (num_elements--) { + switch (data_type) { + case kDataType_Byte: + *((unsigned char*)dst) = (unsigned char)vertices[i].asUInt(); + break; + case kDataType_Float: + *((float*)dst) = (float)vertices[i].asFloat(); + break; + case kDataType_Int: + *((int*)dst) = vertices[i].asInt(); + break; + case kDataType_Short: + *((short*)dst) = (short)vertices[i].asInt(); + break; + case kDataType_UInt: + *((unsigned int*)dst) = vertices[i].asUInt(); + break; + case kDataType_UShort: + *((unsigned short*)dst) = (unsigned short)vertices[i].asUInt(); + break; + default: + assert(false); + return false; + } + dst += type_size; + ++i; + } + } + } + return true; +} + +size_t Mesh::GetVertexSize() const { + unsigned int size = 0; + for (auto& attr : vertex_description_) { + size += std::get<2>(attr) * std::get<3>(attr); + } + return size; +} + +size_t Mesh::GetIndexSize() const { + switch (index_description_) { + case kDataType_Byte: + return sizeof(char); + case kDataType_UShort: + return sizeof(unsigned short); + case kDataType_UInt: + return sizeof(unsigned int); + default: + return 0; + } +} + +} // namespace eng diff --git a/src/engine/mesh.h b/src/engine/mesh.h new file mode 100644 index 0000000..e246e2d --- /dev/null +++ b/src/engine/mesh.h @@ -0,0 +1,55 @@ +#ifndef MESH_H +#define MESH_H + +#include +#include +#include "renderer/renderer_types.h" + +namespace eng { + +class Mesh { + public: + static const char kLayoutDelimiter[]; + + Mesh() = default; + ~Mesh() = default; + + bool Create(Primitive primitive, + const std::string& vertex_description, + size_t num_vertices, + const void* vertices, + DataType index_description = kDataType_Invalid, + size_t num_indices = 0, + const void* indices = nullptr); + + bool Load(const std::string& file_name); + + const void* GetVertices() const { return (void*)vertices_.get(); } + const void* GetIndices() const { return (void*)indices_.get(); } + + size_t GetVertexSize() const; + size_t GetIndexSize() const; + + Primitive primitive() const { return primitive_; } + const VertexDescripton& vertex_description() const { + return vertex_description_; + } + size_t num_vertices() const { return num_vertices_; } + DataType index_description() const { return index_description_; } + size_t num_indices() const { return num_indices_; } + + bool IsValid() const { return !!vertices_.get(); } + + protected: + Primitive primitive_ = kPrimitive_TriangleStrip; + VertexDescripton vertex_description_; + size_t num_vertices_ = 0; + DataType index_description_ = kDataType_Invalid; + size_t num_indices_ = 0; + std::unique_ptr vertices_; + std::unique_ptr indices_; +}; + +} // namespace eng + +#endif // MESH_H diff --git a/src/engine/platform/asset_file.cc b/src/engine/platform/asset_file.cc new file mode 100644 index 0000000..b88f5d3 --- /dev/null +++ b/src/engine/platform/asset_file.cc @@ -0,0 +1,41 @@ +#include "asset_file.h" +#include "../../base/log.h" + +namespace eng { + +std::unique_ptr AssetFile::ReadWholeFile(const std::string& file_name, + const std::string& root_path, + size_t* length, + bool null_terminate) { + AssetFile file; + if (!file.Open(file_name, root_path)) + return nullptr; + + size_t size = file.GetSize(); + if (size == 0) + return nullptr; + + // Allocate a new buffer and add space for a null terminator. + std::unique_ptr buffer = + std::make_unique(size + (null_terminate ? 1 : 0)); + + // Read all of it. + size_t bytes_read = file.Read(buffer.get(), size); + if (!bytes_read) { + LOG << "Failed to read a buffer of size: " << size << " from file " + << file_name; + return nullptr; + } + + // Return the buffer size if the caller is interested. + if (length) + *length = bytes_read; + + // Null terminate the buffer. + if (null_terminate) + buffer[size] = 0; + + return buffer; +} + +} // namespace eng diff --git a/src/engine/platform/asset_file.h b/src/engine/platform/asset_file.h new file mode 100644 index 0000000..a65a4f2 --- /dev/null +++ b/src/engine/platform/asset_file.h @@ -0,0 +1,43 @@ +#ifndef ASSET_FILE_H +#define ASSET_FILE_H + +#if defined(__ANDROID__) +#include +#include "../../third_party/minizip/unzip.h" +#elif defined(__linux__) +#include "../../base/file.h" +#endif +#include +#include + +namespace eng { + +class AssetFile { + public: + AssetFile(); + ~AssetFile(); + + bool Open(const std::string& file_name, const std::string& root_path); + void Close(); + + size_t GetSize(); + + size_t Read(char* data, size_t size); + + static std::unique_ptr ReadWholeFile(const std::string& file_name, + const std::string& root_path, + size_t* length = 0, + bool null_terminate = false); + + private: +#if defined(__ANDROID__) + unzFile archive_ = 0; + size_t uncompressed_size_ = 0; +#elif defined(__linux) + base::ScopedFILE file_; +#endif +}; + +} // namespace eng + +#endif // ASSET_FILE_H diff --git a/src/engine/platform/asset_file_android.cc b/src/engine/platform/asset_file_android.cc new file mode 100644 index 0000000..515d73e --- /dev/null +++ b/src/engine/platform/asset_file_android.cc @@ -0,0 +1,74 @@ +#include +#include +#include "../../base/log.h" +#include "asset_file.h" + +namespace eng { + +AssetFile::AssetFile() = default; + +AssetFile::~AssetFile() { + Close(); +} + +bool AssetFile::Open(const std::string& file_name, + const std::string& root_path) { + do { + // Try to open the zip archive. + archive_ = unzOpen(root_path.c_str()); + if (!archive_) { + LOG << "Failed to open zip file: " << root_path; + break; + } + + // Try to find the file. + std::string full_name = "assets/" + file_name; + if (UNZ_OK != unzLocateFile(archive_, full_name.c_str(), 1)) { + LOG << "Failed to locate file in zip archive: " << file_name; + break; + } + + // Need to get the uncompressed size of the file. + unz_file_info info; + if (UNZ_OK != + unzGetCurrentFileInfo(archive_, &info, NULL, 0, NULL, 0, NULL, 0)) { + LOG << "Failed to get file info: " << file_name; + break; + } + uncompressed_size_ = info.uncompressed_size; + + // Open the current file. + if (UNZ_OK != unzOpenCurrentFile(archive_)) { + LOG << "Failed to open file: " << file_name; + break; + } + + return true; + } while (false); + + Close(); + return false; +} + +void AssetFile::Close() { + if (archive_) { + // This could potentially be called without having opened a file, but that + // should be a harmless nop. + unzCloseCurrentFile(archive_); + + unzClose(archive_); + archive_ = 0; + } +} + +size_t AssetFile::GetSize() { + return uncompressed_size_; +} + +size_t AssetFile::Read(char* data, size_t size) { + // Uncompress data into the buffer. + int result = unzReadCurrentFile(archive_, data, size); + return result < 0 ? 0 : result; +} + +} // namespace eng diff --git a/src/engine/platform/asset_file_linux.cc b/src/engine/platform/asset_file_linux.cc new file mode 100644 index 0000000..505bafe --- /dev/null +++ b/src/engine/platform/asset_file_linux.cc @@ -0,0 +1,41 @@ +#include +#include "asset_file.h" + +namespace eng { + +AssetFile::AssetFile() = default; + +AssetFile::~AssetFile() = default; + +bool AssetFile::Open(const std::string& file_name, + const std::string& root_path) { + std::string full_path = root_path + "assets/" + file_name; + file_.reset(fopen(full_path.c_str(), "rb")); + return !!file_; +} + +void AssetFile::Close() { + file_.reset(); +} + +size_t AssetFile::GetSize() { + size_t size = 0; + + if (file_) { + if (!fseek(file_.get(), 0, SEEK_END)) { + size = ftell(file_.get()); + rewind(file_.get()); + } + } + + return size; +} + +size_t AssetFile::Read(char* data, size_t size) { + if (file_) + return fread(data, 1, size, file_.get()); + + return 0; +} + +} // namespace eng diff --git a/src/engine/platform/platform.cc b/src/engine/platform/platform.cc new file mode 100644 index 0000000..f179661 --- /dev/null +++ b/src/engine/platform/platform.cc @@ -0,0 +1,77 @@ +#include "platform.h" + +#include + +#include "../../base/log.h" +#include "../audio/audio.h" +#include "../engine.h" +#include "../renderer/renderer.h" + +// Save battery on mobile devices. +#define USE_SLEEP + +namespace eng { + +Platform::InternalError Platform::internal_error; + +void Platform::Shutdown() { + LOG << "Shutting down platform."; + audio_->Shutdown(); + renderer_->Shutdown(); +} + +void Platform::RunMainLoop() { + engine_ = std::make_unique(this, renderer_.get(), audio_.get()); + if (!engine_->Initialize()) { + LOG << "Failed to initialize the engine."; + throw internal_error; + } + + // Use fixed time steps. + constexpr float time_step = 1.0f / 60.0f; + +#ifdef USE_SLEEP + constexpr float epsilon = 0.0001f; +#endif // USE_SLEEP + + timer_.Reset(); + float accumulator = 0.0; + float frame_frac = 0.0f; + + for (;;) { + engine_->Draw(frame_frac); + + // Accumulate time. +#ifdef USE_SLEEP + while (accumulator < time_step) { + timer_.Update(); + accumulator += timer_.GetSecondsPassed(); + if (time_step - accumulator > epsilon) { + float sleep_time = time_step - accumulator - epsilon; + std::this_thread::sleep_for( + std::chrono::microseconds((int)(sleep_time * 1000000.0f))); + } + }; +#else + timer_.Update(); + accumulator += timer_.GetSecondsPassed(); +#endif // USE_SLEEP + + // Subdivide the frame time. + while (accumulator >= time_step) { + Update(); + if (should_exit_) { + engine_->Shutdown(); + engine_.reset(); + return; + } + engine_->Update(time_step); + accumulator -= time_step; + }; + + // Calculate frame fraction from remainder of the frame time. + frame_frac = accumulator / time_step; + } +} + +} // namespace eng diff --git a/src/engine/platform/platform.h b/src/engine/platform/platform.h new file mode 100644 index 0000000..32e23d3 --- /dev/null +++ b/src/engine/platform/platform.h @@ -0,0 +1,88 @@ +#ifndef PLATFORM_H +#define PLATFORM_H + +#include +#include +#include + +#include "../../base/timer.h" +#include "../audio/audio_forward.h" + +#if defined(__ANDROID__) +struct android_app; +struct AInputEvent; + +namespace ndk_helper { +class TapDetector; +class PinchDetector; +class DragDetector; +} // namespace ndk_helper +#endif + +namespace eng { + +class Renderer; +class Engine; + +class Platform { + public: + Platform(); + ~Platform(); + +#if defined(__ANDROID__) + void Initialize(android_app* app); +#elif defined(__linux__) + void Initialize(); +#endif + + void Shutdown(); + + void Update(); + + void RunMainLoop(); + + void Exit(); + + int GetDeviceDpi() const { return device_dpi_; } + + const std::string& GetRootPath() const { return root_path_; } + + bool mobile_device() const { return mobile_device_; } + + static class InternalError : public std::exception { + } internal_error; + + private: + base::Timer timer_; + + bool mobile_device_ = false; + int device_dpi_ = 200; + std::string root_path_; + + bool has_focus_ = false; + bool should_exit_ = false; + + std::unique_ptr