• 🏆 Texturing Contest #33 is OPEN! Contestants must re-texture a SD unit model found in-game (Warcraft 3 Classic), recreating the unit into a peaceful NPC version. 🔗Click here to enter!
  • It's time for the first HD Modeling Contest of 2024. Join the theme discussion for Hive's HD Modeling Contest #6! Click here to post your idea!

[vJASS] Wander System 2.04 revamp

This bundle is marked as useful / simple. Simplicity is bliss, low effort and/or may contain minor bugs.
Wander System
Will keep it short and simple, made this system for my RPG project, this system makes a map feel much more alive as you can make units move around a certain point.
All details can be found in the code comments.

Current code

Demo GUI triggers

Old and outdated code


JASS:
native UnitAlive takes unit u returns boolean
//! zinc
	library Wander requires TimerUtils, Table
	{	
	
		/*
		***************************************************************************************************************************
			Info:
				The system is supposed to be a coded version of the 'Wander' ability.
				Benefits being that you can modify it slightly, pause duration and
				the area the units walk in. Also the unit will NOT become neutral
				and will actually attack nearby enemies.

			 API:
				static method create(unit u, real x, real y) -> thistype
				static method getByUnit(unit target) -> thistype
				method destroy()

			Config:
				real wanderCooldown
					Duration between each time the unit is ordered to move.
					
				real cooldownModifier
					To make the cooldown a bit random, this is used to make the cooldown equal to
						cooldown +- modifier.
					
				real maxWanderArea = 550;
					Max range from start point.
					
				real minWanderArea = 100;
					Min range from start point.
					
				real loopSpeed = 0.03;
					How often the system updates, higher value equals better performence but
						reduces accuracy.
						
			Event Ids
				0 = nothing
				1 = Added to system
				2 = New order
				3 = Removed from system

			Import:
				-Copy this trigger + the TimerUtils one into your map.
				-Requires JNGP to use, can be downloaded from Hive's tool section or from wc3c.net (offical)
				-Unless you copied the demo triggers into your map, you need to create the following GUI variables:
					real wanderEvent
					real whereToX
					real wheretoY
					unit wanderer

			Credit
				Chaosy @ www.hiveworkshop.com

		***************************************************************************************************************************
		*/
		public struct WanderStruct
		{
			//Wander Events
				// 1. Added to system
				// 2. New order
			    // 3. removed from system
				
			//Configurable variables bellow
			//these are default values and can be changed to be unique for each unit if needed.
			real wanderCooldown   = 5;
			real cooldownModifier = 2;
			real maxWanderArea    = 550;
			real minWanderArea    = 100;
			real loopSpeed        = 0.03125;
			
			//end of config
			private boolean onCooldown = false;
			private real    counter = 0;
			private real    x, y, finalCooldown;
			private unit    u;
			private timer   t;
			private static Table table;
			
			private method onDestroy()
			{
				if(this.t != null)
				{
					ReleaseTimer(this.t);
				}
				if(this.u != null)
				{
					table.remove(GetHandleId(u));
					udg_wanderer    = this.u;
					udg_wanderEvent = 3;
					udg_wanderEvent = 0;
				}
				t = null;
				u = null;
			}
			
			private static method Update()
			{
				
				thistype this = GetTimerData(GetExpiredTimer());
				real angle, dist, dx, dy;
				if(!UnitAlive(u))
				{
					this.destroy();
				}
				if(GetUnitCurrentOrder(this.u) == null && !this.onCooldown)
				{
					angle = GetRandomReal(-bj_PI, bj_PI);
					dist  = GetRandomReal(minWanderArea, maxWanderArea);
					dx    = this.x + dist * Cos(angle);
					dy    = this.y + dist * Sin(angle);
					IssuePointOrder(u, "attack", dx, dy);
					this.finalCooldown = GetRandomReal(this.wanderCooldown - this.cooldownModifier,
						this.wanderCooldown + this.cooldownModifier);
					this.onCooldown = true;
					udg_wanderer    = this.u;
					udg_whereToX    = dx;
					udg_whereToY    = dy;
					udg_wanderEvent = 2;
					udg_wanderEvent = 0;
				}
				else if(this.onCooldown)
				{
					this.counter += this.loopSpeed;
					if(this.counter >= this.finalCooldown)
					{
						this.onCooldown = false;
						this.counter    = 0;
					}
				}
			}
			
			static method getByUnit(unit target) -> thistype
			{
				return table[GetHandleId(target)];
			}
			
			static method create(unit u, real x, real y) -> thistype
			{
				thistype this = thistype.allocate();
				integer h = GetHandleId(u);
				if(!table.has(h))
				{
					table[h] = this;
	
					this.t = NewTimerEx(this);
					TimerStart(this.t, this.loopSpeed, true, function thistype.Update);
					this.x = x;
					this.y = y;
					this.u = u;
					
					udg_wanderer    = this.u;
					udg_wanderEvent = 1;
					udg_wanderEvent = 0;
				}
				else
				{
					BJDebugMsg(GetUnitName(u) + " is already wandering!");
					this.destroy();
				}
				return this;
			}
			
			static method onInit()
			{
				table = TableArray[0x2000];
			}
		}
	}
