为什么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
,还可以是 int
、double
等等。那此时在 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
无论是 int
、float
还是 double
,甚至是更复杂的类型例如 record struct Foo<U>(Bar A, float B, U C)
都能获得独一无二的代码生成,并且其内存布局也是编译时就确定的,效率也有保证。
免责声明:本文由卡卷网编辑并发布,但不代表本站的观点和立场,只提供分享给大家。
- 上一篇:域名相关的知识点有哪些?
- 下一篇:一般自学PS达到接单水平需要多久?
相关推荐

你 发表评论:
欢迎