feat(minecraft-demo): add C++ backend and Godot extension for Minecraft demo

- Add GDExtension ignore files for C++ and GDExtension source directories
- Implement CMakeLists.txt for Godot extension with proper library linking
- Create PHP stub functions for Minecraft crafting operations
- Add comprehensive C++ backend implementation with OpenGL rendering
- Implement window management, texture loading, and block rendering
- Add chunk system with face building and distance-based visibility
- Include mouse capture, camera controls, and crosshair rendering
- Implement world initialization, block placement, and chunk management
- Add sky gradient, fog effects, and transparent object rendering
pull/16/head
韩天峰 2 months ago
parent 7e28e8441b
commit 348c3d01fc
  1. BIN
      examples/minecraft-demo/1.png
  2. BIN
      examples/minecraft-demo/2.png
  3. BIN
      examples/minecraft-demo/3.png
  4. 19
      examples/minecraft-demo/LICENSE.Craft.md
  5. 67
      examples/minecraft-demo/README.md
  6. 1039
      examples/minecraft-demo/cpp-src/craft_backend.cc
  7. 6268
      examples/minecraft-demo/deps/lodepng/lodepng.c
  8. 1715
      examples/minecraft-demo/deps/lodepng/lodepng.h
  9. 422
      examples/minecraft-demo/main.php
  10. 23
      examples/minecraft-demo/php-src/craft.stub.php
  11. 16
      examples/minecraft-demo/project.yml
  12. BIN
      examples/minecraft-demo/textures/sky.png
  13. BIN
      examples/minecraft-demo/textures/texture.png
  14. 91
      examples/minecraft-godot/README.md
  15. 19
      examples/minecraft-godot/assets/craft/LICENSE.md
  16. BIN
      examples/minecraft-godot/assets/craft/font.png
  17. 40
      examples/minecraft-godot/assets/craft/font.png.import
  18. BIN
      examples/minecraft-godot/assets/craft/sky.png
  19. 40
      examples/minecraft-godot/assets/craft/sky.png.import
  20. BIN
      examples/minecraft-godot/assets/craft/texture.png
  21. 40
      examples/minecraft-godot/assets/craft/texture.png.import
  22. 1
      examples/minecraft-godot/cpp-src/.gdignore
  23. 84
      examples/minecraft-godot/cpp-src/typephp_world_api.cc
  24. 1
      examples/minecraft-godot/gdextension-src/.gdignore
  25. 24
      examples/minecraft-godot/gdextension-src/CMakeLists.txt
  26. 33
      examples/minecraft-godot/gdextension-src/register_types.cpp
  27. 94
      examples/minecraft-godot/gdextension-src/typephp_bridge.cpp
  28. 37
      examples/minecraft-godot/gdextension-src/typephp_bridge.hpp
  29. 7
      examples/minecraft-godot/icon.svg
  30. 43
      examples/minecraft-godot/icon.svg.import
  31. 154
      examples/minecraft-godot/php-src/world.php
  32. 64
      examples/minecraft-godot/project.godot
  33. 85
      examples/minecraft-godot/scenes/main.tscn
  34. 104
      examples/minecraft-godot/scripts/player_controller.gd
  35. 1
      examples/minecraft-godot/scripts/player_controller.gd.uid
  36. 555
      examples/minecraft-godot/scripts/voxel_world.gd
  37. 1
      examples/minecraft-godot/scripts/voxel_world.gd.uid
  38. 7
      examples/minecraft-godot/typephp.yml
  39. 10
      examples/minecraft-godot/typephp_bridge.gdextension
  40. 1
      examples/minecraft-godot/typephp_bridge.gdextension.uid

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 MiB

@ -0,0 +1,19 @@
Copyright (C) 2013 Michael Fogleman
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

@ -0,0 +1,67 @@
# TypePHP Minecraft Demo
这是一个不依赖 Godot 的方块世界渲染示例,技术路线参考 Craft:
- Win32/WGL 创建窗口和 OpenGL 上下文。
- 使用 Craft 的 `texture.png` 方块 atlas、`sky.png` 天空贴图和 lodepng 加载 PNG。
- 使用方块六面 tile 映射、可见面剔除、植物交叉面、水面和云层透明绘制。
- PHP/TypePHP 负责世界生成、chunk 队列、角色移动等业务逻辑。
- C++ 只负责窗口、输入、OpenGL 渲染、贴图加载和 chunk mesh 缓存。
- 渲染时参考 Craft 的 chunk 可见性策略,只绘制相机附近且位于视野方向内的 chunk。
当前范围只实现基础世界画面渲染:
- 天空贴图背景
- 河流和湖泊
- 山峰和高地
- 草地、沙地、泥土、石头、雪地
- 树干、树叶、草和花朵
- 云层和远处线性雾
- 第一人称飞行观察
- 居中准星
- 初始化进度 UI
- chunk display list 缓存、视野剔除和透明层远近排序
不包含联机、背包、建造、存档等复杂系统。
## 编译
```powershell
php bin\compiler.php examples\minecraft-demo\project.yml
```
编译产物输出到仓库根目录:
```text
minecraft_demo.exe
```
## 运行
```powershell
.\minecraft_demo.exe
```
## 操作
- `W/A/S/D` 移动
- 鼠标移动视角
- `Space` 上升
- `Shift` 加速并下降
- `Esc` 弹出退出确认框
## 项目结构
```text
main.php PHP 世界规则和主循环
php-src/craft.stub.php C++ 渲染 API 的 PHP 声明
cpp-src/craft_backend.cc Win32/WGL/OpenGL 后端
deps/lodepng/ PNG 加载
textures/texture.png Craft 方块 atlas
textures/sky.png Craft 天空贴图
LICENSE.Craft.md Craft MIT License
```
## 授权
`textures/texture.png`、`textures/sky.png` 和 lodepng 来自 Craft 项目及其依赖。Craft 使用 MIT License,许可证已保留在 `LICENSE.Craft.md`

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

