Flankers (BETA) [Update 12/18/21]

Post your example zscripts/ACS scripts/etc here.
Forum rules
The Projects forums are only for projects. If you are asking questions about a project, either find that project's thread, or start a thread in the General section instead.

Got a cool project idea but nothing else? Put it in the project ideas thread instead!

Projects for any Doom-based engine (especially 3DGE) are perfectly acceptable here too.

Please read the full rules for more details.
Hey Doomer
Posts: 283
Joined: Sat Sep 25, 2021 3:38 am

Flankers (BETA) [Update 12/18/21]

Post by Hey Doomer »

Enhancing monster AI in Doom is interesting, trying to answer questions such as How can they find the player? and How can they alert others? These attempt to get monsters "unstuck" in complex map geometry (a mapper and not AI issue?). Anyhoo this got me thinking about attacking flanks of an opponent as an alternative to a head-on approach. The tactic is a random number of critters are "flankers," and while most of the meat charges head on flankers sneak around to either side of the player and whack him unawares. Basic stuff, right? This is my attempt at that.
Spoiler:
The idea is simple. In the WorldLoaded event ID_Point markers are placed at the center of each sector. I have kept this at the Sector property for now. Similar to other AI mods, these are monster waypoints. For the present all these are marked by a flat sprite and all monsters are flankers. The flank() function uses a ThinkerIterator to find all monsters within a certain distance of the player in a See state. Then another thinker loops through the ID_Points to find the first one farther away than the player and on either side of the player. If a point is found the monster attempts to move in that direction. For debugging purposes I apply a different RenderStyle when that happens.

I've just started looking at monster AI, and there are issues around map geometry, infighting, placement of monsters, etc. A player is seldom still, another significant variable. All the same, my early testing seems promising. VectorAngle() can select ID_Points in the direction of the player (a positive or negative angle), and I think AngleTo can find player flanks (I assume this depends on the player's current facing angle). I'm still playing with this, but the idea is to implement a general attack strategy that isn't head on.

Work in progress...

Update 12/14/21
Spoiler:
Last edited by Hey Doomer on Sat Dec 18, 2021 12:01 pm, edited 4 times in total.
User avatar
Enjay
 
 
Posts: 26540
Joined: Tue Jul 15, 2003 4:58 pm
Location: Scotland

Re: Flankers (Experimental)

Post by Enjay »

This sounds very interesting. As someone who likes realistic-ish military style mods/scenarios, having some of the enemies behave with a bit more intelligence (apparent intelligence anyway) would be good. Some way of including/excluding particular enemy types from the AI improvement would be nice. Like, soldiers might use sensible tactics to try and out flank the player but guard dogs are more likely to just run the target. Both types of enemies could easily appear in a single mod.
Hey Doomer
Posts: 283
Joined: Sat Sep 25, 2021 3:38 am

Re: Flankers (ALPHA) [Update 12/14/21]

Post by Hey Doomer »

I've got this to a point where monster behavior is more or less what I'd like to see, although it's far from perfect. Here's the logic:
  • A thinker searches for monsters that are flankers in a See state within a certain radius of the player
  • If a monster has already targeted a sector ID_Point, it keeps going unless it's flanking the player
  • If a monster isn't targeting a sector ID_Point, it looks for one between itself and the player, preferring those that flank the player
Here's the meat:

