Extensiones Nativas y Integración con C++
Extensiones Nativas y Integración con C++
Las extensiones nativas permiten integrar código C++ de alto rendimiento, acceder a APIs específicas de plataforma y utilizar bibliotecas externas. En esta lección aprenderás a crear y optimizar extensiones nativas para Defold.
🔧 Fundamentos de Extensiones Nativas
Estructura de Proyecto
/myextension/
├── ext.manifest
├── src/
│ ├── extension.cpp
│ └── extension.h
├── lib/
│ ├── android/
│ │ └── libmylib.a
│ ├── ios/
│ │ └── libmylib.a
│ └── common/
│ └── mylib.h
├── manifests/
│ ├── android/
│ │ └── AndroidManifest.xml
│ └── ios/
│ └── Info.plist
└── include/
└── myextension/
└── myextension.h
ext.manifest Configuration
# ext.manifest
name: "MyExtension"
platforms:
android:
context:
aaptExtraPackages: ["com.example.myextension"]
libs: ["android/libmylib.a"]
linkFlags: ["-llog", "-landroid"]
jetifier: true
proguard: "proguard-project.txt"
ios:
libs: ["ios/libmylib.a"]
frameworks: ["CoreMotion", "CoreLocation", "StoreKit"]
linkFlags: ["-ObjC"]
osx:
libs: ["osx/libmylib.a"]
frameworks: ["CoreServices"]
win32:
libs: ["win32/mylib.lib"]
linux:
libs: ["linux/libmylib.a"]
html5:
libs: ["js-web/library_myextension.js"]
💻 Extensión Básica en C++
Header File (extension.h)
// extension.h
#pragma once
#include <dmsdk/sdk.h>
#if defined(DM_PLATFORM_ANDROID)
#include <jni.h>
#include <android/log.h>
#endif
#if defined(DM_PLATFORM_IOS) || defined(DM_PLATFORM_OSX)
#include <Foundation/Foundation.h>
#endif
namespace myextension {
// Estructura para datos de configuración
struct ExtensionConfig {
bool enabled;
float update_frequency;
int max_items;
};
// Funciones principales
int Initialize();
int Finalize();
void Update(float dt);
// Funciones específicas de plataforma
#if defined(DM_PLATFORM_ANDROID)
void InitializeAndroid(JNIEnv* env, jobject activity);
#endif
#if defined(DM_PLATFORM_IOS)
void InitializeIOS();
#endif
// Funciones exportadas a Lua
int LuaGetDeviceInfo(lua_State* L);
int LuaStartService(lua_State* L);
int LuaStopService(lua_State* L);
int LuaGetSensorData(lua_State* L);
} // namespace myextension
Implementation (extension.cpp)
// extension.cpp
#include "extension.h"
#include <cmath>
#include <vector>
#include <string>
namespace myextension {
static ExtensionConfig g_config = {true, 60.0f, 100};
static std::vector<float> g_sensor_data;
static bool g_service_running = false;
// === Platform-specific implementations ===
#if defined(DM_PLATFORM_ANDROID)
static JNIEnv* g_env = nullptr;
static jobject g_activity = nullptr;
void InitializeAndroid(JNIEnv* env, jobject activity) {
g_env = env;
g_activity = activity;
__android_log_print(ANDROID_LOG_INFO, "MyExtension", "Android initialization complete");
}
// JNI function to call Java methods
jstring CallJavaMethod(const char* method_name, const char* signature) {
if (!g_env || !g_activity) return nullptr;
jclass activity_class = g_env->GetObjectClass(g_activity);
jmethodID method_id = g_env->GetMethodID(activity_class, method_name, signature);
if (method_id) {
return (jstring)g_env->CallObjectMethod(g_activity, method_id);
}
g_env->DeleteLocalRef(activity_class);
return nullptr;
}
#elif defined(DM_PLATFORM_IOS)
void InitializeIOS() {
NSLog(@"MyExtension: iOS initialization complete");
}
NSString* GetIOSDeviceInfo() {
UIDevice* device = [UIDevice currentDevice];
return [NSString stringWithFormat:@"%@ %@ %@",
device.model, device.systemName, device.systemVersion];
}
#endif
// === Cross-platform implementation ===
int Initialize() {
dmLogInfo("MyExtension: Initializing...");
// Configuración inicial
g_config.enabled = true;
g_sensor_data.reserve(1000);
#if defined(DM_PLATFORM_ANDROID)
// La inicialización Android se hace en app_initialize
#elif defined(DM_PLATFORM_IOS)
InitializeIOS();
#endif
dmLogInfo("MyExtension: Initialization complete");
return 0;
}
int Finalize() {
dmLogInfo("MyExtension: Finalizing...");
g_sensor_data.clear();
g_service_running = false;
dmLogInfo("MyExtension: Finalization complete");
return 0;
}
void Update(float dt) {
if (!g_config.enabled || !g_service_running) return;
// Simular datos de sensor
static float time_accumulator = 0.0f;
time_accumulator += dt;
if (time_accumulator >= 1.0f / g_config.update_frequency) {
float sensor_value = std::sin(time_accumulator) * 100.0f;
g_sensor_data.push_back(sensor_value);
// Mantener solo los últimos 100 valores
if (g_sensor_data.size() > g_config.max_items) {
g_sensor_data.erase(g_sensor_data.begin());
}
time_accumulator = 0.0f;
}
}
// === Lua bindings ===
int LuaGetDeviceInfo(lua_State* L) {
std::string device_info = "Unknown";
#if defined(DM_PLATFORM_ANDROID)
jstring java_result = CallJavaMethod("getDeviceInfo", "()Ljava/lang/String;");
if (java_result) {
const char* chars = g_env->GetStringUTFChars(java_result, nullptr);
device_info = std::string(chars);
g_env->ReleaseStringUTFChars(java_result, chars);
g_env->DeleteLocalRef(java_result);
}
#elif defined(DM_PLATFORM_IOS)
NSString* ios_info = GetIOSDeviceInfo();
device_info = std::string([ios_info UTF8String]);
#elif defined(DM_PLATFORM_WIN32)
device_info = "Windows PC";
#elif defined(DM_PLATFORM_LINUX)
device_info = "Linux PC";
#elif defined(DM_PLATFORM_OSX)
device_info = "macOS";
#endif
lua_pushstring(L, device_info.c_str());
return 1;
}
int LuaStartService(lua_State* L) {
if (lua_gettop(L) >= 1) {
g_config.update_frequency = luaL_checknumber(L, 1);
}
g_service_running = true;
lua_pushboolean(L, 1);
dmLogInfo("MyExtension: Service started with frequency %.2f", g_config.update_frequency);
return 1;
}
int LuaStopService(lua_State* L) {
g_service_running = false;
g_sensor_data.clear();
lua_pushboolean(L, 1);
dmLogInfo("MyExtension: Service stopped");
return 1;
}
int LuaGetSensorData(lua_State* L) {
lua_newtable(L);
for (size_t i = 0; i < g_sensor_data.size(); ++i) {
lua_pushnumber(L, g_sensor_data[i]);
lua_rawseti(L, -2, i + 1);
}
return 1;
}
// === Extension lifecycle ===
static const luaL_Reg lua_functions[] = {
{"get_device_info", LuaGetDeviceInfo},
{"start_service", LuaStartService},
{"stop_service", LuaStopService},
{"get_sensor_data", LuaGetSensorData},
{nullptr, nullptr}
};
static dmExtension::Result AppInitialize(dmExtension::AppParams* params) {
dmLogInfo("MyExtension: App Initialize");
return dmExtension::RESULT_OK;
}
static dmExtension::Result Initialize(dmExtension::Params* params) {
dmLogInfo("MyExtension: Extension Initialize");
// Registrar funciones Lua
luaL_register(params->m_L, "myextension", lua_functions);
lua_pop(params->m_L, 1);
return Initialize() == 0 ? dmExtension::RESULT_OK : dmExtension::RESULT_INIT_ERROR;
}
static dmExtension::Result AppFinalize(dmExtension::AppParams* params) {
dmLogInfo("MyExtension: App Finalize");
return dmExtension::RESULT_OK;
}
static dmExtension::Result Finalize(dmExtension::Params* params) {
dmLogInfo("MyExtension: Extension Finalize");
return Finalize() == 0 ? dmExtension::RESULT_OK : dmExtension::RESULT_UNKNOWN_ERROR;
}
static dmExtension::Result OnUpdate(dmExtension::Params* params) {
Update(params->m_DT);
return dmExtension::RESULT_OK;
}
} // namespace myextension
// Registro de la extensión
DM_DECLARE_EXTENSION(MyExtension, "MyExtension", myextension::AppInitialize, myextension::AppFinalize, myextension::Initialize, 0, myextension::OnUpdate, myextension::Finalize);
📱 Integración Android (JNI)
Java Bridge Class
// MyExtensionActivity.java
package com.example.myextension;
import android.app.Activity;
import android.content.Context;
import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
import android.os.Build;
import android.util.Log;
public class MyExtensionActivity extends Activity implements SensorEventListener {
private static final String TAG = "MyExtension";
private SensorManager sensorManager;
private Sensor accelerometer;
private float[] lastAccelerometer = new float[3];
private boolean isAccelerometerActive = false;
public void initialize() {
Log.i(TAG, "Java bridge initializing...");
sensorManager = (SensorManager) getSystemService(Context.SENSOR_SERVICE);
accelerometer = sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
if (accelerometer != null) {
sensorManager.registerListener(this, accelerometer, SensorManager.SENSOR_DELAY_GAME);
isAccelerometerActive = true;
Log.i(TAG, "Accelerometer activated");
}
}
public String getDeviceInfo() {
return String.format("%s %s (API %d)",
Build.MANUFACTURER,
Build.MODEL,
Build.VERSION.SDK_INT);
}
public float[] getAccelerometerData() {
if (isAccelerometerActive) {
return lastAccelerometer.clone();
}
return new float[]{0, 0, 0};
}
public boolean startVibration(int duration) {
try {
android.os.Vibrator vibrator = (android.os.Vibrator) getSystemService(Context.VIBRATOR_SERVICE);
if (vibrator != null && vibrator.hasVibrator()) {
vibrator.vibrate(duration);
return true;
}
} catch (Exception e) {
Log.e(TAG, "Vibration failed: " + e.getMessage());
}
return false;
}
@Override
public void onSensorChanged(SensorEvent event) {
if (event.sensor.getType() == Sensor.TYPE_ACCELEROMETER) {
System.arraycopy(event.values, 0, lastAccelerometer, 0, event.values.length);
}
}
@Override
public void onAccuracyChanged(Sensor sensor, int accuracy) {
// No necesario para este ejemplo
}
@Override
protected void onDestroy() {
super.onDestroy();
if (sensorManager != null) {
sensorManager.unregisterListener(this);
}
}
}
JNI Bridge Functions
// android_bridge.cpp
#if defined(DM_PLATFORM_ANDROID)
#include <jni.h>
#include <android/log.h>
extern "C" {
// Funciones llamadas desde Java
JNIEXPORT void JNICALL
Java_com_example_myextension_MyExtensionActivity_nativeAccelerometerUpdate(
JNIEnv *env, jobject thiz, jfloatArray values) {
jfloat* array_elements = env->GetFloatArrayElements(values, nullptr);
jsize array_length = env->GetArrayLength(values);
if (array_length >= 3) {
// Procesar datos del acelerómetro
float x = array_elements[0];
float y = array_elements[1];
float z = array_elements[2];
// Enviar datos al sistema de extensión
myextension::UpdateAccelerometerData(x, y, z);
}
env->ReleaseFloatArrayElements(values, array_elements, JNI_ABORT);
}
JNIEXPORT jstring JNICALL
Java_com_example_myextension_MyExtensionActivity_nativeGetVersion(
JNIEnv *env, jobject thiz) {
return env->NewStringUTF("MyExtension v1.0.0");
}
} // extern "C"
namespace myextension {
// Funciones helper para JNI
jclass FindClass(JNIEnv* env, const char* class_name) {
jclass clazz = env->FindClass(class_name);
if (!clazz) {
__android_log_print(ANDROID_LOG_ERROR, "MyExtension",
"Failed to find class: %s", class_name);
}
return clazz;
}
jmethodID GetMethodID(JNIEnv* env, jclass clazz, const char* method_name, const char* signature) {
jmethodID method = env->GetMethodID(clazz, method_name, signature);
if (!method) {
__android_log_print(ANDROID_LOG_ERROR, "MyExtension",
"Failed to find method: %s", method_name);
}
return method;
}
// Llamar método Java desde C++
bool CallJavaVibrate(int duration) {
if (!g_env || !g_activity) return false;
jclass activity_class = g_env->GetObjectClass(g_activity);
jmethodID vibrate_method = GetMethodID(g_env, activity_class, "startVibration", "(I)Z");
if (vibrate_method) {
jboolean result = g_env->CallBooleanMethod(g_activity, vibrate_method, duration);
g_env->DeleteLocalRef(activity_class);
return result;
}
g_env->DeleteLocalRef(activity_class);
return false;
}
} // namespace myextension
#endif // DM_PLATFORM_ANDROID
🍎 Integración iOS (Objective-C)
Objective-C Bridge
// ios_bridge.mm
#if defined(DM_PLATFORM_IOS)
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
#import <CoreMotion/CoreMotion.h>
#import <AVFoundation/AVFoundation.h>
@interface MyExtensionBridge : NSObject
@property (strong, nonatomic) CMMotionManager *motionManager;
@property (strong, nonatomic) AVAudioPlayer *audioPlayer;
@end
@implementation MyExtensionBridge
- (instancetype)init {
self = [super init];
if (self) {
self.motionManager = [[CMMotionManager alloc] init];
[self setupMotionUpdates];
}
return self;
}
- (void)setupMotionUpdates {
if (self.motionManager.accelerometerAvailable) {
self.motionManager.accelerometerUpdateInterval = 1.0 / 60.0; // 60 Hz
[self.motionManager startAccelerometerUpdatesToQueue:[NSOperationQueue mainQueue]
withHandler:^(CMAccelerometerData *data, NSError *error) {
if (!error) {
myextension::UpdateAccelerometerData(data.acceleration.x,
data.acceleration.y,
data.acceleration.z);
}
}];
}
}
- (NSString*)getDeviceInfo {
UIDevice *device = [UIDevice currentDevice];
return [NSString stringWithFormat:@"%@ %@ %@",
device.model, device.systemName, device.systemVersion];
}
- (BOOL)playSound:(NSString*)filename {
NSString *soundPath = [[NSBundle mainBundle] pathForResource:filename ofType:nil];
if (!soundPath) return NO;
NSURL *soundURL = [NSURL fileURLWithPath:soundPath];
NSError *error;
self.audioPlayer = [[AVAudioPlayer alloc] initWithContentsOfURL:soundURL error:&error];
if (error) {
NSLog(@"Audio player error: %@", error.localizedDescription);
return NO;
}
[self.audioPlayer play];
return YES;
}
- (void)vibrate {
AudioServicesPlaySystemSound(kSystemSoundID_Vibrate);
}
- (void)dealloc {
[self.motionManager stopAccelerometerUpdates];
}
@end
// C++ wrapper functions
namespace myextension {
static MyExtensionBridge* g_bridge = nullptr;
void InitializeIOSBridge() {
g_bridge = [[MyExtensionBridge alloc] init];
}
void FinalizeIOSBridge() {
g_bridge = nullptr;
}
std::string GetIOSDeviceInfo() {
if (g_bridge) {
NSString* info = [g_bridge getDeviceInfo];
return std::string([info UTF8String]);
}
return "iOS Device";
}
bool PlayIOSSound(const char* filename) {
if (g_bridge) {
NSString* file = [NSString stringWithUTF8String:filename];
return [g_bridge playSound:file];
}
return false;
}
void TriggerIOSVibration() {
if (g_bridge) {
[g_bridge vibrate];
}
}
} // namespace myextension
#endif // DM_PLATFORM_IOS
⚡ Optimización de Rendimiento
Threading y Async Operations
// threaded_extension.cpp
#include <thread>
#include <mutex>
#include <queue>
#include <condition_variable>
#include <atomic>
namespace myextension {
// Estructura para tareas asíncronas
struct AsyncTask {
std::function<void()> function;
std::function<void()> callback;
int priority;
};
class ThreadPool {
private:
std::vector<std::thread> workers;
std::queue<AsyncTask> tasks;
std::mutex queue_mutex;
std::condition_variable condition;
std::atomic<bool> stop{false};
public:
ThreadPool(size_t num_threads = std::thread::hardware_concurrency()) {
for (size_t i = 0; i < num_threads; ++i) {
workers.emplace_back([this] {
while (!stop) {
AsyncTask task;
{
std::unique_lock<std::mutex> lock(queue_mutex);
condition.wait(lock, [this] { return stop || !tasks.empty(); });
if (stop && tasks.empty()) return;
task = std::move(tasks.front());
tasks.pop();
}
// Ejecutar tarea
task.function();
// Ejecutar callback en main thread si existe
if (task.callback) {
// Aquí normalmente añadirías el callback a una cola para main thread
main_thread_callbacks.push(task.callback);
}
}
});
}
}
~ThreadPool() {
stop = true;
condition.notify_all();
for (std::thread& worker : workers) {
if (worker.joinable()) {
worker.join();
}
}
}
void EnqueueTask(AsyncTask task) {
{
std::unique_lock<std::mutex> lock(queue_mutex);
tasks.push(std::move(task));
}
condition.notify_one();
}
private:
std::queue<std::function<void()>> main_thread_callbacks;
};
static std::unique_ptr<ThreadPool> g_thread_pool;
// Función para procesar callbacks en main thread
void ProcessMainThreadCallbacks() {
// Esta función debe llamarse desde el main thread regularmente
while (!main_thread_callbacks.empty()) {
auto callback = main_thread_callbacks.front();
main_thread_callbacks.pop();
callback();
}
}
// Ejemplo de tarea pesada asíncrona
void PerformHeavyCalculation(const std::vector<float>& data,
std::function<void(float)> on_complete) {
AsyncTask task;
task.function = [data]() {
// Simulación de cálculo pesado
float result = 0.0f;
for (float value : data) {
result += std::sqrt(value * value + 1.0f);
std::this_thread::sleep_for(std::chrono::microseconds(1));
}
return result;
};
task.callback = [on_complete, result = 0.0f]() mutable {
on_complete(result);
};
g_thread_pool->EnqueueTask(std::move(task));
}
} // namespace myextension
Memory Management
// memory_manager.cpp
namespace myextension {
// Pool de memoria personalizado
template<typename T, size_t PoolSize>
class ObjectPool {
private:
alignas(T) char storage[PoolSize * sizeof(T)];
std::bitset<PoolSize> used;
std::mutex pool_mutex;
public:
template<typename... Args>
T* Acquire(Args&&... args) {
std::lock_guard<std::mutex> lock(pool_mutex);
for (size_t i = 0; i < PoolSize; ++i) {
if (!used[i]) {
used[i] = true;
T* obj = reinterpret_cast<T*>(&storage[i * sizeof(T)]);
new(obj) T(std::forward<Args>(args)...);
return obj;
}
}
// Pool lleno, usar memoria dinámica como fallback
dmLogWarning("ObjectPool full, falling back to dynamic allocation");
return new T(std::forward<Args>(args)...);
}
void Release(T* obj) {
std::lock_guard<std::mutex> lock(pool_mutex);
// Verificar si el objeto está en el pool
char* obj_ptr = reinterpret_cast<char*>(obj);
if (obj_ptr >= storage && obj_ptr < storage + sizeof(storage)) {
size_t index = (obj_ptr - storage) / sizeof(T);
if (index < PoolSize && used[index]) {
obj->~T();
used[index] = false;
return;
}
}
// Objeto no está en el pool, eliminar dinámicamente
delete obj;
}
};
// Ejemplo de uso
static ObjectPool<SensorData, 1000> g_sensor_pool;
SensorData* CreateSensorData(float x, float y, float z) {
return g_sensor_pool.Acquire(x, y, z);
}
void DestroySensorData(SensorData* data) {
g_sensor_pool.Release(data);
}
} // namespace myextension
🔌 Integración de Bibliotecas Externas
Ejemplo: OpenCV Integration
// opencv_extension.cpp
#ifdef USE_OPENCV
#include <opencv2/opencv.hpp>
#include <opencv2/imgproc.hpp>
namespace myextension {
class ImageProcessor {
private:
cv::Mat current_frame;
cv::CascadeClassifier face_cascade;
public:
bool Initialize() {
// Cargar clasificador de caras
std::string cascade_path = "/assets/haarcascade_frontalface_alt.xml";
if (!face_cascade.load(cascade_path)) {
dmLogError("Failed to load face cascade");
return false;
}
dmLogInfo("OpenCV Image Processor initialized");
return true;
}
std::vector<cv::Rect> DetectFaces(const uint8_t* image_data,
int width, int height, int channels) {
cv::Mat image(height, width, channels == 4 ? CV_8UC4 : CV_8UC3,
const_cast<uint8_t*>(image_data));
cv::Mat gray;
if (channels > 1) {
cv::cvtColor(image, gray, cv::COLOR_RGB2GRAY);
} else {
gray = image;
}
std::vector<cv::Rect> faces;
face_cascade.detectMultiScale(gray, faces, 1.1, 3, 0, cv::Size(30, 30));
return faces;
}
bool ApplyFilter(uint8_t* image_data, int width, int height,
int channels, const std::string& filter_type) {
cv::Mat image(height, width, channels == 4 ? CV_8UC4 : CV_8UC3, image_data);
if (filter_type == "blur") {
cv::GaussianBlur(image, image, cv::Size(15, 15), 0);
} else if (filter_type == "edge") {
cv::Mat gray, edges;
cv::cvtColor(image, gray, cv::COLOR_RGB2GRAY);
cv::Canny(gray, edges, 100, 200);
cv::cvtColor(edges, image, cv::COLOR_GRAY2RGB);
} else if (filter_type == "sepia") {
cv::Mat kernel = (cv::Mat_<float>(4, 4) <<
0.272, 0.534, 0.131, 0,
0.349, 0.686, 0.168, 0,
0.393, 0.769, 0.189, 0,
0, 0, 0, 1);
cv::transform(image, image, kernel);
}
return true;
}
};
static std::unique_ptr<ImageProcessor> g_image_processor;
// Funciones Lua
int LuaProcessImage(lua_State* L) {
if (!g_image_processor) {
lua_pushboolean(L, 0);
lua_pushstring(L, "Image processor not initialized");
return 2;
}
// Obtener datos de imagen desde Lua
size_t data_size;
const char* image_data = luaL_checklstring(L, 1, &data_size);
int width = luaL_checkinteger(L, 2);
int height = luaL_checkinteger(L, 3);
int channels = luaL_optinteger(L, 4, 3);
const char* filter = luaL_optstring(L, 5, "none");
// Crear copia de los datos para modificar
std::vector<uint8_t> mutable_data(image_data, image_data + data_size);
bool success = g_image_processor->ApplyFilter(mutable_data.data(),
width, height, channels, filter);
if (success) {
lua_pushlstring(L, reinterpret_cast<char*>(mutable_data.data()), data_size);
return 1;
} else {
lua_pushnil(L);
return 1;
}
}
} // namespace myextension
#endif // USE_OPENCV
🎮 Proyecto Práctico: Sistema de Analytics Nativo
Vamos a crear una extensión completa para analytics:
1. Analytics Core
// analytics_extension.cpp
#include <json/json.h>
#include <curl/curl.h>
#include <chrono>
namespace analytics {
struct AnalyticsEvent {
std::string event_name;
Json::Value parameters;
int64_t timestamp;
std::string session_id;
};
class AnalyticsManager {
private:
std::queue<AnalyticsEvent> event_queue;
std::mutex queue_mutex;
std::string api_endpoint;
std::string api_key;
std::string session_id;
std::thread upload_thread;
std::atomic<bool> running{true};
public:
bool Initialize(const std::string& endpoint, const std::string& key) {
api_endpoint = endpoint;
api_key = key;
session_id = GenerateSessionId();
// Inicializar CURL
curl_global_init(CURL_GLOBAL_DEFAULT);
// Iniciar hilo de subida
upload_thread = std::thread(&AnalyticsManager::UploadWorker, this);
dmLogInfo("Analytics initialized with session: %s", session_id.c_str());
return true;
}
void TrackEvent(const std::string& event_name, const Json::Value& parameters = Json::Value()) {
AnalyticsEvent event;
event.event_name = event_name;
event.parameters = parameters;
event.timestamp = GetCurrentTimestamp();
event.session_id = session_id;
{
std::lock_guard<std::mutex> lock(queue_mutex);
event_queue.push(event);
}
dmLogInfo("Event tracked: %s", event_name.c_str());
}
void Shutdown() {
running = false;
if (upload_thread.joinable()) {
upload_thread.join();
}
curl_global_cleanup();
}
private:
std::string GenerateSessionId() {
auto now = std::chrono::system_clock::now();
auto timestamp = std::chrono::duration_cast<std::chrono::milliseconds>(
now.time_since_epoch()).count();
return "session_" + std::to_string(timestamp);
}
int64_t GetCurrentTimestamp() {
auto now = std::chrono::system_clock::now();
return std::chrono::duration_cast<std::chrono::milliseconds>(
now.time_since_epoch()).count();
}
void UploadWorker() {
while (running) {
std::vector<AnalyticsEvent> events_to_upload;
{
std::lock_guard<std::mutex> lock(queue_mutex);
while (!event_queue.empty() && events_to_upload.size() < 50) {
events_to_upload.push_back(event_queue.front());
event_queue.pop();
}
}
if (!events_to_upload.empty()) {
UploadEvents(events_to_upload);
}
std::this_thread::sleep_for(std::chrono::seconds(30));
}
}
bool UploadEvents(const std::vector<AnalyticsEvent>& events) {
CURL* curl = curl_easy_init();
if (!curl) return false;
// Construir JSON
Json::Value batch;
batch["session_id"] = session_id;
batch["events"] = Json::Value(Json::arrayValue);
for (const auto& event : events) {
Json::Value json_event;
json_event["name"] = event.event_name;
json_event["timestamp"] = event.timestamp;
json_event["parameters"] = event.parameters;
batch["events"].append(json_event);
}
Json::StreamWriterBuilder builder;
std::string json_data = Json::writeString(builder, batch);
// Configurar CURL
curl_easy_setopt(curl, CURLOPT_URL, api_endpoint.c_str());
curl_easy_setopt(curl, CURLOPT_POSTFIELDS, json_data.c_str());
struct curl_slist* headers = nullptr;
std::string auth_header = "Authorization: Bearer " + api_key;
headers = curl_slist_append(headers, "Content-Type: application/json");
headers = curl_slist_append(headers, auth_header.c_str());
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
CURLcode res = curl_easy_perform(curl);
curl_slist_free_all(headers);
curl_easy_cleanup(curl);
if (res == CURLE_OK) {
dmLogInfo("Uploaded %zu analytics events", events.size());
return true;
} else {
dmLogError("Failed to upload analytics: %s", curl_easy_strerror(res));
return false;
}
}
};
static std::unique_ptr<AnalyticsManager> g_analytics;
// Funciones Lua
int LuaInitializeAnalytics(lua_State* L) {
const char* endpoint = luaL_checkstring(L, 1);
const char* api_key = luaL_checkstring(L, 2);
g_analytics = std::make_unique<AnalyticsManager>();
bool success = g_analytics->Initialize(endpoint, api_key);
lua_pushboolean(L, success);
return 1;
}
int LuaTrackEvent(lua_State* L) {
if (!g_analytics) {
lua_pushboolean(L, 0);
return 1;
}
const char* event_name = luaL_checkstring(L, 1);
Json::Value parameters;
if (lua_istable(L, 2)) {
// Convertir tabla Lua a JSON
parameters = LuaTableToJson(L, 2);
}
g_analytics->TrackEvent(event_name, parameters);
lua_pushboolean(L, 1);
return 1;
}
} // namespace analytics
📚 Recursos y Referencias
APIs de Defold SDK
dmExtension::*- Lifecycle de extensionesdmScript::*- Integración con LuadmLogInfo/Error/Warning- Sistema de loggingdmMath::*- Funciones matemáticas
Herramientas de Desarrollo
- CMake - Sistema de build multiplataforma
- vcpkg - Gestor de paquetes C++
- Conan - Gestor de dependencias
- clang-format - Formato de código
Links Útiles
🎯 Ejercicios Propuestos
-
Camera Extension: Crea una extensión que acceda a la cámara del dispositivo para capturar imágenes.
-
File System Bridge: Implementa acceso completo al sistema de archivos nativo.
-
Social SDK Integration: Integra un SDK de redes sociales (Facebook, Twitter) usando extensiones nativas.
-
Performance Monitor: Crea un monitor de rendimiento que reporte métricas del dispositivo.
-
Bluetooth Extension: Implementa comunicación Bluetooth para juegos multijugador local.
Las extensiones nativas son el puente hacia el poder completo de las plataformas nativas. Dominando C++ y las APIs específicas de cada plataforma, podrás integrar cualquier funcionalidad que tu juego necesite.