The main goal of today was to get the Attack state working, along with the other states associated with it (namely, Fleeing and Satiated). I got to work adding in the Glimmerstalker "charging" the Player when it entered the Attack state. There will need to be some tweaks made to the speed and distance at which it charges from but overall, it was working pretty good.
One glaring problem though, is that the Glimmerstalker is way too dark/hard to see with the default texture that came with the model. We plan to change the texture to be white to match the lore but we haven't gotten to that yet. I think that will really help make the Glimmerstalker pop, especially if we add a slight glow to it. It is supposed to "glimmer" after all!
In the image below, see if you can find the Glimmerstalker. I'm sure the image compression and viewing it at a smaller resolution doesn't help but it isn't much better "in person".

In case you didn't find it, here it is. See how it blends in way too well with the surroundings? We want it to be scary but without the texture change, it is just straight up not fair. You do get some help because it is animated, so you can catch the movement but still.

We already have it in the plan to retexture the Glimmertalker, so for now I will just soldier on, trying not to get eaten by the almost invisible killer.
Once I got it to collide with the Player or the Crew, I stubbed out what it was supposed to do. For now, it would just send out a debug log to tell me that a character got hit. I didn't want the game to end or the Crew to die quite yet because I wanted to test more functionality, like what happens when the Glimmerstalker interacts with a light.
The original plan for the light/Glimmerstalker interaction was that the Glimmerstalker would skirt around the edges of the Avoid zone of the light and would "flee" if it entered the Flee zone. The intent was to either give the Player a short reprieve while they were in the light or to give them a longer reprieve if they timed casting the Light spell just right to catch the Glimmerstalker in the Flee zone. This would give the Player the option of going the easier route of not having to time it perfectly, at the cost of the Glimmerstalker resuming their attack quicker or going with the high risk, high reward that would make the Glimmerstalker go away for longer.

The problem I had with the initial plan, is that I had already tried to implement the "skirting" behavior previously and I did not like how it looked. I think it could be good but I think we would need to take a lot more control over the movement, rather than just telling the NavMeshAgent to go to the target. This was more work than I am willing to do for the game jam, since we are already like halfway through it and we have a ton of cleanup and polishing to do.
This led to start rethinking what should happen when the Glimmerstalker encountered a light. I had some ideas and talked it through with Terra to help narrow down the options. The Avoid zone was definitely the problem, conceptually, whereas the Flee was much simpler and defined (i.e. enter Flee zone, it enters the Fleeing state and goes away).
My initial thought was to just remove the Avoid zone altogether and just have the Flee. Since we didn't have the Glimmerstalker have a corporeal body when not attacking anymore, the utility of the Avoid zone was greatly diminished. However, Terra liked the concept of the different zones and thought it gave more variety. So now it was just down to figuring out what the Avoid zone did.
An idea Terra had was to just have the Glimmerstalker stop when it hit the Avoid zone and stare at the Crew/Player until they left the safety of the light again (up to a certain duration, at least). Kind of going for the spooky, "You may be safe now, but I'm always here" atmosphere. However, that came with a couple of problems, like:
- What happens if the Glimmerstalker is caught in the middle of the Avoid zone (i.e. the Player cast Light and caught it as opposed to the Glimmerstalker just ran into the light)?
- We thought about having it flap backwards out of the light then stand but that brought us to the next question
- What happens if the light is moving, like on the sled? Does it try to "keep up" with it?
- It would look silly if the Glimmerstalker would reposition itself every couple of seconds to stare from outside the light.
Ultimately, we thought that the solution would work if all of the lights were stationary (especially if you added logic to make it stalk around the periphery of the light). However, because we have moving lights, it probably wasn't our best option.
The final solution we settled on was to basically mimic the Flee concept but smaller. If it hit the Avoid zone, an animation/particle/sound/etc would play of the Glimmerstalker moving away and it would disappear. Then it would wait until the character was no longer protected by the light then move right back into the Attack state. There would still be the slight delay that already exists granting the characters a couple seconds of safety after leaving the light. The Flee zone would be similar but with bigger animations/particles/etc and the Glimmerstalker would return to the Stalking state instead, which would restart the Attack delay timer, giving the character even more time to react.
This seemed like our best option for now. A good balance between visuals, code complexity, and time to implement. It could always be reworked later if it ended up not being fun. When I got into the nitty-gritty of implementing it though, I ran into some issues.
While testing the Avoid and Flee functionality, I was seeing some inconsistencies with which characters I expected to be protected by lights vs who actually was. When starting the game, all of the Crew should have been protected by lights, since they all start right next to the Caravan. Looking at the Debug panel though, some Crew would be protected, while others weren't. This appeared to be random and I couldn't see any rhyme or reason for their values.
Digging in further, I stumbled onto a potential lead related to the interactions between the colliders we have on the lights and the Crew/Player. To summarize how the lights currently work, we have two colliders, one each for the Avoid and Flee zones, that detect when a character enters/exist. This would set an _isProtectedByLight field true or false if they were entering or exiting, respectively. Now, the problem comes when we leave a collider (which sets the field to false) but the character is still within another Light zone. Because we are using colliders, it doesn't "hit" the other collider, so the field is never set to true again until they leave completely and come back in. Essentially, we were using multiple flags to denote the state but we were only really looking at whatever the last flag change was.