Code: Select all

	void flank(float distance)
	{
		ThinkerIterator MonsterFinder;
		ThinkerIterator ID_PointFinder;
		MonsterFinder = ThinkerIterator.Create("Actor");
		ID_PointFinder = ThinkerIterator.Create("ID_Point");

		Actor mo;
		int x, y, z;
		vector2 v2;
		vector3 v3;
		ID_Point idpoint;

		MonsterFinder.Reinit();
		while (mo = Actor(MonsterFinder.Next()))
		{
			if (mo.bIsMonster && mo.args[MOFLAG] == 1)
			{
				if (!mo.InStateSequence(mo.CurState, mo.ResolveState("See")))
				{
					mo.args[FLSTATUS] = 0;
					mo.A_SetRenderStyle(1.0, STYLE_Normal);
				}
				else
				{
					if (mo.args[FLSTATUS] > 0) // still on target
					{
						mo.A_FaceMovementDirection(0, 0, 0, FMDF_INTERPOLATE);

						if (mo.CheckIfTargetInLOS())
						{
							double fAngle = mo.AbsAngle(mo.AngleTo(player.mo),mo.angle);
							if (fAngle < 45 || (fAngle > 135 && fAngle < 225) || fAngle > 315)
							{
								mo.args[FLSTATUS] = 0;
							}
						}
						else
						{
							mo.args[FLSTATUS]--;
						}
						if (mo.args[FLSTATUS] == 0)
						{
							mo.bNOTARGETSWITCH = false;
							mo.target = null;
							mo.A_Face(player.mo);
						}
					}
					else // find new target
					{
						float pldist = mo.Distance2D(player.mo);
						if (pldist < distance)
						{
							float nearest = distance;

							ID_Point ptr = null;
							ID_PointFinder.Reinit();
							while (idpoint = ID_Point(ID_PointFinder.Next()))
							{
								// z height that is reachable
								if ((idpoint.floorz > mo.floorz && (idpoint.floorz - mo.floorz) >= mo.maxstepheight)
								|| (idpoint.floorz < mo.floorz && (mo.floorz - idpoint.floorz) <= mo.maxdropoffheight)
								|| (idpoint.floorz == mo.floorz) && mo.isVisible(idpoint, true))
								{
									// always move closer to player
									float iddist = player.mo.Distance2D(idpoint);
									float modist = mo.Distance2D(idpoint);

									if (modist < nearest && iddist < pldist)
									{
										ptr = idpoint;
										nearest = pldist;

										double fAngle = player.mo.AbsAngle(player.mo.AngleTo(idpoint),player.mo.angle);
										if (fAngle < 45 || (fAngle > 135 && fAngle < 225) || fAngle > 315)
										{
											break; // always flank
										}
									}
								}
							}
							if (ptr) // a sector point was found to move toward
							{
								mo.target = ptr;
								mo.bNOTARGETSWITCH = true;
								mo.args[FLSTATUS] = 5; // stay on this target x5
								mo.A_SetRenderStyle(1.0, STYLE_Shadow);

								int steps = 5;
								while (steps--)
								{
									x = mo.pos.x + (mo.pos.x == ptr.pos.x ? 0 : mo.pos.x > ptr.pos.x ? Random(-1,-2) : Random(1,2));
									y = mo.pos.y + (mo.pos.y == ptr.pos.y ? 0 : mo.pos.y > ptr.pos.y ? Random(-1,-2) : Random(1,2));

									v2 = (x, y);
									if (ptr.floorz <= mo.floorz)
									{
										v3 = (x, y, mo.floorz); // drop off
									}
									else
									{
										v3 = (x, y, ptr.GetZAt(x, y, 0, GZF_ABSOLUTEPOS)); // climb
									}

									if (mo.CheckPosition(v2))
									{
										mo.A_StartSound("breath", CHAN_AUTO, CHANF_OVERLAP, 0.5, ATTN_STATIC);
										mo.SetOrigin(v3, true);
										mo.A_FaceMovementDirection(0, 0, 0, FMDF_INTERPOLATE);
									}
								}
							}
						}
					}
				}
			}
		}
	}
For testing purposes a few things are happening. The distance is defined at 2048. Flanking is 90 degrees on either side of the player. Flankers spawn at a rate of 50% and include all monsters. Once a flanker in a See state targets an ID_Point I change its render style to shadow. Interrupting the Chase movement in the See state means the monster can still attack, but it jitters the sprite. I chose this for now instead of defining a new state, since I'd prefer this be universal. Generally this seems to work, although I've seen monsters stuck in walls here and there.

What I have seen that is good is monsters that will flank to one side while I'm attacked head on. If I'm hiding behind a beam and peek out to see a flanker, he will immediately run off to one side or the other.

I also added a breathy sound as an audio cue for the flankers. Perhaps, that helps.
Last edited by Hey Doomer on Tue Dec 14, 2021 12:40 pm, edited 1 time in total.
User avatar
Enjay
 
 
Posts: 26540
Joined: Tue Jul 15, 2003 4:58 pm
Location: Scotland

Re: Flankers (Experimental) [Update 12/14/21]

Post by Enjay »

It took me a while to figure out what was going on until I re-read the description. It seems to be doing much as you described (including the health warnings about getting stuck etc).

I noticed a couple of warnings on startup:

Code: Select all

Script warning, "flankers.pk3:flankers_events.zs" line 142:
Truncation of floating point value
Script warning, "flankers.pk3:flankers_events.zs" line 143:
Truncation of floating point value
Hey Doomer
Posts: 283
Joined: Sat Sep 25, 2021 3:38 am

Re: Flankers (Experimental) [Update 12/14/21]

Post by Hey Doomer »

Enjay wrote:It took me a while to figure out what was going on until I re-read the description. It seems to be doing much as you described (including the health warnings about getting stuck etc).

I noticed a couple of warnings on startup:

Code: Select all