//! endzinc


  • Apply Wander
    • Events
      • Player - Player 1 (Red) skips a cinematic sequence
    • Conditions
    • Actions
      • Set u = Footman 0000 <gen>
      • Set loc = (Position of Farm 0001 <gen>)
      • Custom script: set udg_myWanderer = WanderStruct.create(udg_u, GetLocationX(udg_loc), GetLocationY(udg_loc))
      • Custom script: call RemoveLocation(udg_loc)
  • Remove Wander
    • Events
      • Player - Player 1 (Red) types a chat message containing remove as An exact match
    • Conditions
    • Actions
      • Custom script: call WanderStruct(udg_myWanderer).destroy()

  • Created Message
    • Events
      • Game - wanderEvent becomes Equal to 1.00
    • Conditions
    • Actions
      • Game - Display to (All players) the text: ((Name of wanderer) + is added to the wander system!)

  • Order Message
    • Events
      • Game - wanderEvent becomes Equal to 2.00
    • Conditions
    • Actions
      • Game - Display to (All players) the text: ((Name of wanderer) + ( is wandering to: [ + ((String(whereToX)) + (, + ((String(whereToY)) + ])))))
  • Remove Message
    • Events
      • Game - wanderEvent becomes Equal to 3.00
    • Conditions
    • Actions
      • Game - Display to (All players) the text: ((Name of wanderer) + is removed from the wander system!)

