Debugging linkage stuff
This commit is contained in:
Generated
+6
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
@@ -25,7 +25,7 @@ project("jnicpp")
|
||||
# System.loadLibrary() and pass the name of the library defined here;
|
||||
# for GameActivity/NativeActivity derived applications, the same library name must be
|
||||
# used in the AndroidManifest.xml file.
|
||||
set(SRC_DIR ../../../../../my_gl_app)
|
||||
set(SRC_DIR my_gl_app)
|
||||
# Get source files from my_gl_app directory
|
||||
file(GLOB_RECURSE MY_GL_APP_SRC CONFIGURE_DEPENDS
|
||||
"${SRC_DIR}/*.cpp"
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
# For more information about using CMake with Android Studio, read the
|
||||
# documentation: https://d.android.com/studio/projects/add-native-code.html.
|
||||
# For more examples on how to use CMake, see https://github.com/android/ndk-samples.
|
||||
# Sets the minimum CMake version required for this project.
|
||||
cmake_minimum_required(VERSION 3.22.1)
|
||||
# Set C++ standard
|
||||
set(CMAKE_CXX_STANDARD 17)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
# Declares the project name. The project name can be accessed via ${ PROJECT_NAME},
|
||||
# Since this is the top level CMakeLists.txt, the project name is also accessible
|
||||
# with ${CMAKE_PROJECT_NAME} (both CMake variables are in-sync within the top level
|
||||
# build script scope).
|
||||
|
||||
project("jnicpp")
|
||||
# Creates and names a library, sets it as either STATIC
|
||||
# or SHARED, and provides the relative paths to its source code.
|
||||
# You can define multiple libraries, and CMake builds them for you.
|
||||
# Gradle automatically packages shared libraries with your APK.
|
||||
#
|
||||
# In this top level CMakeLists.txt, ${CMAKE_PROJECT_NAME} is used to define
|
||||
# the target library name; in the sub-module's CMakeLists.txt, ${PROJECT_NAME}
|
||||
# is preferred for the same purpose.
|
||||
#
|
||||
# In order to load a library into your app from Java/Kotlin, you must call
|
||||
# System.loadLibrary() and pass the name of the library defined here;
|
||||
# for GameActivity/NativeActivity derived applications, the same library name must be
|
||||
# used in the AndroidManifest.xml file.
|
||||
set(SRC_DIR ../../../../../my_gl_app)
|
||||
# Get source files from my_gl_app directory
|
||||
file(GLOB_RECURSE MY_GL_APP_SRC CONFIGURE_DEPENDS
|
||||
"${SRC_DIR}/*.cpp"
|
||||
"${SRC_DIR}/*.h"
|
||||
"${SRC_DIR}/*.hpp"
|
||||
)
|
||||
# Include directories
|
||||
include_directories(${SRC_DIR})
|
||||
include_directories(${CMAKE_CURRENT_SOURCE_DIR})
|
||||
add_library(${CMAKE_PROJECT_NAME} SHARED
|
||||
# List C/C++ source files with relative paths to this CMakeLists.txt.
|
||||
NativeTemplate.cpp
|
||||
${MY_GL_APP_SRC})
|
||||
# Specifies libraries CMake should link to your target library. You
|
||||
# can link libraries from various origins, such as libraries defined in this
|
||||
# build script, prebuilt third-party libraries, or Android system libraries.
|
||||
target_link_libraries(${CMAKE_PROJECT_NAME}
|
||||
# List libraries link to the target library
|
||||
android
|
||||
log
|
||||
GLESv3
|
||||
EGL)
|
||||
@@ -0,0 +1,105 @@
|
||||
#include <jni.h>
|
||||
#include <string>
|
||||
#include <android/log.h>
|
||||
// Ensure JNI macros are available
|
||||
#ifndef JNIEXPORT
|
||||
|
||||
#define JNIEXPORT
|
||||
#endif
|
||||
#ifndef JNICALL
|
||||
#define JNICALL
|
||||
#endif
|
||||
#include "GLRenderer.h"
|
||||
#include "UIRenderer.h"
|
||||
#include "PlatformBridge.h"
|
||||
// Define logging macros for this file
|
||||
#ifndef LOG_TAG
|
||||
#define LOG_TAG "NativeTemplate"
|
||||
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)
|
||||
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)
|
||||
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__)
|
||||
#endif
|
||||
// Global renderer instance
|
||||
static GLRenderer* g_renderer = nullptr;
|
||||
extern "C" JNIEXPORT jstring JNICALL
|
||||
Java_com_example_jnicpp_MainActivity_stringFromJNI(
|
||||
JNIEnv* env,
|
||||
jobject /* this */) {
|
||||
LOGI("stringFromJNI called");
|
||||
std::string hello = "OpenGL ES 3.0 Triangle Demo";
|
||||
return env->NewStringUTF(hello.c_str());
|
||||
}
|
||||
extern "C" JNIEXPORT jboolean JNICALL
|
||||
Java_com_example_jnicpp_MainActivity_initGL(
|
||||
JNIEnv* env,
|
||||
jobject thiz) {
|
||||
LOGI("initGL called");
|
||||
// Cache a global ref to the activity so native code (e.g. Menu3State,
|
||||
// running on this same GL thread) can call back into Java later, e.g. to
|
||||
// launch BowlingCameraActivity. See PlatformBridge.h.
|
||||
PlatformBridge::SetAndroidActivity(env, thiz);
|
||||
// Create renderer if not exists
|
||||
if (g_renderer == nullptr) {
|
||||
LOGI("Creating new GLRenderer instance");
|
||||
g_renderer = new GLRenderer();
|
||||
if (g_renderer == nullptr) {
|
||||
LOGE("Failed to create GLRenderer instance");
|
||||
return JNI_FALSE;
|
||||
}
|
||||
}
|
||||
// Initialize OpenGL renderer (no longer needs surface parameter)
|
||||
bool success = g_renderer->initialize();
|
||||
if (success) {
|
||||
LOGI("OpenGL initialization successful");
|
||||
} else {
|
||||
LOGE("OpenGL initialization failed");
|
||||
}
|
||||
|
||||
return success ? JNI_TRUE : JNI_FALSE;
|
||||
}
|
||||
extern "C" JNIEXPORT void JNICALL
|
||||
Java_com_example_jnicpp_MainActivity_renderFrame(
|
||||
JNIEnv* env,
|
||||
jobject /* this */) {
|
||||
if (g_renderer != nullptr) {
|
||||
g_renderer->render();
|
||||
} else {
|
||||
LOGE("Renderer is null in renderFrame");
|
||||
}
|
||||
}
|
||||
extern "C" JNIEXPORT void JNICALL
|
||||
Java_com_example_jnicpp_MainActivity_onSurfaceResized(
|
||||
JNIEnv* env,
|
||||
jobject /* this */,
|
||||
jint width,
|
||||
jint height) {
|
||||
LOGI("onSurfaceResized called: %dx%d", width, height);
|
||||
glViewport(0, 0, width, height);
|
||||
UI::Init(width, height); // no-op besides updating cached size if already initialized
|
||||
}
|
||||
|
||||
extern "C" JNIEXPORT void JNICALL
|
||||
Java_com_example_jnicpp_MainActivity_nativeOnTouch(
|
||||
JNIEnv* env,
|
||||
jobject /* this */,
|
||||
jfloat x,
|
||||
jfloat y,
|
||||
jboolean isDown) {
|
||||
UI::SetPointerPosition(x, y);
|
||||
UI::SetPointerDown(isDown == JNI_TRUE);
|
||||
}
|
||||
|
||||
extern "C" JNIEXPORT void JNICALL
|
||||
Java_com_example_jnicpp_MainActivity_cleanupGL(
|
||||
JNIEnv* env,
|
||||
jobject /* this */) {
|
||||
LOGI("cleanupGL called");
|
||||
if (g_renderer != nullptr) {
|
||||
g_renderer->cleanup();
|
||||
delete g_renderer;
|
||||
g_renderer = nullptr;
|
||||
LOGI("GLRenderer cleaned up and deleted");
|
||||
} else {
|
||||
LOGI("GLRenderer was already null");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,347 @@
|
||||
#include "GLRenderer.h"
|
||||
#include "UIRenderer.h"
|
||||
#include "GameStateManager.h"
|
||||
#include <cstring>
|
||||
|
||||
// Define LOG_TAG for this file
|
||||
#define LOG_TAG "GLRenderer"
|
||||
|
||||
GLRenderer::GLRenderer() : program(0), vertexShader(0),
|
||||
fragmentShader(0), vbo(0), vao(0)
|
||||
#ifdef PLATFORM_WINDOWS
|
||||
, window(nullptr)
|
||||
#endif
|
||||
{
|
||||
LOGI("GLRenderer constructor called");
|
||||
}
|
||||
|
||||
GLRenderer::~GLRenderer() {
|
||||
LOGI("GLRenderer destructor called");
|
||||
cleanup();
|
||||
}
|
||||
|
||||
bool GLRenderer::initialize() {
|
||||
LOGI("Initializing OpenGL ES renderer");
|
||||
|
||||
#ifdef PLATFORM_WINDOWS
|
||||
if (!initializeGLFW()) {
|
||||
LOGE("Failed to initialize GLFW");
|
||||
return false;
|
||||
}
|
||||
#endif
|
||||
|
||||
if (!createShaders()) {
|
||||
LOGE("Failed to create shaders");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!createBuffers()) {
|
||||
LOGE("Failed to create buffers");
|
||||
return false;
|
||||
}
|
||||
|
||||
GameStateManager::Instance().RequestStateChange(StateID::MainMenu);
|
||||
|
||||
LOGI("OpenGL ES renderer initialized successfully");
|
||||
return true;
|
||||
}
|
||||
|
||||
#ifdef PLATFORM_WINDOWS
|
||||
bool GLRenderer::initializeGLFW() {
|
||||
LOGI("Initializing GLFW");
|
||||
|
||||
// Initialize GLFW
|
||||
if (!glfwInit()) {
|
||||
LOGE("Failed to initialize GLFW");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Configure GLFW for OpenGL ES
|
||||
glfwWindowHint(GLFW_CLIENT_API, GLFW_OPENGL_ES_API);
|
||||
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
|
||||
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 0);
|
||||
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
|
||||
|
||||
// Create window. Tall enough that the menu layout (which also has to
|
||||
// clear a ~350px safe-top margin on Android, see GameStateManager
|
||||
// usage in render()) fits without buttons running off the bottom.
|
||||
window = glfwCreateWindow(900, 1000, "OpenGL ES 3.0 Triangle Demo", nullptr, nullptr);
|
||||
if (!window) {
|
||||
LOGE("Failed to create GLFW window");
|
||||
glfwTerminate();
|
||||
return false;
|
||||
}
|
||||
|
||||
glfwMakeContextCurrent(window);
|
||||
|
||||
// Initialize GLEW
|
||||
glewExperimental = GL_TRUE;
|
||||
if (glewInit() != GLEW_OK) {
|
||||
LOGE("Failed to initialize GLEW");
|
||||
return false;
|
||||
}
|
||||
|
||||
glViewport(0, 0, 900, 1000);
|
||||
UI::Init(900, 1000);
|
||||
|
||||
// Forward mouse input to the UI system so UI::WasClicked() works.
|
||||
glfwSetCursorPosCallback(window, [](GLFWwindow*, double x, double y) {
|
||||
UI::SetPointerPosition(static_cast<float>(x), static_cast<float>(y));
|
||||
});
|
||||
glfwSetMouseButtonCallback(window, [](GLFWwindow*, int button, int action, int /*mods*/) {
|
||||
if (button == GLFW_MOUSE_BUTTON_LEFT) {
|
||||
UI::SetPointerDown(action == GLFW_PRESS);
|
||||
}
|
||||
});
|
||||
glfwSetFramebufferSizeCallback(window, [](GLFWwindow*, int width, int height) {
|
||||
glViewport(0, 0, width, height);
|
||||
UI::OnScreenResize(width, height);
|
||||
});
|
||||
|
||||
LOGI("GLFW initialized successfully");
|
||||
return true;
|
||||
}
|
||||
|
||||
void GLRenderer::setWindow(GLFWwindow* win) {
|
||||
window = win;
|
||||
}
|
||||
#endif
|
||||
|
||||
bool GLRenderer::createShaders() {
|
||||
LOGI("Creating shaders");
|
||||
|
||||
// Determine OpenGL version
|
||||
const char* version = (const char*)glGetString(GL_VERSION);
|
||||
LOGI("OpenGL version: %s", version ? version : "unknown");
|
||||
|
||||
bool useOpenGLES3 = false;
|
||||
if (version && strstr(version, "OpenGL ES 3")) {
|
||||
useOpenGLES3 = true;
|
||||
LOGI("Using OpenGL ES 3.0 shaders");
|
||||
} else {
|
||||
LOGI("Using OpenGL ES 2.0 shaders");
|
||||
}
|
||||
|
||||
// Choose appropriate shader sources
|
||||
const char* vsSource = useOpenGLES3 ? vertexShaderSource : vertexShaderSource2;
|
||||
const char* fsSource = useOpenGLES3 ? fragmentShaderSource : fragmentShaderSource2;
|
||||
|
||||
// Create vertex shader
|
||||
vertexShader = glCreateShader(GL_VERTEX_SHADER);
|
||||
if (vertexShader == 0) {
|
||||
LOGE("Failed to create vertex shader");
|
||||
return false;
|
||||
}
|
||||
|
||||
glShaderSource(vertexShader, 1, &vsSource, nullptr);
|
||||
glCompileShader(vertexShader);
|
||||
|
||||
// Check vertex shader compilation
|
||||
GLint success;
|
||||
glGetShaderiv(vertexShader, GL_COMPILE_STATUS, &success);
|
||||
if (!success) {
|
||||
GLchar infoLog[512];
|
||||
glGetShaderInfoLog(vertexShader, 512, nullptr, infoLog);
|
||||
LOGE("Vertex shader compilation failed: %s", infoLog);
|
||||
return false;
|
||||
}
|
||||
LOGI("Vertex shader compiled successfully");
|
||||
|
||||
// Create fragment shader
|
||||
fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
|
||||
if (fragmentShader == 0) {
|
||||
LOGE("Failed to create fragment shader");
|
||||
return false;
|
||||
}
|
||||
|
||||
glShaderSource(fragmentShader, 1, &fsSource, nullptr);
|
||||
glCompileShader(fragmentShader);
|
||||
|
||||
// Check fragment shader compilation
|
||||
glGetShaderiv(fragmentShader, GL_COMPILE_STATUS, &success);
|
||||
if (!success) {
|
||||
GLchar infoLog[512];
|
||||
glGetShaderInfoLog(fragmentShader, 512, nullptr, infoLog);
|
||||
LOGE("Fragment shader compilation failed: %s", infoLog);
|
||||
return false;
|
||||
}
|
||||
LOGI("Fragment shader compiled successfully");
|
||||
|
||||
// Create shader program
|
||||
program = glCreateProgram();
|
||||
if (program == 0) {
|
||||
LOGE("Failed to create shader program");
|
||||
return false;
|
||||
}
|
||||
|
||||
glAttachShader(program, vertexShader);
|
||||
glAttachShader(program, fragmentShader);
|
||||
glLinkProgram(program);
|
||||
|
||||
// Check program linking
|
||||
glGetProgramiv(program, GL_LINK_STATUS, &success);
|
||||
if (!success) {
|
||||
GLchar infoLog[512];
|
||||
glGetProgramInfoLog(program, 512, nullptr, infoLog);
|
||||
LOGE("Shader program linking failed: %s", infoLog);
|
||||
return false;
|
||||
}
|
||||
LOGI("Shader program linked successfully");
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool GLRenderer::createBuffers() {
|
||||
LOGI("Creating buffers");
|
||||
|
||||
// Triangle vertices (position + color)
|
||||
float vertices[] = {
|
||||
// Position (x, y, z) // Color (r, g, b)
|
||||
0.0f, 0.5f, 0.0f, 1.0f, 0.0f, 0.0f, // Top vertex (red)
|
||||
-0.5f, -0.5f, 0.0f, 0.0f, 1.0f, 0.0f, // Bottom left (green)
|
||||
0.5f, -0.5f, 0.0f, 0.0f, 0.0f, 1.0f // Bottom right (blue)
|
||||
};
|
||||
|
||||
// Check if VAOs are supported (OpenGL ES 3.0+)
|
||||
const char* version = (const char*)glGetString(GL_VERSION);
|
||||
bool useVAO = (version && strstr(version, "OpenGL ES 3"));
|
||||
|
||||
if (useVAO) {
|
||||
// Create VAO (OpenGL ES 3.0+)
|
||||
glGenVertexArrays(1, &vao);
|
||||
if (vao == 0) {
|
||||
LOGE("Failed to create VAO");
|
||||
return false;
|
||||
}
|
||||
glBindVertexArray(vao);
|
||||
LOGI("Using VAO for vertex attributes");
|
||||
} else {
|
||||
LOGI("VAO not supported, using direct attribute binding");
|
||||
}
|
||||
|
||||
// Create VBO
|
||||
glGenBuffers(1, &vbo);
|
||||
if (vbo == 0) {
|
||||
LOGE("Failed to create VBO");
|
||||
return false;
|
||||
}
|
||||
glBindBuffer(GL_ARRAY_BUFFER, vbo);
|
||||
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
|
||||
|
||||
if (useVAO) {
|
||||
// Position attribute
|
||||
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), nullptr);
|
||||
glEnableVertexAttribArray(0);
|
||||
|
||||
// Color attribute
|
||||
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)(3 * sizeof(float)));
|
||||
glEnableVertexAttribArray(1);
|
||||
|
||||
// Unbind
|
||||
glBindBuffer(GL_ARRAY_BUFFER, 0);
|
||||
glBindVertexArray(0);
|
||||
} else {
|
||||
// For OpenGL ES 2.0, we'll bind attributes in render function
|
||||
LOGI("Will bind attributes in render function for OpenGL ES 2.0");
|
||||
}
|
||||
|
||||
LOGI("Buffers created successfully");
|
||||
return true;
|
||||
}
|
||||
|
||||
void GLRenderer::render() const {
|
||||
#ifdef PLATFORM_WINDOWS
|
||||
// Poll input first so this frame's Update()/Render() see clicks that
|
||||
// just came in, rather than acting on them a frame late (previously
|
||||
// this ran after UI::EndFrame() below, so every click was consumed one
|
||||
// frame after it happened).
|
||||
if (window) {
|
||||
glfwPollEvents();
|
||||
}
|
||||
#endif
|
||||
|
||||
// Clear the screen
|
||||
glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
|
||||
glClear(GL_COLOR_BUFFER_BIT);
|
||||
|
||||
// Use shader program
|
||||
glUseProgram(program);
|
||||
|
||||
// Check if VAOs are supported
|
||||
const char* version = (const char*)glGetString(GL_VERSION);
|
||||
bool useVAO = (version && strstr(version, "OpenGL ES 3"));
|
||||
|
||||
if (useVAO) {
|
||||
// Bind VAO and draw (OpenGL ES 3.0+)
|
||||
glBindVertexArray(vao);
|
||||
glDrawArrays(GL_TRIANGLES, 0, 3);
|
||||
} else {
|
||||
// For OpenGL ES 2.0, bind attributes manually
|
||||
glBindBuffer(GL_ARRAY_BUFFER, vbo);
|
||||
|
||||
// Position attribute
|
||||
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), nullptr);
|
||||
glEnableVertexAttribArray(0);
|
||||
|
||||
// Color attribute
|
||||
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)(3 * sizeof(float)));
|
||||
glEnableVertexAttribArray(1);
|
||||
|
||||
glDrawArrays(GL_TRIANGLES, 0, 3);
|
||||
}
|
||||
|
||||
// Drive and draw the active game state on top of the scene. This runs
|
||||
// every frame on both Windows and Android, since both call into this
|
||||
// same render() function. States themselves are responsible for
|
||||
// keeping top-anchored UI below ~350px - on Android this app is
|
||||
// edge-to-edge (targetSdk 36), and the ActionBar is drawn as an overlay
|
||||
// on top of the GLSurfaceView rather than pushing it down, so anything
|
||||
// anchored near y=0 renders underneath it and is invisible.
|
||||
GameStateManager::Instance().Update();
|
||||
GameStateManager::Instance().Render();
|
||||
|
||||
// Marks the click event (if any) as consumed for this frame - call
|
||||
// after all UI draw/hit-test calls above.
|
||||
UI::EndFrame();
|
||||
|
||||
#ifdef PLATFORM_WINDOWS
|
||||
// Swap buffers for Windows (input already polled at the top of this
|
||||
// function).
|
||||
if (window) {
|
||||
glfwSwapBuffers(window);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
void GLRenderer::cleanup() {
|
||||
LOGI("Cleaning up OpenGL resources");
|
||||
|
||||
UI::Shutdown();
|
||||
|
||||
// Clean up OpenGL resources
|
||||
if (program != 0) {
|
||||
glDeleteProgram(program);
|
||||
}
|
||||
if (vertexShader != 0) {
|
||||
glDeleteShader(vertexShader);
|
||||
}
|
||||
if (fragmentShader != 0) {
|
||||
glDeleteShader(fragmentShader);
|
||||
}
|
||||
if (vbo != 0) {
|
||||
glDeleteBuffers(1, &vbo);
|
||||
}
|
||||
if (vao != 0) {
|
||||
glDeleteVertexArrays(1, &vao);
|
||||
}
|
||||
|
||||
#ifdef PLATFORM_WINDOWS
|
||||
if (window) {
|
||||
glfwDestroyWindow(window);
|
||||
window = nullptr;
|
||||
}
|
||||
glfwTerminate();
|
||||
#endif
|
||||
|
||||
LOGI("OpenGL resources cleaned up");
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
#ifndef GLRENDERER_H
|
||||
#define GLRENDERER_H
|
||||
|
||||
#include "Platform.h"
|
||||
|
||||
class GLRenderer {
|
||||
public:
|
||||
GLRenderer();
|
||||
~GLRenderer();
|
||||
|
||||
bool initialize();
|
||||
void render() const;
|
||||
void cleanup();
|
||||
|
||||
#ifdef PLATFORM_WINDOWS
|
||||
// Windows-specific methods
|
||||
bool initializeGLFW();
|
||||
void setWindow(GLFWwindow* window);
|
||||
GLFWwindow* getWindow() const { return window; }
|
||||
#endif
|
||||
|
||||
private:
|
||||
bool createShaders();
|
||||
bool createBuffers();
|
||||
|
||||
// OpenGL variables
|
||||
GLuint program;
|
||||
GLuint vertexShader;
|
||||
GLuint fragmentShader;
|
||||
GLuint vbo;
|
||||
GLuint vao;
|
||||
|
||||
#ifdef PLATFORM_WINDOWS
|
||||
GLFWwindow* window;
|
||||
#endif
|
||||
|
||||
// Shader source code - compatible with both OpenGL ES 2.0 and 3.0
|
||||
const char* vertexShaderSource = R"(#version 300 es
|
||||
layout(location = 0) in vec3 position;
|
||||
layout(location = 1) in vec3 color;
|
||||
out vec3 fragColor;
|
||||
void main() {
|
||||
gl_Position = vec4(position, 1.0);
|
||||
fragColor = color;
|
||||
})";
|
||||
|
||||
const char* fragmentShaderSource = R"(#version 300 es
|
||||
precision mediump float;
|
||||
in vec3 fragColor;
|
||||
out vec4 outColor;
|
||||
void main() {
|
||||
outColor = vec4(fragColor, 1.0);
|
||||
})";
|
||||
|
||||
// Fallback shaders for OpenGL ES 2.0
|
||||
const char* vertexShaderSource2 = R"(attribute vec3 position;
|
||||
attribute vec3 color;
|
||||
varying vec3 fragColor;
|
||||
void main() {
|
||||
gl_Position = vec4(position, 1.0);
|
||||
fragColor = color;
|
||||
})";
|
||||
|
||||
const char* fragmentShaderSource2 = R"(precision mediump float;
|
||||
varying vec3 fragColor;
|
||||
void main() {
|
||||
gl_FragColor = vec4(fragColor, 1.0);
|
||||
})";
|
||||
};
|
||||
|
||||
#endif // GLRENDERER_H
|
||||
@@ -0,0 +1,31 @@
|
||||
#ifndef GAME_STATE_H
|
||||
#define GAME_STATE_H
|
||||
|
||||
enum class StateID {
|
||||
MainMenu,
|
||||
Settings,
|
||||
Menu1,
|
||||
Menu2,
|
||||
Menu3
|
||||
};
|
||||
|
||||
// Base interface every game state implements. GameStateManager owns exactly
|
||||
// one active GameState at a time and drives it each frame.
|
||||
class GameState {
|
||||
public:
|
||||
virtual ~GameState() = default;
|
||||
|
||||
// Called once when the manager switches to this state.
|
||||
virtual void Enter() {}
|
||||
|
||||
// Called once when the manager switches away from this state.
|
||||
virtual void Exit() {}
|
||||
|
||||
// Called once per frame, before Render().
|
||||
virtual void Update() {}
|
||||
|
||||
// Called once per frame to draw this state's menu/UI.
|
||||
virtual void Render() = 0;
|
||||
};
|
||||
|
||||
#endif // GAME_STATE_H
|
||||
@@ -0,0 +1,42 @@
|
||||
#include "GameStateManager.h"
|
||||
#include "StateFactories.h"
|
||||
|
||||
GameStateManager& GameStateManager::Instance() {
|
||||
static GameStateManager instance;
|
||||
return instance;
|
||||
}
|
||||
|
||||
void GameStateManager::RequestStateChange(StateID id) {
|
||||
hasPendingChange = true;
|
||||
pendingState = id;
|
||||
}
|
||||
|
||||
void GameStateManager::Update() {
|
||||
if (hasPendingChange) {
|
||||
hasPendingChange = false;
|
||||
|
||||
if (currentState) {
|
||||
currentState->Exit();
|
||||
}
|
||||
|
||||
switch (pendingState) {
|
||||
case StateID::MainMenu: currentState = CreateMainMenuState(); break;
|
||||
case StateID::Settings: currentState = CreateSettingsState(); break;
|
||||
case StateID::Menu1: currentState = CreateMenu1State(); break;
|
||||
case StateID::Menu2: currentState = CreateMenu2State(); break;
|
||||
case StateID::Menu3: currentState = CreateMenu3State(); break;
|
||||
}
|
||||
|
||||
currentState->Enter();
|
||||
}
|
||||
|
||||
if (currentState) {
|
||||
currentState->Update();
|
||||
}
|
||||
}
|
||||
|
||||
void GameStateManager::Render() {
|
||||
if (currentState) {
|
||||
currentState->Render();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
#ifndef GAME_STATE_MANAGER_H
|
||||
#define GAME_STATE_MANAGER_H
|
||||
|
||||
#include <memory>
|
||||
#include "GameState.h"
|
||||
|
||||
class GameStateManager {
|
||||
public:
|
||||
static GameStateManager& Instance();
|
||||
|
||||
// Queues a switch to `id`, applied at the start of the next Update().
|
||||
// Deferred rather than immediate so a state can safely request its own
|
||||
// replacement from inside a button click handled during Render().
|
||||
void RequestStateChange(StateID id);
|
||||
|
||||
void Update();
|
||||
void Render();
|
||||
|
||||
private:
|
||||
GameStateManager() = default;
|
||||
|
||||
std::unique_ptr<GameState> currentState;
|
||||
bool hasPendingChange = false;
|
||||
StateID pendingState = StateID::MainMenu;
|
||||
};
|
||||
|
||||
#endif // GAME_STATE_MANAGER_H
|
||||
@@ -0,0 +1,32 @@
|
||||
#ifndef PLATFORM_H
|
||||
#define PLATFORM_H
|
||||
// Platform detection
|
||||
#ifdef _WIN32
|
||||
#define PLATFORM_WINDOWS
|
||||
#elif defined(__ANDROID__)
|
||||
#define PLATFORM_ANDROID
|
||||
#endif
|
||||
// Platform-specific includes
|
||||
#ifdef PLATFORM_WINDOWS
|
||||
#include <GL/glew.h>
|
||||
#include <GLFW/glfw3.h>
|
||||
#include <iostream>
|
||||
#include <cstring>
|
||||
// Windows logging macros
|
||||
#include <cstdio>
|
||||
#define LOGI(...) printf("[INFO] " __VA_ARGS__); printf("\n")
|
||||
#define LOGE(...) printf("[ERROR] " __VA_ARGS__); printf("\n")
|
||||
#define LOGD(...) printf("[DEBUG] " __VA_ARGS__); printf("\n")
|
||||
#elif defined(PLATFORM_ANDROID)
|
||||
#include <GLES3/gl3.h>
|
||||
#include <android/log.h>
|
||||
#include <cstring>
|
||||
|
||||
// Android logging macros (LOG_TAG should be defined in each source file)
|
||||
#ifndef LOGI
|
||||
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)
|
||||
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)
|
||||
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__)
|
||||
#endif
|
||||
#endif
|
||||
#endif // PLATFORM_H
|
||||
@@ -0,0 +1,77 @@
|
||||
#include "PlatformBridge.h"
|
||||
|
||||
#define LOG_TAG "PlatformBridge"
|
||||
|
||||
#ifdef PLATFORM_ANDROID
|
||||
|
||||
namespace {
|
||||
JavaVM* g_jvm = nullptr;
|
||||
// Global ref so the MainActivity instance stays valid for as long as
|
||||
// this bridge might need to call back into it - local/weak refs from
|
||||
// the original initGL() call wouldn't survive past that call.
|
||||
jobject g_activityRef = nullptr;
|
||||
}
|
||||
|
||||
namespace PlatformBridge {
|
||||
|
||||
void SetAndroidActivity(JNIEnv* env, jobject activity) {
|
||||
if (env->GetJavaVM(&g_jvm) != JNI_OK) {
|
||||
LOGE("SetAndroidActivity: failed to cache JavaVM");
|
||||
g_jvm = nullptr;
|
||||
return;
|
||||
}
|
||||
if (g_activityRef != nullptr) {
|
||||
env->DeleteGlobalRef(g_activityRef);
|
||||
}
|
||||
g_activityRef = env->NewGlobalRef(activity);
|
||||
LOGI("SetAndroidActivity: MainActivity reference cached");
|
||||
}
|
||||
|
||||
void LaunchBowlingCamera() {
|
||||
if (g_jvm == nullptr || g_activityRef == nullptr) {
|
||||
LOGE("LaunchBowlingCamera: Android activity not set up yet (SetAndroidActivity never called?)");
|
||||
return;
|
||||
}
|
||||
|
||||
JNIEnv* env = nullptr;
|
||||
// This is only ever called from a thread Java originally called into
|
||||
// native from (the GL render thread, via
|
||||
// GameStateManager::Update() -> Menu3State::Enter()), so it's already
|
||||
// attached to the JVM and GetEnv should always succeed without needing
|
||||
// AttachCurrentThread.
|
||||
if (g_jvm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6) != JNI_OK || env == nullptr) {
|
||||
LOGE("LaunchBowlingCamera: current thread is not attached to the JVM");
|
||||
return;
|
||||
}
|
||||
|
||||
jclass activityClass = env->GetObjectClass(g_activityRef);
|
||||
jmethodID launchMethod = env->GetMethodID(activityClass, "launchBowlingCamera", "()V");
|
||||
env->DeleteLocalRef(activityClass);
|
||||
|
||||
if (launchMethod == nullptr) {
|
||||
LOGE("LaunchBowlingCamera: MainActivity.launchBowlingCamera() not found");
|
||||
env->ExceptionClear();
|
||||
return;
|
||||
}
|
||||
|
||||
env->CallVoidMethod(g_activityRef, launchMethod);
|
||||
if (env->ExceptionCheck()) {
|
||||
LOGE("LaunchBowlingCamera: exception thrown while calling into Java");
|
||||
env->ExceptionDescribe();
|
||||
env->ExceptionClear();
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace PlatformBridge
|
||||
|
||||
#else // Desktop (Windows/GLFW) build - no camera feature there yet.
|
||||
|
||||
namespace PlatformBridge {
|
||||
|
||||
void LaunchBowlingCamera() {
|
||||
LOGI("LaunchBowlingCamera: requested, but this platform has no camera feature");
|
||||
}
|
||||
|
||||
} // namespace PlatformBridge
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,35 @@
|
||||
#ifndef PLATFORM_BRIDGE_H
|
||||
#define PLATFORM_BRIDGE_H
|
||||
|
||||
#include "Platform.h"
|
||||
|
||||
#ifdef PLATFORM_ANDROID
|
||||
#include <jni.h>
|
||||
#endif
|
||||
|
||||
// Thin seam between shared game-state code and platform-specific "launch a
|
||||
// native platform feature" hooks, so states like Menu3State don't need to
|
||||
// know or care whether they're running on Android or the Windows/GLFW
|
||||
// build.
|
||||
namespace PlatformBridge {
|
||||
|
||||
#ifdef PLATFORM_ANDROID
|
||||
// Called once from NativeTemplate.cpp's initGL() JNI entry. Caches a
|
||||
// JavaVM + a global ref to the MainActivity instance so LaunchBowlingCamera()
|
||||
// can call back into Java later from whatever thread it's invoked on
|
||||
// (in practice, the GL render thread that drives GameStateManager -
|
||||
// see GLRenderer::render()).
|
||||
void SetAndroidActivity(JNIEnv* env, jobject activity);
|
||||
#endif
|
||||
|
||||
// Requests that the host platform open the bowling camera + pose
|
||||
// detection screen. On Android this launches BowlingCameraActivity via
|
||||
// a JNI callback into MainActivity; front/back camera switching happens
|
||||
// on that screen itself, not here. On Windows there's no camera feature
|
||||
// (the desktop build has no CameraX/ML Kit equivalent), so this just
|
||||
// logs the request.
|
||||
void LaunchBowlingCamera();
|
||||
|
||||
} // namespace PlatformBridge
|
||||
|
||||
#endif // PLATFORM_BRIDGE_H
|
||||
@@ -0,0 +1,16 @@
|
||||
#ifndef STATE_FACTORIES_H
|
||||
#define STATE_FACTORIES_H
|
||||
|
||||
#include <memory>
|
||||
#include "GameState.h"
|
||||
|
||||
// Each state's .cpp defines its class privately (in an anonymous namespace)
|
||||
// and exposes only its factory function here, so GameStateManager doesn't
|
||||
// need every state's full class declaration - just these five functions.
|
||||
std::unique_ptr<GameState> CreateMainMenuState();
|
||||
std::unique_ptr<GameState> CreateSettingsState();
|
||||
std::unique_ptr<GameState> CreateMenu1State();
|
||||
std::unique_ptr<GameState> CreateMenu2State();
|
||||
std::unique_ptr<GameState> CreateMenu3State();
|
||||
|
||||
#endif // STATE_FACTORIES_H
|
||||
@@ -0,0 +1,473 @@
|
||||
#include "UIRenderer.h"
|
||||
#define LOG_TAG "UIRenderer"
|
||||
|
||||
#include <vector>
|
||||
#include <cctype>
|
||||
#include <cmath>
|
||||
#include <algorithm>
|
||||
#include <mutex>
|
||||
|
||||
namespace UI {
|
||||
|
||||
namespace {
|
||||
|
||||
int s_screenWidth = 1;
|
||||
int s_screenHeight = 1;
|
||||
bool s_initialized = false;
|
||||
|
||||
GLuint s_lineProgram = 0;
|
||||
GLuint s_lineVBO = 0;
|
||||
GLuint s_lineVAO = 0;
|
||||
GLint s_lineScreenSizeLoc = -1;
|
||||
GLint s_lineColorLoc = -1;
|
||||
|
||||
GLuint s_imageProgram = 0;
|
||||
GLuint s_imageVBO = 0;
|
||||
GLuint s_imageVAO = 0;
|
||||
GLint s_imageScreenSizeLoc = -1;
|
||||
|
||||
// Guards the pointer/click state below. On Android, touch events arrive
|
||||
// on the app's UI thread (MainActivity.onTouchEvent -> nativeOnTouch),
|
||||
// while Update()/Render()/EndFrame() run on GLSurfaceView's own
|
||||
// dedicated GL rendering thread - without this lock, those two threads
|
||||
// read/write this state unsynchronized, which can drop a click
|
||||
// (in-flight write raced by EndFrame's clear) or leave s_clickPos
|
||||
// holding a torn mix of an old/new touch position. Windows doesn't hit
|
||||
// this: GLFW callbacks fire from glfwPollEvents() on the same thread
|
||||
// that runs Update()/Render(), so there's nothing to race there.
|
||||
std::mutex s_inputMutex;
|
||||
Vec2 s_pointerPos{};
|
||||
bool s_pointerIsDown = false;
|
||||
bool s_pointerWasDown = false;
|
||||
bool s_clickPending = false;
|
||||
Vec2 s_clickPos{};
|
||||
|
||||
const char* kLineVS = R"(#version 300 es
|
||||
layout(location = 0) in vec2 aPos;
|
||||
uniform vec2 uScreenSize;
|
||||
void main() {
|
||||
vec2 ndc = vec2((aPos.x / uScreenSize.x) * 2.0 - 1.0,
|
||||
1.0 - (aPos.y / uScreenSize.y) * 2.0);
|
||||
gl_Position = vec4(ndc, 0.0, 1.0);
|
||||
})";
|
||||
|
||||
const char* kLineFS = R"(#version 300 es
|
||||
precision mediump float;
|
||||
out vec4 outColor;
|
||||
uniform vec4 uColor;
|
||||
void main() { outColor = uColor; })";
|
||||
|
||||
const char* kImageVS = R"(#version 300 es
|
||||
layout(location = 0) in vec2 aPos;
|
||||
layout(location = 1) in vec2 aUV;
|
||||
out vec2 vUV;
|
||||
uniform vec2 uScreenSize;
|
||||
void main() {
|
||||
vec2 ndc = vec2((aPos.x / uScreenSize.x) * 2.0 - 1.0,
|
||||
1.0 - (aPos.y / uScreenSize.y) * 2.0);
|
||||
gl_Position = vec4(ndc, 0.0, 1.0);
|
||||
vUV = aUV;
|
||||
})";
|
||||
|
||||
const char* kImageFS = R"(#version 300 es
|
||||
precision mediump float;
|
||||
in vec2 vUV;
|
||||
out vec4 outColor;
|
||||
uniform sampler2D uTexture;
|
||||
void main() { outColor = texture(uTexture, vUV); })";
|
||||
|
||||
GLuint CompileShader(GLenum type, const char* source) {
|
||||
GLuint shader = glCreateShader(type);
|
||||
glShaderSource(shader, 1, &source, nullptr);
|
||||
glCompileShader(shader);
|
||||
GLint success = 0;
|
||||
glGetShaderiv(shader, GL_COMPILE_STATUS, &success);
|
||||
if (!success) {
|
||||
GLchar infoLog[512];
|
||||
glGetShaderInfoLog(shader, 512, nullptr, infoLog);
|
||||
LOGE("UI shader compilation failed: %s", infoLog);
|
||||
glDeleteShader(shader);
|
||||
return 0;
|
||||
}
|
||||
return shader;
|
||||
}
|
||||
|
||||
GLuint LinkProgram(const char* vsSrc, const char* fsSrc) {
|
||||
GLuint vs = CompileShader(GL_VERTEX_SHADER, vsSrc);
|
||||
GLuint fs = CompileShader(GL_FRAGMENT_SHADER, fsSrc);
|
||||
if (vs == 0 || fs == 0) {
|
||||
return 0;
|
||||
}
|
||||
GLuint program = glCreateProgram();
|
||||
glAttachShader(program, vs);
|
||||
glAttachShader(program, fs);
|
||||
glLinkProgram(program);
|
||||
GLint success = 0;
|
||||
glGetProgramiv(program, GL_LINK_STATUS, &success);
|
||||
if (!success) {
|
||||
GLchar infoLog[512];
|
||||
glGetProgramInfoLog(program, 512, nullptr, infoLog);
|
||||
LOGE("UI program link failed: %s", infoLog);
|
||||
glDeleteProgram(program);
|
||||
program = 0;
|
||||
}
|
||||
glDeleteShader(vs);
|
||||
glDeleteShader(fs);
|
||||
return program;
|
||||
}
|
||||
|
||||
// Built-in vector font: each glyph is a handful of line segments in a
|
||||
// normalized 0..1 (x) by 0..1 (y) cell, origin bottom-left. Covers
|
||||
// digits, A-Z and common punctuation - no font file required.
|
||||
void AppendGlyphSegments(std::vector<float>& verts, char c, float penX, float topY, float w, float h) {
|
||||
constexpr float L = 0.0f, R = 1.0f, B = 0.0f, T = 1.0f, M = 0.5f;
|
||||
|
||||
// Stroke half-thickness in screen pixels. Kept comfortably wide
|
||||
// (rather than a hairline) since very thin quads were observed to
|
||||
// drop out during rasterization on some GLES driver/emulator
|
||||
// combinations at high framebuffer resolutions.
|
||||
const float strokeHalf = std::max(h * 0.08f, 4.0f);
|
||||
|
||||
auto seg = [&](float x0, float y0, float x1, float y1) {
|
||||
float sx0 = penX + x0 * w;
|
||||
float sy0 = topY + (1.0f - y0) * h;
|
||||
float sx1 = penX + x1 * w;
|
||||
float sy1 = topY + (1.0f - y1) * h;
|
||||
|
||||
float dx = sx1 - sx0;
|
||||
float dy = sy1 - sy0;
|
||||
float len = std::sqrt(dx * dx + dy * dy);
|
||||
float nx = 0.0f, ny = strokeHalf;
|
||||
if (len > 0.0001f) {
|
||||
nx = -dy / len * strokeHalf;
|
||||
ny = dx / len * strokeHalf;
|
||||
}
|
||||
|
||||
float ax = sx0 + nx, ay = sy0 + ny;
|
||||
float bx = sx0 - nx, by = sy0 - ny;
|
||||
float cx = sx1 + nx, cy = sy1 + ny;
|
||||
float dxp = sx1 - nx, dyp = sy1 - ny;
|
||||
|
||||
float quad[] = {
|
||||
ax, ay, bx, by, cx, cy,
|
||||
cx, cy, bx, by, dxp, dyp,
|
||||
};
|
||||
verts.insert(verts.end(), std::begin(quad), std::end(quad));
|
||||
};
|
||||
|
||||
switch (std::toupper(static_cast<unsigned char>(c))) {
|
||||
case '0': seg(L,T,R,T); seg(R,T,R,B); seg(R,B,L,B); seg(L,B,L,T); seg(L,B,R,T); break;
|
||||
case '1': seg(M,T,M,B); seg(L,B,R,B); break;
|
||||
case '2': seg(L,T,R,T); seg(R,T,R,M); seg(R,M,M,M); seg(M,M,L,B); seg(L,B,R,B); break;
|
||||
case '3': seg(L,T,R,T); seg(R,T,R,B); seg(L,B,R,B); seg(L,M,R,M); break;
|
||||
case '4': seg(L,T,L,M); seg(L,M,R,M); seg(R,T,R,B); break;
|
||||
case '5': seg(R,T,L,T); seg(L,T,L,M); seg(L,M,R,M); seg(R,M,R,B); seg(R,B,L,B); break;
|
||||
case '6': seg(R,T,L,T); seg(L,T,L,B); seg(L,B,R,B); seg(R,B,R,M); seg(R,M,L,M); break;
|
||||
case '7': seg(L,T,R,T); seg(R,T,M,B); break;
|
||||
case '8': seg(L,T,R,T); seg(R,T,R,B); seg(R,B,L,B); seg(L,B,L,T); seg(L,M,R,M); break;
|
||||
case '9': seg(L,B,R,B); seg(R,B,R,T); seg(R,T,L,T); seg(L,T,L,M); seg(L,M,R,M); break;
|
||||
|
||||
case 'A': seg(L,B,M,T); seg(M,T,R,B); seg(L,M,R,M); break;
|
||||
case 'B': seg(L,T,L,B); seg(L,T,R,T); seg(R,T,R,M); seg(L,M,R,M); seg(R,M,R,B); seg(L,B,R,B); break;
|
||||
case 'C': seg(R,T,L,T); seg(L,T,L,B); seg(L,B,R,B); break;
|
||||
case 'D': seg(L,T,L,B); seg(L,T,R,T); seg(R,T,R,B); seg(L,B,R,B); break;
|
||||
case 'E': seg(R,T,L,T); seg(L,T,L,B); seg(L,B,R,B); seg(L,M,R,M); break;
|
||||
case 'F': seg(R,T,L,T); seg(L,T,L,B); seg(L,M,R,M); break;
|
||||
case 'G': seg(R,T,L,T); seg(L,T,L,B); seg(L,B,R,B); seg(R,B,R,M); seg(R,M,M,M); break;
|
||||
case 'H': seg(L,T,L,B); seg(R,T,R,B); seg(L,M,R,M); break;
|
||||
case 'I': seg(L,T,R,T); seg(M,T,M,B); seg(L,B,R,B); break;
|
||||
case 'J': seg(L,T,R,T); seg(R,T,R,B); seg(R,B,L,B); break;
|
||||
case 'K': seg(L,T,L,B); seg(R,T,L,M); seg(L,M,R,B); break;
|
||||
case 'L': seg(L,T,L,B); seg(L,B,R,B); break;
|
||||
case 'M': seg(L,B,L,T); seg(L,T,M,M); seg(M,M,R,T); seg(R,T,R,B); break;
|
||||
case 'N': seg(L,B,L,T); seg(L,T,R,B); seg(R,B,R,T); break;
|
||||
case 'O': seg(L,T,R,T); seg(R,T,R,B); seg(R,B,L,B); seg(L,B,L,T); break;
|
||||
case 'P': seg(L,T,L,B); seg(L,T,R,T); seg(R,T,R,M); seg(R,M,L,M); break;
|
||||
case 'Q': seg(L,T,R,T); seg(R,T,R,B); seg(R,B,L,B); seg(L,B,L,T); seg(M,M,R,B); break;
|
||||
case 'R': seg(L,T,L,B); seg(L,T,R,T); seg(R,T,R,M); seg(R,M,L,M); seg(L,M,R,B); break;
|
||||
case 'S': seg(R,T,L,T); seg(L,T,L,M); seg(L,M,R,M); seg(R,M,R,B); seg(R,B,L,B); break;
|
||||
case 'T': seg(L,T,R,T); seg(M,T,M,B); break;
|
||||
case 'U': seg(L,T,L,B); seg(L,B,R,B); seg(R,B,R,T); break;
|
||||
case 'V': seg(L,T,M,B); seg(M,B,R,T); break;
|
||||
case 'W': seg(L,T,L,B); seg(L,B,M,M); seg(M,M,R,B); seg(R,B,R,T); break;
|
||||
case 'X': seg(L,T,R,B); seg(R,T,L,B); break;
|
||||
case 'Y': seg(L,T,M,M); seg(R,T,M,M); seg(M,M,M,B); break;
|
||||
case 'Z': seg(L,T,R,T); seg(R,T,L,B); seg(L,B,R,B); break;
|
||||
|
||||
case '.': seg(M-0.05f,B,M+0.05f,B); break;
|
||||
case ',': seg(M,B,M-0.15f,B-0.25f); break;
|
||||
case '!': seg(M,T,M,M+0.1f); seg(M,0.12f,M,B); break;
|
||||
case '?': seg(L+0.1f,T,R-0.1f,T); seg(R-0.1f,T,R-0.1f,M); seg(R-0.1f,M,M,M); seg(M,M,M,M-0.15f); seg(M,0.12f,M,B); break;
|
||||
case ':': seg(M,0.65f,M,0.6f); seg(M,0.35f,M,0.3f); break;
|
||||
case '-': seg(L,M,R,M); break;
|
||||
case ' ': default: break; // blank / unsupported characters just advance the pen
|
||||
}
|
||||
}
|
||||
|
||||
// Shared by DrawText and Button so hit-testing always matches what's
|
||||
// actually drawn.
|
||||
float TextTotalWidth(const std::string& text, float textSize) {
|
||||
const float glyphW = textSize * 0.6f;
|
||||
const float advance = glyphW * 1.3f;
|
||||
return advance * static_cast<float>(text.size());
|
||||
}
|
||||
|
||||
float TextStartX(const std::string& text, float textSize, float locationX, TextAlign alignment) {
|
||||
const float totalWidth = TextTotalWidth(text, textSize);
|
||||
if (alignment == TextAlign::Center) return locationX - totalWidth * 0.5f;
|
||||
if (alignment == TextAlign::Right) return locationX - totalWidth;
|
||||
return locationX;
|
||||
}
|
||||
|
||||
// Uploads `verts` (x,y pairs) and draws them as opaque white triangles
|
||||
// via the shared line/shape program. Used by both DrawText and
|
||||
// DrawRectOutline.
|
||||
void DrawWhiteTriangles(const std::vector<float>& verts) {
|
||||
if (verts.empty()) return;
|
||||
|
||||
glUseProgram(s_lineProgram);
|
||||
glUniform2f(s_lineScreenSizeLoc, static_cast<float>(s_screenWidth), static_cast<float>(s_screenHeight));
|
||||
glUniform4f(s_lineColorLoc, 1.0f, 1.0f, 1.0f, 1.0f);
|
||||
|
||||
// Quads here aren't wound consistently (mixed by design, since
|
||||
// strokes/edges run in arbitrary directions), so back-face culling
|
||||
// must be off regardless of whatever state the rest of the app
|
||||
// leaves enabled.
|
||||
glDisable(GL_CULL_FACE);
|
||||
|
||||
glBindVertexArray(s_lineVAO);
|
||||
glBindBuffer(GL_ARRAY_BUFFER, s_lineVBO);
|
||||
glBufferData(GL_ARRAY_BUFFER, verts.size() * sizeof(float), verts.data(), GL_DYNAMIC_DRAW);
|
||||
glDrawArrays(GL_TRIANGLES, 0, static_cast<GLsizei>(verts.size() / 2));
|
||||
glBindVertexArray(0);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool Init(int screenWidth, int screenHeight) {
|
||||
s_screenWidth = screenWidth > 0 ? screenWidth : 1;
|
||||
s_screenHeight = screenHeight > 0 ? screenHeight : 1;
|
||||
|
||||
if (s_initialized) {
|
||||
return true; // already set up - this call was just a resize
|
||||
}
|
||||
|
||||
s_lineProgram = LinkProgram(kLineVS, kLineFS);
|
||||
s_imageProgram = LinkProgram(kImageVS, kImageFS);
|
||||
if (s_lineProgram == 0 || s_imageProgram == 0) {
|
||||
LOGE("UI::Init failed to build shader programs");
|
||||
return false;
|
||||
}
|
||||
|
||||
s_lineScreenSizeLoc = glGetUniformLocation(s_lineProgram, "uScreenSize");
|
||||
s_lineColorLoc = glGetUniformLocation(s_lineProgram, "uColor");
|
||||
s_imageScreenSizeLoc = glGetUniformLocation(s_imageProgram, "uScreenSize");
|
||||
|
||||
glGenVertexArrays(1, &s_lineVAO);
|
||||
glBindVertexArray(s_lineVAO);
|
||||
glGenBuffers(1, &s_lineVBO);
|
||||
glBindBuffer(GL_ARRAY_BUFFER, s_lineVBO);
|
||||
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(float), (void*)0);
|
||||
glEnableVertexAttribArray(0);
|
||||
glBindVertexArray(0);
|
||||
|
||||
glGenVertexArrays(1, &s_imageVAO);
|
||||
glBindVertexArray(s_imageVAO);
|
||||
glGenBuffers(1, &s_imageVBO);
|
||||
glBindBuffer(GL_ARRAY_BUFFER, s_imageVBO);
|
||||
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(float), (void*)0);
|
||||
glEnableVertexAttribArray(0);
|
||||
glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(float), (void*)(2 * sizeof(float)));
|
||||
glEnableVertexAttribArray(1);
|
||||
glBindVertexArray(0);
|
||||
|
||||
s_initialized = true;
|
||||
LOGI("UI renderer initialized (%dx%d)", s_screenWidth, s_screenHeight);
|
||||
return true;
|
||||
}
|
||||
|
||||
void OnScreenResize(int screenWidth, int screenHeight) {
|
||||
s_screenWidth = screenWidth > 0 ? screenWidth : 1;
|
||||
s_screenHeight = screenHeight > 0 ? screenHeight : 1;
|
||||
}
|
||||
|
||||
void Shutdown() {
|
||||
if (!s_initialized) return;
|
||||
glDeleteProgram(s_lineProgram);
|
||||
glDeleteProgram(s_imageProgram);
|
||||
glDeleteBuffers(1, &s_lineVBO);
|
||||
glDeleteBuffers(1, &s_imageVBO);
|
||||
glDeleteVertexArrays(1, &s_lineVAO);
|
||||
glDeleteVertexArrays(1, &s_imageVAO);
|
||||
s_lineProgram = s_imageProgram = s_lineVBO = s_imageVBO = s_lineVAO = s_imageVAO = 0;
|
||||
s_initialized = false;
|
||||
LOGI("UI renderer shut down");
|
||||
}
|
||||
|
||||
void EndFrame() {
|
||||
std::lock_guard<std::mutex> lock(s_inputMutex);
|
||||
s_pointerWasDown = s_pointerIsDown;
|
||||
s_clickPending = false;
|
||||
}
|
||||
|
||||
// ---- Function 1: text -------------------------------------------------
|
||||
|
||||
void DrawText(const std::string& text, float textSize, Vec2 textLocation, TextAlign alignment) {
|
||||
if (!s_initialized || text.empty() || textSize <= 0.0f) return;
|
||||
|
||||
const float glyphW = textSize * 0.6f;
|
||||
const float glyphH = textSize;
|
||||
const float advance = glyphW * 1.3f;
|
||||
const float startX = TextStartX(text, textSize, textLocation.x, alignment);
|
||||
|
||||
std::vector<float> verts;
|
||||
verts.reserve(text.size() * 12);
|
||||
float penX = startX;
|
||||
for (char c : text) {
|
||||
AppendGlyphSegments(verts, c, penX, textLocation.y, glyphW, glyphH);
|
||||
penX += advance;
|
||||
}
|
||||
DrawWhiteTriangles(verts);
|
||||
}
|
||||
|
||||
// ---- Function 2: images -------------------------------------------------
|
||||
|
||||
Texture CreateTexture(const unsigned char* pixelsRGBA, int width, int height) {
|
||||
Texture tex;
|
||||
if (pixelsRGBA == nullptr || width <= 0 || height <= 0) {
|
||||
LOGE("UI::CreateTexture called with invalid pixel data");
|
||||
return tex;
|
||||
}
|
||||
glGenTextures(1, &tex.id);
|
||||
glBindTexture(GL_TEXTURE_2D, tex.id);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
|
||||
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, pixelsRGBA);
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
tex.width = width;
|
||||
tex.height = height;
|
||||
return tex;
|
||||
}
|
||||
|
||||
void DestroyTexture(Texture& texture) {
|
||||
if (texture.id != 0) {
|
||||
glDeleteTextures(1, &texture.id);
|
||||
texture = Texture{};
|
||||
}
|
||||
}
|
||||
|
||||
void DrawImage(const Texture& image, Vec2 locationOfImage, Vec2 sizeOfImage) {
|
||||
if (!s_initialized || image.id == 0) return;
|
||||
|
||||
const float x0 = locationOfImage.x;
|
||||
const float y0 = locationOfImage.y;
|
||||
const float x1 = locationOfImage.x + sizeOfImage.x;
|
||||
const float y1 = locationOfImage.y + sizeOfImage.y;
|
||||
|
||||
// pos.x, pos.y, uv.x, uv.y - two triangles
|
||||
float verts[] = {
|
||||
x0, y0, 0.0f, 0.0f,
|
||||
x0, y1, 0.0f, 1.0f,
|
||||
x1, y0, 1.0f, 0.0f,
|
||||
|
||||
x1, y0, 1.0f, 0.0f,
|
||||
x0, y1, 0.0f, 1.0f,
|
||||
x1, y1, 1.0f, 1.0f,
|
||||
};
|
||||
|
||||
glUseProgram(s_imageProgram);
|
||||
glUniform2f(s_imageScreenSizeLoc, static_cast<float>(s_screenWidth), static_cast<float>(s_screenHeight));
|
||||
glDisable(GL_CULL_FACE);
|
||||
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
glBindTexture(GL_TEXTURE_2D, image.id);
|
||||
|
||||
glEnable(GL_BLEND);
|
||||
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
|
||||
|
||||
glBindVertexArray(s_imageVAO);
|
||||
glBindBuffer(GL_ARRAY_BUFFER, s_imageVBO);
|
||||
glBufferData(GL_ARRAY_BUFFER, sizeof(verts), verts, GL_DYNAMIC_DRAW);
|
||||
glDrawArrays(GL_TRIANGLES, 0, 6);
|
||||
glBindVertexArray(0);
|
||||
|
||||
glDisable(GL_BLEND);
|
||||
}
|
||||
|
||||
// ---- Function 3: click detection ---------------------------------------
|
||||
|
||||
void SetPointerPosition(float x, float y) {
|
||||
std::lock_guard<std::mutex> lock(s_inputMutex);
|
||||
s_pointerPos.x = x;
|
||||
s_pointerPos.y = y;
|
||||
}
|
||||
|
||||
void SetPointerDown(bool isDown) {
|
||||
std::lock_guard<std::mutex> lock(s_inputMutex);
|
||||
if (isDown && !s_pointerWasDown) {
|
||||
s_clickPending = true;
|
||||
s_clickPos = s_pointerPos;
|
||||
}
|
||||
s_pointerIsDown = isDown;
|
||||
}
|
||||
|
||||
bool WasClicked(Vec2 location, Vec2 size) {
|
||||
std::lock_guard<std::mutex> lock(s_inputMutex);
|
||||
if (!s_clickPending) return false;
|
||||
return s_clickPos.x >= location.x && s_clickPos.x <= location.x + size.x &&
|
||||
s_clickPos.y >= location.y && s_clickPos.y <= location.y + size.y;
|
||||
}
|
||||
|
||||
void DrawRectOutline(Vec2 location, Vec2 size, float thickness) {
|
||||
if (!s_initialized) return;
|
||||
|
||||
const float x0 = location.x, y0 = location.y;
|
||||
const float x1 = location.x + size.x, y1 = location.y + size.y;
|
||||
const float t = thickness;
|
||||
|
||||
std::vector<float> verts;
|
||||
verts.reserve(24 * 4);
|
||||
auto addRect = [&](float rx0, float ry0, float rx1, float ry1) {
|
||||
float quad[] = {
|
||||
rx0, ry0, rx0, ry1, rx1, ry0,
|
||||
rx1, ry0, rx0, ry1, rx1, ry1,
|
||||
};
|
||||
verts.insert(verts.end(), std::begin(quad), std::end(quad));
|
||||
};
|
||||
|
||||
addRect(x0, y0, x1, y0 + t); // top edge
|
||||
addRect(x0, y1 - t, x1, y1); // bottom edge
|
||||
addRect(x0, y0, x0 + t, y1); // left edge
|
||||
addRect(x1 - t, y0, x1, y1); // right edge
|
||||
|
||||
DrawWhiteTriangles(verts);
|
||||
}
|
||||
|
||||
bool Button(const std::string& text, float textSize, Vec2 location, TextAlign alignment) {
|
||||
// Pad the outline out from the raw glyph bounds so the button reads as
|
||||
// an actual rectangular button, not a box hugging the text - and give
|
||||
// it a minimum width so short labels (e.g. "BACK") still get a
|
||||
// comfortably large, easy-to-hit target instead of a narrow sliver.
|
||||
const float padding = std::max(textSize * 0.35f, 20.0f);
|
||||
const float minWidth = textSize * 4.5f;
|
||||
const float rectWidth = std::max(TextTotalWidth(text, textSize) + padding * 2.0f, minWidth);
|
||||
Vec2 rectLocation{
|
||||
TextStartX(text, textSize, location.x, alignment) - padding,
|
||||
location.y - padding
|
||||
};
|
||||
Vec2 rectSize{rectWidth, textSize + padding * 2.0f};
|
||||
|
||||
// Outline thickness scales with text size but never drops below a
|
||||
// pixel width that reliably rasterizes (thin geometry has been
|
||||
// observed to drop out entirely on some Android GPU/driver combos).
|
||||
const float thickness = std::max(textSize * 0.06f, 4.0f);
|
||||
|
||||
DrawRectOutline(rectLocation, rectSize, thickness);
|
||||
DrawText(text, textSize, location, alignment);
|
||||
|
||||
return WasClicked(rectLocation, rectSize);
|
||||
}
|
||||
|
||||
} // namespace UI
|
||||
@@ -0,0 +1,76 @@
|
||||
#ifndef UI_RENDERER_H
|
||||
#define UI_RENDERER_H
|
||||
|
||||
#include "Platform.h"
|
||||
#include <string>
|
||||
|
||||
// Minimal immediate-mode UI helpers built directly on the app's existing
|
||||
// OpenGL ES context. Text is drawn with a built-in vector font (no font
|
||||
// files needed); images are plain textured quads from RGBA pixel data.
|
||||
namespace UI {
|
||||
|
||||
enum class TextAlign { Left, Center, Right };
|
||||
|
||||
struct Vec2 {
|
||||
float x = 0.0f;
|
||||
float y = 0.0f;
|
||||
};
|
||||
|
||||
struct Texture {
|
||||
GLuint id = 0;
|
||||
int width = 0;
|
||||
int height = 0;
|
||||
};
|
||||
|
||||
// Call once the GL context exists (e.g. right after GLEW/EGL init),
|
||||
// with the current screen/framebuffer size in pixels. Safe to call
|
||||
// again on resize - later calls just update the cached screen size.
|
||||
bool Init(int screenWidth, int screenHeight);
|
||||
void OnScreenResize(int screenWidth, int screenHeight);
|
||||
void Shutdown();
|
||||
|
||||
// Must be called once per rendered frame, after all DrawText/DrawImage/
|
||||
// WasClicked calls for that frame, so click events don't leak into the
|
||||
// next frame.
|
||||
void EndFrame();
|
||||
|
||||
// ---- Function 1: text -------------------------------------------------
|
||||
// textLocation is the anchor point in screen pixels (origin top-left).
|
||||
// alignment controls how the text is positioned relative to that anchor.
|
||||
void DrawText(const std::string& text, float textSize, Vec2 textLocation, TextAlign alignment);
|
||||
|
||||
// ---- Function 2: images ------------------------------------------------
|
||||
// Uploads raw RGBA8 pixels (width * height * 4 bytes) as a GL texture.
|
||||
Texture CreateTexture(const unsigned char* pixelsRGBA, int width, int height);
|
||||
void DestroyTexture(Texture& texture);
|
||||
|
||||
// locationOfImage is the top-left corner in screen pixels, sizeOfImage
|
||||
// is the width/height to draw it at (also in screen pixels).
|
||||
void DrawImage(const Texture& image, Vec2 locationOfImage, Vec2 sizeOfImage);
|
||||
|
||||
// ---- Function 3: click detection ---------------------------------------
|
||||
// Fed by platform input glue (GLFW mouse callbacks on Windows, forwarded
|
||||
// touch events on Android). x/y are in screen pixels, origin top-left -
|
||||
// the same space as textLocation/locationOfImage above.
|
||||
void SetPointerPosition(float x, float y);
|
||||
void SetPointerDown(bool isDown);
|
||||
|
||||
// Returns true on the frame the pointer went down (mouse click / touch
|
||||
// tap) inside the rectangle [location, location + size]. Call once per
|
||||
// button per frame, followed by UI::EndFrame().
|
||||
bool WasClicked(Vec2 location, Vec2 size);
|
||||
|
||||
// Draws an unfilled rectangle border (not solid-filled) `thickness`
|
||||
// pixels wide, at `location` with the given `size`.
|
||||
void DrawRectOutline(Vec2 location, Vec2 size, float thickness);
|
||||
|
||||
// Convenience wrapper: draws `text` as a label inside a rectangular
|
||||
// outline and reports whether it was clicked/tapped this frame. The
|
||||
// drawn outline always exactly matches the clickable hit area, padded
|
||||
// out a bit from the raw glyph bounds so the button doesn't hug the
|
||||
// text too tightly.
|
||||
bool Button(const std::string& text, float textSize, Vec2 location, TextAlign alignment);
|
||||
|
||||
} // namespace UI
|
||||
|
||||
#endif // UI_RENDERER_H
|
||||
@@ -0,0 +1,40 @@
|
||||
#include "Platform.h"
|
||||
// [IMPORTANT] This main entry file is only for Desktop Based applications support Window, Ubuntu, Mac etc
|
||||
#ifdef PLATFORM_WINDOWS
|
||||
|
||||
#include "GLRenderer.h"
|
||||
#include <iostream>
|
||||
|
||||
int main() {
|
||||
std::cout << "Starting OpenGL ES 3.0 Triangle Demo on Windows" << std::endl;
|
||||
|
||||
// Create renderer
|
||||
GLRenderer renderer;
|
||||
|
||||
// Initialize
|
||||
if (!renderer.initialize()) {
|
||||
std::cerr << "Failed to initialize renderer" << std::endl;
|
||||
return -1;
|
||||
}
|
||||
|
||||
std::cout << "Renderer initialized successfully" << std::endl;
|
||||
std::cout << "Press ESC to exit" << std::endl;
|
||||
|
||||
// Main render loop
|
||||
while (!glfwWindowShouldClose(renderer.getWindow())) {
|
||||
// Render frame
|
||||
renderer.render();
|
||||
|
||||
// Check for ESC key to exit
|
||||
if (glfwGetKey(renderer.getWindow(), GLFW_KEY_ESCAPE) == GLFW_PRESS) {
|
||||
glfwSetWindowShouldClose(renderer.getWindow(), GLFW_TRUE);
|
||||
}
|
||||
}
|
||||
|
||||
// Cleanup
|
||||
renderer.cleanup();
|
||||
|
||||
std::cout << "Application closed successfully" << std::endl;
|
||||
return 0;
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,35 @@
|
||||
#include "../GameState.h"
|
||||
#include "../GameStateManager.h"
|
||||
#include "../UIRenderer.h"
|
||||
|
||||
namespace {
|
||||
|
||||
// Text anchored below this line clears the Android ActionBar overlay - see
|
||||
// the note in GLRenderer.cpp.
|
||||
constexpr float kSafeTop = 350.0f;
|
||||
|
||||
class MainMenuState : public GameState {
|
||||
public:
|
||||
void Render() override {
|
||||
UI::DrawText("MAIN MENU", 60.0f, {20.0f, kSafeTop}, UI::TextAlign::Left);
|
||||
|
||||
if (UI::Button("SETTINGS", 56.0f, {20.0f, kSafeTop + 110.0f}, UI::TextAlign::Left)) {
|
||||
GameStateManager::Instance().RequestStateChange(StateID::Settings);
|
||||
}
|
||||
if (UI::Button("MENU 1", 56.0f, {20.0f, kSafeTop + 230.0f}, UI::TextAlign::Left)) {
|
||||
GameStateManager::Instance().RequestStateChange(StateID::Menu1);
|
||||
}
|
||||
if (UI::Button("MENU 2", 56.0f, {20.0f, kSafeTop + 350.0f}, UI::TextAlign::Left)) {
|
||||
GameStateManager::Instance().RequestStateChange(StateID::Menu2);
|
||||
}
|
||||
if (UI::Button("MENU 3", 56.0f, {20.0f, kSafeTop + 470.0f}, UI::TextAlign::Left)) {
|
||||
GameStateManager::Instance().RequestStateChange(StateID::Menu3);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
std::unique_ptr<GameState> CreateMainMenuState() {
|
||||
return std::make_unique<MainMenuState>();
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
#include "../GameState.h"
|
||||
#include "../GameStateManager.h"
|
||||
#include "../UIRenderer.h"
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr float kSafeTop = 350.0f;
|
||||
|
||||
class Menu1State : public GameState {
|
||||
public:
|
||||
void Render() override {
|
||||
UI::DrawText("MENU 1", 60.0f, {20.0f, kSafeTop}, UI::TextAlign::Left);
|
||||
UI::DrawText("THIS IS MENU ONE", 36.0f, {20.0f, kSafeTop + 90.0f}, UI::TextAlign::Left);
|
||||
|
||||
if (UI::Button("BACK", 56.0f, {20.0f, kSafeTop + 220.0f}, UI::TextAlign::Left)) {
|
||||
GameStateManager::Instance().RequestStateChange(StateID::MainMenu);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
std::unique_ptr<GameState> CreateMenu1State() {
|
||||
return std::make_unique<Menu1State>();
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
#include "../GameState.h"
|
||||
#include "../GameStateManager.h"
|
||||
#include "../UIRenderer.h"
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr float kSafeTop = 350.0f;
|
||||
|
||||
class Menu2State : public GameState {
|
||||
public:
|
||||
void Render() override {
|
||||
UI::DrawText("MENU 2", 60.0f, {20.0f, kSafeTop}, UI::TextAlign::Left);
|
||||
UI::DrawText("THIS IS MENU TWO", 36.0f, {20.0f, kSafeTop + 90.0f}, UI::TextAlign::Left);
|
||||
|
||||
if (UI::Button("BACK", 56.0f, {20.0f, kSafeTop + 220.0f}, UI::TextAlign::Left)) {
|
||||
GameStateManager::Instance().RequestStateChange(StateID::MainMenu);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
std::unique_ptr<GameState> CreateMenu2State() {
|
||||
return std::make_unique<Menu2State>();
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
#include "../GameState.h"
|
||||
#include "../GameStateManager.h"
|
||||
#include "../UIRenderer.h"
|
||||
#include "../PlatformBridge.h"
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr float kSafeTop = 350.0f;
|
||||
|
||||
class Menu3State : public GameState {
|
||||
public:
|
||||
// Fires once, the moment GameStateManager switches into this state -
|
||||
// i.e. as soon as the "MENU 3" button is pressed on the main menu. On
|
||||
// Android this opens BowlingCameraActivity (permission request, camera
|
||||
// preview, pose overlay); on the desktop/GLFW build it's a no-op log,
|
||||
// since there's no camera feature there.
|
||||
void Enter() override {
|
||||
PlatformBridge::LaunchBowlingCamera();
|
||||
}
|
||||
|
||||
void Render() override {
|
||||
UI::DrawText("MENU 3", 60.0f, {20.0f, kSafeTop}, UI::TextAlign::Left);
|
||||
UI::DrawText("BOWLING FORM CAMERA", 36.0f, {20.0f, kSafeTop + 90.0f}, UI::TextAlign::Left);
|
||||
|
||||
// The camera screen also opens automatically via Enter() above,
|
||||
// but if the user backs out of it without recording anything,
|
||||
// this lets them reopen it without leaving and re-entering Menu 3.
|
||||
// Front/back camera switching and the way back out live on the
|
||||
// camera screen itself (see BowlingCameraActivity's SWITCH CAMERA
|
||||
// and BACK buttons), not here.
|
||||
if (UI::Button("OPEN CAMERA", 56.0f, {20.0f, kSafeTop + 220.0f}, UI::TextAlign::Left)) {
|
||||
PlatformBridge::LaunchBowlingCamera();
|
||||
}
|
||||
if (UI::Button("BACK", 56.0f, {20.0f, kSafeTop + 340.0f}, UI::TextAlign::Left)) {
|
||||
GameStateManager::Instance().RequestStateChange(StateID::MainMenu);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
std::unique_ptr<GameState> CreateMenu3State() {
|
||||
return std::make_unique<Menu3State>();
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
#include "../GameState.h"
|
||||
#include "../GameStateManager.h"
|
||||
#include "../UIRenderer.h"
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr float kSafeTop = 350.0f;
|
||||
|
||||
class SettingsState : public GameState {
|
||||
public:
|
||||
void Render() override {
|
||||
UI::DrawText("SETTINGS", 60.0f, {20.0f, kSafeTop}, UI::TextAlign::Left);
|
||||
UI::DrawText("SOUND: ON", 36.0f, {20.0f, kSafeTop + 90.0f}, UI::TextAlign::Left);
|
||||
UI::DrawText("MUSIC: ON", 36.0f, {20.0f, kSafeTop + 140.0f}, UI::TextAlign::Left);
|
||||
|
||||
if (UI::Button("BACK", 56.0f, {20.0f, kSafeTop + 250.0f}, UI::TextAlign::Left)) {
|
||||
GameStateManager::Instance().RequestStateChange(StateID::MainMenu);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
std::unique_ptr<GameState> CreateSettingsState() {
|
||||
return std::make_unique<SettingsState>();
|
||||
}
|
||||
Reference in New Issue
Block a user