Jump to content

Bem vindo à Unidev
Registre para ter acesso a todos os recursos do site. Uma vez registrado e logado, você poderá criar tópicos, postar em tópicos já existentes, gerenciar seu perfil e muito mais. Se você já tem uma conta, faça login aqui - ou então crie aqui uma conta agora mesmo!
- - - - -
Photo

Usando DLL gerenciada em C# no UDK


por Donald May

O seguinte artigo é similar aos artigos que postei nos fóruns da UDK e no meu blog. Este artigo foi atualizado para usar a última versão do Visual Studio Express e a última versão do Unreal Development Kit.

Para seguir este artigo você vai precisar de uma cópia do Unreal Development Kit [http://www.unrealeng.../udk/downloads/] e do Visual Studio 2012 Express [http://www.microsoft...o/eng/downloads]. Se por alguma razão você não puder usar o Visual Studio 2012 Express, falarei brevemente sobre possíveis alternativas no fim do artigo.

Este artigo usará o UDK Beta de fevereiro de 2013.

O PROBLEMA

Então você quer usar a DLLBind do UDK para adicionar características que você precisa para um projeto em particular. Mas e se você não é um guru do C ou fica mais confortável usando uma linguagem gerenciada? Bem, você poderia dizer "Vou simplesmente criar minha DLL em C#!", entretanto isso pode se provar problemático já que DLL em C# usará código gerenciado que é bem diferente de uma DLL não gerenciada (código gerenciado não é um arquivo binário direto mas se transforma em arquivo IL e então há problemas exportando tabelas e todo o tipo de detalhes técnicos que você pode ler em um vasto número de artigos melhores escritos).

POSSÍVEIS SOLUÇÕES

Se nós tivessemos acesso ao código em C++ do UDK poderiamos usar vários esquemas para interoperar com nossa DLL como PInvoke ou Com interop, mas estamos presos com o mecanismo DLLBind que a UDK nos fornece. Precisamos trabalhar dentro do que ele espera e isso nos deixa com poucas opções.

A primeira opção é escrever dois arquivos DLL. Um que é a nossa DLL gerenciada e então escrever uma DLL wrapper em C/C++ que permite interação com o DLLBind. Esta solução é um grande problema porque somos forçados a usar C/C++ de qualquer jeito! Isto torna escrever DLL em C# muito desagradavel.

A segunda opção é desmontar a DLL em um código IL e adicionar manualmente uma VTable ou acerto de VTable. É possível mas dá muito trabalho e pode ser um pouco estressante (por exemplo, você teria que aprender IL e torcer que você não tenha estragado nada).

A terceira opção (e a opção que usaremos neste artigo) é usar um template para permitir que DLL não-gerenciada exporte do C# (usando uma tarefa MSBuild que automatiza muito trabalho para nós).

COMEÇANDO

O primeiro passo é criar um novo projeto do tipo Visual C# Class Livrary no Visual Studio 2012 Express. No campo "Nome" coloque "UDKManagedTestDLL" e clique em OK.

Posted Image
[Figura 1]

Você verá um workspace que se parece o seguinte:

Posted Image
[Figura 2]

O próximo passo que precisamos é pegar o Unmanaged Exports Template []https://nuget.org/packages/UnmanagedExports] de Robert Giesecek. Seu template irá automatizar muito trabalho necessário para permitir nossa dll em C# ser usada no UDK. Essencialmente ele irá fornecer exportações não-gerenciadas que podemos chamar no DLLBind.

Seguindo as instruções, iremos para Tools->Library Package Manager->Package Manager Console

Posted Image
[Figura 3]

Execute o comando no console:
Install-Package UnmanagedExports
Posted Image
[Figura 4]

UM EXEMPLO SIMPLES

Comecei com um teste muito simples para testar as capacidades dessa abordagem. Aqui está o código em C# que usei:
using System;
using System.Runtime.InteropServices;
using System.Collections.Generic;
using System.Text;
using RGiesecke.DllExport;


namespace UDKManagedTestDLL
{
    internal static class UnmanagedExports
    {
        [DllExport("GetGreeting", CallingConvention = CallingConvention.StdCall)]
        [return: MarshalAs(UnmanagedType.LPWStr)]
        static String GetGreeting()
        {
            return "Happy Managed Coding!";
        }
    }
}
A MarshalAs LPWStr é uma maneira de dizer ao C# como dirigir a string de retorno para o código não gerenciado. Uma LPWStr é um ponteiro de 32 bit para uma string de caracteres unicode de 16 bit. Esta é uma string UTF-16 que o DLLBind do UDK suporta. Veja a seção de limitações aqui:[]http://udn.epicgames.com/Three/DLLBind.html]

O nome da função que nossa DLL vai exportar é GetGreeting. Nós especificamos o stdcall como convenção de chamada (calling convention) porque é ela que é suportada pelo DLLBind do UDK.

Precisamos configurar as propriedades desta DLL para usar x86 e permitir código não seguro (unsafe code).

Posted Image
[Figura 5]

Compilei esta DLL e copiei para dentro da pasta User Code. Para mim, a pasta User Code está em C:\UDK\UDK-2013-02\Binaries\Win32\UserCode
Então criei uma classe no UnrealScript chamada TestManagedDll.uc. Coloquei o arquivo aqui: C:\UDK\UDK-2013-02\Development\Src\UTGame\Classes

class TestManagedDLL extends Object
	DLLBind(UDKManagedTestDLL);


dllimport final function string GetGreeting();




DefaultProperties
{
}
Aqui estamos importando a função GetGreeting que exportamos em nossa DLL. Especificamos que o tipo de retorno é uma string. Atualmente a função GetGreeting não recebe nenhum parâmetro. Eu então modifiquei o arquivo UTCheatManager para incluir:

exec function testdll()
{
	local TestManagedDLL dllTest;
	local PlayerController PC;
	dllTest = new class'UTGame.TestManagedDLL';
	foreach WorldInfo.AllControllers(class'PlayerController',PC)
	{
		PC.ClientMessage(dllTest.GetGreeting());
	}
}
Estamos rodando um loop por todos os controladores de jogador (player controllers) e chamando o método ClientMessage que irá imprimir uma mensagem no console do UDK. Chamar o método GetGreeting do dllTest irá retornar a mensagem "Happy Managed Coding!" que definimos em nossa DLL em C#.

Então usei o Unreal Frontend para compilar meus scripts:

Posted Image
[Figura 6]

Podemos então ver os resultados no UnrealEd. Vá em View->World Properties. Precisamos configurar o Default Game Type e o Game Type for PIE para utgame (já que foi lá onde fizemos nosso código).

Posted Image
[Figura 7]

Aperte F8 para executar o jogo, digite ~ para abrir o console, e então digite testdll. Você deve ver o seguinte:

Posted Image
[Figura 8]

PASSANDO PARÂMETROS

Para passar parâmetros, podemos fazer o seguinte em C#:

[DllExport("GetGreetingName", CallingConvention = CallingConvention.StdCall)]
[return: MarshalAs(UnmanagedType.LPWStr)]
static String GetGreetingName([MarshalAs(UnmanagedType.LPWStr)] String msgName)
{
     return "Happy Managed Coding " + msgName + "!";
}
Aqui adicionamos um parâmetro e especificamos que o parâmetro deve ser direcionado como um LPWStr. Concatenação de string é usada para formar a mensagem que é retornada para o UDK. Compile a DLL e copie na pasta UserCode.

Nosso arquivo TestManagedDLL.uc agora se parecerá com isso:

class TestManagedDLL extends Object
	DLLBind(UDKManagedTestDLL);


dllimport final function string GetGreeting();
dllimport final function string GetGreetingName(string msgName);


DefaultProperties
{
}
A função GetGreetingName foi adicionada e especificado o parâmetro string. Atualizamos o UTCheatManager.uc para usar o GetGreetingName:

exec function testdll(string greetName)
{
	local TestManagedDLL dllTest;
	local PlayerController PC;
	dllTest = new class'UTGame.TestManagedDLL';
	foreach WorldInfo.AllControllers(class'PlayerController',PC)
	{
		PC.ClientMessage(dllTest.GetGreetingName(greetName));
	}
}
Passando o parâmetro da função exec para o método GetGreetingName da dllTest significa que podemos prover um parâmetro string no cheat console e tê-lo passado para a DLL em C#.
Use a UDK Frontend para recompilar os scripts. Testando no UnrealEd resulta no seguinte:

Posted Image
[Figura 9]

LENDO ARQUIVOS

Presumo que muita gente vai usar essa técnica para escrever DLL's em C# que usem .NET para trabalhar com arquivos, já que o suporte da UDK para ler vários formatos de arquivos é muito baixo. Para fazer isso precisamos pegar o diretório que a DLL está, assim podemos localizar nossos arquivos apropriadamente. Podemos fazer isso usando Reflection.

[DllExport("ReadTxtFile", CallingConvention = CallingConvention.StdCall)]
[return: MarshalAs(UnmanagedType.LPWStr)]
static String ReadTxtFile([MarshalAs(UnmanagedType.LPWStr)] String fileName)
{
    string retVal = "";
    StreamReader test = null;
    try
    {
        test = new StreamReader(Path.Combine(GetAssemblyPath(), fileName));
        retVal = test.ReadToEnd();
    }
    catch (Exception ex)
    {
        retVal = ex.Message;
    }
    finally
    {
        if (test != null)
        {
            test.Close();
        }
    }
    return retVal;
}
Assim você deve conseguir colocar um arquivo na mesma pasta que a DLL e então carregá-lo e trabalhar seu conteúdo. Ele também vai precisar de:

using System.IO;
using System.Reflection;
Assim deve ser perfeitamente possível com algum conhecimento rudimentar fazer DLLs que usam XML (eu realmente fiz isso e coloquei o link nas referências). Aqui está o conteúdo de TestManagerDll.uc:

class TestManagedDLL extends Object
	DLLBind(UDKManagedTestDLL);


dllimport final function string GetGreeting();
dllimport final function string GetGreetingName(string msgName);
dllimport final function string ReadTxtFile(string fileName);


DefaultProperties
{
}
E aqui a modificação no UTCheatManager.uc:

exec function testdll(string fileName)
{
	local TestManagedDLL dllTest;
	local PlayerController PC;
	dllTest = new class'UTGame.TestManagedDLL';
	foreach WorldInfo.AllControllers(class'PlayerController',PC)
	{
		PC.ClientMessage(dllTest.ReadTxtFile(fileName));
	}
}

Coloquei um arquivo chamado greeting.txt na pasta UserCode. O conteúdo de greeting.txt é o seguinte: "Hello there! This is a test of reading files!"
E aqui está a saída:

Posted Image
[Figura 10]

PASSANDO ESTRUTURAS

Nota: O seguinte conteúdo pode ou não ser a melhor maneira de abordagem do problema. Irei compartilhar o que tenho até agora como ponto de partida, entretanto você pode querer pesquisar isso um pouco mais. O uso do IntPtr pode ter algumas tendências a erros e bugs, mas algumas vezes direcionamento customizado (custom marshalling) é a única maneira de resolver problemas particulares quando fazendo este tipo de trabalho. Usei um pouco de "ref" e deixei ele cuidar do marshalling para mim, entretanto parece que ele não funciona em todas as situações.

Aqui está o código:

private static IntPtr MarshalToPointer(object data)
{
    IntPtr buf = Marshal.AllocHGlobal(
        Marshal.SizeOf(data));
    Marshal.StructureToPtr(data,
        buf, false);
    return buf;
}


struct MyVector
{
    public float x, y, z;
}


[DllExport("ReturnStruct", CallingConvention = CallingConvention.StdCall)]
static IntPtr ReturnStruct()
{
    MyVector v = new MyVector();
    v.x = 0.45f;
    v.y = 0.56f;
    v.z = 0.24f;


    IntPtr lpstruct = MarshalToPointer(v);
    return lpstruct;
}

Este código está preenchendo uma estrutura para passar para o UnrealScript pelo DLLBind.

Aqui está o código para TestManagedDLL.uc

class TestManagedDLL extends Object
	DLLBind(UDKManagedTestDLL);


dllimport final function string GetGreeting();
dllimport final function string GetGreetingName(string msgName);
dllimport final function string ReadTxtFile(string fileName);
dllimport final function vector ReturnStruct();


DefaultProperties
{
}
Aqui está as modificações no UTCheatManager.uc:

exec function testdll()
{
	local TestManagedDLL dllTest;
	local PlayerController PC;
	local Vector v;
	dllTest = new class'UTGame.TestManagedDLL';
	foreach WorldInfo.AllControllers(class'PlayerController',PC)
	{
		v = dllTest.ReturnStruct();
		PC.ClientMessage(v.Y);
	}
}
Aqui está a saída:

Posted Image
[Figura 11]

RETORNANDO ESTRUTURAS DO UNREALSCRIPT

Aqui está o código para passar a estrutura do UnrealScript para C#:

private static T MarshalToStruct<T>(IntPtr buf)
{
    return (T)Marshal.PtrToStructure(buf, typeof(T));
}


[DllExport("SumVector", CallingConvention = CallingConvention.StdCall)]
static float SumVector(IntPtr vec)
{
    MyVector v = MarshalToStruct<MyVector>(vec);
    return v.x + v.y + v.z;
}

Agradecimentos ao Chris Charabaruk [http://www.gamedev.n...17703-coldacid/] pelos melhoramentos no MarshalStruct.

Aqui está a linha adicionada ao TestManagedDLL.uc:

dllimport final function float SumVector(Vector vec);
E a modificação no UTCheatManager.uc:

exec function testdll()
{
	local TestManagedDLL dllTest;
	local PlayerController PC;
	local Vector v;
	v.X = 2;
	v.Y = 3;
	v.Z = 5;
	dllTest = new class'UTGame.TestManagedDLL';
	foreach WorldInfo.AllControllers(class'PlayerController',PC)
	{
		PC.ClientMessage(dllTest.SumVector(v));
	}
}
Aqui está a saída:

Posted Image
[Figura 12]

USANDO KEYWORD COM ESTRUTURA CUSTOMIZADA DO UNREALSCRIPT

Nota: Usar strings em estruturas pode ser problemático devido ao jeito que o UnrealScript lida com strings. Este artigo não irá falar sobre string em estruturas, mas se você tem um exemplo funcional, por favor compartilhe nos comentários!

Quando passar uma estrutura como parâmetro de saída, você pode fazer o seguinte:

struct TestStruct
{
    public int val;
}


[DllExport("OutTestStruct", CallingConvention = CallingConvention.StdCall)]
static void OutTestStruct(ref TestStruct testStruct)
{
    testStruct.val = 7;
}
Aqui está o conteúdo de TestManaged.uc:

class TestManagedDLL extends Object
	DLLBind(UDKManagedTestDLL);


struct TestStruct
{
	var int val;
};


dllimport final function string GetGreeting();
dllimport final function string GetGreetingName(string msgName);
dllimport final function string ReadTxtFile(string fileName);
dllimport final function vector ReturnStruct();
dllimport final function float SumVector(Vector vec);
dllimport final function OutTestStruct(out TestStruct test);


DefaultProperties
{
}
Aqui está as modificações em UTCheatManager.uc:

exec function testdll()
{
	local TestManagedDLL dllTest;
	local PlayerController PC;
	local TestStruct s;
	dllTest = new class'UTGame.TestManagedDLL';
	dllTest.OutTestStruct(s);
	foreach WorldInfo.AllControllers(class'PlayerController',PC)
	{
		PC.ClientMessage(s.val);
	}
}
Aqui está a saída:

Posted Image
[Figura 13]

CÓDIGO COMPLETO

using System;
using System.Runtime.InteropServices;
using System.Collections.Generic;
using System.Text;
using System.IO;
using System.Reflection;
using RGiesecke.DllExport;


namespace UDKManagedTestDLL
{
    internal static class UnmanagedExports
    {
        [DllExport("GetGreeting", CallingConvention = CallingConvention.StdCall)]
        [return: MarshalAs(UnmanagedType.LPWStr)]
        static String GetGreeting()
        {
            return "Happy Managed Coding!";
        }


        [DllExport("GetGreetingName", CallingConvention = CallingConvention.StdCall)]
        [return: MarshalAs(UnmanagedType.LPWStr)]
        static String GetGreetingName([MarshalAs(UnmanagedType.LPWStr)] String msgName)
        {
            return "Happy Managed Coding " + msgName + "!";
        }


        public static String GetAssemblyPath()
        {
            string codeBase = Assembly.GetExecutingAssembly().CodeBase;
            UriBuilder uri = new UriBuilder(codeBase);
            string path = Uri.UnescapeDataString(uri.Path);
            return Path.GetDirectoryName(path);
        }


        [DllExport("ReadTxtFile", CallingConvention = CallingConvention.StdCall)]
        [return: MarshalAs(UnmanagedType.LPWStr)]
        static String ReadTxtFile([MarshalAs(UnmanagedType.LPWStr)] String fileName)
        {
            string retVal = "";
            StreamReader test = null;
            try
            {
                test = new StreamReader(Path.Combine(GetAssemblyPath(), fileName));
                retVal = test.ReadToEnd();
            }
            catch (Exception ex)
            {
                retVal = ex.Message;
            }
            finally
            {
                if (test != null)
                {
                    test.Close();
                }
            }
            return retVal;
        }


        private static IntPtr MarshalToPointer(object data)
        {
            IntPtr buf = Marshal.AllocHGlobal(
                Marshal.SizeOf(data));
            Marshal.StructureToPtr(data,
                buf, false);
            return buf;
        }


        struct MyVector
        {
            public float x, y, z;
        }


        [DllExport("ReturnStruct", CallingConvention = CallingConvention.StdCall)]
        static IntPtr ReturnStruct()
        {
            MyVector v = new MyVector();
            v.x = 0.45f;
            v.y = 0.56f;
            v.z = 0.24f;


            IntPtr lpstruct = MarshalToPointer(v);
            return lpstruct;
        }


        private static T MarshalToStruct<T>(IntPtr buf)
        {
            return (T)Marshal.PtrToStructure(buf, typeof(T));
        }


        [DllExport("SumVector", CallingConvention = CallingConvention.StdCall)]
        static float SumVector(IntPtr vec)
        {
            MyVector v = MarshalToStruct<MyVector>(vec);
            return v.x + v.y + v.z;
        }


        struct TestStruct
        {
            public int val;
        }


        [DllExport("OutTestStruct", CallingConvention = CallingConvention.StdCall)]
        static void OutTestStruct(ref TestStruct testStruct)
        {
            testStruct.val = 7;
        }
    }
}

class TestManagedDLL extends Object
	DLLBind(UDKManagedTestDLL);


struct TestStruct
{
	var int val;
};


dllimport final function string GetGreeting();
dllimport final function string GetGreetingName(string msgName);
dllimport final function string ReadTxtFile(string fileName);
dllimport final function vector ReturnStruct();
dllimport final function float SumVector(Vector vec);
dllimport final function OutTestStruct(out TestStruct test);


DefaultProperties
{
}

PROBLEMAS CONHECIDOS

cooking

De acordo com este tópico [http://forums.epicga...14#post30970414], cooking pode ser problemático usando Unreal Frontend. O problema é que o Unreal Frontend não quer fazer cook de uma unsigned DLL. Você poderia comprar um certificado ou usar um instalador customizado para empacotar seu jogo para lançar. O tópico também menciona modificar o Binaries\UnSetup.Manifests.xml na tag GameFilesToInclude para ter o seguinte:
<string>Binaries/Win32/UserCode/(.*).dll</string>
Não testei nada disso, mas mesmo assim queria mencioná-los.

ALTERNATIVAS PARA O VISUAL STUDIO 2012

O template Unmanaged Export usado neste artigo é um pacote NuGet. Se você está usando Mono e quer usar o Unmanaged Export, você pode conseguir fazê-lo funcionar lendo a seguinte fonte: [http://monomvc.wordp.../nuget-on-mono/]. Não testei isso e não é garantido que irá funcionar sem problemas.

Para versões mais antigas do NuGet para Visual Studio (2010, por exemplo) podem funcionar se você fizer o download [http://nuget.codeplex.com/]. Se tiver problemas em fazer a extensão funcionar, o NuGet também pode ser usado em linha de comando [http://blog.davidebb...ectly-from.html].

CONCLUSÃO

Espero que tenham aprendido algo sobre usar C# para criar DLL's para o UDK. Certifique-se de checar as referências para mais leitura e informação.
Gostaria de agradecer ao produtor executivo da GDNet Drew "Gaiiden" Sikora por requisitar que eu postasse este artigo. O que começou como um port de um velho relatório terminou como uma edição significante do antigo trabalho!

Obrigado por ler!

DISCLAIMER

Este método pode ter sérias limitações e desvantagens. Os métodos apresentados podem transbordar a memória. É deixado ao leitor que tais questões sejam gerenciadas caso apareçam. Estou meramente dando base técnica, mas será de sua responsabilidade assumir os riscos de implementar em seu projeto particular. Não tenho responsabilidade pelo uso ou mal uso desta informação por qualquer razão que seja.

REFERÊNCIAS

http://www.unrealeng.../udk/downloads/
http://www.microsoft...o/eng/downloads
http://nuget.codeplex.com/
http://udn.epicgames...ee/DLLBind.html
http://www.developer...g-structs-in-c/
http://stackoverflow...-the-code-is-in
https://nuget.org/pa...nmanagedExports
http://www.gamedev.n...ed-application/
http://www.mavrikgam...llbind-tutorial
http://forums.epicga...14#post30970414
http://code.google.c...dk-xml-library/
http://monomvc.wordp.../nuget-on-mono/
http://blog.davidebb...ectly-from.html

Este artigo foi originalmente postado em Gamedev.net
http://www.gamedev.n...ls-in-udk-r3203



0 Comments