@ -0,0 +1,422 @@
<?php
declare(strict_types=1);
const BLOCK_GRASS = 1;
const BLOCK_SAND = 2;
const BLOCK_STONE = 3;
const BLOCK_WOOD = 5;
const BLOCK_DIRT = 7;
const BLOCK_SNOW = 9;
const BLOCK_COBBLE = 11;
const BLOCK_LEAVES = 15;
const BLOCK_CLOUD = 16;
const BLOCK_TALL_GRASS = 17;
const BLOCK_YELLOW_FLOWER = 18;
const BLOCK_RED_FLOWER = 19;
const BLOCK_PURPLE_FLOWER = 20;
const BLOCK_SUN_FLOWER = 21;
const BLOCK_WHITE_FLOWER = 22;
const BLOCK_BLUE_FLOWER = 23;
const BLOCK_WATER = 64;
const KEY_W = 0x57;
const KEY_A = 0x41;
const KEY_S = 0x53;
const KEY_D = 0x44;
const KEY_SPACE = 0x20;
const KEY_SHIFT = 0x10;
const KEY_ESCAPE = 0x1B;
const WORLD_RADIUS = 4096;
const CHUNK_SIZE = 16;
const RENDER_CHUNK_RADIUS = 4;
const KEEP_CHUNK_RADIUS = 5;
const CHUNKS_PER_FRAME = 1;
const WATER_LEVEL = 4;
const MAX_Y = 34;
const CLOUD_MIN_Y = 29;
const CLOUD_MAX_Y = 31;
function demo_noise2(int $x, int $z, float $scale): float
{
$a = sin($x * $scale + $z * $scale * 0.71);
$b = cos($x * $scale * 1.37 - $z * $scale * 0.83);
$c = sin(($x + $z) * $scale * 0.53 + cos($z * $scale));
return ($a + $b + $c) / 3.0;
}
function demo_hash2(int $x, int $z): float
{
$n = sin($x * 127.1 + $z * 311.7) * 43758.5453123;
return $n - floor($n);
}
function world_is_river(int $x, int $z): bool
{
$spawnLake = (($x - 7) * ($x - 7) + ($z - 7) * ($z - 7)) <= 28;
if ($spawnLake) {
return true;
}
$center = sin($z * 0.22) * 5.0 + sin($z * 0.07) * 2.0;
$width = 3.4 + cos($z * 0.13) * 1.0;
return abs($x - $center) <= $width;
}
function world_height_at(int $x, int $z): int
{
$continent = demo_noise2($x, $z, 0.018) * 7.0;
$ridge = abs(demo_noise2($x - 451, $z + 173, 0.042)) * 8.5;
$rolling = demo_noise2($x + 917, $z - 431, 0.092) * 3.2;
$detail = demo_noise2(-$x, $z + 331, 0.23) * 1.4;
$height = 7 + (int) round($continent + $ridge + $rolling + $detail);
if ($height < 2) {
$height = 2;
} elseif ($height > 24) {
$height = 24;
}
if (world_is_river($x, $z)) {
$height -= 5;
if ($height < 1) {
$height = 1;
} elseif ($height > WATER_LEVEL - 1) {
$height = WATER_LEVEL - 1;
}
}
return $height;
}
function world_is_steep(int $x, int $z, int $height): bool
{
$n1 = world_height_at($x + 1, $z);
$n2 = world_height_at($x - 1, $z);
$n3 = world_height_at($x, $z + 1);
$n4 = world_height_at($x, $z - 1);
return $height - min($n1, $n2, $n3, $n4) >= 2;
}
function world_has_tree(int $x, int $z): bool
{
if (world_is_river($x, $z)) {
return false;
}
if (($x % 13) !== 0 || ($z % 13) !== 0) {
return false;
}
return demo_hash2($x, $z) > 0.72;
}
function world_plant_type_at(int $x, int $z): int
{
if (world_is_river($x, $z)) {
return 0;
}
$flowerNoise = demo_noise2($x + 93, -$z - 17, 0.17);
if ($flowerNoise > 0.46) {
$pick = (int) floor(demo_hash2($x, $z) * 6.0);
if ($pick === 0) {
return BLOCK_YELLOW_FLOWER;
}
if ($pick === 1) {
return BLOCK_RED_FLOWER;
}
if ($pick === 2) {
return BLOCK_PURPLE_FLOWER;
}
if ($pick === 3) {
return BLOCK_SUN_FLOWER;
}
if ($pick === 4) {
return BLOCK_WHITE_FLOWER;
}
return BLOCK_BLUE_FLOWER;
}
$grassNoise = demo_noise2(-$x, $z, 0.31);
if ($grassNoise > 0.24 || demo_hash2($x + 11, $z - 19) > 0.70) {
return BLOCK_TALL_GRASS;
}
return 0;
}
function world_has_cloud(int $x, int $y, int $z): bool
{
if ($y < CLOUD_MIN_Y || $y > CLOUD_MAX_Y) {
return false;
}
$layer = $y - CLOUD_MIN_Y;
$shape = demo_noise2($x + 701, $z - 223, 0.045);
$detail = demo_noise2($x * 2 + 17, $z * 2 - 31, 0.12) * 0.25;
$threshold = $layer === 1 ? 0.46 : 0.58;
return $shape + $detail > $threshold;
}
function block_type_at(int $x, int $y, int $z): int
{
if ($y < 0 || $y > MAX_Y) {
return 0;
}
if (world_has_cloud($x, $y, $z)) {
return BLOCK_CLOUD;
}
$height = world_height_at($x, $z);
$river = world_is_river($x, $z);
if ($river && $y === WATER_LEVEL) {
return BLOCK_WATER;
}
if ($y <= $height) {
if ($y === $height) {
if ($river || $height <= WATER_LEVEL) {
return BLOCK_SAND;
}
if ($height >= 20) {
return BLOCK_SNOW;
}
if (world_is_steep($x, $z, $height)) {
return BLOCK_STONE;
}
return BLOCK_GRASS;
}
if ($height >= 17 && $y >= $height - 3) {
return BLOCK_STONE;
}
return $y >= $height - 2 ? BLOCK_DIRT : BLOCK_STONE;
}
if ($y === $height + 1) {
return world_plant_type_at($x, $z);
}
for ($tx = $x - 3; $tx <= $x + 3; $tx++) {
for ($tz = $z - 3; $tz <= $z + 3; $tz++) {
if (!world_has_tree($tx, $tz)) {
continue;
}
$base = world_height_at($tx, $tz) + 1;
if ($x === $tx && $z === $tz && $y >= $base && $y <= $base + 5) {
return BLOCK_WOOD;
}
$dx = $x - $tx;
$dz = $z - $tz;
$dy = $y - ($base + 4);
if ($y >= $base + 2 && $y <= $base + 6 && ($dx * $dx + $dz * $dz + $dy * $dy) <= 10) {
return BLOCK_LEAVES;
}
}
}
return 0;
}
function floor_chunk(int $value): int
{
if ($value >= 0) {
return intdiv($value, CHUNK_SIZE);
}
return -intdiv(-$value + CHUNK_SIZE - 1, CHUNK_SIZE);
}
function chunk_key(int $chunkX, int $chunkZ): string
{
return (string) $chunkX . ':' . (string) $chunkZ;
}
function sort_pending_chunks(array $pendingChunks, int $centerChunkX, int $centerChunkZ): array
{
uasort($pendingChunks, static function (array $a, array $b) use ($centerChunkX, $centerChunkZ): int {
$adx = (int) $a[0] - $centerChunkX;
$adz = (int) $a[1] - $centerChunkZ;
$bdx = (int) $b[0] - $centerChunkX;
$bdz = (int) $b[1] - $centerChunkZ;
$da = $adx * $adx + $adz * $adz;
$db = $bdx * $bdx + $bdz * $bdz;
return $da <=> $db;
});
return $pendingChunks;
}
function generate_chunk(int $chunkX, int $chunkZ): int
{
$startX = $chunkX * CHUNK_SIZE;
$startZ = $chunkZ * CHUNK_SIZE;
$endX = $startX + CHUNK_SIZE - 1;
$endZ = $startZ + CHUNK_SIZE - 1;
$count = 0;
craft_begin_chunk($chunkX, $chunkZ);
for ($x = $startX; $x <= $endX; $x++) {
if ($x < -WORLD_RADIUS || $x > WORLD_RADIUS) {
continue;
}
for ($z = $startZ; $z <= $endZ; $z++) {
if ($z < -WORLD_RADIUS || $z > WORLD_RADIUS) {
continue;
}
for ($y = 0; $y <= MAX_Y; $y++) {
$type = block_type_at($x, $y, $z);
if ($type !== 0) {
craft_set_chunk_block($x, $y, $z, $type);
$count++;
}
}
}
}
craft_commit_chunk($chunkX, $chunkZ);
return $count;
}
function enqueue_visible_chunks(int $centerChunkX, int $centerChunkZ, array $loadedChunks, array $pendingChunks): array
{
$needed = [];
$queued = 0;
for ($cx = $centerChunkX - RENDER_CHUNK_RADIUS; $cx <= $centerChunkX + RENDER_CHUNK_RADIUS; $cx++) {
for ($cz = $centerChunkZ - RENDER_CHUNK_RADIUS; $cz <= $centerChunkZ + RENDER_CHUNK_RADIUS; $cz++) {
$chunkKey = chunk_key($cx, $cz);
$needed[$chunkKey] = 1;
if (!isset($loadedChunks[$chunkKey]) && !isset($pendingChunks[$chunkKey])) {
$pendingChunks[$chunkKey] = [$cx, $cz];
$queued++;
}
}
}
return $queued > 0 ? sort_pending_chunks($pendingChunks, $centerChunkX, $centerChunkZ) : $pendingChunks;
}
function unload_far_chunks(int $centerChunkX, int $centerChunkZ, array $loadedChunks): array
{
$removed = 0;
foreach ($loadedChunks as $loadedKey => $chunk) {
$dx = abs((int) $chunk[0] - $centerChunkX);
$dz = abs((int) $chunk[1] - $centerChunkZ);
if ($dx > KEEP_CHUNK_RADIUS || $dz > KEEP_CHUNK_RADIUS) {
craft_remove_chunk((int) $chunk[0], (int) $chunk[1]);
unset($loadedChunks[$loadedKey]);
$removed++;
}
}
return $loadedChunks;
}
function process_chunk_queue(array $loadedChunks, array $pendingChunks, int $maxChunks): array
{
$loaded = 0;
foreach ($pendingChunks as $pendingKey => $chunk) {
generate_chunk((int) $chunk[0], (int) $chunk[1]);
$loadedChunks[$pendingKey] = [(int) $chunk[0], (int) $chunk[1]];
unset($pendingChunks[$pendingKey]);
$loaded++;
if ($loaded >= $maxChunks) {
break;
}
}
return [$loadedChunks, $pendingChunks];
}
function main(): void
{
if (!craft_init('TypePHP Minecraft Demo - Craft/OpenGL', 1280, 720, 'examples/minecraft-demo/textures/texture.png')) {
echo "OpenGL 初始化失败\n";
return;
}
craft_set_sky_texture('examples/minecraft-demo/textures/sky.png');
$x = 8.0;
$z = 18.0;
$y = (float) world_height_at((int) $x, (int) $z) + 2.4;
$yaw = -2.55;
$pitch = 0.35;
$last = craft_get_time();
$escapeWasDown = false;
$centerChunkX = floor_chunk((int) floor($x));
$centerChunkZ = floor_chunk((int) floor($z));
$loadedChunks = [];
$pendingChunks = [];
craft_begin_world();
$pendingChunks = enqueue_visible_chunks($centerChunkX, $centerChunkZ, $loadedChunks, $pendingChunks);
$initialTotal = count($pendingChunks);
$initialDone = 0;
craft_render_loading($initialDone, $initialTotal);
while (count($pendingChunks) > 0) {
craft_poll_events();
$before = count($pendingChunks);
$queueState = process_chunk_queue($loadedChunks, $pendingChunks, 1);
$loadedChunks = $queueState[0];
$pendingChunks = $queueState[1];
$initialDone += $before - count($pendingChunks);
craft_render_loading($initialDone, $initialTotal);
}
while (!craft_should_close()) {
$now = craft_get_time();
$dt = max(0.001, min(0.05, $now - $last));
$last = $now;
craft_poll_events();
$escapeDown = craft_key_pressed(KEY_ESCAPE);
if ($escapeDown && !$escapeWasDown) {
if (craft_confirm_exit()) {
break;
}
$last = craft_get_time();
}
$escapeWasDown = $escapeDown;
$yaw -= craft_mouse_delta_x() * 0.0022;
$pitch += craft_mouse_delta_y() * 0.0022;
$pitch = max(-1.45, min(1.45, $pitch));
$speed = craft_key_pressed(KEY_SHIFT) ? 14.0 : 7.0;
$forwardX = -sin($yaw);
$forwardZ = -cos($yaw);
$rightX = cos($yaw);
$rightZ = -sin($yaw);
if (craft_key_pressed(KEY_W)) {
$x += $forwardX * $speed * $dt;
$z += $forwardZ * $speed * $dt;
}
if (craft_key_pressed(KEY_S)) {
$x -= $forwardX * $speed * $dt;
$z -= $forwardZ * $speed * $dt;
}
if (craft_key_pressed(KEY_D)) {
$x += $rightX * $speed * $dt;
$z += $rightZ * $speed * $dt;
}
if (craft_key_pressed(KEY_A)) {
$x -= $rightX * $speed * $dt;
$z -= $rightZ * $speed * $dt;
}
if (craft_key_pressed(KEY_SPACE)) {
$y += $speed * $dt;
}
if (craft_key_pressed(KEY_SHIFT)) {
$y -= $speed * $dt * 0.6;
}
$currentChunkX = floor_chunk((int) floor($x));
$currentChunkZ = floor_chunk((int) floor($z));
if ($currentChunkX !== $centerChunkX || $currentChunkZ !== $centerChunkZ) {
$centerChunkX = $currentChunkX;
$centerChunkZ = $currentChunkZ;
$pendingChunks = enqueue_visible_chunks($centerChunkX, $centerChunkZ, $loadedChunks, $pendingChunks);
$loadedChunks = unload_far_chunks($centerChunkX, $centerChunkZ, $loadedChunks);
$last = craft_get_time();
}
$queueState = process_chunk_queue($loadedChunks, $pendingChunks, CHUNKS_PER_FRAME);
$loadedChunks = $queueState[0];
$pendingChunks = $queueState[1];
craft_set_camera($x, $y, $z, $yaw, $pitch);
craft_render_frame();
craft_sleep(1);
}
craft_shutdown();
}

