卡卷网
当前位置:卡卷网 / 每日看点 / 正文

为什么c#能做大型游戏,而java不适合,c#跟java不是很相似吗?

作者:卡卷网发布时间:2024-12-29 14:22浏览数量:79次评论数量:0次

就说个编写 3D 游戏里最常见的场景吧。

现在我想要渲染一个 3D 模型,需要将组成模型的面传递给游戏引擎,然后引擎会去调用 DirectX/Vulkan/OpenGL 等等的 API 将我们模型数据提供给 GPU 渲染出来。

我们都知道 3D 模型的面由三角形组成,而每个三角形都有 3 个顶点,最后一大堆三角形组成了整个模型的表面。

那这件事情要怎么做呢?最简单的方法:整一个缓冲区把三角形的各顶点的位置存起来,然后调用图形 API 把数据上传到 GPU 就行了。

那这件事情在 C# 里面怎么做呢?

首先定义顶点类型:

[StructLayout(LayoutKind.Sequential)] record struct Vertex(float X, float Y, float Z);

然后我们整个顶点数组:

var vertices = new Vertex[12345];

然后把我们的三角形扔进去之后直接扔给引擎就完事了。

填充的时候简单 for 循环里往 vertices 里塞 Vertex 就行了。

假设引擎用的 OpenGL,那此时引擎接收到我们的数组后需要调用 glBufferData 来给 VAO 提供顶点数据:

const uint GL_ARRAY_BUFFER = 0x8892; const uint GL_STATIC_DRAW = 0x88E4; fixed (Vertex* p = vertices) glBufferData(GL_ARRAY_BUFFER, sizeof(Vertex) * vertices.Length, p, GL_STATIC_DRAW);

glBufferData 这个函数要怎么在 C# 里定义呢?

[DllImport("glew32.dll", EntryPoint = "__glewBufferData"), SuppressGCTransition] static extern void glBufferData(uint target, nint size, void* data, uint usage);

搞定。

至此为止我没有用到任何的(除了 OpenGL 本身之外的)库,也没有编写任何的 C# 以外的代码。

那写法这么简单,性能怎么样呢?我们看看最终调用 glBufferData 的部分 JIT 给我们生成了什么代码:

add rax, 16 ; address of vertices + method table (8) + array length (8) mov r8, rax ; = data of vertices mov edx, 0x242AC ; sizeof(Vertex) * vertices.Length mov ecx, 0x8892 ; GL_ARRAY_BUFFER mov r9d, 0x88E4 ; GL_STATIC_DRAW mov eax, 0x5B589850 ; function pointer of glBufferData call rax ; call glBufferData

可以看到,没有任何额外的内存分配和开销,数据直接从指针传出去,压根不需要拷贝,从头到尾的零成本抽象。

甚至可以说 C# 就是一个可以用 GC 的现代版 C++,这对于需要频繁与 native 进行交互的场景而言最适合不过。更不要提 C# 的 P/Invoke 在 NativeAOT 之下支持静态链接,连额外的 dll 都省了。


以上东西你用 Java 做一个试试,看看能有多复杂,再看看填充 buffer 的时候能否直接利用定义好的数据结构和类型,再看看能不能做到零拷贝。


更新:

鉴于 Java 22 之后有了 FFM API,以上东西虽然做起来很麻烦,但是好歹能写出来了,虽然我不知道他们现在的 FFM API 打算如何支持静态链接,毕竟 Java FFM API 这种运行时定义布局和查找入口点跟静态链接就是天生不兼容的。


那我们现在进一步加大难度。

我们都知道给 GPU 送的数据不一定非得是 float ,还可以是 intdouble 等等。那此时在 C# 里面我们只需要把顶点类型改成:

[StructLayout(LayoutKind.Sequential)] record struct Vertex<T>(T X, T Y, T Z) where T : unmanaged;

然后调用 glBufferData 那句改成:

fixed (Vertex<T>* p = vertices) glBufferData(GL_ARRAY_BUFFER, sizeof(Vertex<T>) * vertices.Length, p, GL_STATIC_DRAW);

就搞定了。

由于 .NET 会针对泛型参数给代码进行特化,因此 T 无论是 intfloat 还是 double,甚至是更复杂的类型例如 record struct Foo<U>(Bar A, float B, U C) 都能获得独一无二的代码生成,并且其内存布局也是编译时就确定的,效率也有保证。

END

免责声明:本文由卡卷网编辑并发布,但不代表本站的观点和立场,只提供分享给大家。

卡卷网

卡卷网 主页 联系他吧

请记住:卡卷网 Www.Kajuan.Net

欢迎 发表评论:

请填写验证码