Created
October 28, 2025 16:02
-
-
Save mzandvliet/38dfdfe416d0ee37e842c3deab879f0e to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| using System.Collections.Generic; | |
| using System.Linq; | |
| using System.Runtime.CompilerServices; | |
| using UnityEngine; | |
| using Unity.Collections; | |
| using Unity.Mathematics; | |
| using Rng = Unity.Mathematics.Random; | |
| using Shapes; | |
| using Unity.Burst; | |
| using Unity.Jobs; | |
| /* | |
| * A jam loosely based on Damien Isla's Occupancy Maps algorithm | |
| * | |
| * Todo: | |
| * - Limited FOV, agent lookdir, and wobbly pathing are currently feedbacking in a jarring way | |
| * - mechanics-aware probability diffusion models | |
| * - Improve pathing | |
| * - Try abstract sound propagation | |
| * - kind of like pathfinding steps or floodfill | |
| * - information propagation 1 wavefront rolling outwards, not rebounding back | |
| * - a bit flag specifying what type of sound it is | |
| * - amplitude falloff as it goes, noise floor | |
| * - guard directional hearing prefers earliest loudest | |
| * | |
| * Game-wise we have a lot to do: | |
| * - Vision cone + seeing audio waves is a lot more fun than only vision cone | |
| * - see audio waves beyond your vision cone | |
| * - YOU see a knowledge model as well | |
| * - You can see the map as you remember it | |
| * - and it fuzzes out over time | |
| * - and it can be wrong | |
| * - you can see blurry estimates of guard behind you? | |
| * | |
| * - This ain't fun to play with yet | |
| * - Guard needs slower movement modes (non-chase, or chasing with self-preservation, not wanting to run into an attack) | |
| * - Need objectives | |
| * - Need game fail when caught | |
| * - Need ability to peek corners with some level of safety | |
| * | |
| * And this way it turns into something very traditional and I'm already feeling bored | |
| * | |
| * First visibility algorithm based on: | |
| * http://journal.stuffwithstuff.com/2015/09/07/what-the-hero-sees/ | |
| * found through redblobgames | |
| * https://www.redblobgames.com/articles/visibility/ | |
| * | |
| */ | |
| public struct byte4 { | |
| public byte x; | |
| public byte y; | |
| public byte z; | |
| public byte w; | |
| } | |
| public class OccupancyMapApplication : MonoBehaviour { | |
| [SerializeField] private Camera _camera; | |
| [SerializeField] private MeshRenderer _textureRenderer; | |
| [SerializeField] private Texture2D _mapGeometryTexture; | |
| private static readonly int2 Size = new int2(64, 64); | |
| private Texture2D _tex; | |
| private NativeArray<float4> _pixels; | |
| private NativeArray<float> _occupancyA, _occupancyB; | |
| private NativeArray<float> _guardVision; | |
| private NativeArray<float> _playerVision; | |
| private NativeArray<byte> _geometry; | |
| private NativeArray<float> _bufferA; | |
| private NativeArray<float> _bufferB; | |
| private NativeArray<float> _bufferC; | |
| private Rng _rng; | |
| private int2 _playerPos; | |
| private int2 _playerVel; | |
| private int2 _playerDir; | |
| private int2 _guardPos; | |
| private int2 _guardVel; | |
| private int2 _guardDir; | |
| private int2 _guardGoalPos; | |
| private float _guardGoalSalience; | |
| private NativeList<int2> _guardPath; | |
| private int _guardBark; | |
| private float _guardBarkTime; | |
| const float SimDeltaTime = 1f / 12f; | |
| private float _frameTime; | |
| private ulong _simTick; | |
| private bool _drawDebug; | |
| private JobHandle _handle; | |
| private readonly Dictionary<int, string> _guardBarks = new Dictionary<int, string> { | |
| { 0, "" }, | |
| { 1, "What the..." }, | |
| { 2, "Where are they?!" }, | |
| { 3, "I see you!" }, | |
| { 4, "Stop right there!" }, | |
| { 5, "Freeze!" }, | |
| }; | |
| void Start() { | |
| Camera.onPreRender += OnPreRenderCallback; | |
| _rng = new Rng(1234); | |
| _geometry = new NativeArray<byte>(Size.x * Size.y, Allocator.Persistent); | |
| _tex = new Texture2D(Size.x, Size.y, TextureFormat.RGBAFloat, 1, true); | |
| _tex.wrapMode = TextureWrapMode.Clamp; | |
| _tex.filterMode = FilterMode.Point; | |
| _textureRenderer.sharedMaterial.SetTexture("_MainTex", _tex); | |
| _pixels = _tex.GetPixelData<float4>(0); | |
| var geomPixels = _mapGeometryTexture.GetPixelData<byte4>(0); | |
| for (int i = 0; i < geomPixels.Length; i++) { | |
| _geometry[i] = geomPixels[i].x; | |
| } | |
| _occupancyA = new NativeArray<float>(Size.x * Size.y, Allocator.Persistent); | |
| _occupancyB = new NativeArray<float>(Size.x * Size.y, Allocator.Persistent); | |
| _guardVision = new NativeArray<float>(Size.x * Size.y, Allocator.Persistent); | |
| _playerVision = new NativeArray<float>(Size.x * Size.y, Allocator.Persistent); | |
| _bufferA = new NativeArray<float>(Size.x * Size.y, Allocator.Persistent); | |
| _bufferB = new NativeArray<float>(Size.x * Size.y, Allocator.Persistent); | |
| _bufferC = new NativeArray<float>(Size.x * Size.y, Allocator.Persistent); | |
| _playerPos = new int2(50, 10); | |
| _playerDir = new int2(0, 1); | |
| _guardPos = new int2(10, 10); | |
| _guardDir = new int2(0, 1); | |
| _guardGoalPos = _guardPos; | |
| _guardPath = new NativeList<int2>(64, Allocator.Persistent); | |
| } | |
| void OnDestroy() { | |
| _geometry.Dispose(); | |
| _occupancyA.Dispose(); | |
| _occupancyB.Dispose(); | |
| _guardVision.Dispose(); | |
| _playerVision.Dispose(); | |
| _bufferA.Dispose(); | |
| _bufferB.Dispose(); | |
| _bufferC.Dispose(); | |
| _guardPath.Dispose(); | |
| } | |
| void Update() { | |
| if (Input.GetKeyDown(KeyCode.Space)) { | |
| _drawDebug = !_drawDebug; | |
| } | |
| _playerVel = int2.zero; | |
| if (Input.GetKey(KeyCode.A)) { _playerVel.x = -1; } | |
| if (Input.GetKey(KeyCode.D)) { _playerVel.x = +1; } | |
| if (Input.GetKey(KeyCode.S)) { _playerVel.y = -1; } | |
| if (Input.GetKey(KeyCode.W)) { _playerVel.y = +1; } | |
| _frameTime += Time.deltaTime; | |
| SimulateSound(); | |
| if (_frameTime > SimDeltaTime) { | |
| UpdateSimulation(); | |
| _frameTime -= SimDeltaTime; | |
| } | |
| RenderGame(); | |
| } | |
| void UpdateSimulation() { | |
| UpdateGuardAI(); | |
| UpdateAgent(ref _playerPos, ref _playerVel, ref _playerDir, _geometry); | |
| UpdateAgent(ref _guardPos, ref _guardVel, ref _guardDir, _geometry); | |
| RefreshVisibility(_guardPos, _guardDir, _geometry, _guardVision, Size); // Purely so the visuals don't lag a frame | |
| RefreshVisibility(_playerPos, _playerDir, _geometry, _playerVision, Size); | |
| if (_simTick % 5 == 0) { | |
| if (math.length(_guardVel) > 0) { | |
| int2 impulsePos = _guardPos; | |
| float impulse = -0.75f; | |
| _bufferB[PosToIdx(impulsePos + new int2(-1, +0))] += 0.5f * impulse; | |
| _bufferB[PosToIdx(impulsePos + new int2(+1, +0))] += 0.5f * impulse; | |
| _bufferB[PosToIdx(impulsePos + new int2(+0, -1))] += 0.5f * impulse; | |
| _bufferB[PosToIdx(impulsePos + new int2(+0, +1))] += 0.5f * impulse; | |
| _bufferB[PosToIdx(impulsePos)] += impulse; | |
| } | |
| } | |
| _simTick++; | |
| } | |
| private void SimulateSound() { | |
| // prime steps so that if there's any fast flipflop oscillation it gets fuzzed | |
| const int ticksPerFrame = 11; | |
| for (int i = 0; i < ticksPerFrame; i++) { | |
| var j = new PropagateJobParallel() | |
| { | |
| tick = (ulong)Time.frameCount + (ulong)i * 90647, | |
| space = _geometry, | |
| prev = _bufferA, | |
| curr = _bufferB, | |
| next = _bufferC | |
| }; | |
| _handle = j.Schedule(_bufferA.Length, 64, _handle); | |
| _handle.Complete(); | |
| var temp = _bufferA; | |
| _bufferA = _bufferB; | |
| _bufferB = _bufferC; | |
| _bufferC = temp; | |
| } | |
| } | |
| private void RenderGame() { | |
| float3 playerColor = new float3(1, 0, 1); | |
| float3 agentColor = new float3(0, 1, 1); | |
| RenderGeometry(_geometry, _pixels, Size); | |
| // if (_drawDebug) { | |
| // RenderOccupancy(_occupancyA, playerColor, _pixels, Size); | |
| // } | |
| // RenderVisibility(_guardVision, _pixels, Size); | |
| RenderVisibility(_playerVision, _pixels, Size); | |
| var rj = new RenderJobParallel() | |
| { | |
| buf = _bufferA, | |
| colors = _pixels | |
| }; | |
| _handle = rj.Schedule(_pixels.Length, 64, _handle); | |
| _handle.Complete(); | |
| if (_drawDebug) { | |
| RenderPoint(_guardGoalPos, new float4(agentColor * 0.1f,1), _pixels, Size); | |
| } | |
| bool guardIsVisible = _playerVision[PosToIdx(_guardPos)] > 0.1f; | |
| if (guardIsVisible) { | |
| RenderPoint(_guardPos, new float4(agentColor, 1), _pixels, Size); | |
| } | |
| RenderPoint(_playerPos, new float4(playerColor,1), _pixels, Size); | |
| _tex.Apply(); | |
| } | |
| private static void UpdateAgent(ref int2 pos, ref int2 vel, ref int2 dir, in NativeArray<byte> geometry) { | |
| if (geometry[PosToIdx(pos + vel)] > 0.5f && math.lengthsq(vel) > 0) { | |
| pos += vel; | |
| dir = vel; | |
| } | |
| } | |
| private int _flabbergastedCount; | |
| private bool _playerWasVisible; | |
| private void UpdateGuardAI() { | |
| // Update bark system | |
| UpdateGuardBarks(); | |
| /* Update visibility */ | |
| RefreshVisibility(_guardPos, _guardDir, _geometry, _guardVision, Size); | |
| bool repath = false; | |
| /* Todo: this is wrong, we're using external world information to determine behaviour | |
| * We should be deriving whether a player is visible from internal agent state | |
| */ | |
| bool playerIsVisible = _guardVision[PosToIdx(_playerPos)] > 0.1f; | |
| if (playerIsVisible) { | |
| if (!_playerWasVisible) { | |
| // Zero out saliency everywhere | |
| for (int i = 0; i < _occupancyA.Length; i++) { | |
| _occupancyA[i] = 0; | |
| } | |
| repath = true; | |
| GuardBark(_rng.NextInt(3, 6)); | |
| } | |
| // Infuse location with highest salience | |
| _occupancyA[PosToIdx(_playerPos)] = 1f; | |
| } | |
| _playerWasVisible = playerIsVisible; | |
| /* Update salience map */ | |
| Normalize(_occupancyA, Size); | |
| Collapse(_guardVision, _occupancyA, Size); | |
| for (int i = 0; i < 4; i++) { | |
| Diffuse(_occupancyA, _occupancyB, _geometry, Size); | |
| (_occupancyA, _occupancyB) = (_occupancyB, _occupancyA); // Whoa, swap via deconstruction, nice | |
| // Todo: diffusion allocates temp double buffer internally | |
| } | |
| if (_flabbergastedCount > 0) { | |
| _flabbergastedCount--; | |
| return; | |
| } | |
| float currentGoalSalience = _occupancyA[PosToIdx(_guardGoalPos)]; | |
| float currentGoalVisibility = _guardVision[PosToIdx(_guardGoalPos)]; | |
| float currentPlayerVisibility = _guardVision[PosToIdx(_playerPos)]; | |
| // Repath when agent has reached goal | |
| repath |= _guardPos.Equals(_guardGoalPos); | |
| // Repath when agent can see goal pos but it has very low salience (visibility implies new salience values) | |
| repath |= currentGoalVisibility > 0.5f && currentGoalSalience < 0.1f; | |
| if (currentPlayerVisibility < 0.5f && currentGoalSalience < 0.1f * _guardGoalSalience) { | |
| GuardBark(_rng.NextInt(1, 3)); | |
| _flabbergastedCount += 12; | |
| _guardGoalSalience = currentGoalSalience; | |
| repath = true; | |
| } | |
| (int2 salientPos, float salience) = FindMostSalient(_guardPos, _occupancyB, Size); | |
| // Repath when highest salience is found elsewhere | |
| if (salience > _guardGoalSalience) { | |
| repath = true; | |
| } | |
| if (repath) { | |
| _guardGoalPos = salientPos; | |
| _guardGoalSalience = salience; | |
| if (_guardGoalPos.Equals(int2.zero)) { | |
| _guardGoalPos = _rng.NextInt2(Size); // Todo: respect geometry | |
| } | |
| SearchPath(_guardPos, _guardGoalPos, _geometry, Size, _guardPath); | |
| } | |
| if (_guardPath.Length == 0) { | |
| return; | |
| } | |
| if (_flabbergastedCount > 0) { | |
| return; | |
| } | |
| var next = _guardPath[^1]; | |
| if (!_guardPos.Equals(next)) { | |
| var step = next - _guardPos; | |
| step.x = math.clamp(step.x, -1, 1); | |
| step.y = math.clamp(step.y, -1, 1); | |
| _guardVel = step; | |
| } | |
| else { | |
| _guardPath.RemoveAt(_guardPath.Length-1); | |
| } | |
| } | |
| private void UpdateGuardBarks() { | |
| if (_guardBarkTime > 0) { | |
| _guardBarkTime -= SimDeltaTime; | |
| } | |
| else { | |
| _guardBark = 0; | |
| } | |
| } | |
| private void GuardBark(int bark) { | |
| _guardBark = bark; | |
| _guardBarkTime = 2f; | |
| } | |
| private static void Diffuse(NativeArray<float> buffA, NativeArray<float> buffB, NativeArray<byte> geometry, int2 size) { | |
| const float rate = 0.22f; | |
| // Q: how to efficiently handle borders again? | |
| for (int y = 1; y < size.y - 1; y++) { | |
| for (int x = 1; x < size.x - 1; x++) { | |
| float geomC = geometry[(y - 0) * size.x + (x + 0)] / 255f; | |
| float geomB = geometry[(y - 1) * size.x + (x + 0)] / 255f; | |
| float geomT = geometry[(y + 1) * size.x + (x + 0)] / 255f; | |
| float geomL = geometry[(y + 0) * size.x + (x - 1)] / 255f; | |
| float geomR = geometry[(y + 0) * size.x + (x + 1)] / 255f; | |
| float geomSum = geomB + geomT + geomL + geomR; | |
| buffB[y * size.x + x] = | |
| buffA[(y + 0) * size.x + (x + 0)] * geomC + | |
| rate * ( | |
| buffA[(y - 1) * size.x + (x + 0)] * geomB + | |
| buffA[(y + 1) * size.x + (x + 0)] * geomT + | |
| buffA[(y + 0) * size.x + (x - 1)] * geomL + | |
| buffA[(y + 0) * size.x + (x + 1)] * geomR - | |
| buffA[(y + 0) * size.x + (x + 0)] * geomSum | |
| ); | |
| } | |
| } | |
| } | |
| private static void Normalize(NativeArray<float> stuff, int2 size) { | |
| float max = 0.0001f; | |
| for (int y = 0; y < size.y; y++) { | |
| for (int x = 0; x < size.x; x++) { | |
| if (stuff[y * size.x + x] > max) { | |
| max = stuff[y * size.x + x]; | |
| } | |
| } | |
| } | |
| for (int y = 0; y < size.y; y++) { | |
| for (int x = 0; x < size.x; x++) { | |
| stuff[y * size.x + x] /= max; | |
| } | |
| } | |
| } | |
| private static void RenderGeometry(NativeArray<byte> geometry, NativeArray<float4> pixels, int2 size) { | |
| for (int y = 0; y < size.y; y++) { | |
| for (int x = 0; x < size.x; x++) { | |
| int idx = y * size.x + x; | |
| if (geometry[idx] == 0) { | |
| pixels[idx] = new float4( | |
| 0.7f, | |
| 0.6f, 0.2f, 1); | |
| } | |
| else { | |
| pixels[idx] = new float4( | |
| 0.1f, | |
| 0.1f, 0.1f, 1); | |
| } | |
| } | |
| } | |
| } | |
| private static void RenderVisibility(NativeArray<float> visibility, NativeArray<float4> pixels, int2 size) { | |
| for (int y = 0; y < size.y; y++) { | |
| for (int x = 0; x < size.x; x++) { | |
| int idx = y * size.x + x; | |
| // float light = visibility[idx] == 1f ? 1f : 0.05f; // discrete light | |
| float light = math.max(visibility[idx], 0.025f); // continuous light | |
| pixels[idx] = new float4(pixels[idx].xyz * light, 1f); | |
| } | |
| } | |
| } | |
| private static void RenderPoint(int2 pos, float4 col, NativeArray<float4> pixels, int2 size) { | |
| int idx = pos.y * size.x + pos.x; | |
| pixels[idx] = col; | |
| } | |
| private static void RenderOccupancy(NativeArray<float> stuff, float3 color, NativeArray<float4> pixels, int2 size) { | |
| for (int y = 0; y < size.y; y++) { | |
| for (int x = 0; x < size.x; x++) { | |
| int idx = PosToIdx(new int2(x, y)); | |
| pixels[idx] = math.lerp(pixels[idx], new float4(color,1), stuff[idx]); | |
| } | |
| } | |
| } | |
| private static void Collapse(in NativeArray<float> visibility, NativeArray<float> probability, in int2 size) { | |
| for (int i = 0; i < size.x * size.y; i++) { | |
| probability[i] *= 1f - visibility[i] * 0.75f; | |
| } | |
| } | |
| private static (int2, float) FindMostSalient(int2 pos, in NativeArray<float> probability, in int2 size) { | |
| float maxSalience = 0f; | |
| int maxIdx = 0; | |
| for (int i = 0; i < size.x * size.y; i++) { | |
| int2 p = new int2(i % size.x, i / size.x); | |
| float salience = probability[i] / (1f + math.log10(1f + math.lengthsq(p - pos))); | |
| if (salience > maxSalience) { | |
| maxSalience = salience; | |
| maxIdx = i; | |
| } | |
| } | |
| return (new int2(maxIdx % size.x, maxIdx / size.y), maxSalience); | |
| } | |
| private static void RefreshVisibility(in int2 pos, in int2 lookDir, in NativeArray<byte> geometry, NativeArray<float> visibility, in int2 size) { | |
| // Zeroing shouldn't be necessary, but is here for debug | |
| for (int i = 0; i < visibility.Length; i++) { | |
| visibility[i] = 0; | |
| } | |
| for (var octant = 0; octant < 8; octant++) { | |
| RefreshOctant(pos, lookDir, octant, geometry, visibility); | |
| } | |
| } | |
| private static void RefreshOctant(int2 viewPos, int2 lookDir, int octant, in NativeArray<byte> geometry, NativeArray<float> visibility) { | |
| var line = new List<float2>(); // todo: should be NativeList, but how to handle insert operation in ShadowLine_Add()? | |
| var fullShadow = false; | |
| for (var row = 1;; row++) { | |
| // Stop once we go out of bounds. | |
| var pos = viewPos + TransformOctant(row, 0, octant); | |
| if (!InBounds(pos, Size)) break; | |
| for (var col = 0; col <= row; col++) { | |
| pos = viewPos + TransformOctant(row, col, octant); | |
| // If we've traversed out of bounds, bail on this row. | |
| if (!InBounds(pos, Size)) break; | |
| if (fullShadow) { | |
| visibility[PosToIdx(pos)] = 0f; | |
| } else { | |
| var projection = ProjectTile(row, col); | |
| // Set the visibility of this tile. | |
| var visible = !ShadowLine_Contains(line, projection); | |
| float vis = 1f; // discrete | |
| // float vis = 1f / (float)math.max(1, math.pow(row, 0.75f)); // continuous | |
| visibility[PosToIdx(pos)] = visible ? vis : 0f; | |
| // Add any opaque tiles to the shadow map. | |
| bool blocked = geometry[PosToIdx(pos)] < 0.5f; | |
| bool outsideFieldOfView = math.dot((float2)(pos - viewPos), (float2)lookDir) < 0.5f; | |
| if (visible && (blocked || outsideFieldOfView)) { | |
| ShadowLine_Add(line, projection); | |
| fullShadow = ShadowLine_IsFullShadow(line); | |
| } | |
| } | |
| } | |
| } | |
| } | |
| private static int PosToIdx(in int2 pos) { | |
| return pos.y * Size.x + pos.x; | |
| } | |
| private static bool InBounds(int2 pos, int2 bounds) { | |
| return | |
| pos.x >= 0 && | |
| pos.x < bounds.x && | |
| pos.y >= 0 && | |
| pos.y < bounds.y; | |
| } | |
| private static int2 TransformOctant(int row, int col, int octant) => octant switch { | |
| 0 => new int2(col, -row), // BR | |
| 1 => new int2(row, -col), // RB | |
| 2 => new int2(row, col), // RT | |
| 3 => new int2(col, row), // TR | |
| 4 => new int2(-col, row), // TL | |
| 5 => new int2(-row, col), // LT | |
| 6 => new int2(-row, -col), // LB | |
| 7 => new int2(-col, -row), // BL | |
| _ => int2.zero | |
| }; | |
| private static float2 ProjectTile(int row, int col) { | |
| float topleft = col / (float)(row + 2); // todo: float? | |
| float botRight = (col + 1) / (float)(row + 1); // todo: float? | |
| return new float2(topleft, botRight); | |
| } | |
| private static bool Shadow_Contains(float2 shadow, float2 other) { | |
| return shadow.x <= other.x && shadow.y >= other.y; | |
| } | |
| private static bool ShadowLine_Contains(List<float2> shadows, float2 projection) { | |
| for (int i = 0; i < shadows.Count; i++) { | |
| if (Shadow_Contains(shadows[i], projection)) { | |
| return true; | |
| } | |
| } | |
| return false; | |
| } | |
| private static bool ShadowLine_IsFullShadow(List<float2> shadows) { | |
| return shadows.Count == 1 && | |
| shadows[0].x == 0 && | |
| shadows[0].y == 1; | |
| } | |
| private static void ShadowLine_Add(List<float2> shadows, float2 shadow) { | |
| // Figure out where to slot the new shadow in the list. | |
| var index = 0; | |
| for (; index < shadows.Count; index++) { | |
| // Stop when we hit the insertion point. | |
| if (shadows[index].x >= shadow.x) break; | |
| } | |
| // The new shadow is going here. See if it overlaps the | |
| // previous or next. | |
| float2? prev = null; | |
| if (index > 0 && shadows[index - 1].y > shadow.x) { | |
| prev = shadows[index - 1]; | |
| } | |
| float2? next = null; | |
| if (index < shadows.Count && | |
| shadows[index].x < shadow.y) { | |
| next = shadows[index]; | |
| } | |
| // Insert and unify with overlapping shadows. | |
| if (next.HasValue) { | |
| if (prev.HasValue) { | |
| // Overlaps both, so unify one and delete the other. | |
| shadows[index - 1] = new float2(prev.Value.x, next.Value.y); | |
| shadows.RemoveAt(index); | |
| } else { | |
| // Overlaps the next one, so unify it with that. | |
| shadows[index] = new float2(shadow.x, next.Value.y); | |
| } | |
| } else { | |
| if (prev.HasValue) { | |
| // Overlaps the previous one, so unify it with that. | |
| shadows[index - 1] = new float2(prev.Value.x, shadow.y); | |
| } else { | |
| shadows.Insert(index, shadow); | |
| } | |
| } | |
| } | |
| private static void SearchPath(in int2 a, in int2 b, in NativeArray<byte> geometry, in int2 size, | |
| NativeList<int2> path) { | |
| path.Clear(); | |
| var frontier = new NativeQueue<int2>(Allocator.Temp); | |
| var trace = new NativeParallelHashMap<int2, int2>(64, Allocator.Temp); | |
| using (frontier) | |
| using (trace) { | |
| frontier.Enqueue(a); | |
| while (frontier.Count > 0) { | |
| var current = frontier.Dequeue(); | |
| if (current.Equals(b)) { | |
| break; | |
| } | |
| using (var neighbours = Neighbours(current)) | |
| { | |
| foreach (var n in neighbours) { | |
| // add to trace if valid | |
| if (!trace.ContainsKey(n)) { | |
| if (geometry[PosToIdx(n)] != 0) { | |
| frontier.Enqueue(n); | |
| } | |
| trace.Add(n, current); | |
| } | |
| } | |
| } | |
| } | |
| if (!trace.ContainsKey(b)) { | |
| Debug.Log("Goal unreachable"); | |
| return; | |
| } | |
| var t = b; | |
| while (!t.Equals(a)) { | |
| path.Add(t); | |
| t = trace[t]; | |
| } | |
| // Note: we are returning path in reverse order [goal, ..., ..., start] | |
| } | |
| } | |
| private static NativeList<int2> Neighbours(int2 p) { | |
| var n = new NativeList<int2>(4, Allocator.Persistent); | |
| n.Add(p + new int2(-1, 0)); | |
| n.Add(p + new int2(+1, 0)); | |
| n.Add(p + new int2(0, -1)); | |
| n.Add(p + new int2(0, +1)); | |
| return n; | |
| } | |
| [BurstCompile] | |
| public struct PropagateJobParallel : IJobParallelFor { | |
| public ulong tick; | |
| [ReadOnly] public NativeArray<byte> space; | |
| [ReadOnly] public NativeArray<float> prev; | |
| [ReadOnly] public NativeArray<float> curr; | |
| public NativeArray<float> next; | |
| public void Execute(int i) { | |
| const float C = 0.1f; | |
| Rng rng = new Rng(1234 + (uint)i + (uint)tick); | |
| var coord = Coord(i); | |
| var x = coord.x; | |
| var y = coord.y; | |
| bool isBoundary = x < 1 || x >= Size.x - 1 || y < 1 || y >= Size.y - 1; | |
| isBoundary |= space[Idx(x, y)] == 0; | |
| if (isBoundary) { | |
| next[Idx(x, y)] = 0; | |
| return; | |
| } | |
| float spatial = | |
| curr[Idx(x - 1, y)] + | |
| curr[Idx(x + 1, y)] + | |
| curr[Idx(x, y + 1)] + | |
| curr[Idx(x, y - 1)] - | |
| 4 * curr[Idx(x, y)]; | |
| float temporal = 2f * curr[Idx(x, y)] - prev[Idx(x, y)]; | |
| float v = C * spatial + temporal; | |
| v += rng.NextFloat(-0.001f, 0.001f); | |
| v *= 0.99f + 0.009f * math.min(math.abs(v), 1f); | |
| next[Idx(x, y)] = v; | |
| } | |
| } | |
| [BurstCompile] | |
| public struct RenderJobParallel : IJobParallelFor { | |
| [ReadOnly] public NativeArray<float> buf; | |
| public NativeArray<float4> colors; | |
| public void Execute(int i) { | |
| float scaled = buf[i] * 1f; | |
| // var pos = math.max(0f, scaled); | |
| // var neg = math.max(0f, -scaled); | |
| // var cSound = new float4(neg, neg + pos, pos, 1f); | |
| // colors[i] = math.lerp(colors[i], cSound, math.abs(scaled)); | |
| float light = math.max(math.abs(buf[i]) * 2f, 0.025f); // continuous light | |
| colors[i] = new float4(colors[i].xyz * light, 1f); | |
| } | |
| } | |
| [MethodImpl(MethodImplOptions.AggressiveInlining)] | |
| private static int Idx(int x, int y) { | |
| return Mod(y,Size.y) * Size.x + Mod(x,Size.x); | |
| } | |
| [MethodImpl(MethodImplOptions.AggressiveInlining)] | |
| private static int2 Coord(int i) { | |
| return new int2(i % Size.x, i / Size.y); | |
| } | |
| // Naive-but-correct mod that handles negative numbers the way I want | |
| [MethodImpl(MethodImplOptions.AggressiveInlining)] | |
| private static int Mod(int x, int m) { | |
| int r = x % m; | |
| return r < 0 ? r + m : r; | |
| } | |
| private void OnPreRenderCallback(Camera cam) { | |
| if (_tex == null) { | |
| return; | |
| } | |
| // var drawRect = new Rect(0f, 0f, 240f * _zoomLevel.x, 10f * _zoomLevel.y); | |
| // var cameraLocalPos = _textureRenderer.transform.InverseTransformPoint(_camera.transform.position); | |
| // _textureRenderer.transform.localScale = new Vector3( | |
| // drawRect.width, drawRect.height, 1f | |
| // ); | |
| // _textureRenderer.transform.position = new Vector3( | |
| // drawRect.width / 2, drawRect.height / 2, 0f | |
| // ); | |
| // _camera.transform.position = _textureRenderer.transform.TransformPoint(cameraLocalPos); | |
| Draw.LineGeometry = LineGeometry.Flat2D; | |
| using (Draw.Command(cam)) { | |
| float3 drawPos = new float3(_guardPos.x - Size.x / 2, _guardPos.y - Size.y / 2, 0); | |
| drawPos += new float3(0, 1f, 0); | |
| Draw.Text(drawPos, Quaternion.identity, _guardBarks[_guardBark], TextAlign.Top,20); | |
| // Draw.Arc(drawPos, Quaternion.identity, 1f, 0,360); | |
| } | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment