Skip to content

Example: SelectHandler

A native addon that sends signals when the user performs a "select" action via mouse click, touch, or gamepad button. Input is polled entirely in C++ using the Tick callback.


Overview

This example demonstrates: - Using the Tick callback to poll input every frame in C++ - Direct input access via PolyphaseEngineAPI (no Lua roundtrip) - Cross-platform input handling (mouse, touch, gamepad) - Optionally exposing events to Lua scripts


Files

package.json

{
    "name": "SelectHandler",
    "author": "Polyphase Examples",
    "description": "Sends signals on select actions (mouse click, touch, A button).",
    "version": "1.0.0",
    "tags": ["input", "utility"],
    "native": {
        "target": "engine",
        "sourceDir": "Source",
        "binaryName": "selecthandler",
        "apiVersion": 2
    }
}

Source/SelectHandler.cpp

/**
 * @file SelectHandler.cpp
 * @brief Native addon for unified select/click input handling.
 *
 * This example demonstrates:
 * - Using the Tick callback to poll input every frame in C++
 * - Direct input polling via PolyphaseEngineAPI (IsKeyDown, IsMouseButtonDown, etc.)
 * - Optional Lua integration for script callbacks
 * - Cross-platform input abstraction
 *
 * The addon uses direct engine API calls instead of going through Lua for input,
 * which provides better performance for real-time input handling.
 */

#include "Plugins/PolyphasePluginAPI.h"
#include "Plugins/PolyphaseEngineAPI.h"

// Static registration for built games
#if !defined(OCTAVE_PLUGIN_EXPORT)
#include "Plugins/RuntimePluginManager.h"
POLYPHASE_REGISTER_PLUGIN(SelectHandler, PolyphasePlugin_GetDesc)
#endif

// Lua includes (External/Lua) - for type definitions only
extern "C" {
#include "lua.h"
#include "lauxlib.h"
#include "lualib.h"
}

// GLM for math (External/glm)
#include "glm/glm.hpp"

#include <vector>

static PolyphaseEngineAPI* sEngineAPI = nullptr;
static lua_State* sLuaState = nullptr;

//=============================================================================
// Input State Tracking
//=============================================================================

struct SelectState
{
    bool wasPressed = false;
    bool isPressed = false;

    // Callbacks (Lua function references)
    int onPressedRef = LUA_NOREF;
    int onReleasedRef = LUA_NOREF;
    int onHeldRef = LUA_NOREF;

    // Configuration
    bool detectMouse = true;
    bool detectTouch = true;
    bool detectGamepad = true;
    int gamepadButton = 0;  // A button typically
};

// Global list of all active SelectHandlers (for Tick-based updates)
static std::vector<SelectState*> sActiveHandlers;

//=============================================================================
// Lua Bindings
//=============================================================================

// SelectHandler.Create() - Creates a new SelectHandler
static int Lua_SelectHandler_Create(lua_State* L)
{
    SelectState* state = (SelectState*)lua_newuserdata(L, sizeof(SelectState));
    new (state) SelectState();

    // Add to active handlers for automatic Tick updates
    sActiveHandlers.push_back(state);

    luaL_getmetatable(L, "SelectHandler");
    lua_setmetatable(L, -2);

    return 1;
}

// SelectHandler:OnPressed(callback) - Set callback for press events
static int Lua_SelectHandler_OnPressed(lua_State* L)
{
    SelectState* state = (SelectState*)luaL_checkudata(L, 1, "SelectHandler");

    // Release old reference if exists
    if (state->onPressedRef != LUA_NOREF)
    {
        luaL_unref(L, LUA_REGISTRYINDEX, state->onPressedRef);
    }

    // Store new reference
    if (lua_isfunction(L, 2))
    {
        lua_pushvalue(L, 2);
        state->onPressedRef = luaL_ref(L, LUA_REGISTRYINDEX);
    }
    else
    {
        state->onPressedRef = LUA_NOREF;
    }

    return 0;
}

// SelectHandler:OnReleased(callback) - Set callback for release events
static int Lua_SelectHandler_OnReleased(lua_State* L)
{
    SelectState* state = (SelectState*)luaL_checkudata(L, 1, "SelectHandler");

    if (state->onReleasedRef != LUA_NOREF)
    {
        luaL_unref(L, LUA_REGISTRYINDEX, state->onReleasedRef);
    }

    if (lua_isfunction(L, 2))
    {
        lua_pushvalue(L, 2);
        state->onReleasedRef = luaL_ref(L, LUA_REGISTRYINDEX);
    }
    else
    {
        state->onReleasedRef = LUA_NOREF;
    }

    return 0;
}

