Initial commit

This commit is contained in:
Attila Uygun 2020-04-13 13:24:53 +02:00
commit eea7c285ea
133 changed files with 61690 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

17
README.md Normal file
View File

@ -0,0 +1,17 @@
A simple, cross-platform 2D game engine with OpenGL renderer written in modern
C++. Supports Linux and Android platforms. Compiles both with gcc and clang.
Build for Linux:
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,10 @@
precision mediump float;
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,7 @@
precision mediump float;
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);
}

55
assets/sky.glsl_fragment Normal file
View File

@ -0,0 +1,55 @@
precision mediump float;
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 * 4337.5453);
}
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 - sqrt(e)));
}
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,95 @@
#
# 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)
# 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/asset_file_android.cc
../../../src/base/asset_file.cc
../../../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/engine.cc
../../../src/engine/font.cc
../../../src/engine/image_quad.cc
../../../src/engine/image.cc
../../../src/engine/mesh.cc
../../../src/engine/platform/platform_android.cc
../../../src/engine/platform/platform.cc
../../../src/engine/renderer/geometry.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/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
)
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
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'

158
build/linux/Makefile Normal file
View File

@ -0,0 +1,158 @@
.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
# 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 "*.c"))
# --- gltest application ---
ifneq ($(filter gltest,$(APPS)),)
GLTEST_SRC := \
$(SRC_ROOT)/base/asset_file_linux.cc \
$(SRC_ROOT)/base/asset_file.cc \
$(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/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/platform_linux.cc \
$(SRC_ROOT)/engine/platform/platform.cc \
$(SRC_ROOT)/engine/renderer/geometry.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)/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
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 $@ $<

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.

41
src/base/asset_file.cc Normal file
View File

@ -0,0 +1,41 @@
#include "asset_file.h"
#include "log.h"
namespace base {
std::unique_ptr<char[]> AssetFile::ReadWholeFile(const std::string& file_name,
const std::string& root_path,
size_t* length,
bool null_terminate) {
AssetFile file;
if (!file.Open(file_name, root_path))
return nullptr;
int size = file.GetSize();
if (size == 0)
return nullptr;
// Allocate a new buffer and add space for a null terminator.
std::unique_ptr<char[]> buffer =
std::make_unique<char[]>(size + (null_terminate ? 1 : 0));
// Read all of it.
int bytesRead = file.Read(buffer.get(), size);
if (!bytesRead) {
LOG << "Failed to read a buffer of size: " << size << " from file "
<< file_name;
return nullptr;
}
// Return the buffer size if the caller is interested.
if (length)
*length = bytesRead;
// Null terminate the buffer.
if (null_terminate)
buffer[size] = 0;
return buffer;
}
} // namespace base

44
src/base/asset_file.h Normal file
View File

@ -0,0 +1,44 @@
#ifndef ASSET_FILE_H
#define ASSET_FILE_H
#include "file.h"
#if defined(__ANDROID__)
#include <zlib.h>
#include "../third_party/minizip/unzip.h"
#elif defined(__linux__)
#include <stdio.h>
#endif
#include <memory>
#include <string>
namespace base {
class AssetFile {
public:
AssetFile();
~AssetFile();
bool Open(const std::string& file_name, const std::string& root_path);
void Close();
int GetSize();
int Read(char* data, size_t size);
static std::unique_ptr<char[]> ReadWholeFile(const std::string& file_name,
const std::string& root_path,
size_t* length = 0,
bool null_terminate = false);
private:
#if defined(__ANDROID__)
unzFile archive_ = 0;
size_t uncompressed_size_ = 0;
#elif defined(__linux)
ScopedFILE file_;
#endif
};
} // namespace base
#endif // ASSET_FILE_H

View File

@ -0,0 +1,74 @@
#include <assert.h>
#include <string>
#include "asset_file.h"
#include "log.h"
namespace base {
AssetFile::AssetFile() = default;
AssetFile::~AssetFile() {
Close();
}
bool AssetFile::Open(const std::string& file_name,
const std::string& root_path) {
do {
// Try to open the zip archive.
archive_ = unzOpen(root_path.c_str());
if (!archive_) {
LOG << "Failed to open zip file: " << root_path;
break;
}
// Try to find the file.
std::string full_name = "assets/" + file_name;
if (UNZ_OK != unzLocateFile(archive_, full_name.c_str(), 1)) {
LOG << "Failed to locate file in zip archive: " << file_name;
break;
}
// Need to get the uncompressed size of the file.
unz_file_info info;
if (UNZ_OK !=
unzGetCurrentFileInfo(archive_, &info, NULL, 0, NULL, 0, NULL, 0)) {
LOG << "Failed to get file info: " << file_name;
break;
}
uncompressed_size_ = info.uncompressed_size;
// Open the current file.
if (UNZ_OK != unzOpenCurrentFile(archive_)) {
LOG << "Failed to open file: " << file_name;
break;
}
return true;
} while (false);
Close();
return false;
}
void AssetFile::Close() {
if (archive_) {
// This could potentially be called without having opened a file, but that
// should be a harmless nop.
unzCloseCurrentFile(archive_);
unzClose(archive_);
archive_ = 0;
}
}
int AssetFile::GetSize() {
return uncompressed_size_;
}
int AssetFile::Read(char* data, size_t size) {
// Uncompress data into the buffer.
int result = unzReadCurrentFile(archive_, data, size);
return result < size ? 0 : result;
}
} // namespace base

View File

@ -0,0 +1,41 @@
#include <string>
#include "asset_file.h"
namespace base {
AssetFile::AssetFile() = default;
AssetFile::~AssetFile() = default;
bool AssetFile::Open(const std::string& file_name,
const std::string& root_path) {
std::string full_path = root_path + "assets/" + file_name;
file_.reset(fopen(full_path.c_str(), "rb"));
return !!file_;
}
void AssetFile::Close() {
file_.reset();
}
int AssetFile::GetSize() {
int size = 0;
if (file_) {
if (!fseek(file_.get(), 0, SEEK_END)) {
size = ftell(file_.get());
rewind(file_.get());
}
}
return size;
}
int AssetFile::Read(char* data, size_t size) {
if (file_)
return fread(data, 1, size, file_.get());
return 0;
}
} // namespace base

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

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

