- 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 renderingpull/16/head
|
After Width: | Height: | Size: 3.3 MiB |
|
After Width: | Height: | Size: 2.0 MiB |
|
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`。 |
||||||
@ -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" |
||||||
|
|
||||||
|
After Width: | Height: | Size: 77 KiB |
|
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. |
||||||
|
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 |
||||||
|
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 |
||||||
|
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 @@ |
|||||||
|
|
||||||
@ -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 @@ |
|||||||
|
|
||||||
@ -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
|
||||||
|
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 @@ |
|||||||
|
uid://dfsxv18qpmi0t |
||||||
@ -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 @@ |
|||||||
|
uid://lkmcf4k8a4j2 |
||||||
@ -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" |
||||||
@ -0,0 +1 @@ |
|||||||
|
uid://cwqtmabbawov7 |
||||||