Move Files, remove 2D world gen
This commit is contained in:
@@ -1,8 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ff30f393052d2674e94fd1d887712090
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -1,31 +0,0 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace InfiniteWorld
|
||||
{
|
||||
public class CameraFollow2D : MonoBehaviour
|
||||
{
|
||||
[SerializeField] private Transform target;
|
||||
[SerializeField] private float smoothTime = 0.18f;
|
||||
[SerializeField] private Vector3 offset = new Vector3(0f, 0f, -10f);
|
||||
|
||||
private Vector3 velocity;
|
||||
|
||||
private void LateUpdate()
|
||||
{
|
||||
if (target == null)
|
||||
{
|
||||
SimplePlayerInputMover player = FindFirstObjectByType<SimplePlayerInputMover>();
|
||||
if (player == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
target = player.transform;
|
||||
}
|
||||
|
||||
Vector3 desiredPosition = target.position + offset;
|
||||
desiredPosition.z = offset.z;
|
||||
transform.position = Vector3.SmoothDamp(transform.position, desiredPosition, ref velocity, smoothTime);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 98ee4fb5b3ebf80478e6e25afa8fd337
|
||||
@@ -1,109 +0,0 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.InputSystem;
|
||||
|
||||
namespace InfiniteWorld
|
||||
{
|
||||
public class SimplePlayerInputMover : MonoBehaviour
|
||||
{
|
||||
[SerializeField] private float moveSpeed = 5f;
|
||||
|
||||
private InputAction moveAction;
|
||||
private Rigidbody2D rb;
|
||||
private Vector2 moveInput;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
rb = GetComponent<Rigidbody2D>();
|
||||
if (rb == null)
|
||||
{
|
||||
rb = gameObject.AddComponent<Rigidbody2D>();
|
||||
}
|
||||
|
||||
ConfigurePhysics();
|
||||
EnsureVisual();
|
||||
|
||||
moveAction = new InputAction("Move", InputActionType.Value);
|
||||
moveAction.AddCompositeBinding("2DVector")
|
||||
.With("Up", "<Keyboard>/w")
|
||||
.With("Down", "<Keyboard>/s")
|
||||
.With("Left", "<Keyboard>/a")
|
||||
.With("Right", "<Keyboard>/d");
|
||||
moveAction.AddCompositeBinding("2DVector")
|
||||
.With("Up", "<Keyboard>/upArrow")
|
||||
.With("Down", "<Keyboard>/downArrow")
|
||||
.With("Left", "<Keyboard>/leftArrow")
|
||||
.With("Right", "<Keyboard>/rightArrow");
|
||||
moveAction.AddBinding("<Gamepad>/leftStick");
|
||||
}
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
moveAction?.Enable();
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
moveAction?.Disable();
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
moveAction?.Dispose();
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (moveAction == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
moveInput = moveAction.ReadValue<Vector2>().normalized;
|
||||
if (moveInput.x != 0f)
|
||||
{
|
||||
Vector3 scale = transform.localScale;
|
||||
scale.x = Mathf.Abs(scale.x) * Mathf.Sign(moveInput.x);
|
||||
transform.localScale = scale;
|
||||
}
|
||||
}
|
||||
|
||||
private void FixedUpdate()
|
||||
{
|
||||
if (rb == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Vector2 target = rb.position + moveInput * (moveSpeed * Time.fixedDeltaTime);
|
||||
rb.MovePosition(target);
|
||||
}
|
||||
|
||||
private void ConfigurePhysics()
|
||||
{
|
||||
rb.gravityScale = 0f;
|
||||
rb.freezeRotation = true;
|
||||
rb.interpolation = RigidbodyInterpolation2D.Interpolate;
|
||||
rb.collisionDetectionMode = CollisionDetectionMode2D.Continuous;
|
||||
|
||||
CapsuleCollider2D collider = GetComponent<CapsuleCollider2D>();
|
||||
if (collider == null)
|
||||
{
|
||||
collider = gameObject.AddComponent<CapsuleCollider2D>();
|
||||
}
|
||||
collider.direction = CapsuleDirection2D.Vertical;
|
||||
collider.size = new Vector2(0.55f, 0.8f);
|
||||
collider.offset = new Vector2(0f, -0.05f);
|
||||
}
|
||||
|
||||
private void EnsureVisual()
|
||||
{
|
||||
SpriteRenderer renderer = GetComponent<SpriteRenderer>();
|
||||
if (renderer == null)
|
||||
{
|
||||
renderer = gameObject.AddComponent<SpriteRenderer>();
|
||||
}
|
||||
renderer.sprite = ProceduralWorldArt.CreatePlayerSprite();
|
||||
renderer.sortingOrder = 10;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c9187e81c6ec8da4599e04a1694ec18b
|
||||
@@ -1,8 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c58482c592be1ac41ad4ad194e9175c0
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -1,8 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a6692637cf5eb1b41a9d619e7557b2f0
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -1,16 +0,0 @@
|
||||
{
|
||||
"name": "VoxelWorld.Runtime",
|
||||
"rootNamespace": "InfiniteWorld.VoxelWorld",
|
||||
"references": [
|
||||
"UniTask"
|
||||
],
|
||||
"includePlatforms": [],
|
||||
"excludePlatforms": [],
|
||||
"allowUnsafeCode": false,
|
||||
"overrideReferences": false,
|
||||
"precompiledReferences": [],
|
||||
"autoReferenced": true,
|
||||
"defineConstraints": [],
|
||||
"versionDefines": [],
|
||||
"noEngineReferences": false
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f0a085d6765d01448bb424934e24ed9c
|
||||
AssemblyDefinitionImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -1,375 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace InfiniteWorld.VoxelWorld
|
||||
{
|
||||
public enum VoxelSurfaceType : byte
|
||||
{
|
||||
CliffTop,
|
||||
CliffSide,
|
||||
Dirt,
|
||||
WalkableSurface
|
||||
}
|
||||
|
||||
[CreateAssetMenu(menuName = "Infinite World/Voxel Biome Profile", fileName = "VoxelBiomeProfile")]
|
||||
public sealed class VoxelBiomeProfile : ScriptableObject
|
||||
{
|
||||
public Sprite cliffTopSprite;
|
||||
public Sprite cliffSideSprite;
|
||||
public Sprite dirtSprite;
|
||||
public Sprite walkableSurfaceSprite;
|
||||
|
||||
public Sprite GetSprite(VoxelSurfaceType surfaceType)
|
||||
{
|
||||
return surfaceType switch
|
||||
{
|
||||
VoxelSurfaceType.CliffTop => cliffTopSprite,
|
||||
VoxelSurfaceType.CliffSide => cliffSideSprite,
|
||||
VoxelSurfaceType.Dirt => dirtSprite,
|
||||
VoxelSurfaceType.WalkableSurface => walkableSurfaceSprite,
|
||||
_ => walkableSurfaceSprite
|
||||
};
|
||||
}
|
||||
|
||||
public bool HasAnyAssignedSprites()
|
||||
{
|
||||
return cliffTopSprite != null || cliffSideSprite != null || dirtSprite != null || walkableSurfaceSprite != null;
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class VoxelWorldAtlas : IDisposable
|
||||
{
|
||||
private const int FallbackTileSize = 32;
|
||||
private const string ShaderName = "Infinite World/VoxelWorld/TextureArrayUnlit";
|
||||
|
||||
private static readonly VoxelSurfaceType[] SurfaceOrder =
|
||||
{
|
||||
VoxelSurfaceType.CliffTop,
|
||||
VoxelSurfaceType.CliffSide,
|
||||
VoxelSurfaceType.Dirt,
|
||||
VoxelSurfaceType.WalkableSurface
|
||||
};
|
||||
|
||||
private readonly Dictionary<int, Dictionary<VoxelSurfaceType, int>> layerLookup;
|
||||
|
||||
private VoxelWorldAtlas(Texture2DArray textureArray, Material material, Dictionary<int, Dictionary<VoxelSurfaceType, int>> layers)
|
||||
{
|
||||
TextureArray = textureArray;
|
||||
Material = material;
|
||||
layerLookup = layers;
|
||||
}
|
||||
|
||||
public Texture2DArray TextureArray { get; }
|
||||
|
||||
public Material Material { get; }
|
||||
|
||||
public int BiomeCount => layerLookup.Count;
|
||||
|
||||
public int GetTextureLayer(int biomeIndex, VoxelSurfaceType surfaceType)
|
||||
{
|
||||
if (layerLookup.TryGetValue(biomeIndex, out Dictionary<VoxelSurfaceType, int> biomeLayers) && biomeLayers.TryGetValue(surfaceType, out int layer))
|
||||
{
|
||||
return layer;
|
||||
}
|
||||
|
||||
return layerLookup[0][surfaceType];
|
||||
}
|
||||
|
||||
public static VoxelWorldAtlas CreateRuntimeAtlas(IReadOnlyList<VoxelBiomeProfile> biomeProfiles)
|
||||
{
|
||||
List<VoxelBiomeProfile> sourceBiomes = new List<VoxelBiomeProfile>();
|
||||
if (biomeProfiles != null)
|
||||
{
|
||||
for (int i = 0; i < biomeProfiles.Count; i++)
|
||||
{
|
||||
if (biomeProfiles[i] != null)
|
||||
{
|
||||
sourceBiomes.Add(biomeProfiles[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (sourceBiomes.Count == 0)
|
||||
{
|
||||
sourceBiomes.Add(null);
|
||||
}
|
||||
|
||||
int tileSize = DetermineTileSize(sourceBiomes);
|
||||
int layerCount = sourceBiomes.Count * SurfaceOrder.Length;
|
||||
Texture2DArray textureArray = new Texture2DArray(tileSize, tileSize, layerCount, TextureFormat.RGBA32, false, false)
|
||||
{
|
||||
filterMode = FilterMode.Point,
|
||||
wrapMode = TextureWrapMode.Repeat,
|
||||
anisoLevel = 1,
|
||||
name = "VoxelWorld_RuntimeTextureArray",
|
||||
hideFlags = HideFlags.HideAndDontSave
|
||||
};
|
||||
|
||||
Dictionary<int, Dictionary<VoxelSurfaceType, int>> layers = new Dictionary<int, Dictionary<VoxelSurfaceType, int>>(sourceBiomes.Count);
|
||||
int layerIndex = 0;
|
||||
for (int biomeIndex = 0; biomeIndex < sourceBiomes.Count; biomeIndex++)
|
||||
{
|
||||
Dictionary<VoxelSurfaceType, int> biomeLayers = new Dictionary<VoxelSurfaceType, int>(SurfaceOrder.Length);
|
||||
layers[biomeIndex] = biomeLayers;
|
||||
|
||||
for (int surfaceIndex = 0; surfaceIndex < SurfaceOrder.Length; surfaceIndex++)
|
||||
{
|
||||
VoxelSurfaceType surfaceType = SurfaceOrder[surfaceIndex];
|
||||
Texture2D tileTexture = BuildSurfaceTexture(sourceBiomes[biomeIndex], surfaceType, tileSize);
|
||||
CopyTextureToArrayLayer(tileTexture, textureArray, layerIndex);
|
||||
biomeLayers[surfaceType] = layerIndex;
|
||||
layerIndex++;
|
||||
|
||||
if (Application.isPlaying)
|
||||
{
|
||||
UnityEngine.Object.Destroy(tileTexture);
|
||||
}
|
||||
else
|
||||
{
|
||||
UnityEngine.Object.DestroyImmediate(tileTexture);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
textureArray.Apply(false, false);
|
||||
|
||||
Shader shader = Shader.Find(ShaderName);
|
||||
if (shader == null)
|
||||
{
|
||||
shader = Shader.Find("Universal Render Pipeline/Unlit") ?? Shader.Find("Standard");
|
||||
Debug.LogError($"Shader '{ShaderName}' was not found. Falling back to '{shader?.name ?? "<missing>"}', but voxel texture array sampling will not work until the shader asset is imported correctly.");
|
||||
}
|
||||
|
||||
Material material = new Material(shader)
|
||||
{
|
||||
name = "VoxelWorld_RuntimeMaterial",
|
||||
hideFlags = HideFlags.HideAndDontSave
|
||||
};
|
||||
|
||||
if (material.HasProperty("_TextureArray"))
|
||||
{
|
||||
material.SetTexture("_TextureArray", textureArray);
|
||||
}
|
||||
|
||||
if (material.HasProperty("_BaseColor"))
|
||||
{
|
||||
material.SetColor("_BaseColor", Color.white);
|
||||
}
|
||||
|
||||
if (material.HasProperty("_Color"))
|
||||
{
|
||||
material.SetColor("_Color", Color.white);
|
||||
}
|
||||
|
||||
return new VoxelWorldAtlas(textureArray, material, layers);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (Application.isPlaying)
|
||||
{
|
||||
UnityEngine.Object.Destroy(TextureArray);
|
||||
UnityEngine.Object.Destroy(Material);
|
||||
}
|
||||
else
|
||||
{
|
||||
UnityEngine.Object.DestroyImmediate(TextureArray);
|
||||
UnityEngine.Object.DestroyImmediate(Material);
|
||||
}
|
||||
}
|
||||
|
||||
private static int DetermineTileSize(IReadOnlyList<VoxelBiomeProfile> biomeProfiles)
|
||||
{
|
||||
int tileSize = FallbackTileSize;
|
||||
for (int biomeIndex = 0; biomeIndex < biomeProfiles.Count; biomeIndex++)
|
||||
{
|
||||
VoxelBiomeProfile biome = biomeProfiles[biomeIndex];
|
||||
for (int surfaceIndex = 0; surfaceIndex < SurfaceOrder.Length; surfaceIndex++)
|
||||
{
|
||||
Sprite sprite = biome != null ? biome.GetSprite(SurfaceOrder[surfaceIndex]) : null;
|
||||
if (sprite == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
Rect rect = sprite.textureRect;
|
||||
tileSize = Mathf.Max(tileSize, Mathf.CeilToInt(Mathf.Max(rect.width, rect.height)));
|
||||
}
|
||||
}
|
||||
|
||||
return Mathf.Max(FallbackTileSize, tileSize);
|
||||
}
|
||||
|
||||
private static Texture2D BuildSurfaceTexture(VoxelBiomeProfile biome, VoxelSurfaceType surfaceType, int tileSize)
|
||||
{
|
||||
Texture2D texture = new Texture2D(tileSize, tileSize, TextureFormat.RGBA32, false)
|
||||
{
|
||||
filterMode = FilterMode.Point,
|
||||
wrapMode = TextureWrapMode.Repeat,
|
||||
hideFlags = HideFlags.HideAndDontSave
|
||||
};
|
||||
|
||||
Fill(texture, new Color(1f, 0f, 1f, 1f));
|
||||
Sprite sprite = biome != null ? biome.GetSprite(surfaceType) : null;
|
||||
if (sprite == null || !TryBlitSprite(texture, sprite))
|
||||
{
|
||||
DrawFallbackSurface(texture, new RectInt(0, 0, tileSize, tileSize), surfaceType);
|
||||
}
|
||||
|
||||
texture.Apply(false, false);
|
||||
return texture;
|
||||
}
|
||||
|
||||
private static void CopyTextureToArrayLayer(Texture2D source, Texture2DArray destination, int layerIndex)
|
||||
{
|
||||
bool canUseCopyTexture = SystemInfo.supports2DArrayTextures &&
|
||||
SystemInfo.copyTextureSupport != UnityEngine.Rendering.CopyTextureSupport.None &&
|
||||
source.width == destination.width &&
|
||||
source.height == destination.height;
|
||||
|
||||
if (canUseCopyTexture)
|
||||
{
|
||||
try
|
||||
{
|
||||
Graphics.CopyTexture(source, 0, 0, destination, layerIndex, 0);
|
||||
return;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// Fall back to CPU copy when runtime copy is unavailable for this platform/import setup.
|
||||
}
|
||||
}
|
||||
|
||||
destination.SetPixels(source.GetPixels(), layerIndex, 0);
|
||||
}
|
||||
|
||||
private static bool TryBlitSprite(Texture2D target, Sprite sprite)
|
||||
{
|
||||
try
|
||||
{
|
||||
Texture2D source = sprite.texture;
|
||||
Rect spriteRect = sprite.textureRect;
|
||||
|
||||
for (int y = 0; y < target.height; y++)
|
||||
{
|
||||
float sampleY = Mathf.Lerp(spriteRect.yMin, spriteRect.yMax - 1f, y / (float)Mathf.Max(1, target.height - 1));
|
||||
int sourceY = Mathf.Clamp(Mathf.RoundToInt(sampleY), 0, source.height - 1);
|
||||
|
||||
for (int x = 0; x < target.width; x++)
|
||||
{
|
||||
float sampleX = Mathf.Lerp(spriteRect.xMin, spriteRect.xMax - 1f, x / (float)Mathf.Max(1, target.width - 1));
|
||||
int sourceX = Mathf.Clamp(Mathf.RoundToInt(sampleX), 0, source.width - 1);
|
||||
target.SetPixel(x, y, source.GetPixel(sourceX, sourceY));
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
catch (UnityException)
|
||||
{
|
||||
Debug.LogWarning($"Sprite '{sprite.name}' texture is not readable. Falling back to generated tile for voxel biome texture array.");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static void DrawFallbackSurface(Texture2D texture, RectInt rect, VoxelSurfaceType surfaceType)
|
||||
{
|
||||
Color baseColor;
|
||||
Color accentColor;
|
||||
switch (surfaceType)
|
||||
{
|
||||
case VoxelSurfaceType.CliffTop:
|
||||
baseColor = new Color(0.40f, 0.72f, 0.28f, 1f);
|
||||
accentColor = new Color(0.18f, 0.35f, 0.13f, 1f);
|
||||
break;
|
||||
case VoxelSurfaceType.CliffSide:
|
||||
baseColor = new Color(0.47f, 0.34f, 0.22f, 1f);
|
||||
accentColor = new Color(0.25f, 0.52f, 0.18f, 1f);
|
||||
break;
|
||||
case VoxelSurfaceType.Dirt:
|
||||
baseColor = new Color(0.44f, 0.31f, 0.20f, 1f);
|
||||
accentColor = new Color(0.31f, 0.20f, 0.12f, 1f);
|
||||
break;
|
||||
default:
|
||||
baseColor = new Color(0.28f, 0.58f, 0.25f, 1f);
|
||||
accentColor = new Color(0.18f, 0.40f, 0.16f, 1f);
|
||||
break;
|
||||
}
|
||||
|
||||
FillRect(texture, rect, baseColor);
|
||||
ApplyNoise(texture, rect, 0.06f, 101 + (int)surfaceType * 43);
|
||||
|
||||
if (surfaceType == VoxelSurfaceType.CliffSide)
|
||||
{
|
||||
FillRect(texture, new RectInt(rect.x, rect.yMax - Mathf.Max(4, rect.height / 6), rect.width, Mathf.Max(4, rect.height / 6)), accentColor);
|
||||
}
|
||||
else if (surfaceType == VoxelSurfaceType.CliffTop)
|
||||
{
|
||||
FillRect(texture, new RectInt(rect.x, rect.yMax - Mathf.Max(4, rect.height / 5), rect.width, Mathf.Max(4, rect.height / 5)), accentColor);
|
||||
}
|
||||
else
|
||||
{
|
||||
for (int y = rect.y + 4; y < rect.yMax - 4; y += Mathf.Max(6, rect.height / 4))
|
||||
{
|
||||
for (int x = rect.x + 4; x < rect.xMax - 4; x += Mathf.Max(6, rect.width / 4))
|
||||
{
|
||||
DrawDisc(texture, x, y, 2, accentColor);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void Fill(Texture2D texture, Color color)
|
||||
{
|
||||
Color[] pixels = new Color[texture.width * texture.height];
|
||||
for (int i = 0; i < pixels.Length; i++)
|
||||
{
|
||||
pixels[i] = color;
|
||||
}
|
||||
|
||||
texture.SetPixels(pixels);
|
||||
}
|
||||
|
||||
private static void FillRect(Texture2D texture, RectInt rect, Color color)
|
||||
{
|
||||
for (int y = rect.yMin; y < rect.yMax; y++)
|
||||
{
|
||||
for (int x = rect.xMin; x < rect.xMax; x++)
|
||||
{
|
||||
texture.SetPixel(x, y, color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void DrawDisc(Texture2D texture, int centerX, int centerY, int radius, Color color)
|
||||
{
|
||||
int sqrRadius = radius * radius;
|
||||
for (int y = -radius; y <= radius; y++)
|
||||
{
|
||||
for (int x = -radius; x <= radius; x++)
|
||||
{
|
||||
if (x * x + y * y > sqrRadius)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
texture.SetPixel(centerX + x, centerY + y, color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void ApplyNoise(Texture2D texture, RectInt rect, float strength, int seed)
|
||||
{
|
||||
for (int y = rect.yMin; y < rect.yMax; y++)
|
||||
{
|
||||
for (int x = rect.xMin; x < rect.xMax; x++)
|
||||
{
|
||||
Color pixel = texture.GetPixel(x, y);
|
||||
float noise = Mathf.PerlinNoise((x + seed) * 0.18f, (y - seed) * 0.18f) - 0.5f;
|
||||
texture.SetPixel(x, y, pixel * (1f + noise * strength * 2f));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 981858f28439459429fa291e1f6cb935
|
||||
@@ -1,182 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace InfiniteWorld.VoxelWorld
|
||||
{
|
||||
[CreateAssetMenu(menuName = "Infinite World/Voxel World Config", fileName = "VoxelWorldConfig")]
|
||||
public sealed class VoxelWorldConfig : ScriptableObject
|
||||
{
|
||||
[Header("Streaming")]
|
||||
[Min(8)] public int chunkSize = 16;
|
||||
[Min(1)] public int generationRadius = 3;
|
||||
[Min(0)] public int blockingGenerationRadius;
|
||||
public int seed = 12345;
|
||||
[Min(1)] public int maxMountainHeight = 3;
|
||||
|
||||
[Header("Shape Noise")]
|
||||
public float macroNoiseScale = 0.05f;
|
||||
public float detailNoiseScale = 0.12f;
|
||||
public float ridgeNoiseScale = 0.18f;
|
||||
public float wallThreshold = 0.6f;
|
||||
public float rockBias = 0.04f;
|
||||
[Min(0)] public int smoothingPasses = 2;
|
||||
|
||||
[Header("Global Passes")]
|
||||
public float passNoiseScale = 0.018f;
|
||||
public float passDetailScale = 0.041f;
|
||||
public float passThreshold = 0.22f;
|
||||
public float passFeather = 0.12f;
|
||||
|
||||
[Header("Height")]
|
||||
public float heightNoiseScale = 0.08f;
|
||||
public float terraceNoiseScale = 0.17f;
|
||||
public float heightBias = 0.05f;
|
||||
|
||||
[Header("Biomes")]
|
||||
public List<VoxelBiomeProfile> biomeProfiles = new List<VoxelBiomeProfile>();
|
||||
public float biomeNoiseScale = 0.02f;
|
||||
[Min(1f)] public float biomeSize = 48f;
|
||||
|
||||
[Header("Runtime")]
|
||||
[Min(1)] public int maxAsyncChunkJobs = 2;
|
||||
[Min(1)] public int maxChunkBuildsPerFrame = 1;
|
||||
[Min(1)] public int maxChunkMeshBuildsPerFrame = 1;
|
||||
[Min(1)] public int maxColliderAppliesPerFrame = 1;
|
||||
[Min(0)] public int maxNeighborRefreshesPerFrame = 2;
|
||||
[Min(1)] public int renderRegionSizeInChunks = 4;
|
||||
[Min(1)] public int maxRegionBuildsPerFrame = 1;
|
||||
}
|
||||
|
||||
internal readonly struct VoxelWorldResolvedSettings
|
||||
{
|
||||
private static readonly IReadOnlyList<VoxelBiomeProfile> EmptyBiomes = System.Array.Empty<VoxelBiomeProfile>();
|
||||
|
||||
public static readonly VoxelWorldResolvedSettings Default = Resolve(null);
|
||||
|
||||
public VoxelWorldResolvedSettings(
|
||||
int chunkSize,
|
||||
int generationRadius,
|
||||
int blockingGenerationRadius,
|
||||
int seed,
|
||||
int maxMountainHeight,
|
||||
float macroNoiseScale,
|
||||
float detailNoiseScale,
|
||||
float ridgeNoiseScale,
|
||||
float wallThreshold,
|
||||
float rockBias,
|
||||
int smoothingPasses,
|
||||
float passNoiseScale,
|
||||
float passDetailScale,
|
||||
float passThreshold,
|
||||
float passFeather,
|
||||
float heightNoiseScale,
|
||||
float terraceNoiseScale,
|
||||
float heightBias,
|
||||
IReadOnlyList<VoxelBiomeProfile> biomeProfiles,
|
||||
float biomeNoiseScale,
|
||||
float biomeSize,
|
||||
int maxAsyncChunkJobs,
|
||||
int maxChunkBuildsPerFrame,
|
||||
int maxChunkMeshBuildsPerFrame,
|
||||
int maxColliderAppliesPerFrame,
|
||||
int maxNeighborRefreshesPerFrame,
|
||||
int renderRegionSizeInChunks,
|
||||
int maxRegionBuildsPerFrame)
|
||||
{
|
||||
ChunkSize = chunkSize;
|
||||
GenerationRadius = generationRadius;
|
||||
BlockingGenerationRadius = blockingGenerationRadius;
|
||||
Seed = seed;
|
||||
MaxMountainHeight = maxMountainHeight;
|
||||
MacroNoiseScale = macroNoiseScale;
|
||||
DetailNoiseScale = detailNoiseScale;
|
||||
RidgeNoiseScale = ridgeNoiseScale;
|
||||
WallThreshold = wallThreshold;
|
||||
RockBias = rockBias;
|
||||
SmoothingPasses = smoothingPasses;
|
||||
PassNoiseScale = passNoiseScale;
|
||||
PassDetailScale = passDetailScale;
|
||||
PassThreshold = passThreshold;
|
||||
PassFeather = passFeather;
|
||||
HeightNoiseScale = heightNoiseScale;
|
||||
TerraceNoiseScale = terraceNoiseScale;
|
||||
HeightBias = heightBias;
|
||||
BiomeProfiles = biomeProfiles;
|
||||
BiomeNoiseScale = biomeNoiseScale;
|
||||
BiomeSize = biomeSize;
|
||||
MaxAsyncChunkJobs = maxAsyncChunkJobs;
|
||||
MaxChunkBuildsPerFrame = maxChunkBuildsPerFrame;
|
||||
MaxChunkMeshBuildsPerFrame = maxChunkMeshBuildsPerFrame;
|
||||
MaxColliderAppliesPerFrame = maxColliderAppliesPerFrame;
|
||||
MaxNeighborRefreshesPerFrame = maxNeighborRefreshesPerFrame;
|
||||
RenderRegionSizeInChunks = renderRegionSizeInChunks;
|
||||
MaxRegionBuildsPerFrame = maxRegionBuildsPerFrame;
|
||||
}
|
||||
|
||||
public int ChunkSize { get; }
|
||||
public int GenerationRadius { get; }
|
||||
public int BlockingGenerationRadius { get; }
|
||||
public int Seed { get; }
|
||||
public int MaxMountainHeight { get; }
|
||||
public float MacroNoiseScale { get; }
|
||||
public float DetailNoiseScale { get; }
|
||||
public float RidgeNoiseScale { get; }
|
||||
public float WallThreshold { get; }
|
||||
public float RockBias { get; }
|
||||
public int SmoothingPasses { get; }
|
||||
public float PassNoiseScale { get; }
|
||||
public float PassDetailScale { get; }
|
||||
public float PassThreshold { get; }
|
||||
public float PassFeather { get; }
|
||||
public float HeightNoiseScale { get; }
|
||||
public float TerraceNoiseScale { get; }
|
||||
public float HeightBias { get; }
|
||||
public IReadOnlyList<VoxelBiomeProfile> BiomeProfiles { get; }
|
||||
public float BiomeNoiseScale { get; }
|
||||
public float BiomeSize { get; }
|
||||
public int MaxAsyncChunkJobs { get; }
|
||||
public int MaxChunkBuildsPerFrame { get; }
|
||||
public int MaxChunkMeshBuildsPerFrame { get; }
|
||||
public int MaxColliderAppliesPerFrame { get; }
|
||||
public int MaxNeighborRefreshesPerFrame { get; }
|
||||
public int RenderRegionSizeInChunks { get; }
|
||||
public int MaxRegionBuildsPerFrame { get; }
|
||||
|
||||
public static VoxelWorldResolvedSettings Resolve(VoxelWorldConfig config)
|
||||
{
|
||||
IReadOnlyList<VoxelBiomeProfile> biomes = config != null && config.biomeProfiles != null
|
||||
? config.biomeProfiles
|
||||
: EmptyBiomes;
|
||||
|
||||
return new VoxelWorldResolvedSettings(
|
||||
Mathf.Max(8, config != null ? config.chunkSize : 16),
|
||||
Mathf.Max(1, config != null ? config.generationRadius : 3),
|
||||
Mathf.Max(0, config != null ? config.blockingGenerationRadius : 0),
|
||||
config != null ? config.seed : 12345,
|
||||
Mathf.Max(1, config != null ? config.maxMountainHeight : 3),
|
||||
config != null ? config.macroNoiseScale : 0.05f,
|
||||
config != null ? config.detailNoiseScale : 0.12f,
|
||||
config != null ? config.ridgeNoiseScale : 0.18f,
|
||||
config != null ? config.wallThreshold : 0.6f,
|
||||
config != null ? config.rockBias : 0.04f,
|
||||
Mathf.Max(0, config != null ? config.smoothingPasses : 2),
|
||||
config != null ? config.passNoiseScale : 0.018f,
|
||||
config != null ? config.passDetailScale : 0.041f,
|
||||
config != null ? config.passThreshold : 0.22f,
|
||||
config != null ? config.passFeather : 0.12f,
|
||||
config != null ? config.heightNoiseScale : 0.08f,
|
||||
config != null ? config.terraceNoiseScale : 0.17f,
|
||||
config != null ? config.heightBias : 0.05f,
|
||||
biomes,
|
||||
config != null ? config.biomeNoiseScale : 0.02f,
|
||||
Mathf.Max(1f, config != null ? config.biomeSize : 48f),
|
||||
Mathf.Max(1, config != null ? config.maxAsyncChunkJobs : 2),
|
||||
Mathf.Max(1, config != null ? config.maxChunkBuildsPerFrame : 1),
|
||||
Mathf.Max(1, config != null ? config.maxChunkMeshBuildsPerFrame : 1),
|
||||
Mathf.Max(1, config != null ? config.maxColliderAppliesPerFrame : 1),
|
||||
Mathf.Max(0, config != null ? config.maxNeighborRefreshesPerFrame : 2),
|
||||
Mathf.Max(1, config != null ? config.renderRegionSizeInChunks : 4),
|
||||
Mathf.Max(1, config != null ? config.maxRegionBuildsPerFrame : 1));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0cac3da4969c2f94985de9f5fb30a682
|
||||
@@ -1,165 +0,0 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace InfiniteWorld.VoxelWorld
|
||||
{
|
||||
public sealed partial class VoxelWorldGenerator
|
||||
{
|
||||
private ChunkBuildResult GenerateChunkData(Vector2Int coord, int version, int session, int runtimeId)
|
||||
{
|
||||
int margin = Mathf.Max(2, smoothingPasses + 1);
|
||||
int sampleSize = chunkSize + margin * 2;
|
||||
bool[,] sampled = new bool[sampleSize, sampleSize];
|
||||
|
||||
for (int z = 0; z < sampleSize; z++)
|
||||
{
|
||||
for (int x = 0; x < sampleSize; x++)
|
||||
{
|
||||
int localX = x - margin;
|
||||
int localZ = z - margin;
|
||||
Vector2Int worldCell = ChunkToWorldCell(coord, localX, localZ);
|
||||
sampled[x, z] = SampleRock(worldCell);
|
||||
}
|
||||
}
|
||||
|
||||
for (int pass = 0; pass < smoothingPasses; pass++)
|
||||
{
|
||||
sampled = SmoothSampledMask(sampled);
|
||||
}
|
||||
|
||||
int[] heights = new int[chunkSize * chunkSize];
|
||||
byte[] biomeIndices = new byte[chunkSize * chunkSize];
|
||||
for (int z = 0; z < chunkSize; z++)
|
||||
{
|
||||
for (int x = 0; x < chunkSize; x++)
|
||||
{
|
||||
Vector2Int worldCell = ChunkToWorldCell(coord, x, z);
|
||||
biomeIndices[z * chunkSize + x] = SampleBiomeIndex(worldCell);
|
||||
bool hasMountain = sampled[x + margin, z + margin];
|
||||
if (!hasMountain)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
heights[z * chunkSize + x] = SampleHeight(worldCell);
|
||||
}
|
||||
}
|
||||
|
||||
return new ChunkBuildResult(coord, heights, biomeIndices, version, session, runtimeId);
|
||||
}
|
||||
|
||||
private bool[,] SmoothSampledMask(bool[,] source)
|
||||
{
|
||||
int width = source.GetLength(0);
|
||||
int height = source.GetLength(1);
|
||||
bool[,] result = new bool[width, height];
|
||||
|
||||
for (int z = 0; z < height; z++)
|
||||
{
|
||||
for (int x = 0; x < width; x++)
|
||||
{
|
||||
int solidNeighbors = CountSampledNeighbors(source, x, z);
|
||||
if (solidNeighbors >= 5)
|
||||
{
|
||||
result[x, z] = true;
|
||||
}
|
||||
else if (solidNeighbors <= 2)
|
||||
{
|
||||
result[x, z] = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
result[x, z] = source[x, z];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static int CountSampledNeighbors(bool[,] sampled, int x, int z)
|
||||
{
|
||||
int width = sampled.GetLength(0);
|
||||
int height = sampled.GetLength(1);
|
||||
int count = 0;
|
||||
|
||||
for (int oz = -1; oz <= 1; oz++)
|
||||
{
|
||||
for (int ox = -1; ox <= 1; ox++)
|
||||
{
|
||||
if (ox == 0 && oz == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
int nx = x + ox;
|
||||
int nz = z + oz;
|
||||
if (nx < 0 || nz < 0 || nx >= width || nz >= height)
|
||||
{
|
||||
count++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (sampled[nx, nz])
|
||||
{
|
||||
count++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
private bool SampleRock(Vector2Int worldCell)
|
||||
{
|
||||
float macro = Mathf.PerlinNoise((worldCell.x + seed * 0.13f) * macroNoiseScale, (worldCell.y - seed * 0.17f) * macroNoiseScale);
|
||||
float detail = Mathf.PerlinNoise((worldCell.x - seed * 0.23f) * detailNoiseScale, (worldCell.y + seed * 0.19f) * detailNoiseScale);
|
||||
float ridge = 1f - Mathf.Abs(Mathf.PerlinNoise((worldCell.x + seed * 0.31f) * ridgeNoiseScale, (worldCell.y + seed * 0.29f) * ridgeNoiseScale) * 2f - 1f);
|
||||
|
||||
float rockValue = macro * 0.62f + detail * 0.18f + ridge * 0.20f + rockBias;
|
||||
if (IsInsideGlobalPass(worldCell))
|
||||
{
|
||||
rockValue -= 0.45f;
|
||||
}
|
||||
|
||||
return rockValue >= wallThreshold;
|
||||
}
|
||||
|
||||
private bool IsInsideGlobalPass(Vector2Int worldCell)
|
||||
{
|
||||
float primary = Mathf.PerlinNoise((worldCell.x + seed * 0.41f) * passNoiseScale, (worldCell.y - seed * 0.43f) * passNoiseScale);
|
||||
float detail = Mathf.PerlinNoise((worldCell.x - seed * 0.17f) * passDetailScale, (worldCell.y + seed * 0.23f) * passDetailScale);
|
||||
float ridged = Mathf.Abs(primary * 2f - 1f);
|
||||
float warped = Mathf.Lerp(ridged, Mathf.Abs(detail * 2f - 1f), 0.35f);
|
||||
return warped <= passThreshold + passFeather * detail;
|
||||
}
|
||||
|
||||
private int SampleHeight(Vector2Int worldCell)
|
||||
{
|
||||
float macro = Mathf.PerlinNoise((worldCell.x - seed * 0.47f) * heightNoiseScale, (worldCell.y + seed * 0.37f) * heightNoiseScale);
|
||||
float terrace = Mathf.PerlinNoise((worldCell.x + seed * 0.67f) * terraceNoiseScale, (worldCell.y - seed * 0.59f) * terraceNoiseScale);
|
||||
float ridge = 1f - Mathf.Abs(Mathf.PerlinNoise((worldCell.x + seed * 0.71f) * ridgeNoiseScale, (worldCell.y - seed * 0.73f) * ridgeNoiseScale) * 2f - 1f);
|
||||
float heightValue = macro * 0.55f + terrace * 0.2f + ridge * 0.25f + heightBias;
|
||||
int height = 1 + Mathf.FloorToInt(Mathf.Clamp01(heightValue) * maxMountainHeight);
|
||||
return Mathf.Clamp(height, 1, maxMountainHeight);
|
||||
}
|
||||
|
||||
private byte SampleBiomeIndex(Vector2Int worldCell)
|
||||
{
|
||||
int biomeCount = Mathf.Max(1, atlasBiomeCount > 0 ? atlasBiomeCount : CountConfiguredBiomes());
|
||||
if (biomeCount <= 1)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
float effectiveScale = biomeNoiseScale / Mathf.Max(1f, biomeSize);
|
||||
float noise = Mathf.PerlinNoise((worldCell.x + seed * 0.83f) * effectiveScale, (worldCell.y - seed * 0.79f) * effectiveScale);
|
||||
int biomeIndex = Mathf.FloorToInt(Mathf.Clamp01(noise) * biomeCount);
|
||||
return (byte)Mathf.Clamp(biomeIndex, 0, biomeCount - 1);
|
||||
}
|
||||
|
||||
private Vector2Int ChunkToWorldCell(Vector2Int coord, int localX, int localZ)
|
||||
{
|
||||
return new Vector2Int(coord.x * chunkSize + localX, coord.y * chunkSize + localZ);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9da608950686fb345b172db0a56bced5
|
||||
@@ -1,451 +0,0 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace InfiniteWorld.VoxelWorld
|
||||
{
|
||||
public sealed partial class VoxelWorldGenerator
|
||||
{
|
||||
private void RenderChunk(Vector2Int coord)
|
||||
{
|
||||
if (!chunks.TryGetValue(coord, out ChunkRuntime runtime) || !runtime.HasData || atlas == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
runtime.EnsureCreated(coord, chunkRoot, chunkSize);
|
||||
ChunkMeshBuild meshBuild = BuildChunkMesh(coord, runtime.Heights, runtime.BiomeIndices);
|
||||
runtime.ApplyRenderData(meshBuild.RenderSnapshot);
|
||||
EnqueueColliderApply(coord, runtime.Version, runtime.RuntimeId, meshBuild.ColliderMesh);
|
||||
runtime.State = ChunkState.Rendered;
|
||||
MarkRegionDirty(coord);
|
||||
}
|
||||
|
||||
private ChunkMeshBuild BuildChunkMesh(Vector2Int coord, int[] heights, byte[] biomeIndices)
|
||||
{
|
||||
MeshBuffers renderBuffers = new MeshBuffers();
|
||||
MeshBuffers colliderBuffers = new MeshBuffers();
|
||||
|
||||
BuildGroundSurface(heights, biomeIndices, renderBuffers);
|
||||
BuildMountainTops(coord, heights, biomeIndices, renderBuffers, colliderBuffers);
|
||||
BuildMountainSides(coord, heights, biomeIndices, renderBuffers, colliderBuffers);
|
||||
|
||||
ChunkRenderSnapshot renderSnapshot = renderBuffers.ToSnapshot();
|
||||
return new ChunkMeshBuild(colliderBuffers.ToMesh($"Collider_{coord.x}_{coord.y}"), renderSnapshot);
|
||||
}
|
||||
|
||||
private void BuildGroundSurface(int[] heights, byte[] biomeIndices, MeshBuffers renderBuffers)
|
||||
{
|
||||
bool[,] visited = new bool[chunkSize, chunkSize];
|
||||
for (int z = 0; z < chunkSize; z++)
|
||||
{
|
||||
for (int x = 0; x < chunkSize; x++)
|
||||
{
|
||||
int index = z * chunkSize + x;
|
||||
if (visited[x, z] || heights[index] > 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
byte biomeIndex = biomeIndices[index];
|
||||
int width = 1;
|
||||
while (x + width < chunkSize && !visited[x + width, z] && heights[z * chunkSize + x + width] == 0 && biomeIndices[z * chunkSize + x + width] == biomeIndex)
|
||||
{
|
||||
width++;
|
||||
}
|
||||
|
||||
int depth = 1;
|
||||
bool canGrow = true;
|
||||
while (z + depth < chunkSize && canGrow)
|
||||
{
|
||||
for (int ix = 0; ix < width; ix++)
|
||||
{
|
||||
int candidateIndex = (z + depth) * chunkSize + x + ix;
|
||||
if (visited[x + ix, z + depth] || heights[candidateIndex] > 0 || biomeIndices[candidateIndex] != biomeIndex)
|
||||
{
|
||||
canGrow = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (canGrow)
|
||||
{
|
||||
depth++;
|
||||
}
|
||||
}
|
||||
|
||||
for (int dz = 0; dz < depth; dz++)
|
||||
{
|
||||
for (int dx = 0; dx < width; dx++)
|
||||
{
|
||||
visited[x + dx, z + dz] = true;
|
||||
}
|
||||
}
|
||||
|
||||
AddTopQuad(renderBuffers, x, z, width, depth, 0f, atlas.GetTextureLayer(biomeIndex, VoxelSurfaceType.WalkableSurface));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void BuildMountainTops(Vector2Int coord, int[] heights, byte[] biomeIndices, MeshBuffers renderBuffers, MeshBuffers colliderBuffers)
|
||||
{
|
||||
bool[,] visited = new bool[chunkSize, chunkSize];
|
||||
for (int z = 0; z < chunkSize; z++)
|
||||
{
|
||||
for (int x = 0; x < chunkSize; x++)
|
||||
{
|
||||
int height = heights[z * chunkSize + x];
|
||||
if (height <= 0 || visited[x, z])
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
int index = z * chunkSize + x;
|
||||
VoxelSurfaceType surfaceType = IsCliffTop(ChunkToWorldCell(coord, x, z), height) ? VoxelSurfaceType.CliffTop : VoxelSurfaceType.WalkableSurface;
|
||||
byte biomeIndex = biomeIndices[index];
|
||||
|
||||
int width = 1;
|
||||
while (x + width < chunkSize && !visited[x + width, z])
|
||||
{
|
||||
int candidateIndex = z * chunkSize + x + width;
|
||||
if (heights[candidateIndex] != height || biomeIndices[candidateIndex] != biomeIndex)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
VoxelSurfaceType candidateSurface = IsCliffTop(ChunkToWorldCell(coord, x + width, z), height) ? VoxelSurfaceType.CliffTop : VoxelSurfaceType.WalkableSurface;
|
||||
if (candidateSurface != surfaceType)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
width++;
|
||||
}
|
||||
|
||||
int depth = 1;
|
||||
bool canGrow = true;
|
||||
while (z + depth < chunkSize && canGrow)
|
||||
{
|
||||
for (int ix = 0; ix < width; ix++)
|
||||
{
|
||||
if (visited[x + ix, z + depth])
|
||||
{
|
||||
canGrow = false;
|
||||
break;
|
||||
}
|
||||
|
||||
int candidateIndex = (z + depth) * chunkSize + x + ix;
|
||||
if (heights[candidateIndex] != height || biomeIndices[candidateIndex] != biomeIndex)
|
||||
{
|
||||
canGrow = false;
|
||||
break;
|
||||
}
|
||||
|
||||
VoxelSurfaceType candidateSurface = IsCliffTop(ChunkToWorldCell(coord, x + ix, z + depth), height) ? VoxelSurfaceType.CliffTop : VoxelSurfaceType.WalkableSurface;
|
||||
if (candidateSurface != surfaceType)
|
||||
{
|
||||
canGrow = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (canGrow)
|
||||
{
|
||||
depth++;
|
||||
}
|
||||
}
|
||||
|
||||
for (int dz = 0; dz < depth; dz++)
|
||||
{
|
||||
for (int dx = 0; dx < width; dx++)
|
||||
{
|
||||
visited[x + dx, z + dz] = true;
|
||||
}
|
||||
}
|
||||
|
||||
int textureLayer = atlas.GetTextureLayer(biomeIndex, surfaceType);
|
||||
AddTopQuad(renderBuffers, x, z, width, depth, height, textureLayer);
|
||||
AddTopQuad(colliderBuffers, x, z, width, depth, height, 0, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void BuildMountainSides(Vector2Int coord, int[] heights, byte[] biomeIndices, MeshBuffers renderBuffers, MeshBuffers colliderBuffers)
|
||||
{
|
||||
for (int z = 0; z < chunkSize; z++)
|
||||
{
|
||||
BuildNorthSouthFaceSlice(coord, heights, biomeIndices, renderBuffers, colliderBuffers, z, true);
|
||||
BuildNorthSouthFaceSlice(coord, heights, biomeIndices, renderBuffers, colliderBuffers, z, false);
|
||||
}
|
||||
|
||||
for (int x = 0; x < chunkSize; x++)
|
||||
{
|
||||
BuildEastWestFaceSlice(coord, heights, biomeIndices, renderBuffers, colliderBuffers, x, true);
|
||||
BuildEastWestFaceSlice(coord, heights, biomeIndices, renderBuffers, colliderBuffers, x, false);
|
||||
}
|
||||
}
|
||||
|
||||
private void BuildNorthSouthFaceSlice(Vector2Int coord, int[] heights, byte[] biomeIndices, MeshBuffers renderBuffers, MeshBuffers colliderBuffers, int z, bool north)
|
||||
{
|
||||
bool[,] visited = new bool[chunkSize, maxMountainHeight];
|
||||
for (int x = 0; x < chunkSize; x++)
|
||||
{
|
||||
for (int y = 0; y < maxMountainHeight; y++)
|
||||
{
|
||||
if (visited[x, y] || !TryGetNorthSouthFace(coord, heights, biomeIndices, x, z, y, north, out byte biomeIndex, out VoxelSurfaceType surfaceType, out int textureLayer))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
int width = 1;
|
||||
while (x + width < chunkSize && !visited[x + width, y] && TryGetNorthSouthFace(coord, heights, biomeIndices, x + width, z, y, north, out byte candidateBiome, out VoxelSurfaceType candidateSurface, out int candidateTexture) && candidateBiome == biomeIndex && candidateSurface == surfaceType && candidateTexture == textureLayer)
|
||||
{
|
||||
width++;
|
||||
}
|
||||
|
||||
int height = 1;
|
||||
bool canGrow = true;
|
||||
while (y + height < maxMountainHeight && canGrow)
|
||||
{
|
||||
for (int dx = 0; dx < width; dx++)
|
||||
{
|
||||
if (visited[x + dx, y + height] || !TryGetNorthSouthFace(coord, heights, biomeIndices, x + dx, z, y + height, north, out byte candidateBiome, out VoxelSurfaceType candidateSurface, out int candidateTexture) || candidateBiome != biomeIndex || candidateSurface != surfaceType || candidateTexture != textureLayer)
|
||||
{
|
||||
canGrow = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (canGrow)
|
||||
{
|
||||
height++;
|
||||
}
|
||||
}
|
||||
|
||||
for (int dy = 0; dy < height; dy++)
|
||||
{
|
||||
for (int dx = 0; dx < width; dx++)
|
||||
{
|
||||
visited[x + dx, y + dy] = true;
|
||||
}
|
||||
}
|
||||
|
||||
float faceZ = north ? z + 1f : z;
|
||||
Vector3 bl = new Vector3(x, y, faceZ);
|
||||
Vector3 br = new Vector3(x + width, y, faceZ);
|
||||
Vector3 tr = new Vector3(x + width, y + height, faceZ);
|
||||
Vector3 tl = new Vector3(x, y + height, faceZ);
|
||||
|
||||
if (north)
|
||||
{
|
||||
AddVerticalQuad(renderBuffers, bl, br, tr, tl, width, height, textureLayer, Vector3.forward);
|
||||
AddVerticalQuad(colliderBuffers, bl, br, tr, tl, width, height, 0, Vector3.forward, false);
|
||||
}
|
||||
else
|
||||
{
|
||||
AddVerticalQuad(renderBuffers, br, bl, tl, tr, width, height, textureLayer, Vector3.back);
|
||||
AddVerticalQuad(colliderBuffers, br, bl, tl, tr, width, height, 0, Vector3.back, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void BuildEastWestFaceSlice(Vector2Int coord, int[] heights, byte[] biomeIndices, MeshBuffers renderBuffers, MeshBuffers colliderBuffers, int x, bool east)
|
||||
{
|
||||
bool[,] visited = new bool[chunkSize, maxMountainHeight];
|
||||
for (int z = 0; z < chunkSize; z++)
|
||||
{
|
||||
for (int y = 0; y < maxMountainHeight; y++)
|
||||
{
|
||||
if (visited[z, y] || !TryGetEastWestFace(coord, heights, biomeIndices, x, z, y, east, out byte biomeIndex, out VoxelSurfaceType surfaceType, out int textureLayer))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
int width = 1;
|
||||
while (z + width < chunkSize && !visited[z + width, y] && TryGetEastWestFace(coord, heights, biomeIndices, x, z + width, y, east, out byte candidateBiome, out VoxelSurfaceType candidateSurface, out int candidateTexture) && candidateBiome == biomeIndex && candidateSurface == surfaceType && candidateTexture == textureLayer)
|
||||
{
|
||||
width++;
|
||||
}
|
||||
|
||||
int height = 1;
|
||||
bool canGrow = true;
|
||||
while (y + height < maxMountainHeight && canGrow)
|
||||
{
|
||||
for (int dz = 0; dz < width; dz++)
|
||||
{
|
||||
if (visited[z + dz, y + height] || !TryGetEastWestFace(coord, heights, biomeIndices, x, z + dz, y + height, east, out byte candidateBiome, out VoxelSurfaceType candidateSurface, out int candidateTexture) || candidateBiome != biomeIndex || candidateSurface != surfaceType || candidateTexture != textureLayer)
|
||||
{
|
||||
canGrow = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (canGrow)
|
||||
{
|
||||
height++;
|
||||
}
|
||||
}
|
||||
|
||||
for (int dy = 0; dy < height; dy++)
|
||||
{
|
||||
for (int dz = 0; dz < width; dz++)
|
||||
{
|
||||
visited[z + dz, y + dy] = true;
|
||||
}
|
||||
}
|
||||
|
||||
float faceX = east ? x + 1f : x;
|
||||
Vector3 bl = new Vector3(faceX, y, z);
|
||||
Vector3 br = new Vector3(faceX, y, z + width);
|
||||
Vector3 tr = new Vector3(faceX, y + height, z + width);
|
||||
Vector3 tl = new Vector3(faceX, y + height, z);
|
||||
|
||||
if (east)
|
||||
{
|
||||
AddVerticalQuad(renderBuffers, br, bl, tl, tr, width, height, textureLayer, Vector3.right);
|
||||
AddVerticalQuad(colliderBuffers, br, bl, tl, tr, width, height, 0, Vector3.right, false);
|
||||
}
|
||||
else
|
||||
{
|
||||
AddVerticalQuad(renderBuffers, bl, br, tr, tl, width, height, textureLayer, Vector3.left);
|
||||
AddVerticalQuad(colliderBuffers, bl, br, tr, tl, width, height, 0, Vector3.left, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private bool TryGetNorthSouthFace(Vector2Int coord, int[] heights, byte[] biomeIndices, int x, int z, int y, bool north, out byte biomeIndex, out VoxelSurfaceType surfaceType, out int textureLayer)
|
||||
{
|
||||
int current = heights[z * chunkSize + x];
|
||||
int worldX = coord.x * chunkSize + x;
|
||||
int worldZ = coord.y * chunkSize + z;
|
||||
int neighbor = GetHeightAtWorldCell(new Vector2Int(worldX, north ? worldZ + 1 : worldZ - 1));
|
||||
if (y >= current || y < neighbor)
|
||||
{
|
||||
biomeIndex = 0;
|
||||
surfaceType = VoxelSurfaceType.Dirt;
|
||||
textureLayer = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
biomeIndex = biomeIndices[z * chunkSize + x];
|
||||
surfaceType = y == current - 1 ? VoxelSurfaceType.CliffSide : VoxelSurfaceType.Dirt;
|
||||
textureLayer = atlas.GetTextureLayer(biomeIndex, surfaceType);
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool TryGetEastWestFace(Vector2Int coord, int[] heights, byte[] biomeIndices, int x, int z, int y, bool east, out byte biomeIndex, out VoxelSurfaceType surfaceType, out int textureLayer)
|
||||
{
|
||||
int current = heights[z * chunkSize + x];
|
||||
int worldX = coord.x * chunkSize + x;
|
||||
int worldZ = coord.y * chunkSize + z;
|
||||
int neighbor = GetHeightAtWorldCell(new Vector2Int(east ? worldX + 1 : worldX - 1, worldZ));
|
||||
if (y >= current || y < neighbor)
|
||||
{
|
||||
biomeIndex = 0;
|
||||
surfaceType = VoxelSurfaceType.Dirt;
|
||||
textureLayer = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
biomeIndex = biomeIndices[z * chunkSize + x];
|
||||
surfaceType = y == current - 1 ? VoxelSurfaceType.CliffSide : VoxelSurfaceType.Dirt;
|
||||
textureLayer = atlas.GetTextureLayer(biomeIndex, surfaceType);
|
||||
return true;
|
||||
}
|
||||
|
||||
private void AddTopQuad(MeshBuffers buffers, float x, float z, float width, float depth, float height, int textureLayer, bool withUv = true)
|
||||
{
|
||||
int baseIndex = buffers.Vertices.Count;
|
||||
buffers.Vertices.Add(new Vector3(x, height, z));
|
||||
buffers.Vertices.Add(new Vector3(x + width, height, z));
|
||||
buffers.Vertices.Add(new Vector3(x + width, height, z + depth));
|
||||
buffers.Vertices.Add(new Vector3(x, height, z + depth));
|
||||
buffers.Normals.Add(Vector3.up);
|
||||
buffers.Normals.Add(Vector3.up);
|
||||
buffers.Normals.Add(Vector3.up);
|
||||
buffers.Normals.Add(Vector3.up);
|
||||
|
||||
if (withUv)
|
||||
{
|
||||
buffers.Uvs.Add(new Vector2(0f, 0f));
|
||||
buffers.Uvs.Add(new Vector2(width, 0f));
|
||||
buffers.Uvs.Add(new Vector2(width, depth));
|
||||
buffers.Uvs.Add(new Vector2(0f, depth));
|
||||
Vector2 textureData = new Vector2(textureLayer, 0f);
|
||||
buffers.TextureData.Add(textureData);
|
||||
buffers.TextureData.Add(textureData);
|
||||
buffers.TextureData.Add(textureData);
|
||||
buffers.TextureData.Add(textureData);
|
||||
}
|
||||
|
||||
buffers.Triangles.Add(baseIndex);
|
||||
buffers.Triangles.Add(baseIndex + 2);
|
||||
buffers.Triangles.Add(baseIndex + 1);
|
||||
buffers.Triangles.Add(baseIndex);
|
||||
buffers.Triangles.Add(baseIndex + 3);
|
||||
buffers.Triangles.Add(baseIndex + 2);
|
||||
}
|
||||
|
||||
private void AddVerticalQuad(MeshBuffers buffers, Vector3 bottomLeft, Vector3 bottomRight, Vector3 topRight, Vector3 topLeft, float width, float height, int textureLayer, Vector3 normal, bool withUv = true)
|
||||
{
|
||||
int baseIndex = buffers.Vertices.Count;
|
||||
buffers.Vertices.Add(bottomLeft);
|
||||
buffers.Vertices.Add(bottomRight);
|
||||
buffers.Vertices.Add(topRight);
|
||||
buffers.Vertices.Add(topLeft);
|
||||
buffers.Normals.Add(normal);
|
||||
buffers.Normals.Add(normal);
|
||||
buffers.Normals.Add(normal);
|
||||
buffers.Normals.Add(normal);
|
||||
|
||||
if (withUv)
|
||||
{
|
||||
buffers.Uvs.Add(new Vector2(0f, 0f));
|
||||
buffers.Uvs.Add(new Vector2(width, 0f));
|
||||
buffers.Uvs.Add(new Vector2(width, height));
|
||||
buffers.Uvs.Add(new Vector2(0f, height));
|
||||
Vector2 textureData = new Vector2(textureLayer, 0f);
|
||||
buffers.TextureData.Add(textureData);
|
||||
buffers.TextureData.Add(textureData);
|
||||
buffers.TextureData.Add(textureData);
|
||||
buffers.TextureData.Add(textureData);
|
||||
}
|
||||
|
||||
buffers.Triangles.Add(baseIndex);
|
||||
buffers.Triangles.Add(baseIndex + 1);
|
||||
buffers.Triangles.Add(baseIndex + 2);
|
||||
buffers.Triangles.Add(baseIndex);
|
||||
buffers.Triangles.Add(baseIndex + 2);
|
||||
buffers.Triangles.Add(baseIndex + 3);
|
||||
}
|
||||
|
||||
private bool IsCliffTop(Vector2Int worldCell, int currentHeight)
|
||||
{
|
||||
return GetHeightAtWorldCell(worldCell + Vector2Int.up) < currentHeight ||
|
||||
GetHeightAtWorldCell(worldCell + Vector2Int.right) < currentHeight ||
|
||||
GetHeightAtWorldCell(worldCell + Vector2Int.down) < currentHeight ||
|
||||
GetHeightAtWorldCell(worldCell + Vector2Int.left) < currentHeight;
|
||||
}
|
||||
|
||||
private int GetHeightAtWorldCell(Vector2Int worldCell)
|
||||
{
|
||||
Vector2Int coord = new Vector2Int(
|
||||
Mathf.FloorToInt(worldCell.x / (float)chunkSize),
|
||||
Mathf.FloorToInt(worldCell.y / (float)chunkSize));
|
||||
|
||||
if (!chunks.TryGetValue(coord, out ChunkRuntime runtime) || !runtime.HasData)
|
||||
{
|
||||
return SampleRock(worldCell) ? SampleHeight(worldCell) : 0;
|
||||
}
|
||||
|
||||
int localX = worldCell.x - coord.x * chunkSize;
|
||||
int localZ = worldCell.y - coord.y * chunkSize;
|
||||
if (localX < 0 || localZ < 0 || localX >= chunkSize || localZ >= chunkSize)
|
||||
{
|
||||
return SampleRock(worldCell) ? SampleHeight(worldCell) : 0;
|
||||
}
|
||||
|
||||
return runtime.Heights[localZ * chunkSize + localX];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 811ff92b4e36193499cad8631c9443a7
|
||||
@@ -1,339 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace InfiniteWorld.VoxelWorld
|
||||
{
|
||||
public sealed partial class VoxelWorldGenerator
|
||||
{
|
||||
private struct ChunkBuildResult
|
||||
{
|
||||
public ChunkBuildResult(Vector2Int coord, int[] heights, byte[] biomeIndices, int version, int session, int runtimeId)
|
||||
{
|
||||
Coord = coord;
|
||||
Heights = heights;
|
||||
BiomeIndices = biomeIndices;
|
||||
Version = version;
|
||||
Session = session;
|
||||
RuntimeId = runtimeId;
|
||||
}
|
||||
|
||||
public Vector2Int Coord { get; }
|
||||
public int[] Heights { get; }
|
||||
public byte[] BiomeIndices { get; }
|
||||
public int Version { get; }
|
||||
public int Session { get; }
|
||||
public int RuntimeId { get; }
|
||||
}
|
||||
|
||||
private struct ChunkMeshBuild
|
||||
{
|
||||
public ChunkMeshBuild(Mesh colliderMesh, ChunkRenderSnapshot renderSnapshot)
|
||||
{
|
||||
ColliderMesh = colliderMesh;
|
||||
RenderSnapshot = renderSnapshot;
|
||||
}
|
||||
|
||||
public Mesh ColliderMesh { get; }
|
||||
public ChunkRenderSnapshot RenderSnapshot { get; }
|
||||
}
|
||||
|
||||
private struct PendingColliderMeshApply
|
||||
{
|
||||
public PendingColliderMeshApply(Vector2Int coord, int version, int runtimeId, Mesh colliderMesh)
|
||||
{
|
||||
Coord = coord;
|
||||
Version = version;
|
||||
RuntimeId = runtimeId;
|
||||
ColliderMesh = colliderMesh;
|
||||
}
|
||||
|
||||
public Vector2Int Coord { get; }
|
||||
public int Version { get; }
|
||||
public int RuntimeId { get; }
|
||||
public Mesh ColliderMesh { get; }
|
||||
}
|
||||
|
||||
private sealed class ChunkRenderSnapshot
|
||||
{
|
||||
public ChunkRenderSnapshot(Vector3[] vertices, Vector3[] normals, Vector2[] uv0, Vector2[] uv1, int[] triangles)
|
||||
{
|
||||
Vertices = vertices;
|
||||
Normals = normals;
|
||||
Uv0 = uv0;
|
||||
Uv1 = uv1;
|
||||
Triangles = triangles;
|
||||
}
|
||||
|
||||
public Vector3[] Vertices { get; }
|
||||
public Vector3[] Normals { get; }
|
||||
public Vector2[] Uv0 { get; }
|
||||
public Vector2[] Uv1 { get; }
|
||||
public int[] Triangles { get; }
|
||||
public bool IsEmpty => Vertices == null || Vertices.Length == 0 || Triangles == null || Triangles.Length == 0;
|
||||
}
|
||||
|
||||
private struct RegionChunkSnapshot
|
||||
{
|
||||
public RegionChunkSnapshot(ChunkRenderSnapshot snapshot, Vector3 localOffset)
|
||||
{
|
||||
Snapshot = snapshot;
|
||||
LocalOffset = localOffset;
|
||||
}
|
||||
|
||||
public ChunkRenderSnapshot Snapshot { get; }
|
||||
public Vector3 LocalOffset { get; }
|
||||
}
|
||||
|
||||
private struct RegionBuildRequest
|
||||
{
|
||||
public RegionBuildRequest(Vector2Int regionCoord, int version, int session, RegionChunkSnapshot[] chunks)
|
||||
{
|
||||
RegionCoord = regionCoord;
|
||||
Version = version;
|
||||
Session = session;
|
||||
Chunks = chunks;
|
||||
}
|
||||
|
||||
public Vector2Int RegionCoord { get; }
|
||||
public int Version { get; }
|
||||
public int Session { get; }
|
||||
public RegionChunkSnapshot[] Chunks { get; }
|
||||
}
|
||||
|
||||
private struct RegionBuildResult
|
||||
{
|
||||
public RegionBuildResult(Vector2Int regionCoord, int version, int session, Vector3[] vertices, Vector3[] normals, Vector2[] uv0, Vector2[] uv1, int[] triangles, Bounds bounds)
|
||||
{
|
||||
RegionCoord = regionCoord;
|
||||
Version = version;
|
||||
Session = session;
|
||||
Vertices = vertices;
|
||||
Normals = normals;
|
||||
Uv0 = uv0;
|
||||
Uv1 = uv1;
|
||||
Triangles = triangles;
|
||||
Bounds = bounds;
|
||||
}
|
||||
|
||||
public Vector2Int RegionCoord { get; }
|
||||
public int Version { get; }
|
||||
public int Session { get; }
|
||||
public Vector3[] Vertices { get; }
|
||||
public Vector3[] Normals { get; }
|
||||
public Vector2[] Uv0 { get; }
|
||||
public Vector2[] Uv1 { get; }
|
||||
public int[] Triangles { get; }
|
||||
public Bounds Bounds { get; }
|
||||
public bool IsEmpty => Vertices == null || Vertices.Length == 0 || Triangles == null || Triangles.Length == 0;
|
||||
|
||||
public static RegionBuildResult CreateEmpty(Vector2Int regionCoord, int version, int session)
|
||||
{
|
||||
return new RegionBuildResult(regionCoord, version, session, null, null, null, null, null, new Bounds());
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class MeshBuffers
|
||||
{
|
||||
public readonly List<Vector3> Vertices = new List<Vector3>(512);
|
||||
public readonly List<Vector3> Normals = new List<Vector3>(512);
|
||||
public readonly List<Vector2> Uvs = new List<Vector2>(512);
|
||||
public readonly List<Vector2> TextureData = new List<Vector2>(512);
|
||||
public readonly List<int> Triangles = new List<int>(1024);
|
||||
|
||||
public Mesh ToMesh(string meshName)
|
||||
{
|
||||
Mesh mesh = new Mesh { name = meshName };
|
||||
if (Vertices.Count == 0)
|
||||
{
|
||||
return mesh;
|
||||
}
|
||||
|
||||
mesh.indexFormat = Vertices.Count > 65535 ? UnityEngine.Rendering.IndexFormat.UInt32 : UnityEngine.Rendering.IndexFormat.UInt16;
|
||||
mesh.SetVertices(Vertices);
|
||||
if (Normals.Count == Vertices.Count)
|
||||
{
|
||||
mesh.SetNormals(Normals);
|
||||
}
|
||||
if (Uvs.Count == Vertices.Count)
|
||||
{
|
||||
mesh.SetUVs(0, Uvs);
|
||||
}
|
||||
if (TextureData.Count == Vertices.Count)
|
||||
{
|
||||
mesh.SetUVs(1, TextureData);
|
||||
}
|
||||
mesh.SetTriangles(Triangles, 0, true);
|
||||
mesh.RecalculateBounds();
|
||||
return mesh;
|
||||
}
|
||||
|
||||
public ChunkRenderSnapshot ToSnapshot()
|
||||
{
|
||||
return new ChunkRenderSnapshot(
|
||||
Vertices.ToArray(),
|
||||
Normals.ToArray(),
|
||||
Uvs.ToArray(),
|
||||
TextureData.ToArray(),
|
||||
Triangles.ToArray());
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class ChunkRuntime
|
||||
{
|
||||
public Transform Root;
|
||||
public MeshCollider MountainCollider;
|
||||
public BoxCollider GroundCollider;
|
||||
public Mesh ColliderMesh;
|
||||
public ChunkRenderSnapshot RenderSnapshot;
|
||||
public int[] Heights;
|
||||
public byte[] BiomeIndices;
|
||||
public ChunkState State;
|
||||
public int Version;
|
||||
public int RuntimeId;
|
||||
|
||||
public bool HasData => Heights != null && BiomeIndices != null;
|
||||
public bool IsRendered => State == ChunkState.Rendered && Root != null;
|
||||
|
||||
public void EnsureCreated(Vector2Int coord, Transform parent, int chunkSize)
|
||||
{
|
||||
if (Root == null)
|
||||
{
|
||||
GameObject chunkObject = new GameObject($"VoxelChunk_{coord.x}_{coord.y}");
|
||||
chunkObject.transform.SetParent(parent, false);
|
||||
Root = chunkObject.transform;
|
||||
MountainCollider = chunkObject.AddComponent<MeshCollider>();
|
||||
GroundCollider = chunkObject.AddComponent<BoxCollider>();
|
||||
}
|
||||
|
||||
Root.localPosition = new Vector3(coord.x * chunkSize, 0f, coord.y * chunkSize);
|
||||
GroundCollider.size = new Vector3(chunkSize, 0.2f, chunkSize);
|
||||
GroundCollider.center = new Vector3(chunkSize * 0.5f, -0.1f, chunkSize * 0.5f);
|
||||
}
|
||||
|
||||
public void ApplyRenderData(ChunkRenderSnapshot renderSnapshot)
|
||||
{
|
||||
RenderSnapshot = renderSnapshot;
|
||||
}
|
||||
|
||||
public void ApplyColliderMesh(Mesh colliderMesh)
|
||||
{
|
||||
if (ColliderMesh != null)
|
||||
{
|
||||
DestroyMesh(ColliderMesh);
|
||||
}
|
||||
|
||||
ColliderMesh = colliderMesh;
|
||||
MountainCollider.sharedMesh = null;
|
||||
MountainCollider.sharedMesh = ColliderMesh != null && ColliderMesh.vertexCount > 0 ? ColliderMesh : null;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (Root != null)
|
||||
{
|
||||
if (Application.isPlaying)
|
||||
{
|
||||
Object.Destroy(Root.gameObject);
|
||||
}
|
||||
else
|
||||
{
|
||||
Object.DestroyImmediate(Root.gameObject);
|
||||
}
|
||||
}
|
||||
|
||||
DestroyMesh(ColliderMesh);
|
||||
RenderSnapshot = null;
|
||||
}
|
||||
|
||||
private static void DestroyMesh(Mesh mesh)
|
||||
{
|
||||
if (mesh == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (Application.isPlaying)
|
||||
{
|
||||
Object.Destroy(mesh);
|
||||
}
|
||||
else
|
||||
{
|
||||
Object.DestroyImmediate(mesh);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class RegionRuntime
|
||||
{
|
||||
public Transform Root;
|
||||
public MeshFilter Filter;
|
||||
public MeshRenderer Renderer;
|
||||
public Mesh RenderMesh;
|
||||
|
||||
public void EnsureCreated(Vector2Int regionCoord, Transform parent, int chunkSize, int regionSizeInChunks, Material material)
|
||||
{
|
||||
if (Root == null)
|
||||
{
|
||||
GameObject regionObject = new GameObject($"VoxelRegion_{regionCoord.x}_{regionCoord.y}");
|
||||
regionObject.transform.SetParent(parent, false);
|
||||
Root = regionObject.transform;
|
||||
Filter = regionObject.AddComponent<MeshFilter>();
|
||||
Renderer = regionObject.AddComponent<MeshRenderer>();
|
||||
}
|
||||
|
||||
Root.localPosition = new Vector3(regionCoord.x * regionSizeInChunks * chunkSize, 0f, regionCoord.y * regionSizeInChunks * chunkSize);
|
||||
Renderer.sharedMaterial = material;
|
||||
}
|
||||
|
||||
public void ApplyMesh(Mesh mesh)
|
||||
{
|
||||
DestroyMesh(RenderMesh);
|
||||
RenderMesh = mesh;
|
||||
Filter.sharedMesh = RenderMesh;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (Root != null)
|
||||
{
|
||||
if (Application.isPlaying)
|
||||
{
|
||||
Object.Destroy(Root.gameObject);
|
||||
}
|
||||
else
|
||||
{
|
||||
Object.DestroyImmediate(Root.gameObject);
|
||||
}
|
||||
}
|
||||
|
||||
DestroyMesh(RenderMesh);
|
||||
}
|
||||
|
||||
private static void DestroyMesh(Mesh mesh)
|
||||
{
|
||||
if (mesh == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (Application.isPlaying)
|
||||
{
|
||||
Object.Destroy(mesh);
|
||||
}
|
||||
else
|
||||
{
|
||||
Object.DestroyImmediate(mesh);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private enum ChunkState
|
||||
{
|
||||
None,
|
||||
Generating,
|
||||
SyncBuilding,
|
||||
ReadyToRender,
|
||||
Rendered
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 56e0b6ddd79410b40bdda73c4e20515d
|
||||
@@ -1,889 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Cysharp.Threading.Tasks;
|
||||
using UnityEngine;
|
||||
|
||||
namespace InfiniteWorld.VoxelWorld
|
||||
{
|
||||
public sealed partial class VoxelWorldGenerator : MonoBehaviour
|
||||
{
|
||||
[Header("References")]
|
||||
[SerializeField] private Transform streamTarget;
|
||||
[SerializeField] private VoxelWorldConfig config;
|
||||
|
||||
private readonly Dictionary<Vector2Int, ChunkRuntime> chunks = new Dictionary<Vector2Int, ChunkRuntime>();
|
||||
private readonly Dictionary<Vector2Int, RegionRuntime> regions = new Dictionary<Vector2Int, RegionRuntime>();
|
||||
private readonly Dictionary<Vector2Int, int> regionVersions = new Dictionary<Vector2Int, int>();
|
||||
private readonly Queue<ChunkBuildResult> completedBuilds = new Queue<ChunkBuildResult>();
|
||||
private readonly Queue<RegionBuildResult> completedRegionBuilds = new Queue<RegionBuildResult>();
|
||||
private readonly Queue<PendingColliderMeshApply> pendingColliderApplies = new Queue<PendingColliderMeshApply>();
|
||||
private readonly Queue<Vector2Int> dirtyChunkMeshes = new Queue<Vector2Int>();
|
||||
private readonly HashSet<Vector2Int> queuedChunkMeshes = new HashSet<Vector2Int>();
|
||||
private readonly Queue<Vector2Int> dirtyRegions = new Queue<Vector2Int>();
|
||||
private readonly HashSet<Vector2Int> queuedRegions = new HashSet<Vector2Int>();
|
||||
private readonly Queue<Vector2Int> pendingNeighborRefreshes = new Queue<Vector2Int>();
|
||||
private readonly HashSet<Vector2Int> queuedNeighborRefreshes = new HashSet<Vector2Int>();
|
||||
private readonly object generationLock = new object();
|
||||
private readonly object regionBuildLock = new object();
|
||||
|
||||
private Transform chunkRoot;
|
||||
private Transform regionRoot;
|
||||
private Vector2Int lastGeneratedCenter = new Vector2Int(int.MinValue, int.MinValue);
|
||||
private int activeGenerationJobs;
|
||||
private int nextChunkRuntimeId;
|
||||
private int generationSession;
|
||||
private int regionRebuildSession;
|
||||
private VoxelWorldAtlas atlas;
|
||||
private int atlasBiomeCount;
|
||||
private bool regionRebuildLoopRunning;
|
||||
private VoxelWorldResolvedSettings settings = VoxelWorldResolvedSettings.Default;
|
||||
|
||||
private int chunkSize => settings.ChunkSize;
|
||||
private int generationRadius => settings.GenerationRadius;
|
||||
private int blockingGenerationRadius => settings.BlockingGenerationRadius;
|
||||
private int seed => settings.Seed;
|
||||
private int maxMountainHeight => settings.MaxMountainHeight;
|
||||
private float macroNoiseScale => settings.MacroNoiseScale;
|
||||
private float detailNoiseScale => settings.DetailNoiseScale;
|
||||
private float ridgeNoiseScale => settings.RidgeNoiseScale;
|
||||
private float wallThreshold => settings.WallThreshold;
|
||||
private float rockBias => settings.RockBias;
|
||||
private int smoothingPasses => settings.SmoothingPasses;
|
||||
private float passNoiseScale => settings.PassNoiseScale;
|
||||
private float passDetailScale => settings.PassDetailScale;
|
||||
private float passThreshold => settings.PassThreshold;
|
||||
private float passFeather => settings.PassFeather;
|
||||
private float heightNoiseScale => settings.HeightNoiseScale;
|
||||
private float terraceNoiseScale => settings.TerraceNoiseScale;
|
||||
private float heightBias => settings.HeightBias;
|
||||
private IReadOnlyList<VoxelBiomeProfile> biomeProfiles => settings.BiomeProfiles;
|
||||
private float biomeNoiseScale => settings.BiomeNoiseScale;
|
||||
private float biomeSize => settings.BiomeSize;
|
||||
private int maxAsyncChunkJobs => settings.MaxAsyncChunkJobs;
|
||||
private int maxChunkBuildsPerFrame => settings.MaxChunkBuildsPerFrame;
|
||||
private int maxChunkMeshBuildsPerFrame => settings.MaxChunkMeshBuildsPerFrame;
|
||||
private int maxColliderAppliesPerFrame => settings.MaxColliderAppliesPerFrame;
|
||||
private int maxNeighborRefreshesPerFrame => settings.MaxNeighborRefreshesPerFrame;
|
||||
private int renderRegionSizeInChunks => settings.RenderRegionSizeInChunks;
|
||||
private int maxRegionBuildsPerFrame => settings.MaxRegionBuildsPerFrame;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
generationSession++;
|
||||
regionRebuildSession++;
|
||||
EnsureRuntimeData();
|
||||
EnsureChunkRoot();
|
||||
EnsureRegionRoot();
|
||||
TryResolveStreamTarget();
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
EnsureRuntimeData();
|
||||
EnsureChunkRoot();
|
||||
EnsureRegionRoot();
|
||||
if (!TryResolveStreamTarget())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Vector2Int centerChunk = WorldToChunk(streamTarget.position);
|
||||
if (centerChunk != lastGeneratedCenter)
|
||||
{
|
||||
lastGeneratedCenter = centerChunk;
|
||||
UnloadDistantChunks(centerChunk);
|
||||
}
|
||||
|
||||
DrainCompletedBuilds(maxChunkBuildsPerFrame);
|
||||
DrainDirtyChunkMeshes(maxChunkMeshBuildsPerFrame);
|
||||
DrainPendingColliderApplies(maxColliderAppliesPerFrame);
|
||||
DrainNeighborRefreshes(maxNeighborRefreshesPerFrame);
|
||||
DrainCompletedRegionBuilds(maxRegionBuildsPerFrame);
|
||||
ScheduleChunkGeneration(centerChunk);
|
||||
EnsureBlockingChunksLoaded(centerChunk);
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
generationSession++;
|
||||
regionRebuildSession++;
|
||||
lock (generationLock)
|
||||
{
|
||||
completedBuilds.Clear();
|
||||
activeGenerationJobs = 0;
|
||||
}
|
||||
lock (regionBuildLock)
|
||||
{
|
||||
completedRegionBuilds.Clear();
|
||||
}
|
||||
|
||||
CleanupChunks();
|
||||
CleanupRegions();
|
||||
atlas?.Dispose();
|
||||
atlas = null;
|
||||
}
|
||||
|
||||
private void EnsureRuntimeData()
|
||||
{
|
||||
settings = VoxelWorldResolvedSettings.Resolve(config);
|
||||
int configuredBiomeCount = CountConfiguredBiomes();
|
||||
|
||||
if (atlas != null && atlasBiomeCount == configuredBiomeCount)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
atlas?.Dispose();
|
||||
atlas = VoxelWorldAtlas.CreateRuntimeAtlas(biomeProfiles);
|
||||
atlasBiomeCount = atlas.BiomeCount;
|
||||
RefreshRegionMaterials();
|
||||
}
|
||||
|
||||
private void EnsureChunkRoot()
|
||||
{
|
||||
if (chunkRoot != null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Transform existing = transform.Find("VoxelChunks");
|
||||
if (existing != null)
|
||||
{
|
||||
chunkRoot = existing;
|
||||
return;
|
||||
}
|
||||
|
||||
GameObject root = new GameObject("VoxelChunks");
|
||||
root.transform.SetParent(transform, false);
|
||||
chunkRoot = root.transform;
|
||||
}
|
||||
|
||||
private void EnsureRegionRoot()
|
||||
{
|
||||
if (regionRoot != null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Transform existing = transform.Find("VoxelRenderRegions");
|
||||
if (existing != null)
|
||||
{
|
||||
regionRoot = existing;
|
||||
return;
|
||||
}
|
||||
|
||||
GameObject root = new GameObject("VoxelRenderRegions");
|
||||
root.transform.SetParent(transform, false);
|
||||
regionRoot = root.transform;
|
||||
}
|
||||
|
||||
private void RefreshRegionMaterials()
|
||||
{
|
||||
if (atlas == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (KeyValuePair<Vector2Int, RegionRuntime> pair in regions)
|
||||
{
|
||||
if (pair.Value.Renderer != null)
|
||||
{
|
||||
pair.Value.Renderer.sharedMaterial = atlas.Material;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private bool TryResolveStreamTarget()
|
||||
{
|
||||
if (streamTarget != null)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
Camera mainCamera = Camera.main;
|
||||
if (mainCamera == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
streamTarget = mainCamera.transform;
|
||||
return true;
|
||||
}
|
||||
|
||||
private void ScheduleChunkGeneration(Vector2Int centerChunk)
|
||||
{
|
||||
List<Vector2Int> coords = GetCoordsByPriority(centerChunk, generationRadius);
|
||||
for (int i = 0; i < coords.Count; i++)
|
||||
{
|
||||
Vector2Int coord = coords[i];
|
||||
ChunkRuntime runtime = GetOrCreateChunkRuntime(coord);
|
||||
if (runtime.IsRendered || runtime.HasData || runtime.State == ChunkState.Generating)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (IsWithinRadius(coord, centerChunk, blockingGenerationRadius))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!TryReserveGenerationSlot())
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
runtime.State = ChunkState.Generating;
|
||||
runtime.Version++;
|
||||
GenerateChunkDataAsync(coord, runtime.Version, generationSession, runtime.RuntimeId).Forget();
|
||||
}
|
||||
}
|
||||
|
||||
private void EnsureBlockingChunksLoaded(Vector2Int centerChunk)
|
||||
{
|
||||
List<Vector2Int> requiredCoords = GetCoordsByPriority(centerChunk, blockingGenerationRadius);
|
||||
for (int i = 0; i < requiredCoords.Count; i++)
|
||||
{
|
||||
Vector2Int coord = requiredCoords[i];
|
||||
ChunkRuntime runtime = GetOrCreateChunkRuntime(coord);
|
||||
if (runtime.IsRendered || runtime.HasData || runtime.State == ChunkState.Generating || runtime.State == ChunkState.SyncBuilding)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
runtime.Version++;
|
||||
runtime.State = ChunkState.SyncBuilding;
|
||||
if (ApplyBuildResult(GenerateChunkData(coord, runtime.Version, generationSession, runtime.RuntimeId)))
|
||||
{
|
||||
EnqueueChunkMeshBuild(coord);
|
||||
QueueNeighborRefresh(coord);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void UnloadDistantChunks(Vector2Int centerChunk)
|
||||
{
|
||||
List<Vector2Int> coordsToRemove = new List<Vector2Int>();
|
||||
foreach (KeyValuePair<Vector2Int, ChunkRuntime> pair in chunks)
|
||||
{
|
||||
if (IsWithinRadius(pair.Key, centerChunk, generationRadius))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
coordsToRemove.Add(pair.Key);
|
||||
}
|
||||
|
||||
for (int i = 0; i < coordsToRemove.Count; i++)
|
||||
{
|
||||
Vector2Int coord = coordsToRemove[i];
|
||||
if (!chunks.TryGetValue(coord, out ChunkRuntime runtime))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
MarkRegionDirty(coord);
|
||||
chunks.Remove(coord);
|
||||
runtime.Dispose();
|
||||
QueueNeighborRefresh(coord);
|
||||
}
|
||||
}
|
||||
|
||||
private Vector2Int WorldToChunk(Vector3 position)
|
||||
{
|
||||
return new Vector2Int(
|
||||
Mathf.FloorToInt(position.x / chunkSize),
|
||||
Mathf.FloorToInt(position.z / chunkSize));
|
||||
}
|
||||
|
||||
private async UniTaskVoid GenerateChunkDataAsync(Vector2Int coord, int version, int session, int runtimeId)
|
||||
{
|
||||
try
|
||||
{
|
||||
ChunkBuildResult result = await UniTask.RunOnThreadPool(() => GenerateChunkData(coord, version, session, runtimeId));
|
||||
lock (generationLock)
|
||||
{
|
||||
if (session == generationSession)
|
||||
{
|
||||
completedBuilds.Enqueue(result);
|
||||
activeGenerationJobs = Mathf.Max(0, activeGenerationJobs - 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
lock (generationLock)
|
||||
{
|
||||
if (session == generationSession)
|
||||
{
|
||||
activeGenerationJobs = Mathf.Max(0, activeGenerationJobs - 1);
|
||||
}
|
||||
}
|
||||
|
||||
if (session == generationSession && chunks.TryGetValue(coord, out ChunkRuntime runtime) && runtime.Version == version && runtime.RuntimeId == runtimeId)
|
||||
{
|
||||
runtime.State = ChunkState.None;
|
||||
}
|
||||
|
||||
Debug.LogException(exception, this);
|
||||
}
|
||||
}
|
||||
|
||||
private void DrainCompletedBuilds(int maxBuilds)
|
||||
{
|
||||
int builds = 0;
|
||||
while (builds < maxBuilds)
|
||||
{
|
||||
ChunkBuildResult result;
|
||||
lock (generationLock)
|
||||
{
|
||||
if (completedBuilds.Count == 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
result = completedBuilds.Dequeue();
|
||||
}
|
||||
|
||||
if (!ApplyBuildResult(result))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
EnqueueChunkMeshBuild(result.Coord);
|
||||
QueueNeighborRefresh(result.Coord);
|
||||
builds++;
|
||||
}
|
||||
}
|
||||
|
||||
private void DrainDirtyChunkMeshes(int maxBuilds)
|
||||
{
|
||||
int builds = 0;
|
||||
while (builds < maxBuilds)
|
||||
{
|
||||
if (dirtyChunkMeshes.Count == 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
Vector2Int coord = dirtyChunkMeshes.Dequeue();
|
||||
queuedChunkMeshes.Remove(coord);
|
||||
|
||||
if (!chunks.TryGetValue(coord, out ChunkRuntime runtime) || !runtime.HasData)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
RenderChunk(coord);
|
||||
builds++;
|
||||
}
|
||||
}
|
||||
|
||||
private void EnqueueChunkMeshBuild(Vector2Int coord)
|
||||
{
|
||||
if (!queuedChunkMeshes.Add(coord))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
dirtyChunkMeshes.Enqueue(coord);
|
||||
}
|
||||
|
||||
private void EnqueueColliderApply(Vector2Int coord, int version, int runtimeId, Mesh colliderMesh)
|
||||
{
|
||||
pendingColliderApplies.Enqueue(new PendingColliderMeshApply(coord, version, runtimeId, colliderMesh));
|
||||
}
|
||||
|
||||
private void DrainPendingColliderApplies(int maxApplies)
|
||||
{
|
||||
int applies = 0;
|
||||
while (applies < maxApplies)
|
||||
{
|
||||
if (pendingColliderApplies.Count == 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
PendingColliderMeshApply pending = pendingColliderApplies.Dequeue();
|
||||
if (!chunks.TryGetValue(pending.Coord, out ChunkRuntime runtime) || runtime.Version != pending.Version || runtime.RuntimeId != pending.RuntimeId)
|
||||
{
|
||||
DestroyMeshAsset(pending.ColliderMesh);
|
||||
applies++;
|
||||
continue;
|
||||
}
|
||||
|
||||
runtime.ApplyColliderMesh(pending.ColliderMesh);
|
||||
applies++;
|
||||
}
|
||||
}
|
||||
|
||||
private void QueueNeighborRefresh(Vector2Int coord)
|
||||
{
|
||||
if (!queuedNeighborRefreshes.Add(coord))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
pendingNeighborRefreshes.Enqueue(coord);
|
||||
}
|
||||
|
||||
private void DrainNeighborRefreshes(int maxRefreshes)
|
||||
{
|
||||
int refreshes = 0;
|
||||
while (refreshes < maxRefreshes)
|
||||
{
|
||||
if (pendingNeighborRefreshes.Count == 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
Vector2Int coord = pendingNeighborRefreshes.Dequeue();
|
||||
queuedNeighborRefreshes.Remove(coord);
|
||||
RefreshNeighborBorders(coord);
|
||||
refreshes++;
|
||||
}
|
||||
}
|
||||
|
||||
private void RefreshNeighborBorders(Vector2Int coord)
|
||||
{
|
||||
TryRenderNeighbor(coord + Vector2Int.up);
|
||||
TryRenderNeighbor(coord + Vector2Int.right);
|
||||
TryRenderNeighbor(coord + Vector2Int.down);
|
||||
TryRenderNeighbor(coord + Vector2Int.left);
|
||||
}
|
||||
|
||||
private void TryRenderNeighbor(Vector2Int coord)
|
||||
{
|
||||
if (!chunks.TryGetValue(coord, out ChunkRuntime runtime) || !runtime.HasData)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
EnqueueChunkMeshBuild(coord);
|
||||
}
|
||||
|
||||
private void MarkRegionDirty(Vector2Int chunkCoord)
|
||||
{
|
||||
Vector2Int regionCoord = ChunkToRegion(chunkCoord);
|
||||
regionVersions.TryGetValue(regionCoord, out int version);
|
||||
regionVersions[regionCoord] = version + 1;
|
||||
|
||||
if (!queuedRegions.Add(regionCoord))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
dirtyRegions.Enqueue(regionCoord);
|
||||
EnsureRegionRebuildLoop();
|
||||
}
|
||||
|
||||
private void EnsureRegionRebuildLoop()
|
||||
{
|
||||
if (regionRebuildLoopRunning)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
regionRebuildLoopRunning = true;
|
||||
ProcessDirtyRegionsAsync(regionRebuildSession).Forget();
|
||||
}
|
||||
|
||||
private async UniTaskVoid ProcessDirtyRegionsAsync(int session)
|
||||
{
|
||||
try
|
||||
{
|
||||
while (dirtyRegions.Count > 0)
|
||||
{
|
||||
if (!this || !isActiveAndEnabled || session != regionRebuildSession)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
int buildsThisFrame = 0;
|
||||
while (buildsThisFrame < maxRegionBuildsPerFrame && dirtyRegions.Count > 0)
|
||||
{
|
||||
Vector2Int regionCoord = dirtyRegions.Dequeue();
|
||||
queuedRegions.Remove(regionCoord);
|
||||
|
||||
if (!TryCaptureRegionBuildRequest(regionCoord, out RegionBuildRequest request))
|
||||
{
|
||||
buildsThisFrame++;
|
||||
continue;
|
||||
}
|
||||
|
||||
RegionBuildResult result = await UniTask.RunOnThreadPool(() => BuildRegionBuffers(request));
|
||||
lock (regionBuildLock)
|
||||
{
|
||||
completedRegionBuilds.Enqueue(result);
|
||||
}
|
||||
|
||||
buildsThisFrame++;
|
||||
}
|
||||
|
||||
if (dirtyRegions.Count > 0)
|
||||
{
|
||||
await UniTask.Yield(PlayerLoopTiming.Update);
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (session == regionRebuildSession)
|
||||
{
|
||||
regionRebuildLoopRunning = false;
|
||||
if (dirtyRegions.Count > 0)
|
||||
{
|
||||
EnsureRegionRebuildLoop();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private bool TryCaptureRegionBuildRequest(Vector2Int regionCoord, out RegionBuildRequest request)
|
||||
{
|
||||
if (atlas == null || regionRoot == null)
|
||||
{
|
||||
request = new RegionBuildRequest();
|
||||
return false;
|
||||
}
|
||||
|
||||
Vector2Int baseChunk = new Vector2Int(regionCoord.x * renderRegionSizeInChunks, regionCoord.y * renderRegionSizeInChunks);
|
||||
List<RegionChunkSnapshot> chunkSnapshots = new List<RegionChunkSnapshot>(renderRegionSizeInChunks * renderRegionSizeInChunks);
|
||||
|
||||
for (int z = 0; z < renderRegionSizeInChunks; z++)
|
||||
{
|
||||
for (int x = 0; x < renderRegionSizeInChunks; x++)
|
||||
{
|
||||
Vector2Int chunkCoord = new Vector2Int(baseChunk.x + x, baseChunk.y + z);
|
||||
if (!chunks.TryGetValue(chunkCoord, out ChunkRuntime runtime) || runtime.RenderSnapshot == null || runtime.RenderSnapshot.IsEmpty)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
chunkSnapshots.Add(new RegionChunkSnapshot(runtime.RenderSnapshot, new Vector3(x * chunkSize, 0f, z * chunkSize)));
|
||||
}
|
||||
}
|
||||
|
||||
regionVersions.TryGetValue(regionCoord, out int version);
|
||||
request = new RegionBuildRequest(regionCoord, version, generationSession, chunkSnapshots.ToArray());
|
||||
return true;
|
||||
}
|
||||
|
||||
private Vector2Int ChunkToRegion(Vector2Int chunkCoord)
|
||||
{
|
||||
return new Vector2Int(
|
||||
Mathf.FloorToInt(chunkCoord.x / (float)renderRegionSizeInChunks),
|
||||
Mathf.FloorToInt(chunkCoord.y / (float)renderRegionSizeInChunks));
|
||||
}
|
||||
|
||||
private RegionRuntime GetOrCreateRegionRuntime(Vector2Int regionCoord)
|
||||
{
|
||||
if (!regions.TryGetValue(regionCoord, out RegionRuntime region))
|
||||
{
|
||||
region = new RegionRuntime();
|
||||
regions.Add(regionCoord, region);
|
||||
}
|
||||
|
||||
return region;
|
||||
}
|
||||
|
||||
private static RegionBuildResult BuildRegionBuffers(RegionBuildRequest request)
|
||||
{
|
||||
if (request.Chunks == null || request.Chunks.Length == 0)
|
||||
{
|
||||
return RegionBuildResult.CreateEmpty(request.RegionCoord, request.Version, request.Session);
|
||||
}
|
||||
|
||||
int totalVertexCount = 0;
|
||||
int totalTriangleCount = 0;
|
||||
for (int i = 0; i < request.Chunks.Length; i++)
|
||||
{
|
||||
ChunkRenderSnapshot snapshot = request.Chunks[i].Snapshot;
|
||||
totalVertexCount += snapshot.Vertices.Length;
|
||||
totalTriangleCount += snapshot.Triangles.Length;
|
||||
}
|
||||
|
||||
if (totalVertexCount == 0 || totalTriangleCount == 0)
|
||||
{
|
||||
return RegionBuildResult.CreateEmpty(request.RegionCoord, request.Version, request.Session);
|
||||
}
|
||||
|
||||
Vector3[] vertices = new Vector3[totalVertexCount];
|
||||
Vector3[] normals = new Vector3[totalVertexCount];
|
||||
Vector2[] uv0 = new Vector2[totalVertexCount];
|
||||
Vector2[] uv1 = new Vector2[totalVertexCount];
|
||||
int[] triangles = new int[totalTriangleCount];
|
||||
|
||||
int vertexOffset = 0;
|
||||
int triangleOffset = 0;
|
||||
Vector3 min = new Vector3(float.PositiveInfinity, float.PositiveInfinity, float.PositiveInfinity);
|
||||
Vector3 max = new Vector3(float.NegativeInfinity, float.NegativeInfinity, float.NegativeInfinity);
|
||||
|
||||
for (int chunkIndex = 0; chunkIndex < request.Chunks.Length; chunkIndex++)
|
||||
{
|
||||
RegionChunkSnapshot chunk = request.Chunks[chunkIndex];
|
||||
ChunkRenderSnapshot snapshot = chunk.Snapshot;
|
||||
Vector3 offset = chunk.LocalOffset;
|
||||
|
||||
for (int vertexIndex = 0; vertexIndex < snapshot.Vertices.Length; vertexIndex++)
|
||||
{
|
||||
Vector3 vertex = snapshot.Vertices[vertexIndex] + offset;
|
||||
int writeIndex = vertexOffset + vertexIndex;
|
||||
vertices[writeIndex] = vertex;
|
||||
normals[writeIndex] = snapshot.Normals[vertexIndex];
|
||||
uv0[writeIndex] = snapshot.Uv0[vertexIndex];
|
||||
uv1[writeIndex] = snapshot.Uv1[vertexIndex];
|
||||
|
||||
min = Vector3.Min(min, vertex);
|
||||
max = Vector3.Max(max, vertex);
|
||||
}
|
||||
|
||||
for (int triangleIndex = 0; triangleIndex < snapshot.Triangles.Length; triangleIndex++)
|
||||
{
|
||||
triangles[triangleOffset + triangleIndex] = snapshot.Triangles[triangleIndex] + vertexOffset;
|
||||
}
|
||||
|
||||
vertexOffset += snapshot.Vertices.Length;
|
||||
triangleOffset += snapshot.Triangles.Length;
|
||||
}
|
||||
|
||||
Bounds bounds = new Bounds((min + max) * 0.5f, max - min);
|
||||
return new RegionBuildResult(request.RegionCoord, request.Version, request.Session, vertices, normals, uv0, uv1, triangles, bounds);
|
||||
}
|
||||
|
||||
private void DrainCompletedRegionBuilds(int maxBuilds)
|
||||
{
|
||||
int builds = 0;
|
||||
while (builds < maxBuilds)
|
||||
{
|
||||
RegionBuildResult result;
|
||||
lock (regionBuildLock)
|
||||
{
|
||||
if (completedRegionBuilds.Count == 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
result = completedRegionBuilds.Dequeue();
|
||||
}
|
||||
|
||||
ApplyRegionBuildResult(result);
|
||||
builds++;
|
||||
}
|
||||
}
|
||||
|
||||
private void ApplyRegionBuildResult(RegionBuildResult result)
|
||||
{
|
||||
if (result.Session != generationSession || atlas == null || regionRoot == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
regionVersions.TryGetValue(result.RegionCoord, out int currentVersion);
|
||||
if (result.Version != currentVersion)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (result.IsEmpty)
|
||||
{
|
||||
if (regions.TryGetValue(result.RegionCoord, out RegionRuntime regionToRemove))
|
||||
{
|
||||
regionToRemove.Dispose();
|
||||
regions.Remove(result.RegionCoord);
|
||||
}
|
||||
|
||||
regionVersions.Remove(result.RegionCoord);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
RegionRuntime region = GetOrCreateRegionRuntime(result.RegionCoord);
|
||||
region.EnsureCreated(result.RegionCoord, regionRoot, chunkSize, renderRegionSizeInChunks, atlas.Material);
|
||||
|
||||
Mesh mesh = new Mesh { name = $"Region_{result.RegionCoord.x}_{result.RegionCoord.y}" };
|
||||
mesh.indexFormat = result.Vertices.Length > 65535 ? UnityEngine.Rendering.IndexFormat.UInt32 : UnityEngine.Rendering.IndexFormat.UInt16;
|
||||
mesh.vertices = result.Vertices;
|
||||
mesh.normals = result.Normals;
|
||||
mesh.uv = result.Uv0;
|
||||
mesh.uv2 = result.Uv1;
|
||||
mesh.triangles = result.Triangles;
|
||||
mesh.bounds = result.Bounds;
|
||||
region.ApplyMesh(mesh);
|
||||
}
|
||||
|
||||
private bool ApplyBuildResult(ChunkBuildResult result)
|
||||
{
|
||||
if (result.Session != generationSession)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!chunks.TryGetValue(result.Coord, out ChunkRuntime runtime))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (runtime.Version != result.Version)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (runtime.RuntimeId != result.RuntimeId)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
runtime.Heights = result.Heights;
|
||||
runtime.BiomeIndices = result.BiomeIndices;
|
||||
if (!runtime.IsRendered)
|
||||
{
|
||||
runtime.State = ChunkState.ReadyToRender;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool TryReserveGenerationSlot()
|
||||
{
|
||||
lock (generationLock)
|
||||
{
|
||||
if (activeGenerationJobs >= maxAsyncChunkJobs)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
activeGenerationJobs++;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private List<Vector2Int> GetCoordsByPriority(Vector2Int centerChunk, int radius)
|
||||
{
|
||||
List<Vector2Int> coords = new List<Vector2Int>((radius * 2 + 1) * (radius * 2 + 1));
|
||||
for (int z = -radius; z <= radius; z++)
|
||||
{
|
||||
for (int x = -radius; x <= radius; x++)
|
||||
{
|
||||
coords.Add(new Vector2Int(centerChunk.x + x, centerChunk.y + z));
|
||||
}
|
||||
}
|
||||
|
||||
coords.Sort((left, right) => CompareChunkPriority(centerChunk, left, right));
|
||||
return coords;
|
||||
}
|
||||
|
||||
private static int CompareChunkPriority(Vector2Int centerChunk, Vector2Int left, Vector2Int right)
|
||||
{
|
||||
int leftDx = Mathf.Abs(left.x - centerChunk.x);
|
||||
int leftDz = Mathf.Abs(left.y - centerChunk.y);
|
||||
int rightDx = Mathf.Abs(right.x - centerChunk.x);
|
||||
int rightDz = Mathf.Abs(right.y - centerChunk.y);
|
||||
|
||||
int leftChebyshev = Mathf.Max(leftDx, leftDz);
|
||||
int rightChebyshev = Mathf.Max(rightDx, rightDz);
|
||||
int chebyshevCompare = leftChebyshev.CompareTo(rightChebyshev);
|
||||
if (chebyshevCompare != 0)
|
||||
{
|
||||
return chebyshevCompare;
|
||||
}
|
||||
|
||||
int leftDistance = leftDx * leftDx + leftDz * leftDz;
|
||||
int rightDistance = rightDx * rightDx + rightDz * rightDz;
|
||||
int distanceCompare = leftDistance.CompareTo(rightDistance);
|
||||
if (distanceCompare != 0)
|
||||
{
|
||||
return distanceCompare;
|
||||
}
|
||||
|
||||
int zCompare = left.y.CompareTo(right.y);
|
||||
return zCompare != 0 ? zCompare : left.x.CompareTo(right.x);
|
||||
}
|
||||
|
||||
private ChunkRuntime GetOrCreateChunkRuntime(Vector2Int coord)
|
||||
{
|
||||
if (!chunks.TryGetValue(coord, out ChunkRuntime runtime))
|
||||
{
|
||||
runtime = new ChunkRuntime();
|
||||
runtime.RuntimeId = ++nextChunkRuntimeId;
|
||||
chunks.Add(coord, runtime);
|
||||
}
|
||||
|
||||
return runtime;
|
||||
}
|
||||
|
||||
private static bool IsWithinRadius(Vector2Int coord, Vector2Int centerChunk, int radius)
|
||||
{
|
||||
int dx = Mathf.Abs(coord.x - centerChunk.x);
|
||||
int dz = Mathf.Abs(coord.y - centerChunk.y);
|
||||
return dx <= radius && dz <= radius;
|
||||
}
|
||||
|
||||
private void CleanupChunks()
|
||||
{
|
||||
foreach (KeyValuePair<Vector2Int, ChunkRuntime> pair in chunks)
|
||||
{
|
||||
pair.Value.Dispose();
|
||||
}
|
||||
|
||||
chunks.Clear();
|
||||
dirtyChunkMeshes.Clear();
|
||||
queuedChunkMeshes.Clear();
|
||||
pendingNeighborRefreshes.Clear();
|
||||
queuedNeighborRefreshes.Clear();
|
||||
while (pendingColliderApplies.Count > 0)
|
||||
{
|
||||
DestroyMeshAsset(pendingColliderApplies.Dequeue().ColliderMesh);
|
||||
}
|
||||
}
|
||||
|
||||
private static void DestroyMeshAsset(Mesh mesh)
|
||||
{
|
||||
if (mesh == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (Application.isPlaying)
|
||||
{
|
||||
UnityEngine.Object.Destroy(mesh);
|
||||
}
|
||||
else
|
||||
{
|
||||
UnityEngine.Object.DestroyImmediate(mesh);
|
||||
}
|
||||
}
|
||||
|
||||
private void CleanupRegions()
|
||||
{
|
||||
foreach (KeyValuePair<Vector2Int, RegionRuntime> pair in regions)
|
||||
{
|
||||
pair.Value.Dispose();
|
||||
}
|
||||
|
||||
regions.Clear();
|
||||
regionVersions.Clear();
|
||||
dirtyRegions.Clear();
|
||||
queuedRegions.Clear();
|
||||
lock (regionBuildLock)
|
||||
{
|
||||
completedRegionBuilds.Clear();
|
||||
}
|
||||
regionRebuildLoopRunning = false;
|
||||
}
|
||||
|
||||
private int CountConfiguredBiomes()
|
||||
{
|
||||
int count = 0;
|
||||
for (int i = 0; i < biomeProfiles.Count; i++)
|
||||
{
|
||||
if (biomeProfiles[i] != null)
|
||||
{
|
||||
count++;
|
||||
}
|
||||
}
|
||||
|
||||
return Mathf.Max(1, count);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 62cd563780165844caddc098f92ff23f
|
||||
@@ -1,8 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 96adefd3df6641c40b9624eb12185ea4
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -1,232 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace InfiniteWorld
|
||||
{
|
||||
[CreateAssetMenu(menuName = "Infinite World/Chunk Template", fileName = "ChunkTemplate")]
|
||||
public class ChunkTemplate : ScriptableObject
|
||||
{
|
||||
[Min(4)] public int width = 16;
|
||||
[Min(4)] public int height = 16;
|
||||
|
||||
[Header("Exits")]
|
||||
public bool exitTop = true;
|
||||
public bool exitRight = true;
|
||||
public bool exitBottom = true;
|
||||
public bool exitLeft = true;
|
||||
|
||||
[SerializeField] private List<ChunkCellData> cells = new List<ChunkCellData>();
|
||||
|
||||
public int CellCount => width * height;
|
||||
|
||||
public void EnsureCellData()
|
||||
{
|
||||
int target = Mathf.Max(1, width * height);
|
||||
while (cells.Count < target)
|
||||
{
|
||||
cells.Add(new ChunkCellData());
|
||||
}
|
||||
|
||||
while (cells.Count > target)
|
||||
{
|
||||
cells.RemoveAt(cells.Count - 1);
|
||||
}
|
||||
}
|
||||
|
||||
public void Resize(int newWidth, int newHeight)
|
||||
{
|
||||
newWidth = Mathf.Max(4, newWidth);
|
||||
newHeight = Mathf.Max(4, newHeight);
|
||||
|
||||
List<ChunkCellData> oldCells = new List<ChunkCellData>(cells);
|
||||
int oldWidth = width;
|
||||
int oldHeight = height;
|
||||
|
||||
width = newWidth;
|
||||
height = newHeight;
|
||||
cells = new List<ChunkCellData>(newWidth * newHeight);
|
||||
|
||||
for (int i = 0; i < newWidth * newHeight; i++)
|
||||
{
|
||||
cells.Add(new ChunkCellData());
|
||||
}
|
||||
|
||||
for (int y = 0; y < Mathf.Min(oldHeight, newHeight); y++)
|
||||
{
|
||||
for (int x = 0; x < Mathf.Min(oldWidth, newWidth); x++)
|
||||
{
|
||||
int oldIndex = y * oldWidth + x;
|
||||
if (oldIndex < oldCells.Count)
|
||||
{
|
||||
cells[Index(x, y)] = oldCells[oldIndex];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
EnsureCellData();
|
||||
for (int i = 0; i < cells.Count; i++)
|
||||
{
|
||||
cells[i] = new ChunkCellData();
|
||||
}
|
||||
}
|
||||
|
||||
public bool GetWall(int x, int y)
|
||||
{
|
||||
return IsInBounds(x, y) && cells[Index(x, y)].wall;
|
||||
}
|
||||
|
||||
public bool GetEnvironment(int x, int y)
|
||||
{
|
||||
return IsInBounds(x, y) && cells[Index(x, y)].environment;
|
||||
}
|
||||
|
||||
public void SetWall(int x, int y, bool value)
|
||||
{
|
||||
if (!IsInBounds(x, y))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
EnsureCellData();
|
||||
ChunkCellData data = cells[Index(x, y)];
|
||||
data.wall = value;
|
||||
if (value)
|
||||
{
|
||||
data.environment = false;
|
||||
}
|
||||
cells[Index(x, y)] = data;
|
||||
}
|
||||
|
||||
public void SetEnvironment(int x, int y, bool value)
|
||||
{
|
||||
if (!IsInBounds(x, y))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
EnsureCellData();
|
||||
ChunkCellData data = cells[Index(x, y)];
|
||||
data.environment = value;
|
||||
if (value)
|
||||
{
|
||||
data.wall = false;
|
||||
}
|
||||
cells[Index(x, y)] = data;
|
||||
}
|
||||
|
||||
public bool GetExit(ChunkExit exit)
|
||||
{
|
||||
return exit switch
|
||||
{
|
||||
ChunkExit.Top => exitTop,
|
||||
ChunkExit.Right => exitRight,
|
||||
ChunkExit.Bottom => exitBottom,
|
||||
ChunkExit.Left => exitLeft,
|
||||
_ => false
|
||||
};
|
||||
}
|
||||
|
||||
public int ExitCount()
|
||||
{
|
||||
int count = 0;
|
||||
count += exitTop ? 1 : 0;
|
||||
count += exitRight ? 1 : 0;
|
||||
count += exitBottom ? 1 : 0;
|
||||
count += exitLeft ? 1 : 0;
|
||||
return count;
|
||||
}
|
||||
|
||||
public void ApplyBorderWallsFromExits(int openingWidth = 3)
|
||||
{
|
||||
EnsureCellData();
|
||||
Clear();
|
||||
|
||||
for (int x = 0; x < width; x++)
|
||||
{
|
||||
SetWall(x, 0, true);
|
||||
SetWall(x, height - 1, true);
|
||||
}
|
||||
|
||||
for (int y = 0; y < height; y++)
|
||||
{
|
||||
SetWall(0, y, true);
|
||||
SetWall(width - 1, y, true);
|
||||
}
|
||||
|
||||
CarveExit(ChunkExit.Top, openingWidth);
|
||||
CarveExit(ChunkExit.Right, openingWidth);
|
||||
CarveExit(ChunkExit.Bottom, openingWidth);
|
||||
CarveExit(ChunkExit.Left, openingWidth);
|
||||
}
|
||||
|
||||
public void CarveExit(ChunkExit exit, int openingWidth = 3)
|
||||
{
|
||||
if (!GetExit(exit))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
int half = Mathf.Max(1, openingWidth) / 2;
|
||||
switch (exit)
|
||||
{
|
||||
case ChunkExit.Top:
|
||||
for (int x = width / 2 - half; x <= width / 2 + half; x++)
|
||||
{
|
||||
SetWall(x, height - 1, false);
|
||||
SetWall(x, height - 2, false);
|
||||
}
|
||||
break;
|
||||
case ChunkExit.Right:
|
||||
for (int y = height / 2 - half; y <= height / 2 + half; y++)
|
||||
{
|
||||
SetWall(width - 1, y, false);
|
||||
SetWall(width - 2, y, false);
|
||||
}
|
||||
break;
|
||||
case ChunkExit.Bottom:
|
||||
for (int x = width / 2 - half; x <= width / 2 + half; x++)
|
||||
{
|
||||
SetWall(x, 0, false);
|
||||
SetWall(x, 1, false);
|
||||
}
|
||||
break;
|
||||
case ChunkExit.Left:
|
||||
for (int y = height / 2 - half; y <= height / 2 + half; y++)
|
||||
{
|
||||
SetWall(0, y, false);
|
||||
SetWall(1, y, false);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private int Index(int x, int y)
|
||||
{
|
||||
return y * width + x;
|
||||
}
|
||||
|
||||
private bool IsInBounds(int x, int y)
|
||||
{
|
||||
return x >= 0 && y >= 0 && x < width && y < height;
|
||||
}
|
||||
}
|
||||
|
||||
public enum ChunkExit
|
||||
{
|
||||
Top,
|
||||
Right,
|
||||
Bottom,
|
||||
Left
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public struct ChunkCellData
|
||||
{
|
||||
public bool wall;
|
||||
public bool environment;
|
||||
}
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: bae8eeae2da7d3f4396883671b297a47
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,2 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6bacd3a0ac13e6f4a94548426dd89ebb
|
||||
@@ -1,350 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Tilemaps;
|
||||
|
||||
namespace InfiniteWorld
|
||||
{
|
||||
public static class ProceduralWorldArt
|
||||
{
|
||||
private const int TextureSize = 32;
|
||||
private static readonly Dictionary<string, Sprite> SpriteCache = new Dictionary<string, Sprite>();
|
||||
private static readonly Dictionary<string, Tile> TileCache = new Dictionary<string, Tile>();
|
||||
|
||||
public static Sprite CreatePlayerSprite()
|
||||
{
|
||||
const string key = "player_sprite";
|
||||
if (SpriteCache.TryGetValue(key, out Sprite cached))
|
||||
{
|
||||
return cached;
|
||||
}
|
||||
|
||||
Color transparent = new Color(0f, 0f, 0f, 0f);
|
||||
Texture2D texture = CreateTexture(key, transparent);
|
||||
Color cloak = new Color(0.13f, 0.33f, 0.79f, 1f);
|
||||
Color trim = new Color(0.85f, 0.92f, 1f, 1f);
|
||||
Color face = new Color(0.99f, 0.89f, 0.72f, 1f);
|
||||
|
||||
FillRect(texture, 10, 4, 12, 16, cloak);
|
||||
FillRect(texture, 9, 18, 14, 7, cloak);
|
||||
FillRect(texture, 12, 24, 8, 5, face);
|
||||
FillRect(texture, 11, 23, 10, 1, trim);
|
||||
FillRect(texture, 8, 12, 3, 10, trim);
|
||||
FillRect(texture, 21, 12, 3, 10, trim);
|
||||
FillRect(texture, 12, 0, 3, 4, cloak);
|
||||
FillRect(texture, 17, 0, 3, 4, cloak);
|
||||
texture.Apply();
|
||||
|
||||
Sprite sprite = CreateSprite(texture);
|
||||
SpriteCache[key] = sprite;
|
||||
return sprite;
|
||||
}
|
||||
|
||||
public static TileBase CreateSolidTile(string key, Color color, float noiseStrength = 0.04f)
|
||||
{
|
||||
if (TileCache.TryGetValue(key, out Tile cached))
|
||||
{
|
||||
return cached;
|
||||
}
|
||||
|
||||
Texture2D texture = CreateTexture(key, color);
|
||||
ApplyNoise(texture, noiseStrength, 31);
|
||||
texture.Apply();
|
||||
Tile tile = CreateTile(texture, key);
|
||||
TileCache[key] = tile;
|
||||
return tile;
|
||||
}
|
||||
|
||||
public static TileBase CreateFeatureTile(string key, Color fill, Color border, bool top, bool right, bool bottom, bool left, InnerCornerMask innerCornerMask = InnerCornerMask.None, bool solidCollider = false)
|
||||
{
|
||||
if (TileCache.TryGetValue(key, out Tile cached))
|
||||
{
|
||||
return cached;
|
||||
}
|
||||
|
||||
Texture2D texture = CreateTexture(key, fill);
|
||||
ApplyNoise(texture, 0.05f, 71);
|
||||
DrawBorder(texture, top, right, bottom, left, border, 4);
|
||||
CarveInnerCorner(texture, innerCornerMask);
|
||||
texture.Apply();
|
||||
|
||||
Tile tile = CreateTile(texture, key);
|
||||
if (solidCollider)
|
||||
{
|
||||
tile.colliderType = Tile.ColliderType.Grid;
|
||||
}
|
||||
TileCache[key] = tile;
|
||||
return tile;
|
||||
}
|
||||
|
||||
public static TileBase CreateDecorationTile(string key, Color baseColor, Color accentColor, DecorationPattern pattern)
|
||||
{
|
||||
if (TileCache.TryGetValue(key, out Tile cached))
|
||||
{
|
||||
return cached;
|
||||
}
|
||||
|
||||
Color transparent = new Color(0f, 0f, 0f, 0f);
|
||||
Texture2D texture = CreateTexture(key, transparent);
|
||||
|
||||
switch (pattern)
|
||||
{
|
||||
case DecorationPattern.Bush:
|
||||
DrawDisc(texture, 16, 15, 8, baseColor);
|
||||
DrawDisc(texture, 10, 13, 6, baseColor);
|
||||
DrawDisc(texture, 22, 13, 6, baseColor);
|
||||
DrawDisc(texture, 14, 17, 3, accentColor);
|
||||
DrawDisc(texture, 21, 17, 3, accentColor);
|
||||
break;
|
||||
case DecorationPattern.Flower:
|
||||
DrawDisc(texture, 16, 12, 2, baseColor);
|
||||
DrawDisc(texture, 12, 16, 2, baseColor);
|
||||
DrawDisc(texture, 20, 16, 2, baseColor);
|
||||
DrawDisc(texture, 16, 20, 2, baseColor);
|
||||
DrawDisc(texture, 16, 16, 2, accentColor);
|
||||
DrawLine(texture, 16, 0, 16, 12, new Color(0.2f, 0.5f, 0.2f, 1f));
|
||||
break;
|
||||
default:
|
||||
DrawDisc(texture, 12, 12, 4, baseColor);
|
||||
DrawDisc(texture, 20, 18, 3, accentColor);
|
||||
DrawDisc(texture, 9, 20, 2, accentColor);
|
||||
break;
|
||||
}
|
||||
|
||||
texture.Apply();
|
||||
Tile tile = CreateTile(texture, key);
|
||||
TileCache[key] = tile;
|
||||
return tile;
|
||||
}
|
||||
|
||||
private static Texture2D CreateTexture(string key, Color fillColor)
|
||||
{
|
||||
Texture2D texture = new Texture2D(TextureSize, TextureSize, TextureFormat.RGBA32, false)
|
||||
{
|
||||
filterMode = FilterMode.Point,
|
||||
wrapMode = TextureWrapMode.Clamp,
|
||||
name = key
|
||||
};
|
||||
|
||||
Color[] pixels = new Color[TextureSize * TextureSize];
|
||||
for (int i = 0; i < pixels.Length; i++)
|
||||
{
|
||||
pixels[i] = fillColor;
|
||||
}
|
||||
|
||||
texture.SetPixels(pixels);
|
||||
return texture;
|
||||
}
|
||||
|
||||
private static Tile CreateTile(Texture2D texture, string key)
|
||||
{
|
||||
Sprite sprite = CreateSprite(texture);
|
||||
Tile tile = ScriptableObject.CreateInstance<Tile>();
|
||||
tile.sprite = sprite;
|
||||
tile.name = key;
|
||||
tile.hideFlags = HideFlags.HideAndDontSave;
|
||||
return tile;
|
||||
}
|
||||
|
||||
private static Sprite CreateSprite(Texture2D texture)
|
||||
{
|
||||
Sprite sprite = Sprite.Create(texture, new Rect(0f, 0f, texture.width, texture.height), new Vector2(0.5f, 0.5f), TextureSize);
|
||||
sprite.name = texture.name;
|
||||
return sprite;
|
||||
}
|
||||
|
||||
private static void ApplyNoise(Texture2D texture, float strength, int offset)
|
||||
{
|
||||
if (strength <= 0f)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
for (int y = 0; y < texture.height; y++)
|
||||
{
|
||||
for (int x = 0; x < texture.width; x++)
|
||||
{
|
||||
Color pixel = texture.GetPixel(x, y);
|
||||
if (pixel.a <= 0f)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
float noise = Mathf.PerlinNoise((x + offset) * 0.21f, (y + offset) * 0.21f) - 0.5f;
|
||||
texture.SetPixel(x, y, pixel * (1f + noise * strength * 2f));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void DrawBorder(Texture2D texture, bool top, bool right, bool bottom, bool left, Color color, int thickness)
|
||||
{
|
||||
if (top)
|
||||
{
|
||||
FillRect(texture, 0, TextureSize - thickness, TextureSize, thickness, color);
|
||||
}
|
||||
|
||||
if (bottom)
|
||||
{
|
||||
FillRect(texture, 0, 0, TextureSize, thickness, color);
|
||||
}
|
||||
|
||||
if (left)
|
||||
{
|
||||
FillRect(texture, 0, 0, thickness, TextureSize, color);
|
||||
}
|
||||
|
||||
if (right)
|
||||
{
|
||||
FillRect(texture, TextureSize - thickness, 0, thickness, TextureSize, color);
|
||||
}
|
||||
}
|
||||
|
||||
private static void CarveInnerCorner(Texture2D texture, InnerCornerMask innerCornerMask)
|
||||
{
|
||||
if (innerCornerMask == InnerCornerMask.None)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Color transparent = new Color(0f, 0f, 0f, 0f);
|
||||
const int radius = 8;
|
||||
|
||||
if ((innerCornerMask & InnerCornerMask.TopLeft) != 0)
|
||||
{
|
||||
CutCorner(texture, 0, TextureSize - 1, radius, transparent, 1, -1);
|
||||
}
|
||||
|
||||
if ((innerCornerMask & InnerCornerMask.TopRight) != 0)
|
||||
{
|
||||
CutCorner(texture, TextureSize - 1, TextureSize - 1, radius, transparent, -1, -1);
|
||||
}
|
||||
|
||||
if ((innerCornerMask & InnerCornerMask.BottomRight) != 0)
|
||||
{
|
||||
CutCorner(texture, TextureSize - 1, 0, radius, transparent, -1, 1);
|
||||
}
|
||||
|
||||
if ((innerCornerMask & InnerCornerMask.BottomLeft) != 0)
|
||||
{
|
||||
CutCorner(texture, 0, 0, radius, transparent, 1, 1);
|
||||
}
|
||||
}
|
||||
|
||||
private static void CutCorner(Texture2D texture, int originX, int originY, int radius, Color transparent, int dirX, int dirY)
|
||||
{
|
||||
for (int x = 0; x < radius; x++)
|
||||
{
|
||||
for (int y = 0; y < radius; y++)
|
||||
{
|
||||
float dx = x / (float)radius;
|
||||
float dy = y / (float)radius;
|
||||
if (dx * dx + dy * dy > 1f)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
int px = originX + x * dirX;
|
||||
int py = originY + y * dirY;
|
||||
if (px >= 0 && px < TextureSize && py >= 0 && py < TextureSize)
|
||||
{
|
||||
texture.SetPixel(px, py, transparent);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void DrawDisc(Texture2D texture, int centerX, int centerY, int radius, Color color)
|
||||
{
|
||||
int sqrRadius = radius * radius;
|
||||
for (int y = -radius; y <= radius; y++)
|
||||
{
|
||||
for (int x = -radius; x <= radius; x++)
|
||||
{
|
||||
if (x * x + y * y > sqrRadius)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
int px = centerX + x;
|
||||
int py = centerY + y;
|
||||
if (px >= 0 && px < TextureSize && py >= 0 && py < TextureSize)
|
||||
{
|
||||
texture.SetPixel(px, py, color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void DrawLine(Texture2D texture, int x0, int y0, int x1, int y1, Color color)
|
||||
{
|
||||
int dx = Mathf.Abs(x1 - x0);
|
||||
int sx = x0 < x1 ? 1 : -1;
|
||||
int dy = -Mathf.Abs(y1 - y0);
|
||||
int sy = y0 < y1 ? 1 : -1;
|
||||
int error = dx + dy;
|
||||
|
||||
while (true)
|
||||
{
|
||||
if (x0 >= 0 && x0 < TextureSize && y0 >= 0 && y0 < TextureSize)
|
||||
{
|
||||
texture.SetPixel(x0, y0, color);
|
||||
}
|
||||
|
||||
if (x0 == x1 && y0 == y1)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
int twiceError = 2 * error;
|
||||
if (twiceError >= dy)
|
||||
{
|
||||
error += dy;
|
||||
x0 += sx;
|
||||
}
|
||||
|
||||
if (twiceError <= dx)
|
||||
{
|
||||
error += dx;
|
||||
y0 += sy;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void FillRect(Texture2D texture, int x, int y, int width, int height, Color color)
|
||||
{
|
||||
for (int py = y; py < y + height; py++)
|
||||
{
|
||||
if (py < 0 || py >= TextureSize)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
for (int px = x; px < x + width; px++)
|
||||
{
|
||||
if (px < 0 || px >= TextureSize)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
texture.SetPixel(px, py, color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public enum DecorationPattern
|
||||
{
|
||||
Bush,
|
||||
Flower,
|
||||
Rocks
|
||||
}
|
||||
|
||||
[System.Flags]
|
||||
public enum InnerCornerMask
|
||||
{
|
||||
None = 0,
|
||||
TopLeft = 1,
|
||||
TopRight = 2,
|
||||
BottomRight = 4,
|
||||
BottomLeft = 8
|
||||
}
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: fc8c4a69ee2e7a046adf658709613591
|
||||
@@ -1,237 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace InfiniteWorld
|
||||
{
|
||||
public static class RuntimeWorldProfileFactory
|
||||
{
|
||||
public static WorldAutotileProfile CreateFallbackProfile()
|
||||
{
|
||||
WorldAutotileProfile profile = ScriptableObject.CreateInstance<WorldAutotileProfile>();
|
||||
profile.name = "RuntimeFallbackWorldProfile";
|
||||
|
||||
Color grass = new Color(0.35f, 0.62f, 0.26f, 1f);
|
||||
Color wallFill = new Color(0.48f, 0.37f, 0.22f, 1f);
|
||||
Color wallBorder = new Color(0.29f, 0.21f, 0.12f, 1f);
|
||||
|
||||
profile.baseGroundTile = ProceduralWorldArt.CreateSolidTile("base_grass", grass, 0.06f);
|
||||
profile.wallTiles.center = ProceduralWorldArt.CreateFeatureTile("wall_center", wallFill, wallBorder, false, false, false, false, InnerCornerMask.None, true);
|
||||
profile.wallTiles.top = ProceduralWorldArt.CreateFeatureTile("wall_top", wallFill, wallBorder, true, false, false, false, InnerCornerMask.None, true);
|
||||
profile.wallTiles.right = ProceduralWorldArt.CreateFeatureTile("wall_right", wallFill, wallBorder, false, true, false, false, InnerCornerMask.None, true);
|
||||
profile.wallTiles.bottom = ProceduralWorldArt.CreateFeatureTile("wall_bottom", wallFill, wallBorder, false, false, true, false, InnerCornerMask.None, true);
|
||||
profile.wallTiles.left = ProceduralWorldArt.CreateFeatureTile("wall_left", wallFill, wallBorder, false, false, false, true, InnerCornerMask.None, true);
|
||||
profile.wallTiles.outerTopLeft = ProceduralWorldArt.CreateFeatureTile("wall_outer_tl", wallFill, wallBorder, true, false, false, true, InnerCornerMask.None, true);
|
||||
profile.wallTiles.outerTopRight = ProceduralWorldArt.CreateFeatureTile("wall_outer_tr", wallFill, wallBorder, true, true, false, false, InnerCornerMask.None, true);
|
||||
profile.wallTiles.outerBottomRight = ProceduralWorldArt.CreateFeatureTile("wall_outer_br", wallFill, wallBorder, false, true, true, false, InnerCornerMask.None, true);
|
||||
profile.wallTiles.outerBottomLeft = ProceduralWorldArt.CreateFeatureTile("wall_outer_bl", wallFill, wallBorder, false, false, true, true, InnerCornerMask.None, true);
|
||||
profile.wallTiles.innerTopLeft = ProceduralWorldArt.CreateFeatureTile("wall_inner_tl", wallFill, wallBorder, false, false, false, false, InnerCornerMask.TopLeft, true);
|
||||
profile.wallTiles.innerTopRight = ProceduralWorldArt.CreateFeatureTile("wall_inner_tr", wallFill, wallBorder, false, false, false, false, InnerCornerMask.TopRight, true);
|
||||
profile.wallTiles.innerBottomRight = ProceduralWorldArt.CreateFeatureTile("wall_inner_br", wallFill, wallBorder, false, false, false, false, InnerCornerMask.BottomRight, true);
|
||||
profile.wallTiles.innerBottomLeft = ProceduralWorldArt.CreateFeatureTile("wall_inner_bl", wallFill, wallBorder, false, false, false, false, InnerCornerMask.BottomLeft, true);
|
||||
|
||||
profile.environmentTiles.Add(new EnvironmentTileEntry
|
||||
{
|
||||
id = "Bush",
|
||||
tile = ProceduralWorldArt.CreateDecorationTile("env_bush", new Color(0.15f, 0.45f, 0.18f, 1f), new Color(0.23f, 0.58f, 0.25f, 1f), DecorationPattern.Bush),
|
||||
weight = 1.4f
|
||||
});
|
||||
profile.environmentTiles.Add(new EnvironmentTileEntry
|
||||
{
|
||||
id = "FlowerBlue",
|
||||
tile = ProceduralWorldArt.CreateDecorationTile("env_flower_blue", new Color(0.44f, 0.62f, 0.96f, 1f), new Color(0.98f, 0.91f, 0.52f, 1f), DecorationPattern.Flower),
|
||||
weight = 0.75f
|
||||
});
|
||||
profile.environmentTiles.Add(new EnvironmentTileEntry
|
||||
{
|
||||
id = "Rock",
|
||||
tile = ProceduralWorldArt.CreateDecorationTile("env_rock", new Color(0.48f, 0.48f, 0.52f, 1f), new Color(0.69f, 0.69f, 0.74f, 1f), DecorationPattern.Rocks),
|
||||
weight = 0.9f
|
||||
});
|
||||
|
||||
return profile;
|
||||
}
|
||||
|
||||
public static List<ChunkTemplate> CreateFallbackTemplates(int size)
|
||||
{
|
||||
List<ChunkTemplate> templates = new List<ChunkTemplate>
|
||||
{
|
||||
CreateCross(size),
|
||||
CreateStraightHorizontal(size),
|
||||
CreateStraightVertical(size),
|
||||
CreateCorner(size, true, true, false, false),
|
||||
CreateCorner(size, false, true, true, false),
|
||||
CreateCorner(size, false, false, true, true),
|
||||
CreateCorner(size, true, false, false, true),
|
||||
CreateTJunction(size, true, true, true, false),
|
||||
CreateTJunction(size, false, true, true, true),
|
||||
CreateTJunction(size, true, false, true, true),
|
||||
CreateTJunction(size, true, true, false, true)
|
||||
};
|
||||
|
||||
return templates;
|
||||
}
|
||||
|
||||
private static ChunkTemplate CreateCross(int size)
|
||||
{
|
||||
ChunkTemplate template = CreateTemplate("Runtime_Cross", size, size, true, true, true, true);
|
||||
DrawRoom(template, 3, 3, size - 6, size - 6);
|
||||
DrawPillar(template, 4, 4);
|
||||
DrawPillar(template, size - 5, 4);
|
||||
DrawPillar(template, 4, size - 5);
|
||||
DrawPillar(template, size - 5, size - 5);
|
||||
ScatterEnvironment(template, 3);
|
||||
return template;
|
||||
}
|
||||
|
||||
private static ChunkTemplate CreateStraightHorizontal(int size)
|
||||
{
|
||||
ChunkTemplate template = CreateTemplate("Runtime_Straight_H", size, size, false, true, false, true);
|
||||
DrawRoom(template, 2, 4, size - 4, size - 8);
|
||||
DrawSideAlcoves(template, horizontal: true);
|
||||
ScatterEnvironment(template, 2);
|
||||
return template;
|
||||
}
|
||||
|
||||
private static ChunkTemplate CreateStraightVertical(int size)
|
||||
{
|
||||
ChunkTemplate template = CreateTemplate("Runtime_Straight_V", size, size, true, false, true, false);
|
||||
DrawRoom(template, 4, 2, size - 8, size - 4);
|
||||
DrawSideAlcoves(template, horizontal: false);
|
||||
ScatterEnvironment(template, 2);
|
||||
return template;
|
||||
}
|
||||
|
||||
private static ChunkTemplate CreateCorner(int size, bool top, bool right, bool bottom, bool left)
|
||||
{
|
||||
ChunkTemplate template = CreateTemplate($"Runtime_Corner_{top}_{right}_{bottom}_{left}", size, size, top, right, bottom, left);
|
||||
DrawRoom(template, 3, 3, size - 6, size - 6);
|
||||
if (!top)
|
||||
{
|
||||
DrawWallLine(template, 5, size - 5, size - 6, size - 5);
|
||||
}
|
||||
if (!right)
|
||||
{
|
||||
DrawWallLine(template, size - 5, 5, size - 5, size - 6);
|
||||
}
|
||||
if (!bottom)
|
||||
{
|
||||
DrawWallLine(template, 5, 4, size - 6, 4);
|
||||
}
|
||||
if (!left)
|
||||
{
|
||||
DrawWallLine(template, 4, 5, 4, size - 6);
|
||||
}
|
||||
ScatterEnvironment(template, 2);
|
||||
return template;
|
||||
}
|
||||
|
||||
private static ChunkTemplate CreateTJunction(int size, bool top, bool right, bool bottom, bool left)
|
||||
{
|
||||
ChunkTemplate template = CreateTemplate($"Runtime_T_{top}_{right}_{bottom}_{left}", size, size, top, right, bottom, left);
|
||||
DrawRoom(template, 2, 2, size - 4, size - 4);
|
||||
DrawPillar(template, size / 2, size / 2);
|
||||
ScatterEnvironment(template, 3);
|
||||
return template;
|
||||
}
|
||||
|
||||
private static ChunkTemplate CreateTemplate(string name, int width, int height, bool top, bool right, bool bottom, bool left)
|
||||
{
|
||||
ChunkTemplate template = ScriptableObject.CreateInstance<ChunkTemplate>();
|
||||
template.name = name;
|
||||
template.Resize(width, height);
|
||||
template.exitTop = top;
|
||||
template.exitRight = right;
|
||||
template.exitBottom = bottom;
|
||||
template.exitLeft = left;
|
||||
template.ApplyBorderWallsFromExits(3);
|
||||
return template;
|
||||
}
|
||||
|
||||
private static void DrawRoom(ChunkTemplate template, int xMin, int yMin, int width, int height)
|
||||
{
|
||||
for (int x = xMin; x < xMin + width; x++)
|
||||
{
|
||||
template.SetWall(x, yMin, true);
|
||||
template.SetWall(x, yMin + height - 1, true);
|
||||
}
|
||||
|
||||
for (int y = yMin; y < yMin + height; y++)
|
||||
{
|
||||
template.SetWall(xMin, y, true);
|
||||
template.SetWall(xMin + width - 1, y, true);
|
||||
}
|
||||
|
||||
template.CarveExit(ChunkExit.Top, 3);
|
||||
template.CarveExit(ChunkExit.Right, 3);
|
||||
template.CarveExit(ChunkExit.Bottom, 3);
|
||||
template.CarveExit(ChunkExit.Left, 3);
|
||||
}
|
||||
|
||||
private static void DrawPillar(ChunkTemplate template, int x, int y)
|
||||
{
|
||||
for (int py = -1; py <= 1; py++)
|
||||
{
|
||||
for (int px = -1; px <= 1; px++)
|
||||
{
|
||||
template.SetWall(x + px, y + py, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void DrawSideAlcoves(ChunkTemplate template, bool horizontal)
|
||||
{
|
||||
if (horizontal)
|
||||
{
|
||||
DrawWallLine(template, template.width / 2 - 1, 5, template.width / 2 - 1, 7);
|
||||
DrawWallLine(template, template.width / 2 + 1, template.height - 8, template.width / 2 + 1, template.height - 6);
|
||||
}
|
||||
else
|
||||
{
|
||||
DrawWallLine(template, 5, template.height / 2 - 1, 7, template.height / 2 - 1);
|
||||
DrawWallLine(template, template.width - 8, template.height / 2 + 1, template.width - 6, template.height / 2 + 1);
|
||||
}
|
||||
}
|
||||
|
||||
private static void DrawWallLine(ChunkTemplate template, int x0, int y0, int x1, int y1)
|
||||
{
|
||||
int dx = Math.Sign(x1 - x0);
|
||||
int dy = Math.Sign(y1 - y0);
|
||||
int x = x0;
|
||||
int y = y0;
|
||||
|
||||
while (true)
|
||||
{
|
||||
template.SetWall(x, y, true);
|
||||
if (x == x1 && y == y1)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
if (x != x1)
|
||||
{
|
||||
x += dx;
|
||||
}
|
||||
|
||||
if (y != y1)
|
||||
{
|
||||
y += dy;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void ScatterEnvironment(ChunkTemplate template, int spacing)
|
||||
{
|
||||
for (int y = 2; y < template.height - 2; y += spacing + 2)
|
||||
{
|
||||
for (int x = 2; x < template.width - 2; x += spacing + 3)
|
||||
{
|
||||
if (!template.GetWall(x, y))
|
||||
{
|
||||
template.SetEnvironment(x, y, ((x + y) & 1) == 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e5b485257bdc222448c314a7cd173845
|
||||
@@ -1,10 +0,0 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace InfiniteWorld
|
||||
{
|
||||
[DisallowMultipleComponent]
|
||||
public sealed class WorldAutotileAuthoringRoot : MonoBehaviour
|
||||
{
|
||||
public WorldAutotileProfile profile;
|
||||
}
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: bbd0bbbd74f81084bb8661f761798856
|
||||
@@ -1,18 +0,0 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace InfiniteWorld
|
||||
{
|
||||
public enum WorldAutotileAuthoringSectionType
|
||||
{
|
||||
WallShapes,
|
||||
BackgroundSample,
|
||||
EnvironmentPalette
|
||||
}
|
||||
|
||||
[DisallowMultipleComponent]
|
||||
public sealed class WorldAutotileAuthoringSection : MonoBehaviour
|
||||
{
|
||||
public WorldAutotileAuthoringSectionType sectionType;
|
||||
public Vector2Int size = Vector2Int.one;
|
||||
}
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: fbfe986c83f0c96419c804643d142e37
|
||||
@@ -1,161 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Tilemaps;
|
||||
|
||||
namespace InfiniteWorld
|
||||
{
|
||||
[CreateAssetMenu(menuName = "Infinite World/World Autotile Profile", fileName = "WorldAutotileProfile")]
|
||||
public class WorldAutotileProfile : ScriptableObject
|
||||
{
|
||||
public bool autoUpdatePaletteLayout = true;
|
||||
public bool autoRefreshGeneratedWorld = true;
|
||||
public TileBase baseGroundTile;
|
||||
public AutoTileDefinition wallTiles = new AutoTileDefinition();
|
||||
public List<EnvironmentTileEntry> environmentTiles = new List<EnvironmentTileEntry>();
|
||||
public List<RandomPrefabEntry> randomPrefabs = new List<RandomPrefabEntry>();
|
||||
|
||||
public TileBase GetWallTile(AutoTileShape shape)
|
||||
{
|
||||
return wallTiles != null ? wallTiles.GetTile(shape) : null;
|
||||
}
|
||||
|
||||
public bool HasAnyAssignedTiles()
|
||||
{
|
||||
if (baseGroundTile != null)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (wallTiles != null && wallTiles.HasAnyAssignedTiles())
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (environmentTiles != null)
|
||||
{
|
||||
for (int i = 0; i < environmentTiles.Count; i++)
|
||||
{
|
||||
if (environmentTiles[i] != null && environmentTiles[i].tile != null)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public enum AutoTileShape
|
||||
{
|
||||
Center,
|
||||
Top,
|
||||
Right,
|
||||
Bottom,
|
||||
Left,
|
||||
OuterTopLeft,
|
||||
OuterTopRight,
|
||||
OuterBottomRight,
|
||||
OuterBottomLeft,
|
||||
InnerTopLeft,
|
||||
InnerTopRight,
|
||||
InnerBottomRight,
|
||||
InnerBottomLeft
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public class AutoTileDefinition
|
||||
{
|
||||
public TileBase center;
|
||||
public TileBase top;
|
||||
public TileBase right;
|
||||
public TileBase bottom;
|
||||
public TileBase left;
|
||||
public TileBase outerTopLeft;
|
||||
public TileBase outerTopRight;
|
||||
public TileBase outerBottomRight;
|
||||
public TileBase outerBottomLeft;
|
||||
public TileBase innerTopLeft;
|
||||
public TileBase innerTopRight;
|
||||
public TileBase innerBottomRight;
|
||||
public TileBase innerBottomLeft;
|
||||
|
||||
public TileBase GetTile(AutoTileShape shape)
|
||||
{
|
||||
TileBase tile = shape switch
|
||||
{
|
||||
AutoTileShape.Center => center,
|
||||
AutoTileShape.Top => top,
|
||||
AutoTileShape.Right => right,
|
||||
AutoTileShape.Bottom => bottom,
|
||||
AutoTileShape.Left => left,
|
||||
AutoTileShape.OuterTopLeft => outerTopLeft,
|
||||
AutoTileShape.OuterTopRight => outerTopRight,
|
||||
AutoTileShape.OuterBottomRight => outerBottomRight,
|
||||
AutoTileShape.OuterBottomLeft => outerBottomLeft,
|
||||
AutoTileShape.InnerTopLeft => innerTopLeft,
|
||||
AutoTileShape.InnerTopRight => innerTopRight,
|
||||
AutoTileShape.InnerBottomRight => innerBottomRight,
|
||||
AutoTileShape.InnerBottomLeft => innerBottomLeft,
|
||||
_ => center
|
||||
};
|
||||
|
||||
return tile ?? center;
|
||||
}
|
||||
|
||||
public TileBase GetAssignedTile(AutoTileShape shape)
|
||||
{
|
||||
return shape switch
|
||||
{
|
||||
AutoTileShape.Center => center,
|
||||
AutoTileShape.Top => top,
|
||||
AutoTileShape.Right => right,
|
||||
AutoTileShape.Bottom => bottom,
|
||||
AutoTileShape.Left => left,
|
||||
AutoTileShape.OuterTopLeft => outerTopLeft,
|
||||
AutoTileShape.OuterTopRight => outerTopRight,
|
||||
AutoTileShape.OuterBottomRight => outerBottomRight,
|
||||
AutoTileShape.OuterBottomLeft => outerBottomLeft,
|
||||
AutoTileShape.InnerTopLeft => innerTopLeft,
|
||||
AutoTileShape.InnerTopRight => innerTopRight,
|
||||
AutoTileShape.InnerBottomRight => innerBottomRight,
|
||||
AutoTileShape.InnerBottomLeft => innerBottomLeft,
|
||||
_ => null
|
||||
};
|
||||
}
|
||||
|
||||
public bool HasAnyAssignedTiles()
|
||||
{
|
||||
return center != null ||
|
||||
top != null ||
|
||||
right != null ||
|
||||
bottom != null ||
|
||||
left != null ||
|
||||
outerTopLeft != null ||
|
||||
outerTopRight != null ||
|
||||
outerBottomRight != null ||
|
||||
outerBottomLeft != null ||
|
||||
innerTopLeft != null ||
|
||||
innerTopRight != null ||
|
||||
innerBottomRight != null ||
|
||||
innerBottomLeft != null;
|
||||
}
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public class EnvironmentTileEntry
|
||||
{
|
||||
public string id = "Environment";
|
||||
public TileBase tile;
|
||||
[Min(0f)] public float weight = 1f;
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public class RandomPrefabEntry
|
||||
{
|
||||
public string id = "RandomPrefab";
|
||||
public GameObject prefab;
|
||||
[Min(0f)] public float weight = 1f;
|
||||
}
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e36b055e81273a54c8e736e2ef74fa50
|
||||
Reference in New Issue
Block a user