@ -0,0 +1,24 @@
#ifndef FILE_H
#define FILE_H
#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 <stdio.h>
#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

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

@ -0,0 +1,53 @@
#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>;
};
inline void* AlignedAlloc(size_t size) {
const size_t kAlignment = 16;
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

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

@ -0,0 +1,35 @@
#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;
// return ((value & (value - 1)) == 0);
}
inline int RoundUpToPow2(int val) {
int i = 1 << GetHighestBitPos(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 min, int max) {
return Lerp(min, max, 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_); }
// Returns a random int between min and max.
int Roll(int min, int max);
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

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

@ -0,0 +1,141 @@
#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() {
Engine& engine = Engine::Get();
font_ = engine.GetAsset<Font>("PixelCaps!.ttf");
if (!font_)
return false;
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::shared_ptr<Image> Credits::CreateImage() {
int line_height = font_->GetLineHeight() + 1;
auto image = std::make_shared<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();
image->SetImmutable();
return image;
}

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

@ -0,0 +1,49 @@
#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 Font;
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_;
std::shared_ptr<const eng::Font> font_;
int max_text_width_ = 0;
std::shared_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

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

@ -0,0 +1,248 @@
#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 (!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(0, 2);
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;
}

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

@ -0,0 +1,77 @@
#ifndef DEMO_H
#define DEMO_H
#include "../base/closure.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();
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;
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

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

@ -0,0 +1,461 @@
#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 "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() {
font_ = Engine::Get().GetAsset<Font>("PixelCaps!.ttf");
if (!font_)
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);
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(0, 1));
Vector2 s = engine.GetScreenSize();
int col;
col = rnd.Roll(0, 3);
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(1, 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);
}
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::shared_ptr<Image> Enemy::GetScoreImage(int score) {
std::string text = std::to_string(score);
int width, height;
font_->CalculateBoundingBox(text.c_str(), width, height);
auto image = std::make_shared<Image>();
image->Create(width, height);
image->Clear({1, 1, 1, 0});
font_->Print(0, 0, text.c_str(), image->GetBuffer(), image->GetWidth());
image->SetImmutable();
return image;
}
bool Enemy::CreateRenderResources() {
Engine& engine = Engine::Get();
auto skull_image = engine.GetAsset<Image>("enemy_anims_01_frames_ok.png");
auto bug_image = engine.GetAsset<Image>("enemy_anims_02_frames_ok.png");
auto target_image = engine.GetAsset<Image>("enemy_target_single_ok.png");
auto blast_image = engine.GetAsset<Image>("enemy_anims_blast_ok.png");
if (!skull_image || !bug_image || !target_image || !blast_image)
return false;
skull_tex_->Update(skull_image);
bug_tex_->Update(bug_image);
target_tex_->Update(target_image);
blast_tex_->Update(blast_image);
for (int i = 0; i < kEnemyType_Max; ++i)
score_tex_[i]->Update(GetScoreImage(GetScore((EnemyType)i)));
return true;
}

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

@ -0,0 +1,116 @@
#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 "damage_type.h"
namespace eng {
class Image;
class Font;
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;
};
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<const eng::Font> font_;
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::shared_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"
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();
font_ = engine.GetAsset<Font>("PixelCaps!.ttf");
if (!font_)
return false;
int tmp;
font_->CalculateBoundingBox("big_enough_text", max_text_width_, tmp);
auto image = CreateImage();
image->SetImmutable();
for (int i = 0; i < 2; ++i) {
text_[i].GetTexture()->Update(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) {
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());
image->SetImmutable();
text_[i].GetTexture()->Update(image);
}
std::shared_ptr<Image> Hud::CreateImage() {
auto image = std::make_shared<Image>();
image->Create(max_text_width_, font_->GetLineHeight());
image->Clear({1, 1, 1, 0});
return image;
}

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

@ -0,0 +1,56 @@
#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;
class Font;
} // 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];
std::shared_ptr<const eng::Font> font_;
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::shared_ptr<eng::Image> CreateImage();
};
#endif // HUD_H

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

@ -0,0 +1,215 @@
#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"
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() {
Engine& engine = Engine::Get();
font_ = engine.GetAsset<Font>("PixelCaps!.ttf");
if (!font_)
return false;
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::shared_ptr<Image> Menu::CreateImage() {
int line_height = font_->GetLineHeight() + 1;
auto image = std::make_shared<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();
image->SetImmutable();
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;
}

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

@ -0,0 +1,74 @@
#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 Font;
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];
std::shared_ptr<const eng::Font> font_;
int max_text_width_ = 0;
Option selected_option_ = kOption_Invalid;
base::Vector2 tap_pos_[2] = {{0, 0}, {0, 0}};
std::shared_ptr<eng::Image> CreateImage();
bool IsAnimating();
};
#endif // MENU_H

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

@ -0,0 +1,350 @@
#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);
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() {
Engine& engine = Engine::Get();
auto weapon_image = engine.GetAsset<Image>("enemy_anims_flare_ok.png");
auto beam_image = engine.GetAsset<Image>("enemy_ray_ok.png");
if (!weapon_image || !beam_image)
return false;
weapon_tex_->Update(weapon_image);
beam_tex_->Update(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 sky_source = engine.GetAsset<ShaderSource>("sky.glsl");
if (!sky_source)
return false;
shader_->Create(sky_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() override { return 0; }
size_t GetNumFrames() 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() = 0;
virtual size_t GetNumFrames() = 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

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

@ -0,0 +1,269 @@
#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;
}
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);
}
void Animator::SetRotation(float trget,
float duration,
Interpolator interpolator) {
rotation_target_ = trget;
rotation_speed_ = 1.0f / duration;
rotation_interpolator_ = std::move(interpolator);
}
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