@ -0,0 +1,23 @@
<?php
function craft_init(string $title, int $width, int $height, string $texturePath): bool {}
function craft_set_sky_texture(string $texturePath): bool {}
function craft_shutdown(): void {}
function craft_should_close(): bool {}
function craft_poll_events(): void {}
function craft_begin_world(): void {}
function craft_set_block(int $x, int $y, int $z, int $type): void {}
function craft_build_mesh(): void {}
function craft_begin_chunk(int $chunkX, int $chunkZ): void {}
function craft_set_chunk_block(int $x, int $y, int $z, int $type): void {}
function craft_commit_chunk(int $chunkX, int $chunkZ): void {}
function craft_remove_chunk(int $chunkX, int $chunkZ): void {}
function craft_render_loading(int $done, int $total): void {}
function craft_render_frame(): void {}
function craft_sleep(int $milliseconds): void {}
function craft_key_pressed(int $key): bool {}
function craft_mouse_delta_x(): float {}
function craft_mouse_delta_y(): float {}
function craft_set_camera(float $x, float $y, float $z, float $yaw, float $pitch): void {}
function craft_get_time(): float {}
function craft_confirm_exit(): bool {}

@ -0,0 +1,16 @@
name: minecraft_demo
version: 0.1.0
mode: bin
cxx-std: c++17
sources:
- main.php
- php-src
- cpp-src
- deps/lodepng
include-paths:
- deps/lodepng
cxx-flags:
- "/D_CRT_SECURE_NO_WARNINGS"
ld-flags:
- "opengl32.lib"

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

@ -0,0 +1,91 @@
# TypePHP Minecraft Demo
这是一个 demo 级 Godot 方块世界示例,用来验证:
- Godot 负责窗口、3D 渲染、输入、水面 shader 和场景表现。
- TypePHP/PHP 负责世界生成、地形高度、河流位置、水块类型等业务逻辑。
- C++ 只做底层对接:GDExtension、DLL 加载、C ABI 调用和数据转换。
当前版本使用 `-m lib` 编译 TypePHP 动态库,不使用 `-m ext`
## 结构
```text
php-src/world.php PHP 世界生成规则
cpp-src/typephp_world_api.cc TypePHP 动态库 C ABI 导出
gdextension-src/ Godot GDExtension 桥接
typephp_bridge.gdextension Godot 扩展声明
scripts/voxel_world.gd 方块、水面、树、材质与渲染
assets/craft/ Craft 项目的 MIT 授权贴图资源
```
## 编译 TypePHP 动态库
```powershell
php bin\compiler.php examples\minecraft-godot\typephp.yml -f
Copy-Item -LiteralPath typephp_world.dll -Destination examples\minecraft-godot\bin\typephp_world.dll -Force
```
## 编译 Godot GDExtension
```powershell
cmake -S examples\minecraft-godot\gdextension-src -B examples\minecraft-godot\gdextension-src\build-nmake -G "NMake Makefiles" -DCMAKE_TOOLCHAIN_FILE=D:\workspace\vcpkg\scripts\buildsystems\vcpkg.cmake -DVCPKG_TARGET_TRIPLET=x64-windows -DCMAKE_BUILD_TYPE=Release
cmake --build examples\minecraft-godot\gdextension-src\build-nmake --config Release
```
GDExtension 会输出到:
```text
examples\minecraft-godot\bin\typephp_godot_bridge.dll
```
## 运行
```powershell
D:\workspace\godot\Godot_v4.7-stable_win64.exe --path examples\minecraft-godot
```
## 操作
- `W/A/S/D` 移动
- `Space` 跳跃
- 鼠标移动视角
- 鼠标左键挖方块
- 鼠标右键放方块
- `Esc` 释放或重新捕获鼠标
## 对接链路
```text
Godot scene
-> TypePhpBridge GDExtension Node
-> LoadLibrary(typephp_world.dll)
-> typephp_world_block_type_at(x, y, z)
-> PHP world.php 生成规则
-> Godot 根据返回的 block type 渲染地形和水面
```
## 当前画面策略
- 方块尺寸缩小为 `0.6`,生成半径扩大为 `28`,世界密度比初版更高。
- 地形按 chunk 合并为少量 `ArrayMesh`,同一 chunk 内按材质分 surface,避免每个方块都创建独立节点。
- 碰撞按 chunk 生成 `ConcavePolygonShape3D`,编辑方块时只重建受影响 chunk。
- 方块使用 Craft 的 `texture.png` atlas,按方块六个面分别映射 tile,比如草方块的顶部、侧面、底面使用不同贴图。
- 使用 ProceduralSkyMaterial 渲染蓝天。
- 运行时生成两层半透明云面,shader 控制云形和缓慢漂移。
- 水面使用透明 shader,带轻微顶点波动和高光。
后续如果继续追求画质,可以为草地、石头、水面增加贴图/法线贴图,并加入远处低细节 chunk 或雾效过渡。
## 借鉴 Craft 的部分
`assets/craft` 中的 `texture.png`、`sky.png`、`font.png` 来自 Michael Fogleman 的 Craft 项目,原项目使用 MIT License,许可证已保留在 `assets/craft/LICENSE.md`
当前示例借鉴了 Craft 的几个核心思路:
- 使用 16x16 tile atlas,而不是每种方块一张独立纹理。
- Craft 的 tile 编号以 atlas 底部为原点,Godot UV 以顶部为原点,渲染时已做 Y 轴翻转。
- 使用方块 registry 描述每个方块六个面的 tile。
- 透明方块和普通方块分材质处理。
- PHP 层负责世界规则:地形高度、水域、沙滩、树木、方块类型判定。
- C++/Godot 层只负责桥接、渲染、输入和碰撞。