JASS:
/*
***************************************************************************************************************************
    Info:
    	The system is supposed to be a coded version of the 'Wander' ability.
    	Benefits being that you can modify it slightly, pause duration and
    	the area the units walk in. Also the unit will NOT become neutral
    	and will actually attack nearby enemies.

     API:
		function ApplyWander takes unit u, real x, real y returns nothing
		function RemoveWander takes unit u returns nothing

	Config:
		wanderCooldown - The time between each time the unit walks.
		cooldownModifer - To avoid all units from having the exact same interval, the cd will be randomized.
			Example: wanderCooldown = 5, cooldownModifer = 2 each unit will wait somewhere between 3 and 7 seconds.
		wanderArea - How far from the specified location a unit can wander. Low value to stay close.
		loopSpeed - How often the system will check if the unit can move.
			Higher value equals better performance but less accuracy.
			
	Event Ids
		0 = nothing
		1 = Unit is issued an new order

	Import:
		-Copy this Trigger into your map.
		-Requires JNGP to use, can be downloaded from Hive's tool section or from wc3c.net (offical)
		-Unless you copied the demo triggers into your map, you need to create the following GUI variables:
			real wanderEvent
			unit wanderer

	Credit
		Chaosy @ www.hiveworkshop.com

***************************************************************************************************************************
*/
library Wander initializer MyInit

	globals
		private timer t = CreateTimer()
		private hashtable hash = InitHashtable()
		private group g = CreateGroup()
		private group g2 = CreateGroup()
		//Configurable variables bellow
		private real wanderCooldown = 5
		private real cooldownModifier = 2
		private real wanderArea = 550
		private real loopSpeed = 0.03
	endglobals
	
	private function CooldownGroupActions takes nothing returns nothing
		local unit u = GetEnumUnit()
		local integer h = GetHandleId(u)
		local real counter = LoadReal(hash, h, 3) + loopSpeed
		if counter >= LoadReal(hash, h, 4) then
			call GroupRemoveUnit(g2, u)
			call GroupAddUnit(g, u)
		else
			call SaveReal(hash, h, 3, counter)
		endif
		set u = null
	endfunction
	
	private function GroupActions takes nothing returns nothing
		local unit u = GetEnumUnit()
		local real x
		local real x2
		local real y
		local real y2
		local real angle
		local real dist
		local integer h
		
		if GetUnitCurrentOrder(u) == null then
			set h = GetHandleId(u)
			set x = LoadReal(hash, h, 1)
			set y = LoadReal(hash, h, 2)
			set angle = GetRandomReal(1,360)
			set dist = GetRandomReal(100, wanderArea)
			set x2 = x + dist * Cos(angle * bj_DEGTORAD)
			set y2 = y + dist * Sin(angle * bj_DEGTORAD)
			call IssuePointOrder(u, "attack", x2, y2)
			set udg_wanderer = u
			set udg_wanderEvent = 1
			set udg_wanderEvent = 0
			call SaveReal(hash, h, 3, 0)
			call SaveReal(hash, h, 4, GetRandomReal(wanderCooldown - cooldownModifier, wanderCooldown + cooldownModifier))
			call GroupRemoveUnit(g, u)
			call GroupAddUnit(g2, u)
		endif
		set u = null
	endfunction
	
	private function LoopFunc takes nothing returns nothing
		call ForGroup(g, function GroupActions)
		call ForGroup(g2, function CooldownGroupActions)
	endfunction
	
	function ApplyWander takes unit u, real x, real y returns nothing
		local integer h = GetHandleId(u)
		if IsUnitInGroup(u, g) == false and IsUnitInGroup(u, g2) == false then
			call SaveReal(hash, h, 1, x)
			call SaveReal(hash, h, 2, y)
			call GroupAddUnit(g, u)
		else
			debug call BJDebugMsg("Trying to apply 'Wander' to a unit that is already added to the system.")
		endif
	endfunction
	
	function RemoveWander takes unit u returns nothing
		local boolean b = false
		local boolean b2 = false
		
		if IsUnitInGroup(u,g) == true then
			call GroupRemoveUnit(g, u)
			call FlushChildHashtable(hash, GetHandleId(u))
			set b = true
		endif
		if IsUnitInGroup(u,g) == true then
			call GroupRemoveUnit(g2, u)
			call FlushChildHashtable(hash, GetHandleId(u))
			set b2 = true
		endif
		if b == false and b2 == false then
			debug call BJDebugMsg("Trying to remove 'Wander' from a unit who currently isn't in the system.")
		endif
	endfunction
	
	private function MyInit takes nothing returns nothing
		call TimerStart(t, loopSpeed, true, function LoopFunc)
	endfunction
endlibrary


26BkNdbFXXXXLCRDq.gif
l41lO4poYabepRKqk.gif

Credits
UserReason

Killcide
Preview image + GIFs

Chaosy
Creator

Changelog:
VersionChanges

1.0
-Uploaded

1.x
Changes suggest in the thread comments

2.0
Completely revamped code

2.01
Instance is destroyed if unit is found dead during the loop.

2.02
Nulled variables, private-ized stuff.

2.03
System wont null already nulled variables, a unit that
is wandering wont be able to be added to the system again.

2.03
Added getByUnit method

Keywords:
wander, guard, patrol, vjass, system, npc, unit, Chaosy, useful, RPG
Contents

Just another Warcraft III map (Map)

Reviews
23.03.2016; BPower: Update when you're new code is written. If you still struggle with the double free issue ( comment make a post in trigger&scripts or upload it directly here. I'll check it for you. I like the system. 13th Mar 2015...

Moderator

M

Moderator