// SelectHandler:OnHeld(callback) - Set callback for held state
static int Lua_SelectHandler_OnHeld(lua_State* L)
{
    SelectState* state = (SelectState*)luaL_checkudata(L, 1, "SelectHandler");

    if (state->onHeldRef != LUA_NOREF)
    {
        luaL_unref(L, LUA_REGISTRYINDEX, state->onHeldRef);
    }

    if (lua_isfunction(L, 2))
    {
        lua_pushvalue(L, 2);
        state->onHeldRef = luaL_ref(L, LUA_REGISTRYINDEX);
    }
    else
    {
        state->onHeldRef = LUA_NOREF;
    }

    return 0;
}

// SelectHandler:SetDetectMouse(enabled)
static int Lua_SelectHandler_SetDetectMouse(lua_State* L)
{
    SelectState* state = (SelectState*)luaL_checkudata(L, 1, "SelectHandler");
    state->detectMouse = lua_toboolean(L, 2);
    return 0;
}

// SelectHandler:SetDetectTouch(enabled)
static int Lua_SelectHandler_SetDetectTouch(lua_State* L)
{
    SelectState* state = (SelectState*)luaL_checkudata(L, 1, "SelectHandler");
    state->detectTouch = lua_toboolean(L, 2);
    return 0;
}

// SelectHandler:SetDetectGamepad(enabled)
static int Lua_SelectHandler_SetDetectGamepad(lua_State* L)
{
    SelectState* state = (SelectState*)luaL_checkudata(L, 1, "SelectHandler");
    state->detectGamepad = lua_toboolean(L, 2);
    return 0;
}

// SelectHandler:SetGamepadButton(buttonIndex)
static int Lua_SelectHandler_SetGamepadButton(lua_State* L)
{
    SelectState* state = (SelectState*)luaL_checkudata(L, 1, "SelectHandler");
    state->gamepadButton = (int)luaL_checkinteger(L, 2);
    return 0;
}

// SelectHandler:IsPressed() - Check if currently pressed
static int Lua_SelectHandler_IsPressed(lua_State* L)
{
    SelectState* state = (SelectState*)luaL_checkudata(L, 1, "SelectHandler");
    lua_pushboolean(L, state->isPressed);
    return 1;
}

// SelectHandler:JustPressed() - Check if just pressed this frame
static int Lua_SelectHandler_JustPressed(lua_State* L)
{
    SelectState* state = (SelectState*)luaL_checkudata(L, 1, "SelectHandler");
    lua_pushboolean(L, state->isPressed && !state->wasPressed);
    return 1;
}

// SelectHandler:JustReleased() - Check if just released this frame
static int Lua_SelectHandler_JustReleased(lua_State* L)
{
    SelectState* state = (SelectState*)luaL_checkudata(L, 1, "SelectHandler");
    lua_pushboolean(L, !state->isPressed && state->wasPressed);
    return 1;
}

// Helper to call a Lua callback
static void CallCallback(lua_State* L, int ref, const char* inputSource)
{
    if (ref == LUA_NOREF || L == nullptr)
        return;

    lua_rawgeti(L, LUA_REGISTRYINDEX, ref);
    if (lua_isfunction(L, -1))
    {
        lua_pushstring(L, inputSource);
        if (lua_pcall(L, 1, 0, 0) != 0)
        {
            const char* err = lua_tostring(L, -1);
            if (sEngineAPI)
            {
                sEngineAPI->LogError("SelectHandler callback error: %s", err);
            }
            lua_pop(L, 1);
        }
    }
    else
    {
        lua_pop(L, 1);
    }
}

// SelectHandler:Update() - Poll input and fire callbacks
// This now uses the direct PolyphaseEngineAPI for input instead of Lua arguments
static int Lua_SelectHandler_Update(lua_State* L)
{
    SelectState* state = (SelectState*)luaL_checkudata(L, 1, "SelectHandler");

    if (!sEngineAPI)
        return 0;

    state->wasPressed = state->isPressed;

    // Check all enabled input sources using DIRECT ENGINE API
    // This is much more efficient than going through Lua
    state->isPressed = false;
    const char* source = nullptr;

    // Direct mouse button check (left button = 0)
    if (state->detectMouse && sEngineAPI->IsMouseButtonDown(0))
    {
        state->isPressed = true;
        source = "mouse";
    }
    // Note: Touch detection would need additional API support
    // For now, we check mouse as a fallback for touch on desktop

    // Fire callbacks
    if (state->isPressed && !state->wasPressed)
    {
        // Just pressed - check if it was this frame
        if (sEngineAPI->IsMouseButtonJustPressed(0))
        {
            CallCallback(L, state->onPressedRef, source);
        }
    }
    else if (!state->isPressed && state->wasPressed)
    {
        // Just released
        CallCallback(L, state->onReleasedRef, source);
    }
    else if (state->isPressed && state->wasPressed)
    {
        // Held
        CallCallback(L, state->onHeldRef, source);
    }

    return 0;
}

