Initial commit.

This commit is contained in:
Attila Uygun 2020-04-13 13:24:53 +02:00
commit 05466725af
290 changed files with 92781 additions and 0 deletions

8
.gitignore vendored Normal file
View File

@ -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

21
LICENSE Normal file
View File

@ -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.

17
README.md Normal file
View File

@ -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

BIN
assets/PixelCaps!.ttf Normal file

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 332 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 156 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 219 KiB

BIN
assets/enemy_ray_ok.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

View File

@ -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;
}

View File

@ -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);
}

10
assets/engine/quad.mesh Normal file
View File

@ -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]
}

View File

@ -0,0 +1,9 @@
#ifdef GL_ES
precision mediump float;
#endif
uniform vec4 color;
void main() {
gl_FragColor = color;
}

View File

@ -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);
}

BIN
assets/explosion.mp3 Normal file

Binary file not shown.

61
assets/sky.glsl_fragment Normal file
View File

@ -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);
}

17
assets/sky.glsl_vertex Normal file
View File

@ -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);
}

View File

@ -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)

View File

@ -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'
}

View File

@ -0,0 +1,34 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- BEGIN_INCLUDE(manifest) -->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.native_activity"
android:versionCode="1"
android:versionName="1.0">
<!-- This .apk has no Java code itself, so set hasCode to false. -->
<application
android:allowBackup="false"
android:fullBackupContent="false"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:hasCode="false">
<!-- Our activity is the built-in NativeActivity framework class.
This will take care of integrating with our NDK code. -->
<activity android:name="android.app.NativeActivity"
android:label="@string/app_name"
android:theme="@android:style/Theme.NoTitleBar.Fullscreen"
android:screenOrientation="portrait"
android:configChanges="orientation|keyboardHidden">
<!-- Tell NativeActivity the name of our .so -->
<meta-data android:name="android.app.lib_name"
android:value="native-activity" />
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
<!-- END_INCLUDE(manifest) -->

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">NativeActivity</string>
</resources>

View File

@ -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
}

View File

@ -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

Binary file not shown.

View File

@ -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

164
build/android/gradlew vendored Executable file
View File

@ -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 "$@"

View File

@ -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

View File

@ -0,0 +1,2 @@
include ':app'

174
build/linux/Makefile Normal file
View File

@ -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 $@ $<

21
src/LICENSE Normal file
View File

@ -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.

12
src/base/closure.h Normal file
View File

@ -0,0 +1,12 @@
#ifndef CLOSURE_H
#define CLOSURE_H
#include <functional>
namespace base {
using Closure = std::function<void()>;
} // namespace base
#endif // CLOSURE_H

View File

@ -0,0 +1,51 @@
#include "collusion_test.h"
#include <algorithm>
#include <cmath>
#include <limits>
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<float>::min();
float tmax = std::numeric_limits<float>::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

23
src/base/collusion_test.h Normal file
View File

@ -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

25
src/base/file.h Normal file
View File

@ -0,0 +1,25 @@
#ifndef FILE_H
#define FILE_H
#include <cstdio>
#include <memory>
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<FILE, internal::ScopedFILECloser>;
} // namespace base
#endif // FILE_H

21
src/base/hash.h Normal file
View File

@ -0,0 +1,21 @@
#ifndef HASH_H
#define HASH_H
#include <stddef.h>
#define HHASH(x) base::HornerHash(31, x)
namespace base {
// Compile time string hashing function.
template <size_t N>
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

43
src/base/interpolation.h Normal file
View File

@ -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 <class T>
inline T Lerp(const T& a, const T& b, float t) {
return a + (b - a) * t;
}
template <>
inline int Lerp<int>(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

52
src/base/log.cc Normal file
View File

@ -0,0 +1,52 @@
#include "log.h"
#if defined(__ANDROID__)
#include <android/log.h>
#else
#include <cstdio>
#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<<<bool>(const bool& arg) {
stream_ << (arg ? "true" : "false");
return *this;
}
template <>
Log& Log::operator<<<Vector2>(const Vector2& arg) {
stream_ << "(" << arg.x << ", " << arg.y << ")";
return *this;
}
template <>
Log& Log::operator<<<Vector4>(const Vector4& arg) {
stream_ << "(" << arg.x << ", " << arg.y << ", " << arg.z << ", " << arg.w
<< ")";
return *this;
}
} // namespace base