23.03.2016; BPower:
Update when you're new code is written.
If you still struggle with the double free issue ( comment make a post in
trigger&scripts or upload it directly here. I'll check it for you.

I like the system.

13th Mar 2015
BPower:You could null the timer t and the unit u onDestroy for best cleanup results.
In general you want to avoid using onDestroy, instead declare your own destroy method.

(this.onCooldown == true) --> this.onCooldown
^A bit sloppy that you missed that one.

The demo map does not match with the code posted on the first page.

I think the wander events are an overkill.
Can you name a practical utility for each event you are providing?

You should check if a unit is already wandering to prevent a
multiple instance allocation case for the same unit handle.

Simple question. Can a unit order attack without having the ability to attack units?
I don't know the answer, but you should test it out.
Most times you want to neutral units to wander around the map.
You could try this one
JASS:
				if not IssuePointOrder(u, "attack", dx, dy) 
                    IssuePointOrder(u, "move", dx, dy)
                }
I had to rethink from vJass to Zinc is not C, because of the syntax and slightly different
encapsulation rules. Correct me if I make a mistake.
By default a library / struct is completly private.
By declaring the struct as public struct everything inside is public, hence can be accessed by struct(this).var
Therefore you should privatize timer t, boolean onCooldown, unit u and real x, y, finalCooldown.
Furthermore privatize static method Update, better static method update to keep consistency of your function naming.


I feel that you could build in a getter by unit handle.
Either O(n) by looping over the entire stack of allocated instances
or more performant O(1) via unit handle id / unit user data.
Wander.getByUnit(myUnit) -> thistype or static method operator [] unit u -> thistype
I deliberately used the struct name Wander instead of WandererStruct,
because I think a name analogous to the default ability name is well-fitting.

22th Sep 2015
IcemanBo:
Check reply in thread.
 
Level 19
Joined
Mar 18, 2012
Messages
1,716
The system timer shouldn't be running, when no unit is wandering.

You should check for double adding, removing units from the system.

You could run the code in one group.

Add a small description to the globals, which should be configurable.

JPAG ( see link in my signature )
 

Chaosy

Tutorial Reviewer
Level 40
Joined
Jun 9, 2011
Messages
13,183
@Ice
1,2: will do (done 2)
3: I wanted to do that to begin with. But I thought it would be more structured to divide it into two groups somehow..
4: will add. (done)
5. I have read that, more than once. Seems like I have forgotten a thing or two since you bring it up.

@Gis
Wont lag unless there is a extreme number of units, which will lag up any system pretty much.
 
Last edited:
Level 19
Joined
Jul 14, 2011
Messages
875
I just tested your demo and nothing happened. The footman just stayed there without moving an inch.

You could do set angle = GetRandomReal(0, bj_Pi * 2) so you get radians directly and not have to convert it later but thats a minor thing.

You should also make some of the globals constant.
 
Read other's post.

  • Basic API should be like:
    - AddUnit/RemoveUnit
    - Set x/y of "basis location".
    - Set max radius of "basis location".
    - Set time-interval for newOrder.
  • Advanced API could be:
    - Allow user to catch event when unit reaches destination point.
    - Allow user to catch event when unit leaves the home radius.
    - Set min/max time-bound of newOrder interval.
    These are suggestions, but basic API should do it for now.
  • Make proper names. For example real rngCD. rng is a real and CD is a real, the name doesn't make sense.
    Also you could write out some names. Sometimes it's better to use 2 letters more to gain a lot of readability.

Clearly define the configuration part and seperate it from the system code.
 
Last edited:
Level 22
Joined
Feb 6, 2014
Messages
2,466
- Make configurables constant with proper naming (i.e. MY_CONSTANT) and separate the configurables to the system variables as IcemanBo said.
- You can still use Table without forcing downloaders using static ifs
- You can make it angle = GetRandomInt(0, 6.283185) so that you don't have to multiply. EDIT: mentioned already
- How about instead of using another group for cooldown, use a Timer for each unit instead? That way, you don't have to Load from the Hash periodocally. TimerUtils would be a great optional addition if you ever plan to do that.
 