@ -0,0 +1,19 @@
Copyright (C) 2013 Michael Fogleman
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://cmg0olihh4r5v"
path="res://.godot/imported/font.png-5f0f7c0ed5a2ad3b0eb4b3f328cf5d77.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://assets/craft/font.png"
dest_files=["res://.godot/imported/font.png-5f0f7c0ed5a2ad3b0eb4b3f328cf5d77.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://c1kmawqyug26q"
path="res://.godot/imported/sky.png-8bbef8db4a43dd6a610bcc834fcf0c71.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://assets/craft/sky.png"
dest_files=["res://.godot/imported/sky.png-8bbef8db4a43dd6a610bcc834fcf0c71.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://ba4a2hxydi0eb"
path="res://.godot/imported/texture.png-4492dd33ad467ed13478d65921b6b050.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://assets/craft/texture.png"
dest_files=["res://.godot/imported/texture.png-4492dd33ad467ed13478d65921b6b050.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

@ -0,0 +1,84 @@
#include <php_typephp_world_func_decl.h>
#ifdef _WIN32
#define TYPEPHP_WORLD_API extern "C" __declspec(dllexport)
#else
#define TYPEPHP_WORLD_API extern "C" __attribute__((visibility("default")))
#endif
enum DemoBlockType {
DEMO_BLOCK_GRASS_C = 1,
DEMO_BLOCK_SAND_C = 2,
DEMO_BLOCK_STONE_C = 3,
DEMO_BLOCK_WOOD_C = 5,
DEMO_BLOCK_DIRT_C = 7,
DEMO_BLOCK_LEAF_C = 15,
DEMO_BLOCK_WATER_C = 64,
};
static constexpr int DEMO_WATER_LEVEL_C = 4;
extern "C" int php_aot_runtime_init(int argc, char **argv);
extern "C" void php_aot_runtime_shutdown();
static bool g_typephp_world_initialized = false;
static int typephp_world_ensure_runtime()
{
if (g_typephp_world_initialized) {
return 1;
}
char app_name[] = "typephp_world";
char *argv[] = {app_name, nullptr};
if (php_aot_runtime_init(1, argv) != 0) {
return 0;
}
g_typephp_world_initialized = true;
return 1;
}
TYPEPHP_WORLD_API int typephp_world_init()
{
return typephp_world_ensure_runtime();
}
TYPEPHP_WORLD_API void typephp_world_shutdown()
{
if (!g_typephp_world_initialized) {
return;
}
php_aot_runtime_shutdown();
g_typephp_world_initialized = false;
}
TYPEPHP_WORLD_API int typephp_world_height_at(int x, int z)
{
if (!typephp_world_ensure_runtime()) {
return 0;
}
return static_cast<int>(php_demo_world_height_at(x, z));
}
TYPEPHP_WORLD_API int typephp_world_is_river(int x, int z)
{
if (!typephp_world_ensure_runtime()) {
return 0;
}
return php_demo_world_is_river(x, z) ? 1 : 0;
}
TYPEPHP_WORLD_API int typephp_world_water_level()
{
return DEMO_WATER_LEVEL_C;
}
TYPEPHP_WORLD_API int typephp_world_block_type_at(int x, int y, int z)
{
if (!typephp_world_ensure_runtime()) {
return -1;
}
return static_cast<int>(php_demo_world_block_type_at(x, y, z));
}

@ -0,0 +1,24 @@
cmake_minimum_required(VERSION 3.21)
project(typephp_godot_bridge LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
find_package(unofficial-godot-cpp CONFIG REQUIRED)
add_library(typephp_godot_bridge SHARED
typephp_bridge.cpp
register_types.cpp
)
target_link_libraries(typephp_godot_bridge PRIVATE unofficial::godot::cpp)
if (MSVC)
target_compile_options(typephp_godot_bridge PRIVATE /EHsc /bigobj)
endif()
set_target_properties(typephp_godot_bridge PROPERTIES
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/../bin"
LIBRARY_OUTPUT_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/../bin"
ARCHIVE_OUTPUT_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/../bin"
)

@ -0,0 +1,33 @@
#include "typephp_bridge.hpp"
#include <godot_cpp/core/class_db.hpp>
#include <godot_cpp/godot.hpp>
using namespace godot;
void initialize_typephp_bridge(ModuleInitializationLevel level)
{
if (level != MODULE_INITIALIZATION_LEVEL_SCENE) {
return;
}
GDREGISTER_CLASS(TypePhpBridge);
}
void uninitialize_typephp_bridge(ModuleInitializationLevel level)
{
if (level != MODULE_INITIALIZATION_LEVEL_SCENE) {
return;
}
}
extern "C" GDExtensionBool GDE_EXPORT typephp_bridge_init(
GDExtensionInterfaceGetProcAddress get_proc_address,
GDExtensionClassLibraryPtr library,
GDExtensionInitialization *initialization)
{
GDExtensionBinding::InitObject init_obj(get_proc_address, library, initialization);
init_obj.register_initializer(initialize_typephp_bridge);
init_obj.register_terminator(uninitialize_typephp_bridge);
init_obj.set_minimum_library_initialization_level(MODULE_INITIALIZATION_LEVEL_SCENE);
return init_obj.init();
}

@ -0,0 +1,94 @@
#include "typephp_bridge.hpp"
#include <godot_cpp/core/class_db.hpp>
#include <godot_cpp/variant/dictionary.hpp>
#include <algorithm>
#ifdef _WIN32
extern "C" IMAGE_DOS_HEADER __ImageBase;
#endif
namespace godot {
void TypePhpBridge::_bind_methods()
{
ClassDB::bind_method(D_METHOD("generate_world", "radius"), &TypePhpBridge::generate_world);
}
TypePhpBridge::TypePhpBridge() = default;
TypePhpBridge::~TypePhpBridge()
{
#ifdef _WIN32
if (library != nullptr) {
FreeLibrary(library);
library = nullptr;
}
#endif
}
bool TypePhpBridge::ensure_loaded()
{
#ifndef _WIN32
return false;
#else
if (library != nullptr) {
return world_init != nullptr && block_type_at != nullptr;
}
wchar_t module_path[MAX_PATH];
const DWORD len = GetModuleFileNameW(reinterpret_cast<HMODULE>(&__ImageBase), module_path, MAX_PATH);
if (len == 0 || len >= MAX_PATH) {
return false;
}
std::wstring path(module_path, len);
const size_t slash = path.find_last_of(L"\\/");
if (slash != std::wstring::npos) {
path.resize(slash + 1);
} else {
path.clear();
}
path += L"typephp_world.dll";
library = LoadLibraryW(path.c_str());
if (library == nullptr) {
return false;
}
world_init = reinterpret_cast<FnInit>(GetProcAddress(library, "typephp_world_init"));
block_type_at = reinterpret_cast<FnBlockTypeAt>(GetProcAddress(library, "typephp_world_block_type_at"));
return world_init != nullptr && block_type_at != nullptr && world_init() != 0;
#endif
}
Array TypePhpBridge::generate_world(int radius)
{
Array blocks;
if (!ensure_loaded()) {
return blocks;
}
radius = std::clamp(radius, 1, 32);
for (int x = -radius; x <= radius; x++) {
for (int z = -radius; z <= radius; z++) {
for (int y = 0; y <= 22; y++) {
const int type = block_type_at(x, y, z);
if (type < 0) {
continue;
}
Dictionary block;
block["x"] = x;
block["y"] = y;
block["z"] = z;
block["type"] = type;
blocks.append(block);
}
}
}
return blocks;
}
} // namespace godot

@ -0,0 +1,37 @@
#pragma once
#include <godot_cpp/classes/node.hpp>
#include <godot_cpp/variant/array.hpp>
#ifdef _WIN32
#include <windows.h>
#endif
namespace godot {
class TypePhpBridge : public Node {
GDCLASS(TypePhpBridge, Node)
using FnInit = int(__cdecl *)();
using FnBlockTypeAt = int(__cdecl *)(int, int, int);
#ifdef _WIN32
HMODULE library = nullptr;
#endif
FnInit world_init = nullptr;
FnBlockTypeAt block_type_at = nullptr;
protected:
static void _bind_methods();
public:
TypePhpBridge();
~TypePhpBridge();
Array generate_world(int radius);
private:
bool ensure_loaded();
};
} // namespace godot

@ -0,0 +1,7 @@
<svg width="128" height="128" viewBox="0 0 128 128" xmlns="http://www.w3.org/2000/svg">
<rect width="128" height="128" rx="18" fill="#2c7a4b"/>
<path d="M24 44 64 22l40 22-40 22z" fill="#78c257"/>
<path d="M24 44v42l40 22V66z" fill="#5b3b20"/>
<path d="M104 44v42l-40 22V66z" fill="#7b4f28"/>
<path d="M42 64h12v12H42zm32 16h12v12H74z" fill="#2a1a0d" opacity=".7"/>
</svg>

After

Width:  |  Height:  |  Size: 383 B

@ -0,0 +1,43 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://cjqay3grd3y01"
path="res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://icon.svg"
dest_files=["res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
svg/scale=1.0
editor/scale_with_editor_scale=false
editor/convert_colors_with_editor_theme=false

@ -0,0 +1,154 @@
<?php
declare(strict_types=1);
const DEMO_BLOCK_GRASS = 1;
const DEMO_BLOCK_SAND = 2;
const DEMO_BLOCK_STONE = 3;
const DEMO_BLOCK_WOOD = 5;
const DEMO_BLOCK_DIRT = 7;
const DEMO_BLOCK_SNOW = 9;
const DEMO_BLOCK_COBBLE = 11;
const DEMO_BLOCK_LEAF = 15;
const DEMO_BLOCK_WATER = 64;
const DEMO_WATER_LEVEL = 4;
const DEMO_MAX_Y = 22;
function demo_world_height_at(int $x, int $z): int
{
$large = demo_noise2($x, $z, 0.045) * 3.2;
$medium = demo_noise2($x + 917, $z - 431, 0.13) * 1.6;
$small = demo_noise2(-$x, $z + 331, 0.31) * 0.8;
$height = 6 + (int) round($large + $medium + $small);
if ($height < 1) {
$height = 1;
} elseif ($height > 12) {
$height = 12;
}
if (demo_world_is_river($x, $z)) {
$height -= 2;
if ($height < 1) {
$height = 1;
} elseif ($height > DEMO_WATER_LEVEL - 1) {
$height = DEMO_WATER_LEVEL - 1;
}
}
return $height;
}
function demo_noise2(int $x, int $z, float $scale): float
{
$a = sin($x * $scale + $z * $scale * 0.71);
$b = cos($x * $scale * 1.37 - $z * $scale * 0.83);
$c = sin(($x + $z) * $scale * 0.53 + cos($z * $scale));
return ($a + $b + $c) / 3.0;
}
function demo_hash2(int $x, int $z): float
{
$n = sin($x * 127.1 + $z * 311.7) * 43758.5453123;
return $n - floor($n);
}
function demo_world_is_river(int $x, int $z): bool
{
$spawnLake = (($x - 7) * ($x - 7) + ($z - 7) * ($z - 7)) <= 28;
if ($spawnLake) {
return true;
}
$center = sin($z * 0.22) * 5.0 + sin($z * 0.07) * 2.0;
$width = 3.4 + cos($z * 0.13) * 1.0;
return abs($x - $center) <= $width;
}
function demo_world_has_tree(int $x, int $z): bool
{
if (demo_world_is_river($x, $z)) {
return false;
}
if (($x % 6) !== 0 || ($z % 6) !== 0) {
return false;
}
return demo_hash2($x, $z) > 0.58;
}
function demo_world_block_type_at(int $x, int $y, int $z): int
{
if ($y < 0 || $y > DEMO_MAX_Y) {
return -1;
}
$height = demo_world_height_at($x, $z);
$river = demo_world_is_river($x, $z);
if ($river && $y === DEMO_WATER_LEVEL) {
return DEMO_BLOCK_WATER;
}
if ($y <= $height) {
if ($y === $height) {
if ($river || $height <= DEMO_WATER_LEVEL) {
return DEMO_BLOCK_SAND;
}
if ($height >= 10) {
return DEMO_BLOCK_SNOW;
}
if (demo_world_is_steep($x, $z, $height)) {
return DEMO_BLOCK_STONE;
}
return DEMO_BLOCK_GRASS;
}
if ($height >= 9 && $y >= $height - 2) {
return DEMO_BLOCK_STONE;
}
return $y >= $height - 2 ? DEMO_BLOCK_DIRT : DEMO_BLOCK_STONE;
}
for ($tx = $x - 3; $tx <= $x + 3; $tx++) {
for ($tz = $z - 3; $tz <= $z + 3; $tz++) {
if (!demo_world_has_tree($tx, $tz)) {
continue;
}
$base = demo_world_height_at($tx, $tz) + 1;
if ($x === $tx && $z === $tz && $y >= $base && $y <= $base + 5) {
return DEMO_BLOCK_WOOD;
}
$dx = $x - $tx;
$dz = $z - $tz;
$dy = $y - ($base + 4);
if ($y >= $base + 2 && $y <= $base + 6 && ($dx * $dx + $dz * $dz + $dy * $dy) <= 10) {
return DEMO_BLOCK_LEAF;
}
}
}
return -1;
}
function demo_world_is_steep(int $x, int $z, int $height): bool
{
$neighbors = [
demo_world_height_at($x + 1, $z),
demo_world_height_at($x - 1, $z),
demo_world_height_at($x, $z + 1),
demo_world_height_at($x, $z - 1),
];
return $height - min($neighbors) >= 2;
}
/**
* @return list<array{x:int,y:int,z:int,type:int}>
*/
function demo_world_generate(int $radius = 18): array
{
$blocks = [];
for ($x = -$radius; $x <= $radius; $x++) {
for ($z = -$radius; $z <= $radius; $z++) {
for ($y = 0; $y <= DEMO_MAX_Y; $y++) {
$type = demo_world_block_type_at($x, $y, $z);
if ($type >= 0) {
$blocks[] = ['x' => $x, 'y' => $y, 'z' => $z, 'type' => $type];
}
}
}
}
return $blocks;
}

@ -0,0 +1,64 @@
; Engine configuration file.
; It's best edited using the editor UI and not directly,
; since the parameters that go here are not all obvious.
config_version=5
[application]
config/name="TypePHP Minecraft Demo"
run/main_scene="res://scenes/main.tscn"
config/features=PackedStringArray("4.7")
config/icon="res://icon.svg"
[display]
window/size/viewport_width=1280
window/size/viewport_height=720
[input]
move_forward={
"deadzone": 0.5,
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":87,"physical_keycode":0,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
]
}
move_back={
"deadzone": 0.5,
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":83,"physical_keycode":0,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
]
}
move_left={
"deadzone": 0.5,
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":65,"physical_keycode":0,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
]
}
move_right={
"deadzone": 0.5,
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":68,"physical_keycode":0,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
]
}
jump={
"deadzone": 0.5,
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":32,"physical_keycode":0,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
]
}
toggle_mouse={
"deadzone": 0.5,
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":4194305,"physical_keycode":0,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
]
}
break_block={
"deadzone": 0.5,
"events": [Object(InputEventMouseButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"button_mask":0,"position":Vector2(0, 0),"global_position":Vector2(0, 0),"factor":1.0,"button_index":1,"canceled":false,"pressed":false,"double_click":false,"script":null)
]
}
place_block={
"deadzone": 0.5,
"events": [Object(InputEventMouseButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"button_mask":0,"position":Vector2(0, 0),"global_position":Vector2(0, 0),"factor":1.0,"button_index":2,"canceled":false,"pressed":false,"double_click":false,"script":null)
]
}
[rendering]
renderer/rendering_method="gl_compatibility"

