Debugging linkage stuff

This commit is contained in:
2026-08-11 19:58:07 +08:00
parent 8d4535ff00
commit 18fe809d4a
21 changed files with 1584 additions and 1 deletions
+1 -1
View File
@@ -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"
+50
View File
@@ -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)
+105
View File
@@ -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");
}
}
+347
View File
@@ -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");
}
+71
View File
@@ -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
+31
View File
@@ -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
+32
View File
@@ -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
+473
View File
@@ -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
+76
View File
@@ -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
+40
View File
@@ -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>();
}