It was at this point that I realized that we would have to rework how the light zones were managed and how we determined if something was protected by light and to what degree (e.g. No protection, Avoid, or Flee). The original implementation was built exactly how we had talked about, so props to Andrew for building it. We just didn't know about all of the problems we would face going that route. Now we know more about colliders and how (not) to use them!
I wasn't planning on doing that work now, since I just wanted to get the Stalking behavior finished and out to the group. There are a lot of good changes related to it that would help the team. That was the plan, until I got to really testing the new Glimmerstalker Avoid and Flee behaviors.
I got the new Avoid functionality implemented and it was working exactly as we had hoped. Now that that was looking good, I wanted to test the Flee behavior. I would wait until the Glimmerstalker would get really close, then cast the Light spell. I expected the Glimmerstalker to enter the Flee state but it always would enter the new AvoidantStalking state. It appeared that the Glimmerstalker was always colliding with the Avoid zone first, even if it would have also hit the Flee zone. Typing this out, it also could have just been the last state we called MoveToState with, like if the Flee was hit first but the Avoid was also hit on the same frame, then AvoidantStalking would have been the last state. Either way, I tried changing the order of the colliders in the prefab but that didn't work. The Flee zone worked perfectly when I disabled the Avoid zone, so I know everything was set up correctly on that front.
At this point, I was fed up with the colliders and their quirks. They were doing their thing like they are supposed to but I think we were just using the right tool for the wrong job. I decided to rework how the lights worked now, because I couldn't actually test the full Stalking functionality without it.
I wanted to revisit one of the original ideas I had for how the lights could work. The idea was that there is a Light Manager that each light would register itself with and other scripts could query to see if a given point was within a light.
I started with getting the Light Manager class made up. Well, actually, I ended up making it more generic to be a RepellingZoneManager. When Andrew built it, he had the good idea to make the actual mechanism behind the light to be a RepelZone instead of a light, specifically. Then he put the RepelZone on a light in the prefab. This makes a lot of sense and allows us to add other safe areas that aren't related to the lights.
It was pretty streamlined with just those three public methods. For checking the "Repel Level" for the point, I am just delegating that work to the individual RepelZone to tell the manager it's relation to the point. The manager simply iterates through all of the zones and returns the "highest" level of protection that it found. If any of the RepelZones returns back the Flee value, it immediately kicks out because that is the highest protection level. It actually did something different initially but writing this description out made me realize a flaw in the original logic. Gotta love it!
public class RepellingZoneManager : MonoBehaviour
{
public static RepellingZoneManager Instance { get; private set; }
private readonly List<RepelZone> _zones = new List<RepelZone>();
private void Awake()
{
if (Instance == null)
{
Instance = this;
}
else
{
Destroy(gameObject);
}
}
public void RegisterZone(RepelZone zone)
{
if (_zones.Contains(zone)) return;
_zones.Add(zone);
}
public void UnregisterZone(RepelZone zone)
{
_zones.Remove(zone);
}
public RepelLevel GetRepelLevelForPoint(Vector3 point)
{
var highestLevelFound = RepelLevel.None;
foreach (var zone in _zones)
{
var zoneLevel = zone.GetRepelLevelForPoint(point);
if (zoneLevel == RepelLevel.Avoid)
{
highestLevelFound = zoneLevel;
}
else if (zoneLevel == RepelLevel.Flee)
{
highestLevelFound = zoneLevel;
break;
}
}
return highestLevelFound;
}
}
The individual RepelZones have configurable Avoid and Flee radii and does a simple distance check to see if the distance to the point is within one of those radii. There is some validation that I excluded here that validates the Avoid Radius is always larger than the Flee Radius, that way I could ensure that the order I'm checking them in is always correct.
public class RepelZone : MonoBehaviour
{
[Header("Config")]
[SerializeField]
private float _avoidRadius = 2f;
[SerializeField]
private float _fleeRadius = 1f;
private void OnEnable()
{
RepellingZoneManager.Instance.RegisterZone(this);
}
private void OnDisable()
{
RepellingZoneManager.Instance.UnregisterZone(this);
}
public RepelLevel GetRepelLevelForPoint(Vector3 point)
{
var distance = Vector3.Distance(transform.position, point);
if (distance <= _fleeRadius)
{
return RepelLevel.Flee;
}
else if (distance <= _avoidRadius)
{
return RepelLevel.Avoid;
}
return RepelLevel.None;
}
}
While testing the new Repelling Zone Manager, I noticed a deficiency between the old way and this new way. I couldn't tell where the zones ended in relation to the light. In the old system you could tell where the zones ended because they were colliders that showed up in the editor. To remedy this, I fell back on my trusty debug arcs. However, now that I needed to draw a circle in multiple places, I decided to encapsulate that functionality in the same CoreUtils class that draws the arcs.
public static void DrawDebugCircle(Vector3 origin, float radius, int segmentCount, Color color)
{
var halfSegmentCount = segmentCount / 2;
DrawDebugArc(origin, radius, Vector3.forward, 0, 180, halfSegmentCount, color);
DrawDebugArc(origin, radius, -Vector3.forward, 0, 180, halfSegmentCount, color);
}
This way, I don't have to remember how to do it every time and it is in a nice, easy package. Called like this:
private void OnDrawGizmos()
{
if (!_showDebugLines) return;
CoreUtils.DrawDebugCircle(transform.position, _avoidRadius, 20, _avoidRadiusColor);
CoreUtils.DrawDebugCircle(transform.position, _fleeRadius, 20, _fleeRadiusColor);
}
With the end result looking like this, with the lighter yellow being the Avoid Radius and the darker being the Flee Radius.

After reworking the lights to use the new and improved RepelZones I went around and tried buttoning up the rest of the functionality. This mainly entailed making the Crew die when hit or the game end when the Player is hit, as well as some various testing.
As you can see, the Crew now dies when hit by the Glimmerstalker. Once again, this is missing any effects to make it smoother.

The last thing I can think of to add is to make the Glimmerstalker go back to Harassing state if their Stalking target stays within the light for a set amount of time.