@ -0,0 +1,85 @@
[gd_scene load_steps=8 format=3 uid="uid://typephp_minecraft_demo"]
[ext_resource type="Script" path="res://scripts/voxel_world.gd" id="1_world"]
[ext_resource type="Script" path="res://scripts/player_controller.gd" id="2_player"]
[ext_resource type="GDExtension" path="res://typephp_bridge.gdextension" id="3_ext"]
[sub_resource type="ProceduralSkyMaterial" id="ProceduralSkyMaterial_1"]
sky_top_color = Color(0.18, 0.5, 0.94, 1)
sky_horizon_color = Color(0.72, 0.88, 1, 1)
ground_bottom_color = Color(0.22, 0.35, 0.2, 1)
ground_horizon_color = Color(0.62, 0.74, 0.55, 1)
[sub_resource type="Sky" id="Sky_1"]
sky_material = SubResource("ProceduralSkyMaterial_1")
[sub_resource type="Environment" id="Environment_1"]
background_mode = 2
sky = SubResource("Sky_1")
ambient_light_source = 2
ambient_light_color = Color(0.68, 0.74, 0.82, 1)
ambient_light_energy = 0.72
fog_enabled = true
fog_light_color = Color(0.74, 0.82, 0.9, 1)
fog_density = 0.006
[sub_resource type="CapsuleShape3D" id="CapsuleShape3D_1"]
radius = 0.35
height = 1.7
[node name="Main" type="Node3D"]
[node name="TypePhpBridge" type="TypePhpBridge" parent="."]
[node name="World" type="Node3D" parent="."]
script = ExtResource("1_world")
bridge_path = NodePath("../TypePhpBridge")
[node name="Sun" type="DirectionalLight3D" parent="."]
transform = Transform3D(0.766044, -0.321394, 0.55667, 0, 0.866025, 0.5, -0.642788, -0.383022, 0.663414, 0, 18, 0)
light_color = Color(1, 0.96, 0.88, 1)
light_energy = 2.15
shadow_enabled = true
shadow_blur = 1.2
[node name="WorldEnvironment" type="WorldEnvironment" parent="."]
environment = SubResource("Environment_1")
[node name="Player" type="CharacterBody3D" parent="."]
script = ExtResource("2_player")
world_path = NodePath("../World")
[node name="CameraPivot" type="Node3D" parent="Player"]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.6, 0)
[node name="Camera3D" type="Camera3D" parent="Player/CameraPivot"]
current = true
fov = 75.0
near = 0.05
far = 160.0
[node name="CollisionShape3D" type="CollisionShape3D" parent="Player"]
shape = SubResource("CapsuleShape3D_1")
[node name="Hud" type="CanvasLayer" parent="."]
[node name="Crosshair" type="Label" parent="Hud"]
anchors_preset = 8
anchor_left = 0.5
anchor_top = 0.5
anchor_right = 0.5
anchor_bottom = 0.5
offset_left = -6.0
offset_top = -11.0
offset_right = 6.0
offset_bottom = 11.0
text = "+"
horizontal_alignment = 1
vertical_alignment = 1
[node name="Help" type="Label" parent="Hud"]
offset_left = 20.0
offset_top = 18.0
offset_right = 680.0
offset_bottom = 120.0
text = "WASD 移动 Space 跳跃 鼠标左键挖方块 右键放方块 Esc 释放/捕获鼠标"

