[Game Development][Unity]C# calls C++ library method

foreword

First of all, changing an online game to stand-alone is a relatively stupid requirement, and many online games cannot be changed to stand-alone. This online project of ours is about to cease operations. We want to give back to old users so we changed to a stand-alone version, and the second thing is to learn the process of changing an online game to a stand-alone version.

The C++ code of our project can not only save the database, but also save the data as a binary file. Changing online games to stand-alone means storing binary files locally.

Since the C++ code cannot get the sandbox path of Unity, it cannot find a path with read and write permissions, so it is passed to C++ through Unity

text

The server is C++ code, which cannot be run directly in Unity, so it must be packaged into an so library for PC and Android calls. A static library for IOS calls.

First look at the C++ code ServerDLL.h

#pragma once
#ifdef _WIN32
#define  BUILD_DLL
#endif
#if defined(BUILD_DLL)
#define GAMEAPI extern "C" __declspec(dllexport)
#else
#define GAMEAPI extern "C"
#endif    

GAMEAPI int _stdcall Hello(unsigned char* buff, int len);

GAMEAPI void _stdcall Start(int nid);

C++ Server.cpp

#include "ServerDLL.h"

GAMEAPI int Hello(unsigned char* buff, int len)
{
    std::cout << buff << std::endl;
    return 100;
}

GAMEAPI void Start(int nid)
{
}

The C++ code is ready, the next step is to generate the so library


Packaging the so library needs to be done in the linux virtual machine, why should it be generated on the virtual machine? Because the C++ code depends on some Linux libraries. We will not fix it on windows.

Note that you must select the generated library version when packaging. The first version is Android armeabi-v7a for testing. If it succeeds, continue to generate other versions of the so library.


After the so library is generated, throw it into Unity's Plugins folder

The location is not very important. We created a folder ourselves, and we can put it in the Android folder on the root directory, but Android is a must, followed by the armeabi-v7a folder.

Please note! !

After the library is thrown into Unity, be sure to confirm whether the CPU selection of the SO library is correct. Regardless of the correct display in the Unity editor, it may actually be the wrong CPU option. It is recommended to select ARMv7 and apply it.


SO library pit! !

After the SO library is imported, Unity will generate a memory copy. We don’t know why. If you run the Unity editor to replace the so file, the so library is still the original code. You need to close Unity before importing. DLL will not have this problem, and this big pit is not necessarily present, and it may be related to the Unity version.


C# declares C++

SO library name is libgamed.so

When calling from C# on the Android side, remove the lib in front and the .so in the back

The IOS side is __Internal, note that there are two underscores

The following is the code for C# to declare the C++ method

public class GameDLL
{
    #if UNITY_IOS
        const string GAMEDLL = "__Internal";
    #elif UNITY_EDITOR || UNITY_ANDROID
        const string GAMEDLL = "gamed";
    #endif
    
    [DllImport(GAMEDLL, CallingConvention = CallingConvention.Cdecl)]
    public extern static int Hello(byte[] buf, int len);
    
    [DllImport(GAMEDLL, CallingConvention = CallingConvention.Cdecl)]
    public extern static void Start(int x);

    [DllImport(GAMEDLL, CallingConvention = CallingConvention.Cdecl)]
    public extern static void Tick();

    [DllImport(GAMEDLL, CallingConvention = CallingConvention.Cdecl)]
    public extern static void PushEvent(byte[] buf, int len);
    
    [DllImport(GAMEDLL, CallingConvention = CallingConvention.Cdecl)]
    public extern static int PopEvent(byte[] buf, int len);

}

[DLLImport]接口要注意几点

一:第一个参数是DLL名称,这个肯定是固定用法

二:Entrypoint

在不希望外部托管方法具有与 dll 导出相同的名称的情况下,可以设置该属性来指示导出的 dll 函数的入口点名称。当您定义两个调用相同非托管函数的外部方法时,这特别有用。另外,在 windows 中还可以通过它们的序号值绑定到导出的 dll 函数。如果您需要这样做,则诸如“#1”或“#129”的 entrypoint 值指示 dll 中非托管函数的序号值而不是函数名。

三:CallingConvention 枚举选择

  1. Cdecl 调用方清理堆栈。 这使您能够调用具有 varargs 的函数(如 Printf),使之可用于接受可变数目的参数的方法。

  1. FastCall 不支持此调用约定。

  1. StdCall 被调用方清理堆栈。这是使用平台invoke调用非托管函数的默认约定。

  1. ThisCall 第一个参数是 this 指针,它存储在寄存器 ECX 中。 其他参数被推送到堆栈上。 此调用约定用于对从非托管 DLL 导出的类调用方法。

  1. Winapi 此成员实际上不是调用约定,而是使用了默认平台调用约定。 例如,在 Windows 上默认为 StdCall,在 Windows CE.NET 上默认为 Cdecl。

