[游戏开发][Unity]C#调用C++库方法

前言

首先网游改单机是个比较愚蠢的需求,而且很多网游是无法改单机的。我们这个线上项目要停止运营了,回馈老用户所以改个单机版,其次是学习一下网游改单机的流程。

我们项目的C++代码除了可以存数据库,还可以把数据存成二进制文件。网游改单机就是要在本地存二进制文件。

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

正文

服务器是C++代码,不能直接在Unity里运行,因此要打包成so库供PC和Android调用 .a静态库给IOS调用。

先看C++代码 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)
{
}

C++代码准备就绪,下一步是生成so库


打包so库需要在linux虚拟机里完成,为何要在虚拟机上生成?因为C++代码依赖一些Linux库。在windows上咱不会整。

注意打包时要选择生成的库版本,第一个版本打的是安卓armeabi-v7a做测试,如果成功了再继续生成其他版本的so库


so库生成后,扔到Unity的Plugins文件夹里

放的位置没啥大讲究,我们自己建了个文件夹,放在根目录上的Android文件夹里也行,但Android是必备的,其次就是armeabi-v7a文件夹也是必须的,

请注意!!

库扔到Unity后,务必确认SO库的CPU选择是否正确。别看Unity编辑器里显示正确,实际上有可能是错误的CPU选项,建议选择ARMv7后Apply一下。


SO库大坑!!

SO库导入之后,Unity会生成一份内存拷贝,我们也不知道为啥,如果你运行着Unity编辑器替换so文件,结果so库还是原来的代码,需要把Unity关了之后再导入。DLL不会有这个问题,而且这个大坑不是必现,也有可能和Unity版本有关。


C#声明C++

SO库名字叫 libgamed.so

安卓端C#调用时要去掉前面的lib和后面的.so

IOS端是__Internal,注意是两个下划线

下面是C#声明C++方法的代码

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++

猜你喜欢

转载自blog.csdn.net/liuyongjie1992/article/details/131184849