@ -0,0 +1,104 @@
extends CharacterBody3D
@export var world_path: NodePath
@export var speed := 4.8
@export var jump_velocity := 5.4
@export var mouse_sensitivity := 0.0022
@export var spawn_position := Vector3(5.4, 6.0, 4.2)
@export var spawn_yaw_degrees := 90.0
var gravity := ProjectSettings.get_setting("physics/3d/default_gravity") as float
var yaw := 0.0
var pitch := 0.0
var world: Node
@onready var pivot: Node3D = $CameraPivot
@onready var camera: Camera3D = $CameraPivot/Camera3D
func _ready() -> void:
_ensure_input_map()
position = spawn_position
yaw = deg_to_rad(spawn_yaw_degrees)
rotation.y = yaw
world = get_node(world_path)
Input.mouse_mode = Input.MOUSE_MODE_CAPTURED
func _unhandled_input(event: InputEvent) -> void:
if event.is_action_pressed("toggle_mouse"):
Input.mouse_mode = Input.MOUSE_MODE_VISIBLE if Input.mouse_mode == Input.MOUSE_MODE_CAPTURED else Input.MOUSE_MODE_CAPTURED
get_viewport().set_input_as_handled()
return
if event is InputEventMouseMotion and Input.mouse_mode == Input.MOUSE_MODE_CAPTURED:
yaw -= event.relative.x * mouse_sensitivity
pitch = clampf(pitch - event.relative.y * mouse_sensitivity, deg_to_rad(-84), deg_to_rad(84))
rotation.y = yaw
pivot.rotation.x = pitch
get_viewport().set_input_as_handled()
return
if event.is_action_pressed("break_block"):
world.break_from_camera(camera)
get_viewport().set_input_as_handled()
elif event.is_action_pressed("place_block"):
world.place_from_camera(camera)
get_viewport().set_input_as_handled()
func _physics_process(delta: float) -> void:
if not is_on_floor():
velocity.y -= gravity * delta
if Input.is_action_just_pressed("jump") and is_on_floor():
velocity.y = jump_velocity
var input_dir := Input.get_vector("move_left", "move_right", "move_forward", "move_back")
var direction := (transform.basis * Vector3(input_dir.x, 0, input_dir.y)).normalized()
if direction.length_squared() > 0.0001:
velocity.x = direction.x * speed
velocity.z = direction.z * speed
else:
velocity.x = move_toward(velocity.x, 0, speed)
velocity.z = move_toward(velocity.z, 0, speed)
move_and_slide()
func _ensure_input_map() -> void:
_add_key_action("move_forward", [KEY_W, KEY_UP])
_add_key_action("move_back", [KEY_S, KEY_DOWN])
_add_key_action("move_left", [KEY_A, KEY_LEFT])
_add_key_action("move_right", [KEY_D, KEY_RIGHT])
_add_key_action("jump", [KEY_SPACE])
_add_key_action("toggle_mouse", [KEY_ESCAPE])
_add_mouse_action("break_block", MOUSE_BUTTON_LEFT)
_add_mouse_action("place_block", MOUSE_BUTTON_RIGHT)
func _add_key_action(action: StringName, keys: Array) -> void:
if not InputMap.has_action(action):
InputMap.add_action(action)
for key in keys:
var exists := false
for event in InputMap.action_get_events(action):
if event is InputEventKey and event.keycode == key:
exists = true
break
if exists:
continue
var input := InputEventKey.new()
input.keycode = key
input.physical_keycode = key
InputMap.action_add_event(action, input)
func _add_mouse_action(action: StringName, button: MouseButton) -> void:
if not InputMap.has_action(action):
InputMap.add_action(action)
for event in InputMap.action_get_events(action):
if event is InputEventMouseButton and event.button_index == button:
return
var input := InputEventMouseButton.new()
input.button_index = button
InputMap.action_add_event(action, input)