// Garbage collection - cleanup references
static int Lua_SelectHandler_GC(lua_State* L)
{
    SelectState* state = (SelectState*)luaL_checkudata(L, 1, "SelectHandler");

    if (state->onPressedRef != LUA_NOREF)
        luaL_unref(L, LUA_REGISTRYINDEX, state->onPressedRef);
    if (state->onReleasedRef != LUA_NOREF)
        luaL_unref(L, LUA_REGISTRYINDEX, state->onReleasedRef);
    if (state->onHeldRef != LUA_NOREF)
        luaL_unref(L, LUA_REGISTRYINDEX, state->onHeldRef);

    return 0;
}

// Metatable methods
static const luaL_Reg sSelectHandlerMethods[] = {
    {"OnPressed", Lua_SelectHandler_OnPressed},
    {"OnReleased", Lua_SelectHandler_OnReleased},
    {"OnHeld", Lua_SelectHandler_OnHeld},
    {"SetDetectMouse", Lua_SelectHandler_SetDetectMouse},
    {"SetDetectTouch", Lua_SelectHandler_SetDetectTouch},
    {"SetDetectGamepad", Lua_SelectHandler_SetDetectGamepad},
    {"SetGamepadButton", Lua_SelectHandler_SetGamepadButton},
    {"IsPressed", Lua_SelectHandler_IsPressed},
    {"JustPressed", Lua_SelectHandler_JustPressed},
    {"JustReleased", Lua_SelectHandler_JustReleased},
    {"Update", Lua_SelectHandler_Update},
    {"__gc", Lua_SelectHandler_GC},
    {nullptr, nullptr}
};

// Module functions
static const luaL_Reg sSelectHandlerFuncs[] = {
    {"Create", Lua_SelectHandler_Create},
    {nullptr, nullptr}
};

//=============================================================================
// Plugin Tick - Updates all handlers automatically in C++
//=============================================================================

static void UpdateHandler(SelectState* state, lua_State* L)
{
    if (!sEngineAPI || !state)
        return;

    state->wasPressed = state->isPressed;
    state->isPressed = false;
    const char* source = nullptr;

    // Check all enabled input sources using DIRECT ENGINE API
    if (state->detectMouse && sEngineAPI->IsMouseButtonDown(0))
    {
        state->isPressed = true;
        source = "mouse";
    }

    // Fire callbacks
    if (state->isPressed && !state->wasPressed)
    {
        if (sEngineAPI->IsMouseButtonJustPressed(0))
        {
            CallCallback(L, state->onPressedRef, source);
        }
    }
    else if (!state->isPressed && state->wasPressed)
    {
        CallCallback(L, state->onReleasedRef, source);
    }
    else if (state->isPressed && state->wasPressed)
    {
        CallCallback(L, state->onHeldRef, source);
    }
}

// Called every frame during gameplay (PIE or built game)
static void PluginTick(float deltaTime)
{
    if (!sLuaState)
        return;

    // Update ALL active handlers automatically - no Lua Tick needed!
    for (SelectState* state : sActiveHandlers)
    {
        UpdateHandler(state, sLuaState);
    }
}

//=============================================================================
// Plugin Callbacks
//=============================================================================

static int OnLoad(PolyphaseEngineAPI* api)
{
    sEngineAPI = api;
    sActiveHandlers.clear();
    api->LogDebug("SelectHandler addon loaded!");
    return 0;
}

static void OnUnload()
{
    if (sEngineAPI)
    {
        sEngineAPI->LogDebug("SelectHandler addon unloaded.");
    }
    sActiveHandlers.clear();
    sEngineAPI = nullptr;
    sLuaState = nullptr;
}

static void RegisterScriptFuncs(lua_State* L)
{
    sLuaState = L;

    // Create the SelectHandler metatable
    luaL_newmetatable(L, "SelectHandler");

    lua_pushvalue(L, -1);
    lua_setfield(L, -2, "__index");

    luaL_setfuncs(L, sSelectHandlerMethods, 0);
    lua_pop(L, 1);

    // Create the SelectHandler table
    luaL_newlib(L, sSelectHandlerFuncs);
    lua_setglobal(L, "SelectHandler");
}

//=============================================================================
// Plugin Entry Point
//=============================================================================

extern "C" OCTAVE_PLUGIN_API int PolyphasePlugin_GetDesc(PolyphasePluginDesc* desc)
{
    desc->apiVersion = OCTAVE_PLUGIN_API_VERSION;
    desc->pluginName = "SelectHandler";
    desc->pluginVersion = "1.0.0";
    desc->OnLoad = OnLoad;
    desc->OnUnload = OnUnload;
    desc->Tick = PluginTick;       // Automatic input polling every frame
    desc->TickEditor = nullptr;    // No editor tick needed
    desc->RegisterTypes = nullptr;
    desc->RegisterScriptFuncs = RegisterScriptFuncs;
    desc->RegisterEditorUI = nullptr;
    return 0;
}