Script warning, "flankers.pk3:flankers_events.zs" line 142:
Truncation of floating point value
Script warning, "flankers.pk3:flankers_events.zs" line 143:
Truncation of floating point value
Thanks - I appreciate the feedback
User avatar
openroadracer
Posts: 496
Joined: Mon Sep 23, 2019 1:03 pm
Preferred Pronouns: He/Him
Operating System Version (Optional): Windows 7 Professional 64-bit SP1
Graphics Processor: ATI/AMD with Vulkan/Metal Support
Location: Doomworld Forums

Re: Flankers (ALPHA) [Update 12/14/21]

Post by openroadracer »

I just have to ask: How does this mod interact with this other AI Enhancer mod, if at all?

I wonder how having enemies that can intelligently chase and hunt the player would interact with having enemies that intelligently flank the player would work...
Hey Doomer
Posts: 283
Joined: Sat Sep 25, 2021 3:38 am

Re: Flankers (ALPHA) [Update 12/14/21]

Post by Hey Doomer »

openroadracer wrote:I just have to ask: How does this mod interact with this other AI Enhancer mod, if at all?

I wonder how having enemies that can intelligently chase and hunt the player would interact with having enemies that intelligently flank the player would work...
I think these will be compatible, because I'm taking a different approach and modifying different behavior. Testing will tell. 8-)
User avatar
openroadracer
Posts: 496
Joined: Mon Sep 23, 2019 1:03 pm
Preferred Pronouns: He/Him
Operating System Version (Optional): Windows 7 Professional 64-bit SP1
Graphics Processor: ATI/AMD with Vulkan/Metal Support
Location: Doomworld Forums

Re: Flankers (ALPHA) [Update 12/14/21]

Post by openroadracer »

Hey Doomer wrote:
openroadracer wrote:I just have to ask: How does this mod interact with this other AI Enhancer mod, if at all?

I wonder how having enemies that can intelligently chase and hunt the player would interact with having enemies that intelligently flank the player would work...
I think these will be compatible, because I'm taking a different approach and modifying different behavior. Testing will tell. 8-)
Guess I'll just have to download this and let you know how it goes, then. Any suggestions on relative load order?
Hey Doomer
Posts: 283
Joined: Sat Sep 25, 2021 3:38 am

Re: Flankers (ALPHA) [Update 12/14/21]

Post by Hey Doomer »

openroadracer wrote:
Hey Doomer wrote:
openroadracer wrote:I just have to ask: How does this mod interact with this other AI Enhancer mod, if at all?

I wonder how having enemies that can intelligently chase and hunt the player would interact with having enemies that intelligently flank the player would work...
I think these will be compatible, because I'm taking a different approach and modifying different behavior. Testing will tell. 8-)
Guess I'll just have to download this and let you know how it goes, then. Any suggestions on relative load order?
I don't think that matters.

At this stage I'd recommend waiting, but of course you are free to try it. I have refactored code to eliminate all movement jittering and make smarter decisions. The new movement code is pretty seamless, and I've realized it's not so much flanking as also getting behind the player. I'm also making little changes that should greatly improve this.

Example:

Code: Select all

					if (mo.CurState == mo.ResolveState("See"))
					{
						ID_Point ptr = findtarget(mo, player.mo, distance);
						if (ptr)
						{
							flankon(mo, ptr);
							seekflank(mo, ptr);
						}
					}
Rather than look if the monster is anywhere the in "See" state, I capture the start of the state as I understand the CurState return. It has taken some experimentation to understand some of this, and I think the end results will be interesting. Should have an update by tomorrow! :D

It should be compatible with any of the other AI mods.
Hey Doomer
Posts: 283
Joined: Sat Sep 25, 2021 3:38 am

Re: Flankers (BETA) [Update 12/18/21]

Post by Hey Doomer »

This is a major update of the movement and detection code after playing around quite a lot.

Screenshots:
Spoiler:
Summary of changes:
  • Decided to keep CenterSpot after testing various centroid calculations, most of which aren't any better with an irregular polygon
  • Made the movement code less random and now factored around getting closer to the target
  • Cleaned up target point selection, although I'm still having trouble up and down stairs. Not sure Distance3D is accurate.
  • Completely rewrote flank test to use CheckLOF by casting rays to see if the monster can be seen between angles
  • Added menu settings
  • Added a debug mode that shows the sector points as well as a flanking camera view
The first screenshot shows a monster in "flanking mode" flanking to the player's right. The second shows the automatic view of a monster that has flanked and is facing the player.

This works and should be compatible with any of the other AI mods, I think, although I haven't tested that.