53
src/engine/asset.h Normal file
View File

@ -0,0 +1,53 @@
#ifndef ASSET_H
#define ASSET_H
#include <string>
namespace eng {
class Asset {
public:
Asset() = default;
virtual ~Asset() = default;
virtual bool Load(const std::string& file_name) = 0;
void SetName(const std::string& name) { name_ = name; }
const std::string& GetName() const { return name_; }
void SetImmutable() { immutable_ = true; }
bool IsImmutable() const { return immutable_; }
protected:
std::string name_;
bool immutable_ = false;
};
class AssetFactoryBase {
public:
AssetFactoryBase(const std::string& name) : name_(name) {}
virtual ~AssetFactoryBase() = default;
virtual std::shared_ptr<eng::Asset> Create() = 0;
const std::string& name() { return name_; };
private:
std::string name_;
};
template <typename T>
class AssetFactory : public AssetFactoryBase {
public:
~AssetFactory() override = default;
AssetFactory(const std::string& name) : AssetFactoryBase(name) {}
std::shared_ptr<eng::Asset> Create() override {
return std::make_shared<T>();
}
};
} // namespace eng
#endif // ASSET_H

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

@ -0,0 +1,313 @@
#include "engine.h"
#include "../base/log.h"
#include "../base/worker.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)
: platform_(platform), renderer_(renderer) {
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);
}
system_font_ = GetAsset<Font>("engine/RobotoMono-Regular.ttf");
if (!system_font_) {
// Do not fail. Just create a null-font.
auto font = std::make_shared<Font>();
font->SetImmutable();
system_font_ = font;
}
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;
task_runner_.Run();
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::TrimMemory() {
LOG << "Trimming memory.";
assets_.clear();
}
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;
}
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;
}
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();
}
bool Engine::IsMobile() const {
return platform_->mobile_device();
}
std::shared_ptr<RenderResource> Engine::CreateRenderResourceInternal(
RenderResourceFactoryBase& factory) {
return renderer_->CreateResource(factory);
}
std::shared_ptr<Asset> Engine::GetAssetInternal(AssetFactoryBase& factory) {
auto it = assets_.find(factory.name());
if (it != assets_.end())
return it->second;
auto asset = factory.Create();
if (!asset->Load(factory.name().c_str()))
return nullptr;
asset->SetImmutable();
assets_[factory.name()] = asset;
return asset;
}
void Engine::ContextLost() {
if (!task_runner_.IsBoundToCurrentThread()) {
task_runner_.Enqueue(std::bind(&Engine::ContextLost, this));
return;
}
renderer_->DestroyAllResources();
CreateRenderResources();
game_->ContextLost();
}
bool Engine::CreateRenderResources() {
// Create the quad geometry we can reuse for all sprites.
auto quad_mesh = GetAsset<Mesh>("engine/quad.mesh");
if (!quad_mesh) {
LOG << "Could not create quad mesh.";
return false;
}
quad_->Create(quad_mesh);
// Create the shader we can reuse for texture rendering.
auto pts_source = GetAsset<ShaderSource>("engine/pass_through.glsl");
if (!pts_source) {
LOG << "Could not create pass through shader.";
return false;
}
pass_through_shader_->Create(pts_source, quad_->vertex_description());
// Create the shader we can reuse for solid rendering.
auto ss_source = GetAsset<ShaderSource>("engine/solid.glsl");
if (!ss_source) {
LOG << "Could not create solid shader.";
return false;
}
solid_shader_->Create(ss_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_shared<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_, margin, y,
text.c_str(), image->GetBuffer(),
image->GetWidth()));
y += line_height + margin;
}
worker.Join();
image->SetImmutable();
stats_.GetTexture()->Update(image);
stats_.AutoScale();
}
} // namespace eng

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

@ -0,0 +1,154 @@
#ifndef ENGINE_H
#define ENGINE_H
#include <deque>
#include <memory>
#include <unordered_map>
#include "../base/random.h"
#include "../base/task_runner.h"
#include "../base/vecmath.h"
#include "asset.h"
#include "image_quad.h"
#include "renderer/render_resource.h"
namespace eng {
class Font;
class Game;
class InputEvent;
class Renderer;
struct RenderCommand;
class Platform;
class Geometry;
class Shader;
class Engine {
public:
Engine(Platform* platform, Renderer* renderer);
~Engine();
static Engine& Get();
bool Initialize();
void Shutdown();
void Update(float delta_time);
void Draw(float frame_frac);
void LostFocus();
void GainedFocus();
void TrimMemory();
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));
}
// Returns immutable asset that can be accessed between multiple threads
// without locking. Returns nullptr if no asset was found with the given name.
template <typename T>
std::shared_ptr<const T> GetAsset(const std::string& name) {
AssetFactory<T> factory(name);
return std::dynamic_pointer_cast<T>(GetAssetInternal(factory));
}
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_; }
std::shared_ptr<const eng::Font> GetSystemFont() { return system_font_; }
base::Random& GetRandomGenerator() { return random_; }
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;
bool IsMobile() const;
float seconds_accumulated() const { return seconds_accumulated_; }
private:
static Engine* singleton;
Platform* platform_ = nullptr;
Renderer* renderer_ = nullptr;
std::unique_ptr<Game> game_;
// Asset cache.
std::unordered_map<std::string, std::shared_ptr<Asset>> assets_;
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::shared_ptr<const eng::Font> system_font_;
ImageQuad stats_;
float fps_seconds_ = 0;
int fps_ = 0;
float seconds_accumulated_ = 0.0f;
std::deque<std::unique_ptr<InputEvent>> input_queue_;
base::TaskRunner task_runner_;
base::Random random_;
std::shared_ptr<RenderResource> CreateRenderResourceInternal(
RenderResourceFactoryBase& factory);
std::shared_ptr<Asset> GetAssetInternal(AssetFactoryBase& factory);
void ContextLost();
bool CreateRenderResources();
void KillUnusedResources(float delta_time);
void SetSatsVisible(bool visible);
void PrintStats();
Engine(const Engine&) = delete;
Engine& operator=(const Engine&) = delete;
};
} // namespace eng
#endif // ENGINE_H

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