Usage in Lua Scripts

Basic Click Detection (No Tick Required!)

-- ClickableObject.lua
-- The C++ Tick callback handles input polling automatically!

local selectHandler = nil

function Start()
    selectHandler = SelectHandler.Create()

    -- Just set up callbacks - C++ handles the rest
    selectHandler:OnPressed(function(source)
        Log.Debug("Pressed via " .. source)
        self:EmitSignal("Clicked")
    end)

    selectHandler:OnReleased(function(source)
        Log.Debug("Released via " .. source)
    end)
end

-- NO Tick function needed! The native addon polls input in C++
-- This is much more efficient than checking input in Lua every frame

function Destroy()
    if selectHandler then
        selectHandler:Destroy()
        selectHandler = nil
    end
end

Button with Visual Feedback

-- InteractiveButton.lua
-- C++ handles input polling, Lua just handles callbacks

local selectHandler = nil
local originalScale = nil

function Start()
    originalScale = self:GetScale()

    selectHandler = SelectHandler.Create()

    selectHandler:OnPressed(function(source)
        -- Shrink when pressed
        local s = originalScale
        self:SetScale(s.x * 0.9, s.y * 0.9, s.z * 0.9)
    end)

    selectHandler:OnReleased(function(source)
        -- Return to normal size
        local s = originalScale
        self:SetScale(s.x, s.y, s.z)

        -- Trigger the button action
        self:EmitSignal("ButtonActivated")
    end)
end

-- NO Tick function needed! C++ polls input automatically

function Destroy()
    if selectHandler then
        selectHandler:Destroy()
        selectHandler = nil
    end
end

Mobile/Console Adaptive Input

-- AdaptiveSelectHandler.lua
-- Configure once, C++ handles the rest

local selectHandler = nil

function Start()
    selectHandler = SelectHandler.Create()

    -- Configure based on platform - C++ will poll the right inputs
    local platform = Engine.GetPlatform()

    if platform == "Windows" or platform == "Linux" then
        selectHandler:SetDetectMouse(true)
        selectHandler:SetDetectTouch(false)
        selectHandler:SetDetectGamepad(true)
    elseif platform == "Android" or platform == "iOS" then
        selectHandler:SetDetectMouse(false)
        selectHandler:SetDetectTouch(true)
        selectHandler:SetDetectGamepad(true)
    elseif platform == "GameCube" or platform == "Wii" or platform == "3DS" then
        selectHandler:SetDetectMouse(false)
        selectHandler:SetDetectTouch(platform == "3DS")
        selectHandler:SetDetectGamepad(true)
    end

    selectHandler:OnPressed(function(source)
        Log.Debug("Select action from: " .. source)
        HandleSelect()
    end)
end

function HandleSelect()
    -- Your selection logic here
end

-- NO Tick function! C++ Tick polls input based on configuration above

function Destroy()
    if selectHandler then
        selectHandler:Destroy()
        selectHandler = nil
    end
end

API Reference

SelectHandler.Create()

Creates a new SelectHandler instance.

Returns: SelectHandler userdata


handler:OnPressed(callback)

Sets the callback for when select is first pressed.

Parameters: - callback (function): Called with source parameter ("mouse", "touch", or "gamepad")


handler:OnReleased(callback)

Sets the callback for when select is released.

Parameters: - callback (function): Called with source parameter


handler:OnHeld(callback)

Sets the callback for while select is held.

Parameters: - callback (function): Called with source parameter (every frame while held)


handler:SetDetectMouse(enabled)

Enable/disable mouse input detection.


handler:SetDetectTouch(enabled)

Enable/disable touch input detection.


handler:SetDetectGamepad(enabled)

Enable/disable gamepad input detection.


handler:SetGamepadButton(buttonIndex)

Set which gamepad button to detect.


handler:IsPressed()

Check if currently pressed.

Returns: boolean


handler:JustPressed()

Check if pressed this frame (wasn't pressed last frame).

Returns: boolean


handler:JustReleased()

Check if released this frame (was pressed last frame).

Returns: boolean


handler:Destroy()

Remove handler from active list. Call this in your Destroy callback.


C++ Tick Callback

The SelectHandler uses the plugin Tick callback to automatically poll input every frame:

static void PluginTick(float deltaTime)
{
    // Update ALL active handlers automatically
    for (SelectState* state : sActiveHandlers)
    {
        UpdateHandler(state, sLuaState);
    }
}

Benefits of C++ Tick: - Zero Lua overhead - No Lua Tick function needed - Direct API access - Uses sEngineAPI->IsMouseButtonDown() directly - Automatic updates - All handlers updated in a single native loop - Consistent timing - Input checked at same point each frame