One thing I have noticed (just a reality of what a "flank" is) is that a flank range narrows dramatically as a monster approaches. A monster can appear to be almost in front of the player with a 45 degree flank range and technically be flanking. I've tried a wider range of 65 degrees, which is now the default option, and that seems to work OK. This funnel view can be confusing until you see it from the monster's camera, I think.

The movement should also be somewhat smoother than earlier builds. I accomplished this by greatly simplifying the code that turns on the flanking. The only time a new target is sought is when the "See" state begins:

Code: Select all

					else if (mo.CurState == mo.ResolveState("See"))
					{
						ID_Point ptr = findtarget(mo, player.mo);
						if (ptr)
						{
							flankon(mo, ptr);
						}
					}


Update 12-19-21: revamping detection again with monster cam. Seems to be working. Close to release...
Hey Doomer
Posts: 283
Joined: Sat Sep 25, 2021 3:38 am

Re: Flankers (BETA) [Update 12/19/21]

Post by Hey Doomer »

After spending more time on this, I have made the following changes:
  • Since a monster is "targeting" an ID_Point, using A_Chase(). This bypasses movement code although still having trouble with some stairs.
  • Swapped out ray tracing flank detection with an angle comparison (much cleaner):
    Spoiler:
  • Simplified the flanking action.
    Spoiler:
  • Choose flanking ID_Points over anything else.
  • Added menu options for different center finding techniques (there are now three).
The refactored action is about as simple as I can make it. Flankon is called only once - when an ID_Point is acquired, and Flankoff only twice - when a monster is flanked to the player OR out of range. Much simpler I think.

Monsters now rapidly flank and immediately attack once flanked. If this is tried with E1M1 on the easiest level facing a single Zombieman in debug mode, you can turn this way or that and see how you are flanked by the monster's camera (assuming you are fast enough). The previous movement code caused monsters to get stuck in walls here and there even with a CheckMove(); I assume A_Chase() avoids that.

Irregular polygon center algorithm choices in Options menu could make a difference depending on map design:
  • CenterSpot - a GZDoom sector property (blockmap center)
  • Centroid - a simple average of all vectors
  • CenterOfMass - average of center of all triangles defined by the sector
These all yield slightly different layouts. For now I've done this to ensure target consistency, assuming a player is usually moving or focused on whatever is in view. While this seems to work I'm not convinced it's optimal. It varies depending on the map, but I hope in a large battle it will be effective.

This all seems to work, and monsters flank with considerable speed. It seems (I'm still testing) that once a flankers spots the player it runs around the player faster than a Pinky Demon after chugging a gallon of Red Bull. Turning "Flanker Chance" option to 1:1 (all monsters) makes this confusing and somewhat impossible to beat. I have, however, tested the detection method enough to believe that's working and this is ready for another beta release.

Oh, and a few builds ago (in case I did not mention it) the "flankers" are chosen from non-floating, missile-state monsters. This excludes Cacodemons, for example.

This is getting close to where I'd like to see it and certainly makes flankers hard to catch and kill. Also makes it easier for them to kill you.

Again I don't see this conflicting with any other AI mod.
User avatar
inkoalawetrust
Posts: 75
Joined: Mon Aug 26, 2019 9:18 pm
Graphics Processor: nVidia with Vulkan support

Re: Flankers (BETA) [Update 12/18/21]

Post by inkoalawetrust »

Any chance that you could make this and the Ambushers script also work when two NPCs are fighting each other (e.g a friendly NPC fighting hostile NPCs). Instead of only working with NPCs fighting the player(s) specifically ? Would also be useful if these two scripts could be toggled on a per-monster basis instead of universally applying on all NPCs.

I've also noticed a weird bug with the Ambushers script specifically, when I started it up on E1M1 a few times. It seems to cause some monster to move around while only running the first frame of their See state. While also making their sight sound every time they move.
Hey Doomer
Posts: 283
Joined: Sat Sep 25, 2021 3:38 am

Re: Flankers (BETA) [Update 12/18/21]

Post by Hey Doomer »

inkoalawetrust wrote:Any chance that you could make this and the Ambushers script also work when two NPCs are fighting each other (e.g a friendly NPC fighting hostile NPCs). Instead of only working with NPCs fighting the player(s) specifically ? Would also be useful if these two scripts could be toggled on a per-monster basis instead of universally applying on all NPCs.

I've also noticed a weird bug with the Ambushers script specifically, when I started it up on E1M1 a few times. It seems to cause some monster to move around while only running the first frame of their See state. While also making their sight sound every time they move.
These work by targeting the player, but they could target any hostile I suppose.

I haven't noticed this Ambushers bug - is anything else running when that happens?

Return to “Script Library”