@ -0,0 +1,199 @@
#include "font.h"
#include "../base/asset_file.h"
#include "../base/log.h"
#include "engine.h"
#define STB_TRUETYPE_IMPLEMENTATION
#include "../third_party/stb/stb_truetype.h"
namespace eng {
bool Font::Load(const std::string& file_name) {
if (IsImmutable()) {
LOG << "Error: Font is immutable. Failed to load.";
return false;
}
SetName(file_name);
// Read the font file.
size_t buffer_size = 0;
std::unique_ptr<char[]> buffer = base::AssetFile::ReadWholeFile(
file_name.c_str(), Engine::Get().GetRootPath().c_str(), &buffer_size);
if (!buffer) {
LOG << "Failed to read font file.";
return false;
}
bool result = 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: " << result;
glyph_cache_.reset();
break;
}
result = true;
} while (0);
int x0, y0, x1, y1;
CalculateBoundingBox("`IlfKgjy_{)", x0, y0, x1, y1);
line_height_ = y1 - y0;
yoff_ = -y0;
return result;
}
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

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

@ -0,0 +1,55 @@
#ifndef FONT_H
#define FONT_H
#include <stdint.h>
#include <memory>
#include <string>
#include "../third_party/stb/stb_truetype.h"
#include "asset.h"
namespace eng {
class Font : public Asset {
public:
Font() = default;
~Font() override = default;
bool Load(const std::string& file_name) override;
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

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

@ -0,0 +1,268 @@
#include "image.h"
#include <cmath>
#include "../base/asset_file.h"
#include "../base/interpolation.h"
#include "../base/log.h"
#include "../base/misc.h"
#include "engine.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 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) {
if (IsImmutable()) {
LOG << "Error: Image is immutable. Failed to create.";
return false;
}
width_ = w;
height_ = h;
buffer_.reset((uint8_t*)AlignedAlloc(w * h * 4 * sizeof(uint8_t)));
return true;
}
void Image::Copy(const Image& other) {
if (IsImmutable()) {
LOG << "Error: Image is immutable. Failed to copy.";
return;
}
if (other.buffer_) {
int size = other.GetSize();
buffer_.reset((uint8_t*)AlignedAlloc(size));
memcpy(buffer_.get(), other.buffer_.get(), size);
}
width_ = other.width_;
height_ = other.height_;
format_ = other.format_;
}
bool Image::Load(const std::string& file_name) {
if (IsImmutable()) {
LOG << "Error: Image is immutable. Failed to load.";
return false;
}
SetName(file_name);
size_t buffer_size = 0;
std::unique_ptr<char[]> 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(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(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:
return ((width_ + 3) / 4) * ((height_ + 3) / 4) * 8;
case kDXT5:
return ((width_ + 3) / 4) * ((height_ + 3) / 4) * 16;
case kATC:
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 " << GetName() << " 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(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;
}
}
uint8_t* Image::GetBuffer() {
if (IsImmutable()) {
LOG << "Error: Image is immutable. Failed to return writable buffer.";
return nullptr;
}
return buffer_.get();
}
void Image::Clear(Vector4 rgba) {
if (IsImmutable()) {
LOG << "Error: Image is immutable. Failed to clear.";
return;
}
// 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() {
if (IsImmutable()) {
LOG << "Error: Image is immutable. Failed to apply gradient.";
return;
}
// 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) {
if (IsImmutable()) {
LOG << "Error: Image is immutable. Failed to apply gradient.";
return;
}
// 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

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

@ -0,0 +1,55 @@
#ifndef IMAGE_H
#define IMAGE_H
#include <stdint.h>
#include <string>
#include "../base/mem.h"
#include "../base/vecmath.h"
#include "asset.h"
namespace eng {
class Image : public Asset {
public:
enum Format { kRGBA32, kDXT1, kDXT5, kETC1, kATC };
Image();
Image(const Image& other);
~Image() override;
Image& operator=(const Image& other);
bool Create(int width, int height);
void Copy(const Image& other);
bool Load(const std::string& file_name) override;
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;
};
} // 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() {
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() override { return current_frame_; }
size_t GetNumFrames() 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

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

@ -0,0 +1,181 @@
#include "mesh.h"
#include <string.h>
#include <cassert>
#include "../base/asset_file.h"
#include "../base/log.h"
#include "../third_party/jsoncpp/json.h"
#include "engine.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) {
if (IsImmutable()) {
LOG << "Error: Mesh is immutable. Failed to create.";
return false;
}
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) {
if (IsImmutable()) {
LOG << "Error: Mesh is immutable. Failed to load.";
return false;
}
SetName(file_name);
size_t buffer_size = 0;
std::unique_ptr<char[]> json_mesh = base::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

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

@ -0,0 +1,56 @@
#ifndef MESH_H
#define MESH_H
#include <memory>
#include <string>
#include "asset.h"
#include "renderer/renderer_types.h"
namespace eng {
class Mesh : public Asset {
public:
static const char kLayoutDelimiter[];
Mesh() = default;
~Mesh() override = 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) override;
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

View File

@ -0,0 +1,73 @@
#include "platform.h"
#include <thread>
#include "../../base/log.h"
#include "../engine.h"
#include "../renderer/renderer.h"
// Save battery on mobile devices.
#define USE_SLEEP
namespace eng {
Platform::InternalError Platform::internal_error;
void Platform::Shutdown() {
LOG << "Shutting down platform.";
renderer_->Shutdown();
}
void Platform::RunMainLoop() {
engine_ = std::make_unique<Engine>(this, renderer_.get());
if (!engine_->Initialize()) {
LOG << "Failed to initialize the engine.";
throw internal_error;
}
// Use fixed time steps.
constexpr float time_step = 1.0f / 60.0f;
constexpr float speed = 1.0f;
constexpr float epsilon = 0.0001f;
timer_.Reset();
float accumulator = 0.0;
float frame_frac = 0.0f;
for (;;) {
engine_->Draw(frame_frac);
// Accumulate time.
#ifdef USE_SLEEP
while (accumulator < time_step) {
timer_.Update();
accumulator += timer_.GetSecondsPassed();
if (time_step - accumulator > epsilon) {
float sleep_time = time_step - accumulator - epsilon;
std::this_thread::sleep_for(
std::chrono::microseconds((int)(sleep_time * 1000000.0f)));
}
};
#else
timer_.Update();
accumulator += timer_.GetSecondsPassed();
#endif // USE_SLEEP
// Subdivide the frame time.
while (accumulator >= time_step) {
Update();
if (should_exit_) {
engine_->Shutdown();
engine_.reset();
return;
}
engine_->Update(time_step * speed);
accumulator -= time_step;
};
// Calculate frame fraction from remainder of the frame time.
frame_frac = accumulator / time_step;
}
}
} // namespace eng

View File

@ -0,0 +1,86 @@
#ifndef PLATFORM_H
#define PLATFORM_H
#include <exception>
#include <memory>
#include <string>
#include "../../base/timer.h"
#if defined(__ANDROID__)
struct android_app;
struct AInputEvent;
namespace ndk_helper {
class TapDetector;
class PinchDetector;
class DragDetector;
} // namespace ndk_helper
#endif
namespace eng {
class Renderer;
class Engine;
class Platform {
public:
Platform();
~Platform();
#if defined(__ANDROID__)
void Initialize(android_app* app);
#elif defined(__linux__)
void Initialize();
#endif
void Shutdown();
void Update();
void RunMainLoop();
void Exit();
Renderer* GetRenderer() { return renderer_.get(); }
int GetDeviceDpi() const { return device_dpi_; }
const std::string& GetRootPath() const { return root_path_; }
bool mobile_device() const { return mobile_device_; }
static class InternalError : public std::exception {
} internal_error;
private:
base::Timer timer_;
bool mobile_device_ = false;
int device_dpi_ = 200;
std::string root_path_;
bool has_focus_ = false;
bool should_exit_ = false;
std::unique_ptr<Renderer> renderer_;
std::unique_ptr<Engine> engine_;
#if defined(__ANDROID__)
android_app* app_ = nullptr;
std::unique_ptr<ndk_helper::TapDetector> tap_detector_;
std::unique_ptr<ndk_helper::PinchDetector> pinch_detector_;
std::unique_ptr<ndk_helper::DragDetector> drag_detector_;
static int32_t HandleInput(android_app* app, AInputEvent* event);
static void HandleCmd(android_app* app, int32_t cmd);
#endif
Platform(const Platform&) = delete;
Platform& operator=(const Platform&) = delete;
};
} // namespace eng
#endif // PLATFORM_H

View File

@ -0,0 +1,283 @@
#include "platform.h"
#include <android_native_app_glue.h>
#include <unistd.h>
#include <string>
#include "../../base/file.h"
#include "../../base/log.h"
#include "../../third_party/android/gestureDetector.h"
#include "../engine.h"
#include "../input_event.h"
#include "../renderer/renderer.h"
using namespace base;
namespace {
std::string GetApkPath(ANativeActivity* activity) {
JNIEnv* env = nullptr;
activity->vm->AttachCurrentThread(&env, nullptr);
jclass activity_clazz = env->GetObjectClass(activity->clazz);
jmethodID get_application_info_id =
env->GetMethodID(activity_clazz, "getApplicationInfo",
"()Landroid/content/pm/ApplicationInfo;");
jobject app_info_obj =
env->CallObjectMethod(activity->clazz, get_application_info_id);
jclass app_info_clazz = env->GetObjectClass(app_info_obj);
jfieldID source_dir_id =
env->GetFieldID(app_info_clazz, "sourceDir", "Ljava/lang/String;");
jstring source_dir_obj =
(jstring)env->GetObjectField(app_info_obj, source_dir_id);
const char* source_dir = env->GetStringUTFChars(source_dir_obj, nullptr);
std::string apk_path = std::string(source_dir);
env->ReleaseStringUTFChars(source_dir_obj, source_dir);
env->DeleteLocalRef(app_info_clazz);
env->DeleteLocalRef(activity_clazz);
activity->vm->DetachCurrentThread();
return apk_path;
}
int32_t getDensityDpi(android_app* app) {
AConfiguration* config = AConfiguration_new();
AConfiguration_fromAssetManager(config, app->activity->assetManager);
int32_t density = AConfiguration_getDensity(config);
AConfiguration_delete(config);
return density;
}
} // namespace
namespace eng {
Platform::Platform() = default;
Platform::~Platform() = default;
int32_t Platform::HandleInput(android_app* app, AInputEvent* event) {
Platform* platform = reinterpret_cast<Platform*>(app->userData);
if (!platform->engine_)
return 0;
if (AInputEvent_getType(event) == AINPUT_EVENT_TYPE_KEY &&
AKeyEvent_getKeyCode(event) == AKEYCODE_BACK) {
if (AKeyEvent_getAction(event) == AKEY_EVENT_ACTION_UP) {
auto input_event =
std::make_unique<InputEvent>(InputEvent::kNavigateBack);
platform->engine_->AddInputEvent(std::move(input_event));
}
return 1;
}
if (AInputEvent_getType(event) == AINPUT_EVENT_TYPE_MOTION) {
ndk_helper::GESTURE_STATE tap_state =
platform->tap_detector_->Detect(event);
ndk_helper::GESTURE_STATE drag_state =
platform->drag_detector_->Detect(event);
ndk_helper::GESTURE_STATE pinch_state =
platform->pinch_detector_->Detect(event);
// Tap detector has a priority over other detectors
if (tap_state == ndk_helper::GESTURE_STATE_ACTION) {
platform->engine_->AddInputEvent(
std::make_unique<InputEvent>(InputEvent::kDragCancel));
// Detect tap
Vector2 v;
platform->tap_detector_->GetPointer(v);
v = platform->engine_->ToPosition(v);
// DLOG << "Tap: " << v;
auto input_event =
std::make_unique<InputEvent>(InputEvent::kTap, v * Vector2(1, -1));
platform->engine_->AddInputEvent(std::move(input_event));
} else {
// Handle drag state
if (drag_state & ndk_helper::GESTURE_STATE_START) {
// Otherwise, start dragging
Vector2 v;
platform->drag_detector_->GetPointer(v);
v = platform->engine_->ToPosition(v);
// DLOG << "drag-start: " << v;
auto input_event = std::make_unique<InputEvent>(InputEvent::kDragStart,
v * Vector2(1, -1));
platform->engine_->AddInputEvent(std::move(input_event));
} else if (drag_state & ndk_helper::GESTURE_STATE_MOVE) {
Vector2 v;
platform->drag_detector_->GetPointer(v);
v = platform->engine_->ToPosition(v);
// DLOG << "drag: " << v;
auto input_event =
std::make_unique<InputEvent>(InputEvent::kDrag, v * Vector2(1, -1));
platform->engine_->AddInputEvent(std::move(input_event));
} else if (drag_state & ndk_helper::GESTURE_STATE_END) {
// DLOG << "drag-end!";
auto input_event = std::make_unique<InputEvent>(InputEvent::kDragEnd);
platform->engine_->AddInputEvent(std::move(input_event));
}
// Handle pinch state
if (pinch_state & ndk_helper::GESTURE_STATE_START) {
platform->engine_->AddInputEvent(
std::make_unique<InputEvent>(InputEvent::kDragCancel));
// Start new pinch
Vector2 v1;
Vector2 v2;
platform->pinch_detector_->GetPointers(v1, v2);
v1 = platform->engine_->ToPosition(v1);
v2 = platform->engine_->ToPosition(v2);
// DLOG << "pinch-start: " << v1 << " " << v2;
auto input_event = std::make_unique<InputEvent>(
InputEvent::kPinchStart, v1 * Vector2(1, -1), v2 * Vector2(1, -1));
platform->engine_->AddInputEvent(std::move(input_event));
} else if (pinch_state & ndk_helper::GESTURE_STATE_MOVE) {
// Multi touch
// Start new pinch
Vector2 v1;
Vector2 v2;
platform->pinch_detector_->GetPointers(v1, v2);
v1 = platform->engine_->ToPosition(v1);
v2 = platform->engine_->ToPosition(v2);
// DLOG << "pinch: " << v1 << " " << v2;
auto input_event = std::make_unique<InputEvent>(
InputEvent::kPinch, v1 * Vector2(1, -1), v2 * Vector2(1, -1));
platform->engine_->AddInputEvent(std::move(input_event));
}
}
return 1;
}
return 0;
}
void Platform::HandleCmd(android_app* app, int32_t cmd) {
Platform* platform = reinterpret_cast<Platform*>(app->userData);
switch (cmd) {
case APP_CMD_SAVE_STATE:
break;
case APP_CMD_INIT_WINDOW:
DLOG << "APP_CMD_INIT_WINDOW";
if (app->window != NULL) {
if (!platform->renderer_->Initialize(app->window)) {
LOG << "Failed to initialize the renderer.";
throw internal_error;
}
}
break;
case APP_CMD_TERM_WINDOW:
DLOG << "APP_CMD_TERM_WINDOW";
platform->renderer_->Shutdown();
break;
case APP_CMD_CONFIG_CHANGED:
DLOG << "APP_CMD_CONFIG_CHANGED";
if (platform->app_->window != NULL) {
int width = platform->renderer_->screen_width();
int height = platform->renderer_->screen_height();
if (width != ANativeWindow_getWidth(app->window) ||
height != ANativeWindow_getHeight(app->window)) {
platform->renderer_->Shutdown();
if (!platform->renderer_->Initialize(platform->app_->window)) {
LOG << "Failed to initialize the renderer.";
throw internal_error;
}
}
}
break;
case APP_CMD_STOP:
DLOG << "APP_CMD_STOP";
break;
case APP_CMD_GAINED_FOCUS:
DLOG << "APP_CMD_GAINED_FOCUS";
platform->timer_.Reset();
platform->has_focus_ = true;
if (platform->engine_)
platform->engine_->GainedFocus();
break;
case APP_CMD_LOST_FOCUS:
DLOG << "APP_CMD_LOST_FOCUS";
platform->has_focus_ = false;
if (platform->engine_)
platform->engine_->LostFocus();
break;
case APP_CMD_LOW_MEMORY:
DLOG << "APP_CMD_LOW_MEMORY";
if (platform->engine_)
platform->engine_->TrimMemory();
break;
}
}
void Platform::Initialize(android_app* app) {
LOG << "Initializing platform.";
app_ = app;
renderer_ = std::make_unique<Renderer>();
tap_detector_ = std::make_unique<ndk_helper::TapDetector>();
drag_detector_ = std::make_unique<ndk_helper::DragDetector>();
pinch_detector_ = std::make_unique<ndk_helper::PinchDetector>();
tap_detector_->SetConfiguration(app_->config);
drag_detector_->SetConfiguration(app_->config);
pinch_detector_->SetConfiguration(app_->config);
mobile_device_ = true;
root_path_ = GetApkPath(app->activity);
LOG << "Root path: " << root_path_.c_str();
device_dpi_ = getDensityDpi(app);
LOG << "Device DPI: " << device_dpi_;
app->userData = reinterpret_cast<void*>(this);
app->onAppCmd = Platform::HandleCmd;
app->onInputEvent = Platform::HandleInput;
Update();
}
void Platform::Update() {
int id;
int events;
android_poll_source* source;
while ((id = ALooper_pollAll(has_focus_ ? 0 : -1, NULL, &events,
(void**)&source)) >= 0) {
if (source != NULL)
source->process(app_, source);
if (app_->destroyRequested != 0) {
LOG << "App destroy requested.";
should_exit_ = true;
break;
}
if (has_focus_)
break;
}
}
void Platform::Exit() {
ANativeActivity_finish(app_->activity);
}
} // namespace eng
void android_main(android_app* app) {
eng::Platform platform;
try {
platform.Initialize(app);
platform.RunMainLoop();
platform.Shutdown();
} catch (eng::Platform::InternalError& e) {
}
_exit(0);
}

View File

@ -0,0 +1,110 @@
#include "platform.h"
#include <X11/Xlib.h>
#include <X11/Xutil.h>
#include "../../base/log.h"
#include "../../base/vecmath.h"
#include "../engine.h"
#include "../input_event.h"
#include "../renderer/renderer.h"
using namespace base;
namespace eng {
Platform::Platform() = default;
Platform::~Platform() = default;
void Platform::Initialize() {
root_path_ = "../../";
LOG << "Root path: " << root_path_.c_str();
renderer_ = std::make_unique<Renderer>();
if (!renderer_->Initialize()) {
LOG << "Failed to initialize renderer.";
throw internal_error;
}
LOG << "Initialized the renderer.";
Display* display = renderer_->display();
Window window = renderer_->window();
XSelectInput(
display, window,
KeyPressMask | Button1MotionMask | ButtonPressMask | ButtonReleaseMask);
Atom WM_DELETE_WINDOW = XInternAtom(display, "WM_DELETE_WINDOW", false);
XSetWMProtocols(display, window, &WM_DELETE_WINDOW, 1);
}
void Platform::Update() {
if (!engine_)
return;
Display* display = renderer_->display();
while (XPending(display)) {
XEvent e;
XNextEvent(display, &e);
switch (e.type) {
case KeyPress: {
KeySym key = XLookupKeysym(&e.xkey, 0);
auto input_event =
std::make_unique<InputEvent>(InputEvent::kKeyPress, key);
engine_->AddInputEvent(std::move(input_event));
// TODO: e.xkey.state & (ShiftMask | ControlMask | Mod1Mask | Mod4Mask))
break;
}
case MotionNotify: {
Vector2 v(e.xmotion.x, e.xmotion.y);
v = engine_->ToPosition(v);
// DLOG << "drag: " << v;
auto input_event =
std::make_unique<InputEvent>(InputEvent::kDrag, v * Vector2(1, -1));
engine_->AddInputEvent(std::move(input_event));
break;
}
case ButtonPress: {
if (e.xbutton.button == 1) {
Vector2 v(e.xbutton.x, e.xbutton.y);
v = engine_->ToPosition(v);
// DLOG << "drag-start: " << v;
auto input_event = std::make_unique<InputEvent>(
InputEvent::kDragStart, v * Vector2(1, -1));
engine_->AddInputEvent(std::move(input_event));
}
break;
}
case ButtonRelease: {
if (e.xbutton.button == 1) {
// DLOG << "drag-end!";
auto input_event = std::make_unique<InputEvent>(InputEvent::kDragEnd);
engine_->AddInputEvent(std::move(input_event));
}
break;
}
case ClientMessage: {
// TODO: Should check here for other client message types. However the
// only protocol registered above is WM_DELETE_WINDOW for now.
should_exit_ = true;
break;
}
}
}
}
void Platform::Exit() {
should_exit_ = true;
}
} // namespace eng
int main(int argc, char** argv) {
eng::Platform platform;
try {
platform.Initialize();
platform.RunMainLoop();
platform.Shutdown();
} catch (eng::Platform::InternalError& e) {
return -1;
}
return 0;
}

View File

@ -0,0 +1,52 @@
#include "geometry.h"
#include <cassert>
#include "../engine.h"
#include "../mesh.h"
#include "render_command.h"
#include "renderer.h"
namespace eng {
Geometry::Geometry(unsigned resource_id,
std::shared_ptr<void> impl_data,
Renderer* renderer)
: RenderResource(resource_id, impl_data, renderer) {}
Geometry::~Geometry() {
Destroy();
}
void Geometry::Create(std::shared_ptr<const Mesh> mesh) {
assert(mesh->IsImmutable());
Destroy();
valid_ = true;
vertex_description_ = mesh->vertex_description();
auto cmd = std::make_unique<CmdCreateGeometry>();
cmd->mesh = mesh;
cmd->impl_data = std::static_pointer_cast<void>(impl_data_);
renderer_->EnqueueCommand(std::move(cmd));
}
void Geometry::Destroy() {
if (valid_) {
auto cmd = std::make_unique<CmdDestroyGeometry>();
cmd->impl_data = std::static_pointer_cast<void>(impl_data_);
renderer_->EnqueueCommand(std::move(cmd));
valid_ = false;
}
}
void Geometry::Draw() {
if (valid_) {
auto cmd = std::make_unique<CmdDrawGeometry>();
cmd->impl_data = std::static_pointer_cast<void>(impl_data_);
renderer_->EnqueueCommand(std::move(cmd));
}
}
} // namespace eng

View File

@ -0,0 +1,38 @@
#ifndef GEOMETRY_H
#define GEOMETRY_H
#include <memory>
#include <string>
#include "render_resource.h"
#include "renderer_types.h"
namespace eng {
class Renderer;
class Mesh;
class Geometry : public RenderResource {
public:
Geometry(unsigned resource_id,
std::shared_ptr<void> impl_data,
Renderer* renderer);
~Geometry() override;
void Create(std::shared_ptr<const Mesh> mesh);
void Destroy() override;
void Draw();
const VertexDescripton& vertex_description() const {
return vertex_description_;
}
private:
VertexDescripton vertex_description_;
};
} // namespace eng
#endif // GEOMETRY_H

View File

@ -0,0 +1,21 @@
#ifndef OPENGL_H
#define OPENGL_H
#if defined(__ANDROID__)
// Use the modified Khronos header from ndk-helper. This gives access to
// additional functionality the drivers may expose but which the system headers
// do not.
#include "../../third_party/android/gl3stub.h"
#include <GLES2/gl2ext.h>
#elif defined(__linux__)
#include "../../third_party/glew/glew.h"
// Define the missing format for the etc1
#ifndef GL_ETC1_RGB8_OES
#define GL_ETC1_RGB8_OES 0x8D64
#endif
#endif
#endif // OPENGL_H

View File

@ -0,0 +1,140 @@
#ifndef RENDER_COMMAND_H
#define RENDER_COMMAND_H
#include <array>
#include <memory>
#include <string>
#include "../../base/hash.h"
#include "../../base/vecmath.h"
#include "renderer_types.h"
namespace eng {
class Image;
class ShaderSource;
class Mesh;
// Global render commands are guaranteed to be processed. Others commands are
// frame specific and can be discared by the renderer.
#ifdef _DEBUG
#define RENDER_COMMAND_BEGIN(NAME, GLOBAL) \
struct NAME : RenderCommand { \
static constexpr CommandId CMD_ID = HHASH(#NAME); \
NAME() : RenderCommand(CMD_ID, GLOBAL, #NAME) {}
#define RENDER_COMMAND_END };
#else
#define RENDER_COMMAND_BEGIN(NAME, GLOBAL) \
struct NAME : RenderCommand { \
static constexpr CommandId CMD_ID = HHASH(#NAME); \
NAME() : RenderCommand(CMD_ID, GLOBAL) {}
#define RENDER_COMMAND_END };
#endif
struct RenderCommand {
using CommandId = size_t;
static constexpr CommandId INVALID_CMD_ID = 0;
#ifdef _DEBUG
RenderCommand(CommandId id, bool g, const char* name)
: cmd_id(id), global(g), cmd_name(name) {}
#else
RenderCommand(CommandId id, bool g) : cmd_id(id), global(g) {}
#endif
const CommandId cmd_id = INVALID_CMD_ID;
const bool global = false;
#ifdef _DEBUG
std::string cmd_name;
#endif
};
RENDER_COMMAND_BEGIN(CmdEableBlend, false)
RENDER_COMMAND_END
RENDER_COMMAND_BEGIN(CmdClear, false)
std::array<float, 4> rgba;
RENDER_COMMAND_END
RENDER_COMMAND_BEGIN(CmdPresent, false)
RENDER_COMMAND_END
RENDER_COMMAND_BEGIN(CmdUpdateTexture, true)
std::shared_ptr<const Image> image;
std::shared_ptr<void> impl_data;
RENDER_COMMAND_END
RENDER_COMMAND_BEGIN(CmdDestoryTexture, true)
std::shared_ptr<void> impl_data;
RENDER_COMMAND_END
RENDER_COMMAND_BEGIN(CmdActivateTexture, false)
std::shared_ptr<void> impl_data;
RENDER_COMMAND_END
RENDER_COMMAND_BEGIN(CmdCreateGeometry, true)
std::shared_ptr<const Mesh> mesh;
std::shared_ptr<void> impl_data;
RENDER_COMMAND_END
RENDER_COMMAND_BEGIN(CmdDestroyGeometry, true)
std::shared_ptr<void> impl_data;
RENDER_COMMAND_END
RENDER_COMMAND_BEGIN(CmdDrawGeometry, false)
std::shared_ptr<void> impl_data;
RENDER_COMMAND_END
RENDER_COMMAND_BEGIN(CmdCreateShader, true)
std::shared_ptr<const ShaderSource> source;
VertexDescripton vertex_description;
std::shared_ptr<void> impl_data;
RENDER_COMMAND_END
RENDER_COMMAND_BEGIN(CmdDestroyShader, true)
std::shared_ptr<void> impl_data;
RENDER_COMMAND_END
RENDER_COMMAND_BEGIN(CmdActivateShader, false)
std::shared_ptr<void> impl_data;
RENDER_COMMAND_END
RENDER_COMMAND_BEGIN(CmdSetUniformVec2, false)
std::string name;
base::Vector2 v;
std::shared_ptr<void> impl_data;
RENDER_COMMAND_END
RENDER_COMMAND_BEGIN(CmdSetUniformVec3, false)
std::string name;
base::Vector3 v;
std::shared_ptr<void> impl_data;
RENDER_COMMAND_END
RENDER_COMMAND_BEGIN(CmdSetUniformVec4, false)
std::string name;
base::Vector4 v;
std::shared_ptr<void> impl_data;
RENDER_COMMAND_END
RENDER_COMMAND_BEGIN(CmdSetUniformMat4, false)
std::string name;
base::Matrix4x4 m;
std::shared_ptr<void> impl_data;
RENDER_COMMAND_END
RENDER_COMMAND_BEGIN(CmdSetUniformInt, false)
std::string name;
int i;
std::shared_ptr<void> impl_data;
RENDER_COMMAND_END
RENDER_COMMAND_BEGIN(CmdSetUniformFloat, false)
std::string name;
float f;
std::shared_ptr<void> impl_data;
RENDER_COMMAND_END
} // namespace eng
#endif // RENDER_COMMAND_H

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