Repository navigation
Mobile Platform
SparkEngine provides a mobile platform abstraction for iOS and Android, handling touch input, gesture recognition, GPU quality presets, battery-aware performance scaling, and screen orientation management.
Source: SparkEngine/Source/Engine/Mobile/MobilePlatform.h, SparkEngine/Source/Engine/Mobile/MobilePlatform.cpp
CMake toggle: ENABLE_MOBILE=ON
+---------------------------------------------+
| OS Touch Events (iOS/Android) |
+---------------------+-----------------------+
|
v
+---------------------+-----------------------+
| MobilePlatform |
| +---------------+ +-------------------+ |
| | Touch Tracker | | Gesture Recognizer| |
| | (active | | (tap, swipe, | |
| | touches) | | pinch, rotate) | |
| +-------+-------+ +--------+----------+ |
| | | |
| +-------v--------------------v----------+ |
| | Gesture Callback Dispatch | |
| +---------------------------------------+ |
| |
| +-------------------+ +----------------+ |
| | Quality Presets | | Battery-Aware | |
| | (Low/Med/High/ | | Scaling | |
| | Auto) | | (auto-downgrade| |
| +---------+---------+ | on low batt) | |
| | +--------+-------+ |
| +------+------+--------+ |
| | |
| +----------------v-------------------------+|
| | Screen Orientation + Safe Area Management||
| +------------------------------------------+|
+---------------------------------------------+
|
Outputs to: Rendering, Input, UI Systems
| Class | Responsibility |
|---|---|
MobilePlatform |
Touch input, gestures, quality settings, battery, orientation |
TouchEvent |
Raw touch point data (position, pressure, type) |
Gesture |
Recognized gesture (tap, swipe, pinch, rotate) |
MobileQualitySettings |
GPU quality parameters derived from preset |
All types live in the Spark::Mobile namespace.
set(ENABLE_MOBILE ON CACHE BOOL "Enable mobile platform support")When ENABLE_MOBILE is OFF (the default for desktop builds), none of the mobile platform code is compiled. This ensures zero overhead for Windows/Linux builds.
#include "Engine/Mobile/MobilePlatform.h"
Spark::Mobile::MobilePlatform mobile;
mobile.Initialize();
mobile.SetQualityPreset(Spark::Mobile::MobileQualityPreset::Auto);
mobile.SetOrientation(Spark::Mobile::ScreenOrientation::LandscapeLeft);
// Per frame:
mobile.Update(deltaTime);
auto gestures = mobile.GetGestures();
for (const auto& g : gestures)
{
if (g.type == Spark::Mobile::GestureType::Tap)
{
HandleTap(g.x, g.y);
}
if (g.type == Spark::Mobile::GestureType::Pinch)
{
HandleZoom(g.scale);
}
}Describes the lifecycle phase of a single touch point:
enum class TouchEventType
{
Began, // Finger first touches screen
Moved, // Finger moves while touching
Ended, // Finger lifts off screen
Cancelled // Touch interrupted by system (e.g., incoming call)
};| Value | Description | Action Taken |
|---|---|---|
Began |
New finger contact detected | Added to active touches list |
Moved |
Existing touch position changed | Updates x, y, pressure in list |
Ended |
Finger lifted from screen | Removed from active touches list |
Cancelled |
OS interrupted the touch (call, notification, etc.) | Removed from active touches list |
A single raw touch point event from the operating system:
struct TouchEvent
{
uint32_t touchId = 0; // Unique touch identifier
TouchEventType type = TouchEventType::Began;
float x = 0.0f; // Screen X (0-1 normalized)
float y = 0.0f; // Screen Y (0-1 normalized)
float pressure = 1.0f; // Touch pressure (0-1)
float timestamp = 0.0f; // Event timestamp
};| Field | Type | Default | Description |
|---|---|---|---|
touchId |
uint32_t |
0 |
Unique per-finger ID across a touch session |
type |
TouchEventType |
Began |
Phase of the touch event |
x |
float |
0.0f |
Normalized screen X coordinate [0, 1] |
y |
float |
0.0f |
Normalized screen Y coordinate [0, 1] |
pressure |
float |
1.0f |
Pressure sensitivity [0, 1] (1.0 on devices without pressure support) |
timestamp |
float |
0.0f |
Time of the touch event in seconds |
Raw touch events are forwarded from the OS-specific platform layer to the MobilePlatform:
// Called by the platform-specific layer (iOS UITouch handler, Android MotionEvent)
void ProcessTouchEvent(const TouchEvent& event);The internal touch tracker maintains a list of active touches. The lifecycle is:
-
Began: A new
TouchEventis appended to the active touches list. -
Moved: The existing touch with the matching
touchIdis updated (x, y, pressure). -
Ended / Cancelled: The touch with the matching
touchIdis removed from the list.
// Get a snapshot of all currently active touch points
std::vector<TouchEvent> GetActiveTouches() const;This returns a copy of the internal touch list. The returned vector contains one entry per active finger on the screen.
enum class GestureType
{
Tap, // Single finger tap
DoubleTap, // Two taps in quick succession
LongPress, // Finger held stationary for threshold duration
Swipe, // Finger moved quickly across the screen
Pinch, // Two fingers moving together/apart
Rotate // Two fingers rotating around a center point
};A recognized touch gesture with all relevant parameters:
struct Gesture
{
GestureType type = GestureType::Tap;
float x = 0.0f, y = 0.0f; // Gesture center (normalized)
float deltaX = 0.0f, deltaY = 0.0f; // Movement delta (swipe)
float scale = 1.0f; // Scale factor (pinch)
float rotation = 0.0f; // Rotation angle in radians (rotate)
int touchCount = 1; // Number of fingers involved
};| Field | Type | Used By | Description |
|---|---|---|---|
type |
GestureType |
All | What gesture was recognized |
x, y
|
float |
All | Center point of the gesture (normalized 0-1) |
deltaX |
float |
Swipe | Horizontal swipe distance (normalized) |
deltaY |
float |
Swipe | Vertical swipe distance (normalized) |
scale |
float |
Pinch | Scale factor (>1 = spread apart, <1 = pinch together) |
rotation |
float |
Rotate | Rotation angle in radians |
touchCount |
int |
All | Number of fingers involved (1, 2, etc.) |
Gestures recognized since the last frame are available via:
std::vector<Gesture> GetGestures() const;The gesture list is cleared at the start of each Update() call, so gestures are only valid for one frame.
For event-driven gesture handling, register callbacks per gesture type:
void OnGesture(GestureType type, std::function<void(const Gesture&)> callback);Multiple callbacks can be registered for the same gesture type. Callbacks are invoked during Update() when gestures are recognized.
// Register gesture callbacks
mobile.OnGesture(GestureType::Tap, [](const Gesture& g) {
// Handle tap at normalized (g.x, g.y)
SelectObjectAt(g.x, g.y);
});
mobile.OnGesture(GestureType::Swipe, [](const Gesture& g) {
// Move camera based on swipe direction
MoveCamera(g.deltaX, g.deltaY);
});
mobile.OnGesture(GestureType::Pinch, [](const Gesture& g) {
// Zoom camera based on pinch scale
ZoomCamera(g.scale);
});
mobile.OnGesture(GestureType::Rotate, [](const Gesture& g) {
// Rotate selected object
RotateSelection(g.rotation);
});
mobile.OnGesture(GestureType::LongPress, [](const Gesture& g) {
// Show context menu
ShowContextMenu(g.x, g.y);
});
mobile.OnGesture(GestureType::DoubleTap, [](const Gesture& g) {
// Toggle zoom
ToggleZoom(g.x, g.y);
});| Gesture | FPS Action |
|---|---|
| Tap | Fire weapon |
| DoubleTap | Aim down sights toggle |
| LongPress | Pick up item / interact |
| Swipe | Look around (camera rotation) |
| Pinch | Zoom scope / adjust FOV |
| Rotate | Rotate inventory item (menu context) |
enum class MobileQualityPreset
{
Low, // Lowest quality, maximum performance
Medium, // Balanced quality and performance
High, // Highest quality for flagship devices
Auto // Auto-detect based on device capabilities
};Quality settings derived from the active preset:
struct MobileQualitySettings
{
float renderScale = 1.0f; // Render resolution scale (0.5 - 1.0)
int shadowResolution = 512; // Shadow map resolution (px)
bool enablePostProcessing = true; // Post-processing effects
bool enableParticles = true; // Particle effects
int maxDrawCalls = 500; // Maximum draw calls per frame
int textureQuality = 2; // 0=low, 1=medium, 2=high
bool enableSSAO = false; // Screen-space ambient occlusion
float lodBias = 0.0f; // LOD bias (positive = lower quality sooner)
};| Setting | Low | Medium | High | Auto (defaults to High) |
|---|---|---|---|---|
renderScale |
0.5 | 0.75 | 1.0 | 1.0 |
shadowResolution |
256 | 512 | 1024 | 1024 |
enablePostProcessing |
false |
true |
true |
true |
enableParticles |
false |
true |
true |
true |
maxDrawCalls |
200 | 400 | 600 | 600 |
textureQuality |
0 | 1 | 2 | 2 |
enableSSAO |
false |
false |
true |
true |
lodBias |
2.0 | 1.0 | 0.0 | 0.0 |
// Set preset
mobile.SetQualityPreset(MobileQualityPreset::Medium);
// Query current preset
MobileQualityPreset current = mobile.GetQualityPreset();
// Get derived settings for rendering configuration
const MobileQualitySettings& settings = mobile.GetQualitySettings();
// Use in rendering setup
renderer.SetResolutionScale(settings.renderScale);
renderer.SetShadowMapResolution(settings.shadowResolution);
renderer.SetPostProcessingEnabled(settings.enablePostProcessing);
renderer.SetMaxDrawCalls(settings.maxDrawCalls);
renderer.SetSSAOEnabled(settings.enableSSAO);
renderer.SetLODBias(settings.lodBias);The lodBias field controls how aggressively the level-of-detail system selects lower-detail meshes:
lodBias |
Effect |
|---|---|
| 0.0 | Standard LOD selection (highest quality at given distance) |
| 1.0 | Select one LOD level lower than normal |
| 2.0 | Select two LOD levels lower (most aggressive) |
Higher values significantly reduce vertex throughput on low-end GPUs at the cost of visual quality.
When battery-aware scaling is enabled, the system monitors battery level and automatically downgrades quality presets to extend play time. This only takes effect when the device is not charging.
mobile.SetBatteryAwareScaling(true); // Enable (default: true)
float battery = mobile.GetBatteryLevel(); // 0.0 - 1.0, or -1 if unknown
bool charging = mobile.IsCharging();The automatic quality adjustment follows these rules (evaluated every frame in Update()):
| Battery Level | Current Preset | Action |
|---|---|---|
| Below 10% | Any except Low | Downgrade to Low |
| Below 25% | High | Downgrade to Medium |
| 25% or above | Any | No change |
| Charging | Any | No change (scaling disabled while charging) |
Battery Level
100% |=============================================|
| Normal operation -- no quality changes |
25% |---------------------------------------------|
| High -> Medium downgrade |
10% |---------------------------------------------|
| Any -> Low downgrade |
0% |=============================================|
// Returns battery level as a float:
// 0.0 = empty
// 1.0 = fully charged
// -1.0 = unknown (e.g., desktop simulator, unsupported device)
float GetBatteryLevel() const;
// Returns true if the device is plugged in and charging
bool IsCharging() const;
// Enable or disable automatic quality scaling based on battery
void SetBatteryAwareScaling(bool enabled);When GetBatteryLevel() returns -1.0f, the battery-aware scaling logic is skipped entirely, since the battery state is unknown.
enum class ScreenOrientation
{
Portrait, // Home button at bottom
PortraitUpsideDown, // Home button at top
LandscapeLeft, // Home button on left
LandscapeRight, // Home button on right
Auto // Rotate with device
};mobile.SetOrientation(ScreenOrientation::LandscapeLeft);
// Query current orientation
ScreenOrientation current = mobile.GetOrientation();For most FPS games, LandscapeLeft or LandscapeRight is preferred. Use Auto for menu-heavy or casual games that support rotation.
Modern mobile devices have notches, camera cutouts, and home indicators that reduce the usable screen area. The SafeArea provides insets in normalized coordinates:
struct SafeArea
{
float top = 0.0f; // Inset from top edge (e.g., notch)
float bottom = 0.0f; // Inset from bottom edge (e.g., home indicator)
float left = 0.0f; // Inset from left edge
float right = 0.0f; // Inset from right edge
};+--------------------------------------------------+
| top inset (notch/status bar) |
+------+----------------------------------+--------+
| | | |
| left | SAFE AREA | right |
| | (usable screen space) | |
| | | |
+------+----------------------------------+--------+
| bottom inset (home indicator) |
+--------------------------------------------------+
const SafeArea& safe = mobile.GetSafeArea();
// Position HUD elements within the safe area
float hudLeft = safe.left;
float hudRight = 1.0f - safe.right;
float hudTop = safe.top;
float hudBottom = 1.0f - safe.bottom;
// Example: position health bar in bottom-left safe corner
healthBar.SetPosition(hudLeft + 0.02f, hudBottom - 0.05f);
// Example: position minimap in top-right safe corner
minimap.SetPosition(hudRight - 0.12f, hudTop + 0.02f);namespace Spark::Mobile
{
class MobilePlatform
{
public:
MobilePlatform();
~MobilePlatform() = default;
// --- Lifecycle ---
bool Initialize();
void Update(float deltaTime);
// --- Touch input ---
void ProcessTouchEvent(const TouchEvent& event);
std::vector<TouchEvent> GetActiveTouches() const;
std::vector<Gesture> GetGestures() const;
void OnGesture(GestureType type, std::function<void(const Gesture&)> callback);
// --- Quality ---
void SetQualityPreset(MobileQualityPreset preset);
const MobileQualitySettings& GetQualitySettings() const;
MobileQualityPreset GetQualityPreset() const;
// --- Battery ---
float GetBatteryLevel() const;
bool IsCharging() const;
void SetBatteryAwareScaling(bool enabled);
// --- Screen ---
void SetOrientation(ScreenOrientation orientation);
ScreenOrientation GetOrientation() const;
const SafeArea& GetSafeArea() const;
// --- Console ---
std::string Console_GetStatus() const;
private:
void RecognizeGestures();
MobileQualitySettings GetSettingsForPreset(MobileQualityPreset preset) const;
std::vector<TouchEvent> m_activeTouches;
std::vector<Gesture> m_pendingGestures;
std::unordered_map<int, std::vector<std::function<void(const Gesture&)>>> m_gestureCallbacks;
MobileQualityPreset m_qualityPreset = MobileQualityPreset::Auto;
MobileQualitySettings m_qualitySettings;
ScreenOrientation m_orientation = ScreenOrientation::Auto;
SafeArea m_safeArea;
float m_batteryLevel = -1.0f;
bool m_isCharging = false;
bool m_batteryAwareScaling = true;
bool m_initialized = false;
};
}| Member | Type | Default | Description |
|---|---|---|---|
m_activeTouches |
vector<TouchEvent> |
empty | Currently active touch points |
m_pendingGestures |
vector<Gesture> |
empty | Gestures recognized this frame |
m_gestureCallbacks |
unordered_map |
empty | Registered gesture callbacks by type |
m_qualityPreset |
MobileQualityPreset |
Auto |
Active quality preset |
m_qualitySettings |
MobileQualitySettings |
(from preset) | Derived quality settings |
m_orientation |
ScreenOrientation |
Auto |
Preferred screen orientation |
m_safeArea |
SafeArea |
all zeros | Screen safe area insets |
m_batteryLevel |
float |
-1.0f |
Battery level (0-1, -1 = unknown) |
m_isCharging |
bool |
false |
Whether device is charging |
m_batteryAwareScaling |
bool |
true |
Whether auto-scaling is enabled |
m_initialized |
bool |
false |
Whether Initialize() has been called |
The Console_GetStatus() method returns a formatted string for the in-game console:
=== Mobile Platform ===
Initialized: YES
Quality: Medium
Render scale: 0.75
Shadow res: 512
Active touches: 2
Battery: 65%
Charging: NO
Battery scaling: ON
| Command | Description |
|---|---|
mobile_status |
Show mobile platform status |
The mobile platform complements the desktop Input System. A typical setup routes touch events through the mobile platform while keyboard/mouse events go through the standard input system:
#if defined(SPARK_PLATFORM_IOS) || defined(SPARK_PLATFORM_ANDROID)
Spark::Mobile::MobilePlatform mobile;
mobile.Initialize();
// Route OS touch events to mobile platform
#else
// Use standard keyboard/mouse input
inputSystem.Initialize();
#endifQuality settings from the mobile platform should be applied to the Rendering and Graphics system at initialization and whenever the preset changes:
void ApplyMobileQuality(const MobileQualitySettings& settings)
{
auto& renderer = EngineContext::Get<GraphicsEngine>();
renderer.SetResolutionScale(settings.renderScale);
renderer.SetShadowResolution(settings.shadowResolution);
renderer.SetPostProcessingEnabled(settings.enablePostProcessing);
renderer.SetParticlesEnabled(settings.enableParticles);
renderer.SetMaxDrawCalls(settings.maxDrawCalls);
renderer.SetTextureQuality(settings.textureQuality);
renderer.SetSSAOEnabled(settings.enableSSAO);
renderer.SetLODBias(settings.lodBias);
}Use safe area insets from the mobile platform to ensure UI elements are not obscured by hardware features:
void LayoutMobileUI(const MobilePlatform& mobile)
{
const auto& safe = mobile.GetSafeArea();
// Offset all root UI containers by the safe area
uiRoot.SetMargins(safe.left, safe.top, safe.right, safe.bottom);
}-
Touch processing:
ProcessTouchEvent()uses a linear scan of the active touches list. With typical mobile multi-touch (up to 10 simultaneous touches), this is negligible. -
Gesture recognition:
RecognizeGestures()is called once per frame inUpdate()and processes only active touches. No allocations occur during recognition. - Battery polling: Battery level is read from OS APIs. The mobile platform caches the value and does not poll the OS every frame; updates arrive via platform callbacks.
-
Quality switching: Changing quality presets via
SetQualityPreset()only updates the internalMobileQualitySettingsstruct. The rendering system must read these values and apply them, which may cause a brief stutter if shadow maps are reallocated.
| Problem | Cause | Solution |
|---|---|---|
Initialize() returns false
|
Platform support not compiled | Ensure ENABLE_MOBILE=ON in CMake |
| No gestures recognized |
Update() not called each frame |
Call mobile.Update(deltaTime) in the game loop |
Battery level always -1
|
Running on desktop or simulator | Expected behavior; battery API is hardware-dependent |
| Quality not changing on low battery |
SetBatteryAwareScaling(false) was called |
Ensure battery-aware scaling is enabled |
| Touch coordinates seem wrong | Not using normalized coordinates | Touch x/y are [0, 1]; multiply by screen size if needed |
| Safe area insets are all zero | Device has no notch or home indicator | Expected; safe area is only nonzero on notched devices |
| Gesture callbacks not firing | Callbacks registered after Update()
|
Register callbacks before the first Update() call |
- Touch events arrive via
UITouchin theUIViewresponder chain. The platform layer converts these toTouchEventstructs and callsProcessTouchEvent(). -
GetBatteryLevel()maps toUIDevice.current.batteryLevel. -
GetSafeArea()maps toUIView.safeAreaInsets.
- Touch events arrive via
MotionEventin theActivityorSurfaceView. The platform layer converts action types (ACTION_DOWN,ACTION_MOVE,ACTION_UP,ACTION_CANCEL) toTouchEventType. -
GetBatteryLevel()reads fromBatteryManager. -
GetSafeArea()usesWindowInsetsAPI (API level 28+) for display cutout insets.
- Input System — Keyboard/mouse input on desktop
- Rendering and Graphics — Quality settings and render scale
- UI System — Touch-friendly UI layout
- Build System and CMake Modules — Mobile build configuration
Published from 2b03dc797148. Edit the canonical source in wiki/.
- Documentation
- Docs route
- Wiki index
- Guides
- Tutorials
- Samples
- Examples
- API Reference
- API route
- Reference
- Build Guide
- Dependencies
- FAQ
- Changelog
- Roadmap
- Contributing
- Code of Conduct
- Home
- FAQ
- Getting Started
- Quick-Start Tutorial
- Making Your First Game
- Making Your First Multiplayer Game
- Artist Workflow Guide
- Editor Walkthrough
- Migration Guide
- How SparkEngine Works
- Architecture Overview
- Engine Architecture Flowchart
- Creating a Game Module
- Game Modules (catalog)
- Entity Component System
- Rendering and Graphics
- Physics
- Cloth Simulation
- Audio
- Input System
- Camera System
- Scripting with AngelScript
- Visual Scripting
- AI and Navigation
- Animation
- 2D Systems
- Networking
- Dedicated Server
- Multiplayer Quick Start
- Area Server Architecture
- Scene Management
- Large World Support
- Collaborative Editing
- Coroutine System
- Event System
- Event Response System
- Job System
- UI System
- UI Layout Extensions
- Localization
- Dialogue System
- Destruction System
- Replay System
- Achievement System
- Loading System
- Mod System
- Content Delivery
- Tween System
- Memory Integrity
- Gameplay Systems
- Terrain and Procedural Generation
- Save System
- Persistence System
- Day Night Cycle and Weather
- Cinematic Sequencer
- Runtime Prefabs
- SparkEditor
- Editor Tutorials
- SparkConsole
- SparkDaemon
- Shader Pipeline
- Asset Pipeline
- Asset Validation
- Asset Migration
- Game Packaging
- Online Services
- DataTable System
- Loot and Crafting System
- CSG System
- Font System
- Timer Manager
- Movie Render Pipeline
- HLOD and World Partition
- Remote Debug System
- Selection Manager
- Asset Dependency Graph
- Editor Automation
- File Watcher
- Project Templates
- System Requirements
- VR Support
- Mobile Platform
- Accessibility
- Platform Input
- Platform Certification
- Cross-Compilation: Wine Testing
- RHI Abstraction Layer
- D3D11 Backend
- D3D12 Backend
- Vulkan Backend
- OpenGL Backend
- Metal Backend
- DXR Raytracing
- Hybrid Ray Tracing
- Upscaling (DLSS/FSR)
- Render Graph
- Shader Graph
- GPU Particles
- GPU-Driven Rendering
- Volumetric Fog
- Volumetric Clouds
- Global Illumination
- Virtual Texturing
- Water Rendering
- Clustered Lighting
- Material System
- Post-Processing
- Shadow System
- Particle System
- Decal System
- Sky and Atmosphere
- Foliage System
- Mesh Shaders
- Neural Rendering
- Configuration Reference
- Performance Tips
- Benchmark Framework
- Threading Model
- Fuzz Policy and Parser Security
- Memory Safety
- Memory Management Patterns
- Build System and CMake Modules
- Profiler and Debugging
- Performance Profiling Guide
- Telemetry System
- Crash Reporting
- Golden Image Testing
- Utilities
- Testing
- Fuzz Policy and Parser Security
- Codebase Statistics
- Codebase Health
- Error Handling Patterns
- Hot Reload Overview
- Troubleshooting
- Contributing
- Workflow Patterns
- Build Optimizations
- CI Reproducible Builds
- GitHub API and PR Checks
- Git Rebase Conflicts
- Clang-Format
- Code Quality Violations
- AI Bloat Pattern
- MinGW + Wine Cross-Compilation
- Live Editor Testing
- Engine & Renderer Landscape
- DuetOS Portability Catalog
- Five-Engine Analysis
- Eleven-Engine Analysis
- ThorVG / Unity Graphics Analysis
- Advanced Techniques Catalog
- Third-Party Library Evaluation
- Engine Viability Evaluation
- Engine Feature Recommendations
- Project Recommendations
- Mac Compatibility Analysis
- Codebase Observations
- Codebase Bloat Audit
- Test Suite Audit
- Documentation Coverage Audit
- ThirdParty Dependencies Audit
- Load Test Baseline
- Gameplay Systems Status
- SparkGame Module Status
- Stub and Abandoned Features
- Memory Integrity System
- Memory Safety Evaluation
- Hardware Acceleration Systems
- Jolt Physics Integration
- GPU/CPU Separation Plan
- Daemon Services Architecture
- Reflection & Polymorphism Refactoring Plan
- SparkBuild In-Tree
- Wine No-JobSystem Breakthrough
- Wine Role and Fallback Tiers