- Demo map is not up to date.
- Only run loop when there is a wander unit. You always can increase/decrease a counter.
- Flux is right, make config reals just constant.
- wanderCooldown actually only applies after reaching the destination point, right? It should be mentioned.
- What is with the events? I don't see code for it.
- Get rid of "== false" and "== true".
- Couldn't it be shortened to only one group? I think yes.
 

Chaosy

Tutorial Reviewer
Level 40
Joined
Jun 9, 2011
Messages
13,183
I am remaking this from scratch and I am pretty much finished. I just got one minor bug I need to address.

edit: updated.

In the end I am satisfied with the result, more features with less code.
Not to mention it does not take up a hashtable slot anymore.

I decided to make all config variable non-constant because it allows the user to modify them for each unit individually while having a default value.
 
Last edited:
Level 33
Joined
Apr 24, 2012
Messages
5,113
To maintain the usage of radians, just get a random value between -bj_PI and bj_PI.

and also if you want to check if a unit is alive:
native UnitAlive takes unit u returns boolean
and then use that, because it is more accurate.

also loopSpeed should be renamed to Timeout(because that sounds cooler and more formal) then set it's value to 0.031250000 (or if you want it more significant, 1/32)
 
Level 33
Joined
Apr 24, 2012
Messages
5,113
UnitAlive requires a lib though, right?

no. Just implement it in any part inside the library.
The problem is how will you implement it in zinc(i think it is something like native UnitAlive(unit u)->boolean. maybe you should try that)
btw, forgot this:
JASS:
if(GetUnitCurrentOrder(this.u) == null && this.onCooldown == false)
should be:
JASS:
if(GetUnitCurrentOrder(this.u) == null && !this.onCooldown)
I forgot how zinc works so.. I think it should be an exclamation rather than "not"
 

Chaosy

Tutorial Reviewer
Level 40
Joined
Jun 9, 2011
Messages
13,183
Eh.. so I just
JASS:
			static method UnitAlive(unit id) -> boolean
			{
				return true;
			}

or
implement native UnitAlive(unit u)->boolean?

edit: tried native UnitAlive(unit u)->boolean as well.
All give syntax error. Unexpected "(" for this one.

edit2:
It works if I put constant native UnitAlive takes unit u returns boolean at the very top of the trigger, outside the //! zinc.
That might clash with other systems though..
 
I'm not sure the order does need to be "attack", since wander system also is often used for
non-attack units that only travel around in a city/forest, which don't even have an attack command card.

Why no deallocate in method destroy?

The non-config members should be private, as the whole struct seem to be public by default.

method destroy should be part of the API.

I'm thinking about a check if a unit is registered in the wander system, or not.
What do you think?

Sorry, can't test the system in game at the moment. :s
 

Chaosy

Tutorial Reviewer
Level 40
Joined
Jun 9, 2011
Messages
13,183
No I did not check that yet.

At the moment I am working on using a single timer instead of one timer per instance.
I ran into issues though so I will have to continue when I get home.

edit: Bah, this is driving me insane.
instances[i].destroy(); makes JNGP say that I attempt to destroy a nulled struct or w/e.

edit: okay here is the spooky part.
838bd34cb7bad533602bbdce8bf581de.png

JASS:
			static method Destroy(integer i)
			{
				thistype this = thistype(i);
				//this writes out properly. Namely i = 1 and u = Footman yet instances[i].destroy()
				//is an invalid instance apparently
				BJDebugMsg(I2S(i));
				BJDebugMsg("x " + GetUnitName(thistype(i).u) + " x");
				if(u != null)
				{
					table.remove(GetHandleId(u));
					udg_wanderer    = u;
					udg_wanderEvent = 3;
					udg_wanderEvent = 0;
					u = null;
				}
				instances[i].destroy();
                instances[i] = instances[max];
				max -= 1;
			}
 
Last edited:
Level 19
Joined
Mar 18, 2012
Messages
1,716
Table creator API is Table.create(). You however still use the TableArray creator.

BJDebugMsg(GetUnitName(u) + " is already wandering!"); requires
a debug keyword.

onDestroy is a relic of older times, when a custom destroy method was not possible.
Please change onDestroy to destroy.

Instead of instances[i].destroy(); just do this.destroy
 
Top