Unity3D Plugins ¿Que son?

Hoy decidí compartir, algo que creo que no es un “conocimiento común” y no hay muchos recursos sobre ese tema en Internet, después de que varios colegas que están empezando en este mundo, me preguntasen como poder usar APIs en otros lenguajes en Unity3D, como por ejemplo el SKD de Steam que esta en C++ o las librerias .JAR de Android, decidí crear una entrada nueva. Mostrare como crear bibliotecas simples de C/C++ para Unity3D.

Introducción

Alguna vez te has preguntado, ¿Como los modders crean funcionalidad para los juegos?, pues la respuesta es esta, crean plugins que interfieren con la API publica de los juegos (Variables, métodos, funciones públicas) y luego con lenguaje pegamento crean la llamada si es que no se puede llamar desde el mismo core.


Puede preguntar, ¿por que debería molestarme en escribir C o C++ cuando Unity admite C#?

La respuesta es simple. Es mucho más rápido, trabajar a  bajo nivel cambia mucho las cosas. Decidí preparar algunos ejemplos solo para mostrar como C++ supera a C# basado en Mono.

Ejemplos:

Prueba de Estrés Unity C# Biblioteca C/C++
Array de enteros de 10000 x 10000 1.0258 segundos 0.2346 segundos
Array de enteros de 500 x 500 3.794 milisegundos 0.35 milisegundos
Array de enteros de 10000 x 10000 con números aleatorios 4.26 segundos 0.995 segundos

Pros y contras de C/C++ y Unity3D

Pros

  1. Triplica (en algunos casos) la velocidad de calculo.
  2. Las dlls no afectan al rendimiento de unity.

Contras

  1. Unity no admite directamente C++.
  2. La velocidad que se ahorra en runtime, se pierde realizando las dlls.

Crear biblioteca C++

En este caso, yo usare  Code::Blocks, pero podéis usar cualquier IDE que queráis. Voy a crear un proyecto de librería dinámica (.dll) llamado LowLevelPlugin. Ya que vamos a trabajar a bajo nivel.

Nos iremos al encabezado main.h y eliminaremos todo el contenido que se ha generado automáticamente.


#ifndef __MAIN_H__
#define __MAIN_H__

#include <windows.h>

#ifdef BUILD_DLL
    #define DLL_EXPORT __declspec(dllexport)
#else
    #define DLL_EXPORT __declspec(dllimport)
#endif


#ifdef __cplusplus
extern "C"
{
#endif

void DLL_EXPORT SomeFunction(const LPCSTR sometext);

#ifdef __cplusplus
}
#endif

#endif // __MAIN_H__

En su caso colocaremos esto otro ->


#pragma once
#if UNITY_METRO
#define EXPORT_API __declspec(dllexport) __stdcall
#elif UNITY_WIN
#define EXPORT_API __declspec(dllexport)
#else
#define EXPORT_API
#endif

El propósito de este encabezado completo es comportarse de manera diferente basándose en qué plataforma está compilando actualmente su código. Significa que puede compilarlo fácilmente en Visual Studio.


#pragma once

  • #pragma once es una directiva específica C/C++ diseñada para hacer que el archivo fuente actual se incluya solo una vez en una sola compilación.

#if UNITY_METRO

  • Esto le dice al compilador que ejecute este bloque si solo estamos corriendo en Windows basado en Metro, por ejemplo, 8.1. Esto os sonara de Unity si habéis usado las  directivas de compilación de plataforma.

#define EXPORT_API __declspec(dllexport) __stdcall

  • La palabra clave “define” funciona de manera similar a la función replace, su sintaxis se puede definir de la siguiente manera:

define ‘Esto’ ‘Por esto’.

En nuestro caso, cada vez que el compilador ve la secuencia de caracteres EXPORT_API en el código, la reemplaza con la segunda parte. Del mismo modo, el compilador se comportará en caso de que compile su código en ventanas antiguas, reemplace EXPORT_API con __declspec(dllexport) y simplemente elimine EXPORT_API en cualquier otro caso.

Eso es todo cuando se trata de encabezado. Ahora pasemos a algo que sea más interesante, el código fuente real. En el archivo main.cpp pegue el siguiente código:


#include <stdlib.h>
#include <math.h>
#include "main.h"

extern "C" int ** EXPORT_API fillArray(int size)
{
    int i = 0, j = 0;
    int ** array = (int**) calloc(size, sizeof(int*));

    for(i = 0; i < size; i++)
    {
        array[i] = (int*) calloc(size, sizeof(int));
        for(j = 0; j < size; j++)
        {
            array[i][j] = i * size + j;
        }
    }

    return array;
}

Las primeras tres líneas son en mi humilde opinión autoexplicativas. Simplemente estamos agregando algunas bibliotecas estándar y nuestro archivo de encabezado recién creado.

El siguiente es:


extern "C" int ** EXPORT_API fillArray(int size) {

Esta es nuestra declaración de función. Nuestra función se llama “fillArray”, toma un argumento int y devuelve una matriz de enteros bidimensional. (Sí, ** significa dos dimensiones) “extern” extiende la visibilidad a todo el programa, las funciones se pueden usar en cualquier de los archivos de todo el programa y fuera de nuestra biblioteca/plugin. También tenemos nuestra palabra clave EXPORT_API.

La siguiente linea es:


int ** array = (int**) calloc(size, sizeof(int*));

Aquí estoy definiendo int-array bidimensional y luego estoy asignando memoria para ello. Como estoy escribiendo código en C++, no puedo simplemente crear una matriz y asignar valores. Tengo que reservar algo de espacio en memoria para eso y luego hacer cualquier operación. En este caso particular, estoy asignando las siguientes “celdas” de memoria de N tamaño de int* y lo convierto al tipo bidimensional int array.

Lo mismo que estoy haciendo aquí:


array[i] = (int*) calloc(size, sizeof(int));

La única diferencia es que ahora estoy asignando memoria para cada fila “i’t” de una matriz.

Y al final estoy devolviendo un conjunto lleno. Espero que para los bucles y la asignación de valores sea obvio, así que no lo explicaré.

Ahora solo compilamos y se creara la dll.

/img/c/dll.png
.

Implementar puente en Unity3D

Copia la dll en un Proyecto Unity3D dentro de la carpeta plugins en /Assets/Plugins. También debe crear un script C# en el directorio base para que la estructura de su proyecto tenga este aspecto:

/img/c/dll2.png
.

En la parte superior de tu script C# agrega “Using System.Runtime.InteropServices;”, luego la declaración de la función de la dll, crea una matriz y por ultimo asigna a la variable la llamada.


public int size = 512;
[DllImport("LowLevelPlugin")]
private static extern int[,] fillArray(int size);

private void Start()
{
	ArrayFillTest();
}

private void ArrayFillTest()
{
	var start = Time.realtimeSinceStartup;
	int[,] tab = fillArray(size);
	Debug.Log((Time.realtimeSinceStartup - start).ToString("f6") + " secs");
	start = Time.realtimeSinceStartup;
	int[,] array = new int[size, size];
	for (int i = 0; i < size; i++)
	{
		for (int j = 0; j < size; j++)
		{
			array[i, j] = i * size + j;
		}
	}
	Debug.Log((Time.realtimeSinceStartup - start).ToString("f6") + " secs");
}

Conclusiones
Siempre que ejecutes código de bajo nivel, tendrás mejores resultados.

GitHub
/img/ref.png
.