函数声明注意事项

public extern static 声明是外部方法

因为c#和c++之间架构不同,怕程序溢出所以调用库函数时需要在c#程序中使用unsafe。

如果没有指针变量可以省略unsafe,如果有就得加上,Unity设置“configuration properties>build” 中把允许unsafe设为真。


C#调用C++

void Start()
{
    GameDLL.Start(1);
    byte[] bytes = new byte[256];
    int ret = GameDLL.Hello(bytes,bytes.Length);
    Debub.Log("ret: " + ret);
}

经测试打印成功 ret: 100


解决前后端通讯问题

网游变单机需要解决socket通讯的问题,之前走网络,现在走C#和C++互相调用传byte数据,这个和原来的网络通信区别不大,Unity这边需要在Update中不断去C++那要数据,然后把bytes数据传给lua反序列化成proto消息。

private void Update()
{
    GameDLL.Tick();
    ReceiveEvent();
}

private const int MAX_READ = 8192;
private byte[] byteBuffer = new byte[MAX_READ];
private MemoryStream memStream;
private BinaryReader reader;

//接收C++ Socket数据
void ReceiveEvent()
{
    int bytesRead = GameDLL.PopEvent(byteBuffer, MAX_READ);
    if(bytesRead == 0)
    {
        return;
    }
    Debug.Log("Rec Message :" + bytesRead);
    OnReceive(byteBuffer, bytesRead);
}

DefaultNetReader dReader = new DefaultNetReader();
private long RemainingBytes()
{
    return memStream.Length - memStream.Position;
}
void OnReceive(byte[] bytes, int length)
{
    memStream.Seek(0, SeekOrigin.End);
    memStream.Write(bytes, 0, length);
    memStream.Seek(0, SeekOrigin.Begin);
    //Debug.Log ("RemainingBytes():" + RemainingBytes ());
    while (RemainingBytes() > 2)
    {
        ushort messageLen = (ushort)IPAddress.NetworkToHostOrder(reader.ReadInt16());
       
        if (RemainingBytes() >= messageLen)
        {
            MemoryStream ms = new MemoryStream();
            BinaryWriter writer = new BinaryWriter(ms);

            writer.Write(reader.ReadBytes(messageLen));
            ms.Seek(0, SeekOrigin.Begin);

            OnReceivedMessage(ms);
        }
        else
        {

            LogUtility.Instance.Log_Socket_Error("=== Socket Receive else===> messageLen:" + messageLen + "  RemainingBytes()" + RemainingBytes());

            memStream.Position = memStream.Position - 2;
            
            break;
        }
    }
    byte[] leftover = reader.ReadBytes((int)RemainingBytes());
    memStream.SetLength(0);     //Clear
    memStream.Write(leftover, 0, leftover.Length);
}

private void OnReceivedMessage(MemoryStream ms)
{
    BinaryReader reader = new BinaryReader(ms);
    try
    {
        ushort messageType = (ushort)IPAddress.NetworkToHostOrder(reader.ReadInt16());
        int sn = IPAddress.NetworkToHostOrder(reader.ReadInt32());
        byte[] message = reader.ReadBytes((int)(ms.Length - ms.Position));

        if (true)
            LogUtility.Instance.Log_Socket_Error(string.Format("收到消息   OnMessageCallback type = {0}   length = {1}", messageType, message.Length));

        PlanXNet.ClientWrap.Instance.OnMessageCallback(messageType, message);
    }
    catch (Exception ex)
    {
        Debug.LogError(ex.Message);
    }
    finally
    {
        if (ms != null)
        {
            ms.Close();
            ms = null;
        }
        if (reader != null)
        {
            reader.Close();
            reader = null;
        }
    }

}

    
//向C++推 proto数据
public static void PushEvent(byte[] buf, int len)
{
    if(RemoteDebug)
    {
        GameDLL.SHM.Send(buf, len);
    }
    else
    {
        GameDLL.PushEvent(buf, len);
    }
}

C++代码存读档

由于C++代码拿不到Unity的沙盒路径,无法找到有读写权限的路径,因此通过Unity传给C++

Guess you like

Origin blog.csdn.net/liuyongjie1992/article/details/131184849