Skip to content

Instantly share code, notes, and snippets.

@mzandvliet
Created October 28, 2025 16:02
Show Gist options
  • Select an option

  • Save mzandvliet/38dfdfe416d0ee37e842c3deab879f0e to your computer and use it in GitHub Desktop.

Select an option

Save mzandvliet/38dfdfe416d0ee37e842c3deab879f0e to your computer and use it in GitHub Desktop.
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