49
src/base/log.h Normal file
View File

@ -0,0 +1,49 @@
#ifndef LOG_H
#define LOG_H
#include <sstream>
#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 <typename T>
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

52
src/base/mem.h Normal file
View File

@ -0,0 +1,52 @@
#ifndef MEM_H
#define MEM_H
#include <cassert>
#include <cstdlib>
#include <memory>
#if defined(__ANDROID__)
#include <malloc.h>
#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 <typename T>
struct AlignedMem {
using ScoppedPtr = std::unique_ptr<T, internal::ScopedAlignedFree>;
};
template <int kAlignment>
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

34
src/base/misc.h Normal file
View File

@ -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

26
src/base/random.cc Normal file
View File

@ -0,0 +1,26 @@
#include "random.h"
#include <limits>
#include "interpolation.h"
namespace base {
Random::Random() {
std::random_device rd;
generator_ = std::mt19937(rd());
real_distribution_ = std::uniform_real_distribution<float>(0, 1);
}
Random::Random(unsigned seed) {
generator_ = std::mt19937(seed);
real_distribution_ = std::uniform_real_distribution<float>(0, 1);
}
Random::~Random() = default;
int Random::Roll(int sides) {
return Lerp(1, sides, GetFloat());
}
} // namespace base

27
src/base/random.h Normal file
View File

@ -0,0 +1,27 @@
#ifndef RANDOM_GENERATOR_H
#define RANDOM_GENERATOR_H
#include <random>
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<float> real_distribution_;
};
} // namespace base
#endif // RANDOM_GENERATOR_H

30
src/base/task_runner.cc Normal file
View File

@ -0,0 +1,30 @@
#include "task_runner.h"
namespace base {
void TaskRunner::Enqueue(base::Closure task) {
std::unique_lock<std::mutex> scoped_lock(mutex_);
thread_tasks_.emplace_back(std::move(task));
}
void TaskRunner::Run() {
for (;;) {
base::Closure task;
{
std::unique_lock<std::mutex> 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

32
src/base/task_runner.h Normal file
View File

@ -0,0 +1,32 @@
#ifndef TASK_RUNNER_H
#define TASK_RUNNER_H
#include <deque>
#include <mutex>
#include <thread>
#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<base::Closure> thread_tasks_;
TaskRunner(TaskRunner const&) = delete;
TaskRunner& operator=(TaskRunner const&) = delete;
};
} // namespace base
#endif // TASK_RUNNER_H

28
src/base/timer.cc Normal file
View File

@ -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(&currentTime, 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

29
src/base/timer.h Normal file
View File

@ -0,0 +1,29 @@
#ifndef TIMER_H
#define TIMER_H
#include <sys/time.h>
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

15
src/base/vecmath.cc Normal file
View File

@ -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

163
src/base/vecmath.h Normal file
View File

@ -0,0 +1,163 @@
#ifndef VEC_MATH_H
#define VEC_MATH_H
#include <algorithm>
#include <cmath>
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

68
src/base/worker.cc Normal file
View File

@ -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<std::mutex> 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<std::mutex> 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<std::mutex> 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

40
src/base/worker.h Normal file
View File

@ -0,0 +1,40 @@
#ifndef WORKER_H
#define WORKER_H
#include <condition_variable>
#include <deque>
#include <mutex>
#include <thread>
#include <vector>
#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<std::thread> threads_;
std::deque<base::Closure> tasks_;
bool quit_when_idle_ = false;
void WorkerMain();
Worker(Worker const&) = delete;
Worker& operator=(Worker const&) = delete;
};
} // namespace base
#endif // WORKER_H

138
src/demo/credits.cc Normal file
View File

@ -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<Demo*>(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<InputEvent> 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<Demo*>(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<Texture>();
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<Image> Credits::CreateImage() {
const Font& font = static_cast<Demo*>(Engine::Get().GetGame())->GetFont();
int line_height = font.GetLineHeight() + 1;
auto image = std::make_unique<Image>();
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;
}

47
src/demo/credits.h Normal file
View File

@ -0,0 +1,47 @@
#ifndef CREDITS_H
#define CREDITS_H
#include <memory>
#include <string>
#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<eng::InputEvent> event);
void Draw();
void ContextLost();
void Show();
void Hide();
private:
std::shared_ptr<eng::Texture> tex_;
eng::ImageQuad text_[kNumLines];
eng::Animator text_animator_;
int max_text_width_ = 0;
std::unique_ptr<eng::Image> CreateImage();
};
#endif // CREDITS_H

20
src/demo/damage_type.h Normal file
View File

@ -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

251
src/demo/demo.cc Normal file
View File

@ -0,0 +1,251 @@
#include "demo.h"
#include <algorithm>
#include <string>
#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<InputEvent> 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;
}

82
src/demo/demo.h Normal file
View File

@ -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

476
src/demo/enemy.cc Normal file
View File

@ -0,0 +1,476 @@
#include "enemy.h"
#include <cassert>
#include <functional>
#include <limits>
#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<Texture>()),
bug_tex_(Engine::Get().CreateRenderResource<Texture>()),
target_tex_(Engine::Get().CreateRenderResource<Texture>()),
blast_tex_(Engine::Get().CreateRenderResource<Texture>()),
score_tex_{Engine::Get().CreateRenderResource<Texture>(),
Engine::Get().CreateRenderResource<Texture>(),
Engine::Get().CreateRenderResource<Texture>()} {}
Enemy::~Enemy() = default;
bool Enemy::Initialize() {
explosion_sound_ = std::make_shared<Sound>();
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<float>::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<Demo*>(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<Demo*>(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<Image> Enemy::GetScoreImage(int score) {
const Font& font = static_cast<Demo*>(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>();
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<Image>();
if (!skull_image->Load("enemy_anims_01_frames_ok.png"))
return false;
auto bug_image = std::make_unique<Image>();
if (!bug_image->Load("enemy_anims_02_frames_ok.png"))
return false;
auto target_image = std::make_unique<Image>();
if (!target_image->Load("enemy_target_single_ok.png"))
return false;
auto blast_image = std::make_unique<Image>();
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;
}

119
src/demo/enemy.h Normal file
View File

@ -0,0 +1,119 @@
#ifndef ENEMY_H
#define ENEMY_H
#include <array>
#include <list>
#include <memory>
#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<eng::Texture> skull_tex_;
std::shared_ptr<eng::Texture> bug_tex_;
std::shared_ptr<eng::Texture> target_tex_;
std::shared_ptr<eng::Texture> blast_tex_;
std::shared_ptr<eng::Texture> score_tex_[kEnemyType_Max];
std::shared_ptr<eng::Sound> explosion_sound_;
std::list<EnemyUnit> enemies_;
int num_enemies_killed_in_current_wave_ = 0;
std::array<float, kEnemyType_Max> seconds_since_last_spawn_ = {0, 0, 0};
std::array<float, kEnemyType_Max> 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<eng::Image> GetScoreImage(int score);
bool CreateRenderResources();
};
#endif // ENEMY_H

164
src/demo/hud.cc Normal file
View File

@ -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<Texture>());
text_[1].Create(Engine::Get().CreateRenderResource<Texture>());
}
Hud::~Hud() = default;
bool Hud::Initialize() {
Engine& engine = Engine::Get();
const Font& font = static_cast<Demo*>(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<Demo*>(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<Image> Hud::CreateImage() {
const Font& font = static_cast<Demo*>(Engine::Get().GetGame())->GetFont();
auto image = std::make_unique<Image>();
image->Create(max_text_width_, font.GetLineHeight());
image->Clear({1, 1, 1, 0});
return image;
}

54
src/demo/hud.h Normal file
View File

@ -0,0 +1,54 @@
#ifndef HUD_H
#define HUD_H
#include <memory>
#include <string>
#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<eng::Image> CreateImage();
};
#endif // HUD_H

214
src/demo/menu.cc Normal file
View File

@ -0,0 +1,214 @@
#include "menu.h"
#include <cassert>
#include <cmath>
#include <vector>
#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<Texture>()) {}
Menu::~Menu() = default;
bool Menu::Initialize() {
const Font& font = static_cast<Demo*>(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<InputEvent> 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<Image> Menu::CreateImage() {
const Font& font = static_cast<Demo*>(Engine::Get().GetGame())->GetFont();
int line_height = font.GetLineHeight() + 1;
auto image = std::make_unique<Image>();
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;
}

72
src/demo/menu.h Normal file
View File

@ -0,0 +1,72 @@
#ifndef MENU_H
#define MENU_H
#include <memory>
#include <string>
#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<eng::InputEvent> 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<eng::Texture> 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<eng::Image> CreateImage();
bool IsAnimating();
};
#endif // MENU_H

354
src/demo/player.cc Normal file
View File

@ -0,0 +1,354 @@
#include "player.h"
#include <cassert>
#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<Texture>()),
beam_tex_(Engine::Get().CreateRenderResource<Texture>()) {}
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<InputEvent> 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<float>::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<Demo*>(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<Demo*>(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<Demo*>(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<Demo*>(engine.GetGame())->EnterMenuState();
}
bool Player::CreateRenderResources() {
auto weapon_image = std::make_unique<Image>();
if (!weapon_image->Load("enemy_anims_flare_ok.png"))
return false;
auto beam_image = std::make_unique<Image>();
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;
}

79
src/demo/player.h Normal file
View File

@ -0,0 +1,79 @@
#ifndef PLAYER_H
#define PLAYER_H
#include <memory>
#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<eng::InputEvent> event);
void Draw(float frame_frac);
base::Vector2 GetWeaponPos(DamageType type) const;
base::Vector2 GetWeaponScale() const;
private:
std::shared_ptr<eng::Texture> weapon_tex_;
std::shared_ptr<eng::Texture> 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

64
src/demo/sky_quad.cc Normal file
View File

@ -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<Shader>()),
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<ShaderSource>();
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);
}

52
src/demo/sky_quad.h Normal file
View File

@ -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 <array>
#include <memory>
#include <string>
#include <vector>
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<eng::Shader> 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

33
src/engine/animatable.cc Normal file
View File

@ -0,0 +1,33 @@
#include "animatable.h"
#include <cmath>
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

66
src/engine/animatable.h Normal file
View File

@ -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

276
src/engine/animator.cc Normal file
View File

@ -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

136
src/engine/animator.h Normal file
View File

@ -0,0 +1,136 @@
#ifndef ANIMATOR_H
#define ANIMATOR_H
#include <vector>
#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<float(float)>;
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<Element> 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

20
src/engine/audio/audio.h Normal file
View File

@ -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

View File

@ -0,0 +1,187 @@
#include "audio_alsa.h"
#include <alsa/asoundlib.h>
#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<bool> promise;
std::future<bool> 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<bool> promise) {
promise.set_value(true);
size_t num_frames = period_size_ / (num_channels_ * sizeof(float));
auto buffer = std::make_unique<float[]>(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

View File

@ -0,0 +1,46 @@
#ifndef AUDIO_ALSA_H
#define AUDIO_ALSA_H
#include <future>
#include <memory>
#include <thread>
#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<bool> promise);
};
} // namespace eng
#endif // AUDIO_ALSA_H

View File

@ -0,0 +1,138 @@
#include "audio_base.h"
#include <cstring>
#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<AudioSample> sample) {
std::unique_lock<std::mutex> 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<std::mutex> 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<const Sound*>(sound)->GetBuffer(0),
const_cast<const Sound*>(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<const Sound*>(sound)->GetBuffer(0);
src[1] = const_cast<const Sound*>(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

View File

@ -0,0 +1,41 @@
#ifndef AUDIO_BASE_H
#define AUDIO_BASE_H
#include <list>
#include <memory>
#include <mutex>
#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<AudioSample> impl_data);
void Update();
protected:
static constexpr int kChannelCount = 2;
std::list<std::shared_ptr<AudioSample>> 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

View File

@ -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

View File

@ -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<StreamCallback>(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<float*>(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

View File

@ -0,0 +1,50 @@
#ifndef AUDIO_OBOE_H
#define AUDIO_OBOE_H
#include <memory>
#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<StreamCallback> callback_;
bool RestartStream();
};
} // namespace eng
#endif // AUDIO_OBOE_H

View File

@ -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<AudioSample>()), audio_(audio) {}
AudioResource::~AudioResource() {
sample_->flags |= AudioSample::kStopped;
}
void AudioResource::Play(std::shared_ptr<Sound> 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

View File

@ -0,0 +1,41 @@
#ifndef AUDIO_RESOURCE_H
#define AUDIO_RESOURCE_H
#include <memory>
#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> 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<AudioSample> sample_;
Audio* audio_ = nullptr;
AudioResource(const AudioResource&) = delete;
AudioResource& operator=(const AudioResource&) = delete;
};
} // namespace eng
#endif // AUDIO_RESOURCE_H

View File

@ -0,0 +1,32 @@
#ifndef AUDIO_SAMPLE_H
#define AUDIO_SAMPLE_H
#include <memory>
#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> 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

314
src/engine/engine.cc Normal file
View File

@ -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<Geometry>();
pass_through_shader_ = CreateRenderResource<Shader>();
solid_shader_ = CreateRenderResource<Shader>();
}
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<Font>();
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<CmdClear>();
cmd->rgba = {0, 0, 0, 1};
renderer_->EnqueueCommand(std::move(cmd));
renderer_->EnqueueCommand(std::make_unique<CmdEableBlend>());
game_->Draw(frame_frac);
if (stats_.IsVisible())
stats_.Draw();
renderer_->EnqueueCommand(std::make_unique<CmdPresent>());
}
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<AudioResource> Engine::CreateAudioResource() {
return std::make_shared<AudioResource>(audio_);
}
void Engine::AddInputEvent(std::unique_ptr<InputEvent> 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<InputEvent> Engine::GetNextInputEvent() {
std::unique_ptr<InputEvent> 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<RenderResource> 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<Mesh>();
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<ShaderSource>();
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<ShaderSource>();
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<Texture>());
else
stats_.Destory();
}
void Engine::PrintStats() {
constexpr int width = 200;
std::vector<std::string> 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>();
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

148
src/engine/engine.h Normal file
View File

@ -0,0 +1,148 @@
#ifndef ENGINE_H
#define ENGINE_H
#include <deque>
#include <memory>
#include <unordered_map>
#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 <typename T>
std::shared_ptr<T> CreateRenderResource() {
RenderResourceFactory<T> factory;
return std::dynamic_pointer_cast<T>(CreateRenderResourceInternal(factory));
}
std::shared_ptr<AudioResource> CreateAudioResource();
void AddInputEvent(std::unique_ptr<InputEvent> event);
std::unique_ptr<InputEvent> GetNextInputEvent();
// Access to the render resources.
std::shared_ptr<Geometry> GetQuad() { return quad_; }
std::shared_ptr<Shader> GetPassThroughShader() {
return pass_through_shader_;
}
std::shared_ptr<Shader> 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> game_;
std::shared_ptr<Geometry> quad_;
std::shared_ptr<Shader> pass_through_shader_;
std::shared_ptr<Shader> solid_shader_;
base::Vector2 screen_size_ = {0, 0};
base::Matrix4x4 projection_;
std::unique_ptr<Font> system_font_;
std::unique_ptr<TextureCompressor> tex_comp_opaque_;
std::unique_ptr<TextureCompressor> tex_comp_alpha_;
ImageQuad stats_;
float fps_seconds_ = 0;
int fps_ = 0;
float seconds_accumulated_ = 0.0f;
std::deque<std::unique_ptr<InputEvent>> input_queue_;
base::Random random_;
std::shared_ptr<RenderResource> 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

193
src/engine/font.cc Normal file
View File

@ -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<uint8_t[]>(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

54
src/engine/font.h Normal file
View File

@ -0,0 +1,54 @@
#ifndef FONT_H
#define FONT_H
#include <cstdint>
#include <memory>
#include <string>
#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<uint8_t[]> glyph_cache_; // Image data.
stbtt_bakedchar glyph_info_[kNumChars]; // Coordinates and advance.
int line_height_ = 0;
int yoff_ = 0;
};
} // namespace eng
#endif // FONT_H

30
src/engine/game.h Normal file
View File

@ -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

55
src/engine/game_factory.h Normal file
View File

@ -0,0 +1,55 @@
#ifndef GAME_FACTORY_H
#define GAME_FACTORY_H
#include <memory>
#include <string>
#include <vector>
#define DECLARE_GAME_BEGIN \
std::vector<std::pair<std::string, eng::GameFactoryBase*>> \
eng::GameFactoryBase::game_classes = {
#define DECLARE_GAME(CLASS) {#CLASS, new eng::GameFactory<CLASS>()},
#define DECLARE_GAME_END };
namespace eng {
class Game;
class GameFactoryBase {
public:
virtual ~GameFactoryBase() = default;
static std::unique_ptr<Game> 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<Game> CreateGame() { return nullptr; }
static std::vector<std::pair<std::string, GameFactoryBase*>> game_classes;
};
template <typename Type>
class GameFactory : public GameFactoryBase {
public:
~GameFactory() override = default;
private:
using GameType = Type;
std::unique_ptr<Game> CreateGame() override {
return std::make_unique<GameType>();
}
};
} // namespace eng
#endif // GAME_FACTORY_H

359
src/engine/image.cc Normal file
View File

@ -0,0 +1,359 @@
#include "image.h"
#include <algorithm>
#include <cmath>
#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<const uint32_t*>(src);
uint32_t* d = reinterpret_cast<uint32_t*>(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<const uint32_t*>(other.buffer_.get());
uint32_t* d = reinterpret_cast<uint32_t*>(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

59
src/engine/image.h Normal file
View File

@ -0,0 +1,59 @@
#ifndef IMAGE_H
#define IMAGE_H
#include <stdint.h>
#include <string>
#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<uint8_t[]>::ScoppedPtr buffer_;
int width_ = 0;
int height_ = 0;
Format format_ = kRGBA32;
std::string name_;
};
} // namespace eng
#endif // IMAGE_H

87
src/engine/image_quad.cc Normal file
View File

@ -0,0 +1,87 @@
#include "image_quad.h"
#include <cassert>
#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> texture,
std::array<int, 2> 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<Geometry> quad = Engine::Get().GetQuad();
std::shared_ptr<Shader> 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

57
src/engine/image_quad.h Normal file
View File

@ -0,0 +1,57 @@
#ifndef IMAGE_QUAD_H
#define IMAGE_QUAD_H
#include "../base/vecmath.h"
#include "animatable.h"
#include <array>
#include <memory>
namespace eng {
class Texture;
class ImageQuad : public Animatable {
public:
ImageQuad() = default;
~ImageQuad() override = default;
void Create(std::shared_ptr<Texture> texture,
std::array<int, 2> 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<Texture> GetTexture() { return texture_; }
private:
std::shared_ptr<Texture> texture_;
size_t current_frame_ = 0;
std::array<int, 2> 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

49
src/engine/input_event.h Normal file
View File

@ -0,0 +1,49 @@
#ifndef INPUT_EVENT_H
#define INPUT_EVENT_H
#include <cassert>
#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

169
src/engine/mesh.cc Normal file
View File

@ -0,0 +1,169 @@
#include "mesh.h"
#include <string.h>
#include <cassert>
#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<char[]>(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<char[]>(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<Json::CharReader> 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<char[]>(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

55
src/engine/mesh.h Normal file
View File

@ -0,0 +1,55 @@
#ifndef MESH_H
#define MESH_H
#include <memory>
#include <string>
#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<char[]> vertices_;
std::unique_ptr<char[]> indices_;
};
} // namespace eng
#endif // MESH_H

Some files were not shown because too many files have changed in this diff Show More