@ -0,0 +1,555 @@
extends Node3D
class_name VoxelWorld
const BLOCK_SIZE := 0.6
const CHUNK_SIZE := 16
const WORLD_RADIUS := 28
const MAX_HEIGHT := 8
const WATER_LEVEL := 4
const WATER_SURFACE_OFFSET := 0.48
const ATLAS_SIZE := 16.0
const ATLAS_PADDING := 1.0 / 2048.0
@export var bridge_path: NodePath
enum BlockType {
GRASS = 1,
SAND = 2,
STONE = 3,
WOOD = 5,
DIRT = 7,
PLANK = 8,
SNOW = 9,
GLASS = 10,
COBBLE = 11,
LEAF = 15,
WATER = 64,
}
var blocks: Dictionary = {}
var water_blocks: Dictionary = {}
var chunks: Dictionary = {}
var materials: Dictionary = {}
var atlas_material: ShaderMaterial
var cloud_mesh: PlaneMesh
var block_tiles := {
BlockType.GRASS: [16, 16, 32, 0, 16, 16],
BlockType.SAND: [1, 1, 1, 1, 1, 1],
BlockType.STONE: [2, 2, 2, 2, 2, 2],
BlockType.WOOD: [20, 20, 36, 4, 20, 20],
BlockType.DIRT: [6, 6, 6, 6, 6, 6],
BlockType.PLANK: [7, 7, 7, 7, 7, 7],
BlockType.SNOW: [24, 24, 40, 8, 24, 24],
BlockType.GLASS: [9, 9, 9, 9, 9, 9],
BlockType.COBBLE: [10, 10, 10, 10, 10, 10],
BlockType.LEAF: [14, 14, 14, 14, 14, 14],
}
func _ready() -> void:
cloud_mesh = PlaneMesh.new()
cloud_mesh.size = Vector2(72.0, 72.0)
_create_materials()
_create_sky_dome()
if not _generate_world_from_bridge():
_generate_world()
_build_all_chunks()
_create_cloud_layers()
func _create_materials() -> void:
atlas_material = _make_atlas_material()
for type in block_tiles.keys():
materials[type] = atlas_material
materials[BlockType.WATER] = _make_water_material()
func _make_atlas_material() -> ShaderMaterial:
var shader := Shader.new()
shader.code = """
shader_type spatial;
render_mode cull_back, diffuse_lambert, specular_disabled;
uniform sampler2D atlas_texture : filter_nearest, repeat_disable;
void fragment() {
vec4 tex = texture(atlas_texture, UV);
if (tex.r > 0.98 && tex.g < 0.02 && tex.b > 0.98) {
discard;
}
ALBEDO = tex.rgb * COLOR.rgb;
ROUGHNESS = 0.95;
}
"""
var material := ShaderMaterial.new()
material.shader = shader
material.set_shader_parameter("atlas_texture", load("res://assets/craft/texture.png"))
return material
func _make_water_material() -> ShaderMaterial:
var shader := Shader.new()
shader.code = """
shader_type spatial;
render_mode blend_mix, depth_prepass_alpha, cull_back, specular_schlick_ggx;
uniform vec4 shallow_color : source_color = vec4(0.18, 0.58, 0.82, 0.58);
uniform vec4 deep_color : source_color = vec4(0.02, 0.24, 0.42, 0.72);
uniform float wave_height = 0.055;
uniform float wave_speed = 1.4;
void vertex() {
float wave_a = sin((VERTEX.x * 3.7 + TIME * wave_speed) + VERTEX.z * 1.4);
float wave_b = cos((VERTEX.z * 4.1 + TIME * wave_speed * 0.8) + VERTEX.x * 1.8);
VERTEX.y += (wave_a + wave_b) * wave_height;
}
void fragment() {
float ripple = sin((UV.x + UV.y) * 18.0 + TIME * 2.2) * 0.5 + 0.5;
ALBEDO = mix(deep_color.rgb, shallow_color.rgb, 0.55 + ripple * 0.18);
ALPHA = shallow_color.a;
ROUGHNESS = 0.18;
SPECULAR = 0.85;
METALLIC = 0.0;
}
"""
var material := ShaderMaterial.new()
material.shader = shader
return material
func _make_cloud_material(speed: float, density: float) -> ShaderMaterial:
var shader := Shader.new()
shader.code = """
shader_type spatial;
render_mode blend_mix, depth_draw_never, cull_disabled, unshaded;
uniform vec4 cloud_color : source_color = vec4(1.0, 1.0, 1.0, 0.68);
uniform float drift_speed = 0.018;
uniform float density = 0.54;
float cloud_noise(vec2 p) {
float a = sin(p.x * 7.0 + p.y * 2.3);
float b = sin(p.x * 3.1 - p.y * 8.4);
float c = sin((p.x + p.y) * 11.0);
return (a + b + c) / 6.0 + 0.5;
}
void fragment() {
vec2 uv = UV + vec2(TIME * drift_speed, TIME * drift_speed * 0.28);
float n1 = cloud_noise(uv);
float n2 = cloud_noise(uv * 2.2 + vec2(0.31, 0.72));
float shape = smoothstep(density, 1.0, n1 * 0.72 + n2 * 0.38);
ALBEDO = cloud_color.rgb;
ALPHA = shape * cloud_color.a;
}
"""
var material := ShaderMaterial.new()
material.shader = shader
material.set_shader_parameter("drift_speed", speed)
material.set_shader_parameter("density", density)
return material
func _create_sky_dome() -> void:
var dome := MeshInstance3D.new()
dome.name = "SkyDome"
var sphere := SphereMesh.new()
sphere.radius = 90.0
sphere.height = 90.0
sphere.radial_segments = 64
sphere.rings = 24
dome.mesh = sphere
dome.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF
dome.material_override = _make_sky_dome_material()
add_child(dome)
func _make_sky_dome_material() -> ShaderMaterial:
var shader := Shader.new()
shader.code = """
shader_type spatial;
render_mode unshaded, cull_front, depth_draw_never, fog_disabled;
uniform vec4 top_color : source_color = vec4(0.18, 0.50, 0.95, 1.0);
uniform vec4 horizon_color : source_color = vec4(0.72, 0.90, 1.0, 1.0);
uniform vec4 sun_color : source_color = vec4(1.0, 0.86, 0.42, 1.0);
uniform vec3 sun_dir = vec3(-0.45, 0.62, -0.64);
void fragment() {
vec3 view_dir = normalize(VIEW);
float up = clamp(view_dir.y * 0.5 + 0.5, 0.0, 1.0);
vec3 sky = mix(horizon_color.rgb, top_color.rgb, smoothstep(0.18, 1.0, up));
float sun = pow(max(dot(normalize(-view_dir), normalize(sun_dir)), 0.0), 480.0);
ALBEDO = sky + sun_color.rgb * sun * 1.6;
}
"""
var material := ShaderMaterial.new()
material.shader = shader
return material
func _create_cloud_layers() -> void:
var cloud_configs := [
{"height": 14.0, "offset": Vector3(0, 0, 0), "speed": 0.016, "density": 0.54},
{"height": 18.0, "offset": Vector3(14, 0, -18), "speed": 0.011, "density": 0.58},
]
for config in cloud_configs:
var cloud := MeshInstance3D.new()
cloud.name = "CloudLayer"
cloud.mesh = cloud_mesh
cloud.position = Vector3(config["offset"].x, config["height"], config["offset"].z)
cloud.material_override = _make_cloud_material(config["speed"], config["density"])
add_child(cloud)
func _generate_world() -> void:
for x in range(-WORLD_RADIUS, WORLD_RADIUS + 1):
for z in range(-WORLD_RADIUS, WORLD_RADIUS + 1):
var h := _height_at(x, z)
var water_area := _is_water_area(x, z)
for y in range(0, h + 1):
var type := BlockType.STONE
if y == h:
type = BlockType.DIRT if water_area else BlockType.GRASS
elif y >= h - 2:
type = BlockType.DIRT
add_block(Vector3i(x, y, z), type, false)
if water_area:
add_block(Vector3i(x, WATER_LEVEL, z), BlockType.WATER, false)
_generate_tree(Vector3i(-7, _height_at(-7, -4) + 1, -4))
_generate_tree(Vector3i(8, _height_at(8, 5) + 1, 5))
_generate_tree(Vector3i(2, _height_at(2, -10) + 1, -10))
func _generate_world_from_bridge() -> bool:
if bridge_path == NodePath(""):
return false
var bridge := get_node_or_null(bridge_path)
if bridge == null or not bridge.has_method("generate_world"):
return false
var generated: Array = bridge.generate_world(WORLD_RADIUS)
if generated.is_empty():
return false
for block in generated:
if not block is Dictionary:
continue
add_block(
Vector3i(int(block.get("x", 0)), int(block.get("y", 0)), int(block.get("z", 0))),
int(block.get("type", BlockType.GRASS)),
false
)
_generate_tree(Vector3i(-7, _height_at(-7, -4) + 1, -4))
_generate_tree(Vector3i(8, _height_at(8, 5) + 1, 5))
_generate_tree(Vector3i(2, _height_at(2, -10) + 1, -10))
return true
func _height_at(x: int, z: int) -> int:
var rolling := sin(float(x) * 0.34) * 1.6 + cos(float(z) * 0.28) * 1.4
var ridge := sin(float(x + z) * 0.18) * 1.2
var height := clampi(3 + int(round(rolling + ridge)), 1, MAX_HEIGHT)
if _is_water_area(x, z):
height = clampi(height - 2, 1, WATER_LEVEL - 1)
return height
func _is_water_area(x: int, z: int) -> bool:
var spawn_lake := (x - 7) * (x - 7) + (z - 7) * (z - 7) <= 28
if spawn_lake:
return true
var center := sin(float(z) * 0.22) * 5.0 + sin(float(z) * 0.07) * 2.0
var width := 3.4 + cos(float(z) * 0.13) * 1.0
return abs(float(x) - center) <= width
func _generate_tree(base: Vector3i) -> void:
for y in range(0, 4):
add_block(base + Vector3i(0, y, 0), BlockType.WOOD, false)
for x in range(-2, 3):
for y in range(2, 5):
for z in range(-2, 3):
if abs(x) + abs(z) + max(0, y - 3) <= 4:
add_block(base + Vector3i(x, y, z), BlockType.LEAF, false)
func add_block(pos: Vector3i, type: int = BlockType.GRASS, rebuild := true) -> bool:
if type == BlockType.WATER:
if water_blocks.has(pos):
return false
water_blocks[pos] = true
else:
if blocks.has(pos):
return false
blocks[pos] = type
water_blocks.erase(pos)
if rebuild:
_rebuild_related_chunks(pos)
return true
func remove_block(pos: Vector3i) -> bool:
if not blocks.has(pos):
return false
blocks.erase(pos)
_rebuild_related_chunks(pos)
return true
func break_from_camera(camera: Camera3D, max_distance: float = 5.5) -> bool:
var hit := _raycast_from_camera(camera, max_distance)
if hit.is_empty():
return false
var normal := hit.normal as Vector3
var pos := _world_to_block_pos(hit.position - normal * 0.02)
return remove_block(pos)
func place_from_camera(camera: Camera3D, max_distance: float = 5.5) -> bool:
var hit := _raycast_from_camera(camera, max_distance)
if hit.is_empty():
return false
var normal := Vector3i(roundi(hit.normal.x), roundi(hit.normal.y), roundi(hit.normal.z))
var base := _world_to_block_pos(hit.position - hit.normal * 0.02)
return add_block(base + normal, BlockType.GRASS, true)
func _raycast_from_camera(camera: Camera3D, max_distance: float) -> Dictionary:
var viewport := get_viewport()
var center := viewport.get_visible_rect().size * 0.5
var origin := camera.project_ray_origin(center)
var end := origin + camera.project_ray_normal(center) * max_distance
var query := PhysicsRayQueryParameters3D.create(origin, end)
query.collide_with_areas = false
query.collide_with_bodies = true
return get_world_3d().direct_space_state.intersect_ray(query)
func _world_to_block_pos(world_pos: Vector3) -> Vector3i:
return Vector3i(
floori(world_pos.x / BLOCK_SIZE + 0.5),
floori(world_pos.y / BLOCK_SIZE + 0.5),
floori(world_pos.z / BLOCK_SIZE + 0.5)
)
func _chunk_key(pos: Vector3i) -> Vector2i:
return Vector2i(floori(float(pos.x) / CHUNK_SIZE), floori(float(pos.z) / CHUNK_SIZE))
func _build_all_chunks() -> void:
for chunk in chunks.values():
(chunk as Node).queue_free()
chunks.clear()
var keys := {}
for pos in blocks.keys():
keys[_chunk_key(pos)] = true
for pos in water_blocks.keys():
keys[_chunk_key(pos)] = true
for key in keys.keys():
_rebuild_chunk(key)
func _rebuild_related_chunks(pos: Vector3i) -> void:
var keys := {_chunk_key(pos): true}
if pos.x % CHUNK_SIZE == 0:
keys[_chunk_key(pos + Vector3i(-1, 0, 0))] = true
if pos.x % CHUNK_SIZE == CHUNK_SIZE - 1:
keys[_chunk_key(pos + Vector3i(1, 0, 0))] = true
if pos.z % CHUNK_SIZE == 0:
keys[_chunk_key(pos + Vector3i(0, 0, -1))] = true
if pos.z % CHUNK_SIZE == CHUNK_SIZE - 1:
keys[_chunk_key(pos + Vector3i(0, 0, 1))] = true
for key in keys.keys():
_rebuild_chunk(key)
func _rebuild_chunk(key: Vector2i) -> void:
if chunks.has(key):
(chunks[key] as Node).queue_free()
chunks.erase(key)
var start_x := key.x * CHUNK_SIZE
var end_x := start_x + CHUNK_SIZE - 1
var start_z := key.y * CHUNK_SIZE
var end_z := start_z + CHUNK_SIZE - 1
var surface_data := {}
for type in block_tiles.keys():
surface_data[type] = _new_surface_data()
var water_data := _new_surface_data()
var collision_faces := PackedVector3Array()
var has_geometry := false
for pos in blocks.keys():
if pos.x < start_x or pos.x > end_x or pos.z < start_z or pos.z > end_z:
continue
var type: int = blocks[pos]
if not surface_data.has(type):
continue
for face in _visible_faces(pos):
_add_cube_face(surface_data[type], pos, face)
_add_cube_face_to_collision(collision_faces, pos, face)
has_geometry = true
for pos in water_blocks.keys():
if pos.x < start_x or pos.x > end_x or pos.z < start_z or pos.z > end_z:
continue
_add_water_face(water_data, pos)
has_geometry = true
if not has_geometry:
return
var body := StaticBody3D.new()
body.name = "Chunk_%d_%d" % [key.x, key.y]
body.set_meta("chunk_key", key)
add_child(body)
var mesh := ArrayMesh.new()
var surface_index := 0
for type in block_tiles.keys():
if _commit_surface(mesh, surface_data[type]):
mesh.surface_set_material(surface_index, materials[type])
surface_index += 1
if _commit_surface(mesh, water_data):
mesh.surface_set_material(surface_index, materials[BlockType.WATER])
var mesh_instance := MeshInstance3D.new()
mesh_instance.mesh = mesh
body.add_child(mesh_instance)
if not collision_faces.is_empty():
var shape := ConcavePolygonShape3D.new()
shape.set_faces(collision_faces)
var collision := CollisionShape3D.new()
collision.shape = shape
body.add_child(collision)
chunks[key] = body
func _new_surface_data() -> Dictionary:
return {
"vertices": PackedVector3Array(),
"normals": PackedVector3Array(),
"uvs": PackedVector2Array(),
"colors": PackedColorArray(),
"indices": PackedInt32Array(),
}
func _commit_surface(mesh: ArrayMesh, data: Dictionary) -> bool:
var vertices: PackedVector3Array = data["vertices"]
if vertices.is_empty():
return false
var arrays := []
arrays.resize(Mesh.ARRAY_MAX)
arrays[Mesh.ARRAY_VERTEX] = vertices
arrays[Mesh.ARRAY_NORMAL] = data["normals"]
arrays[Mesh.ARRAY_TEX_UV] = data["uvs"]
var colors: PackedColorArray = data["colors"]
if colors.size() == vertices.size():
arrays[Mesh.ARRAY_COLOR] = colors
arrays[Mesh.ARRAY_INDEX] = data["indices"]
mesh.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLES, arrays)
return true
func _visible_faces(pos: Vector3i) -> Array:
var faces := []
for face in _face_defs():
var direction: Vector3i = face["dir"]
var neighbor := pos + direction
if not blocks.has(neighbor):
faces.append(face)
return faces
func _face_defs() -> Array:
var h := BLOCK_SIZE * 0.5
return [
{"dir": Vector3i(1, 0, 0), "tile_face": 1, "normal": Vector3(1, 0, 0), "corners": [Vector3(h, -h, -h), Vector3(h, h, -h), Vector3(h, h, h), Vector3(h, -h, h)]},
{"dir": Vector3i(-1, 0, 0), "tile_face": 0, "normal": Vector3(-1, 0, 0), "corners": [Vector3(-h, -h, h), Vector3(-h, h, h), Vector3(-h, h, -h), Vector3(-h, -h, -h)]},
{"dir": Vector3i(0, 1, 0), "tile_face": 2, "normal": Vector3(0, 1, 0), "corners": [Vector3(-h, h, -h), Vector3(-h, h, h), Vector3(h, h, h), Vector3(h, h, -h)]},
{"dir": Vector3i(0, -1, 0), "tile_face": 3, "normal": Vector3(0, -1, 0), "corners": [Vector3(-h, -h, h), Vector3(-h, -h, -h), Vector3(h, -h, -h), Vector3(h, -h, h)]},
{"dir": Vector3i(0, 0, 1), "tile_face": 4, "normal": Vector3(0, 0, 1), "corners": [Vector3(h, -h, h), Vector3(h, h, h), Vector3(-h, h, h), Vector3(-h, -h, h)]},
{"dir": Vector3i(0, 0, -1), "tile_face": 5, "normal": Vector3(0, 0, -1), "corners": [Vector3(-h, -h, -h), Vector3(-h, h, -h), Vector3(h, h, -h), Vector3(h, -h, -h)]},
]
func _add_cube_face(data: Dictionary, pos: Vector3i, face: Dictionary) -> void:
var base_index := (data["vertices"] as PackedVector3Array).size()
var center := Vector3(pos) * BLOCK_SIZE
var corners: Array = face["corners"]
var normal: Vector3 = face["normal"]
var type: int = blocks[pos]
var tiles: Array = block_tiles[type]
var tile_index: int = tiles[int(face["tile_face"])]
var uvs := _tile_uvs(tile_index)
var shade := _face_shade(normal)
var color := Color(shade, shade, shade, 1.0)
for i in range(4):
data["vertices"].append(center + corners[i])
data["normals"].append(normal)
data["uvs"].append(uvs[i])
data["colors"].append(color)
for i in [0, 1, 2, 0, 2, 3]:
data["indices"].append(base_index + i)
func _face_shade(normal: Vector3) -> float:
if normal.y > 0.5:
return 1.0
if normal.y < -0.5:
return 0.48
if abs(normal.x) > 0.5:
return 0.76
return 0.68
func _tile_uvs(tile_index: int) -> Array:
var tile_size := 1.0 / ATLAS_SIZE
var atlas_column := tile_index % int(ATLAS_SIZE)
var craft_row_from_bottom := int(tile_index / int(ATLAS_SIZE))
var godot_row_from_top := int(ATLAS_SIZE) - 1 - craft_row_from_bottom
var u0 := float(atlas_column) * tile_size + ATLAS_PADDING
var v0 := float(godot_row_from_top) * tile_size + ATLAS_PADDING
var u1 := u0 + tile_size - ATLAS_PADDING * 2.0
var v1 := v0 + tile_size - ATLAS_PADDING * 2.0
return [Vector2(u0, v1), Vector2(u0, v0), Vector2(u1, v0), Vector2(u1, v1)]
func _add_cube_face_to_collision(collision_faces: PackedVector3Array, pos: Vector3i, face: Dictionary) -> void:
var center := Vector3(pos) * BLOCK_SIZE
var corners: Array = face["corners"]
for i in [0, 1, 2, 0, 2, 3]:
collision_faces.append(center + corners[i])
func _add_water_face(data: Dictionary, pos: Vector3i) -> void:
var h := BLOCK_SIZE * 0.5
var y := BLOCK_SIZE * WATER_SURFACE_OFFSET
var center := Vector3(pos) * BLOCK_SIZE
var corners := [
Vector3(-h, y, -h),
Vector3(-h, y, h),
Vector3(h, y, h),
Vector3(h, y, -h),
]
var base_index := (data["vertices"] as PackedVector3Array).size()
var uvs := [Vector2(0, 1), Vector2(0, 0), Vector2(1, 0), Vector2(1, 1)]
for i in range(4):
data["vertices"].append(center + corners[i])
data["normals"].append(Vector3.UP)
data["uvs"].append(uvs[i])
for i in [0, 1, 2, 0, 2, 3]:
data["indices"].append(base_index + i)

@ -0,0 +1,7 @@
name: typephp_world
mode: lib
version: 0.1.0
cxx-std: c++17
sources:
- php-src
- cpp-src

@ -0,0 +1,10 @@
[configuration]
entry_symbol = "typephp_bridge_init"
compatibility_minimum = "4.4"
reloadable = true
[libraries]
windows.debug.x86_64 = "res://bin/typephp_godot_bridge.dll"
windows.release.x86_64 = "res://bin/typephp_godot_bridge.dll"
windows.template_debug.x86_64 = "res://bin/typephp_godot_bridge.dll"
windows.template_release.x86_64 = "res://bin/typephp_godot_bridge.dll"
Loading…
Cancel
Save