logo
Main

Forums

Downloads

Unreal-Netiquette

Donate for Oldunreal:
Donate

borderline

Links to our wiki:
Wiki

Walkthrough

Links

Tutorials

Unreal Reference

Usermaps

borderline

Contact us:
Submit News
Page Index Toggle Pages: 1 Send TopicPrint
Hot Topic (More than 10 Replies) Issue #65. DynamicZoneInfo does not update Region of actors which initially reside in its volume (Read 691 times)
Masterkent
Developer Team
Offline



Posts: 1133
Location: Russia
Joined: Apr 5th, 2013
Gender: Male
Issue #65. DynamicZoneInfo does not update Region of actors which initially reside in its volume
Jun 21st, 2018 at 2:40pm
Print Post  
When an actor initially resides inside the volume of a dynamic zone (determined by a DynamicZoneInfo actor), the actor's Region refers to the enclosing static zone instead of the dynamic zone. I think that initialization of every DynamicZoneInfo actor should modify Region of all actors in the corresponding volume so that their Region would refer to the actual zone.

I added new conformance test: DynamicZoneInfo (see the dev board).
  
Back to top
 
IP Logged
 
Masterkent
Developer Team
Offline



Posts: 1133
Location: Russia
Joined: Apr 5th, 2013
Gender: Male
Re: Issue #65. DynamicZoneInfo does not update Region of actors which initially reside in its volume
Reply #1 - Jul 22nd, 2018 at 7:37pm
Print Post  
What is the purpose of bQuickFilter? Why not just make Region referring to the correct region without setting zone's bQuickFilter to false? Do you think that modders would consider using this flag? Speaking for myself, I wouldn't even figure out that dynamic zones need special handling if Krull0r didn't tell that my initial code doesn't work correctly with them:
https://www.oldunreal.com/cgi-bin/yabb2/YaBB.pl?num=1528704437/9#9

If it's a kind of "optimization", then I'd like to know the estimated performance boost with it. Is it about saving 1 microsecond once in a few minutes or proper updates lead to a really perceivable overhead?
  
Back to top
 
IP Logged
 
.:..:
Board Moderator
Developer Team
*****
Offline



Posts: 1446
Location: Finland
Joined: Aug 16th, 2005
Gender: Male
Re: Issue #65. DynamicZoneInfo does not update Region of actors which initially reside in its volume
Reply #2 - Jul 23rd, 2018 at 6:52pm
Print Post  
QuickFilter option makes it use Collision Octree to find actors initially inside the dynamic zone. Otherwise it iterates through all actors in level and manually check if location is inside the zone.
While in small levels the performance gain may seam non-existing, but it definitively makes a difference with larger maps (especially with a moving dynamic zone).

Problem with QuickFilter is that it will skip actors with bCollideActors false.
  

Shivaxi wrote on Jul 25th, 2013 at 12:50pm:
...and now im stuck trying to fix everything you broke for the next 227 release xD Tongue

(ಠ_ಠ)
Back to top
ICQYIM  
IP Logged
 
Masterkent
Developer Team
Offline



Posts: 1133
Location: Russia
Joined: Apr 5th, 2013
Gender: Male
Re: Issue #65. DynamicZoneInfo does not update Region of actors which initially reside in its volume
Reply #3 - Jul 24th, 2018 at 9:02pm
Print Post  
.:..: wrote on Jul 23rd, 2018 at 6:52pm:
Otherwise it iterates through all actors in level and manually check if location is inside the zone.

Traversal through all actors is indeed a relatively slow operation, but after some testing I have an impression that it's not actually the major bottleneck and you should have started optimizing from other end. According to my tests, when bQuickFilter of all dynamic zones is set to false, the time consumed by their initialization is proportional to square of the number of dynamic zones. When we get 10x more dynamic zones, their initialization takes 100x more time. This is why, for example, initialization of 2000 spherical dynamic zones on map SkaarjTowerF.unr took ridiculously high time on my machine: 381 seconds. Besides, replacing the native checks for belonging to a spherical zone with the scripted function

Code
Select All
simulated event bool FilterZone( vector Position, Actor ZoneActor )
{
	return VSize(Position - Location) > SphereSize;
} 


didn't introduce a notable overhead as could be expected knowing how slow UScript function calls are when compared to native evaluations, that gives me another reason to suspect that your code for the conforming mode uses some really slow algorithm aside from the traversal through the whole actor list.

Meanwhile, I figured out that a simple traversal through all ~5200 actors on SkaarjTowerF takes about 60 microseconds on my laptop. Checking if a certain location resides inside the given sphere should be very fast, since it's just

Code
Select All
square(X - SphrereCentre.X) + square(Y - SphrereCentre.Y) + square(Z - SphrereCentre.Z) <= square(SphereRadius) 


so I'd expect that a properly optimized code might even outperform your current solution with bQuickFilter == true, despite iterating through all actors. In particular, initialization of those 2000 dynamic zones with enabled quick filter took above 0.4 seconds, while according to my estimation it should take about 0.2 seconds or less (well, it's a rough estimation, so I can be wrong here).
  
Back to top
 
IP Logged
 
Masterkent
Developer Team
Offline



Posts: 1133
Location: Russia
Joined: Apr 5th, 2013
Gender: Male
Re: Issue #65. DynamicZoneInfo does not update Region of actors which initially reside in its volume
Reply #4 - Aug 22nd, 2018 at 12:21pm
Print Post  
Is there a possibility to get the relevant source code for DynamicZoneInfo? Maybe I could think about how to tweak it.
  
Back to top
 
IP Logged
 
.:..:
Board Moderator
Developer Team
*****
Offline



Posts: 1446
Location: Finland
Joined: Aug 16th, 2005
Gender: Male
Re: Issue #65. DynamicZoneInfo does not update Region of actors which initially reside in its volume
Reply #5 - Aug 22nd, 2018 at 3:38pm
Print Post  
Sure since I wrote it, it shouldn't be a problem:
https://oldunreal.com/cgi-bin/nopaste/?152
  

Shivaxi wrote on Jul 25th, 2013 at 12:50pm:
...and now im stuck trying to fix everything you broke for the next 227 release xD Tongue

(ಠ_ಠ)
Back to top
ICQYIM  
IP Logged
 
.:..:
Board Moderator
Developer Team
*****
Offline



Posts: 1446
Location: Finland
Joined: Aug 16th, 2005
Gender: Male
Re: Issue #65. DynamicZoneInfo does not update Region of actors which initially reside in its volume
Reply #6 - Aug 22nd, 2018 at 3:41pm
Print Post  
Thinking back into the collision hash, I used there PointCheck, which does collision overlap check with the actors inside. Would probably be better to use collision hash radius actors to check for actors inside.
  

Shivaxi wrote on Jul 25th, 2013 at 12:50pm:
...and now im stuck trying to fix everything you broke for the next 227 release xD Tongue

(ಠ_ಠ)
Back to top
ICQYIM  
IP Logged
 
Masterkent
Developer Team
Offline



Posts: 1133
Location: Russia
Joined: Apr 5th, 2013
Gender: Male
Re: Issue #65. DynamicZoneInfo does not update Region of actors which initially reside in its volume
Reply #7 - Aug 28th, 2018 at 9:06pm
Print Post  
Thanks, this code clarifies many things.

Regarding ADynamicZoneInfo::UpdateActorZones, this part

Code (C++)
Select All
    if (!bQuickFilter || !XLevel->Hash)
    {
        for (int i = 0; i < XLevel->Actors.Num(); i++)
        {
            AActor* A = XLevel->Actors(i);
            if (!A || A == this || !A->Region.Zone)
                continue;
            XLevel->SetActorZone(A);
        }
    } 


implies a huge overhead from two points of view:

1) every actor is forced to update its zone-related properties regardless of whether the actor entered or left the current dynamic zone.

In particular, we can consider 4 possible cases for a plain non-pawn actor A and a dynamic zone DZ:

1. A.Region.Zone does not point to DZ and A.Location is not inside DZ,

2. A.Region.Zone does not point to DZ and A.Location is inside DZ,

3. A.Region.Zone points to DZ and A.Location is not inside DZ,

4. A.Region.Zone points to DZ and A.Location is inside DZ.

If we expect that nothing but the current dynamic zone could make updating A.Region necessary, then it's possible to invoke XLevel->SetActorZone(A) only under conditions #2 or #3:

[method #1]
Code (C++)
Select All
        AActor **A = &XLevel->Actors(0);
        AActor **End = A + XLevel->Actors.Num();
        for (; A != End; ++A)
            if (*A && (*A)->ShouldUpdateZoneInRelationTo(*this))
                XLevel->SetActorZone(*A);
 


Conditions #1 and #4 imply that A.Region.Zone already has a correct value and we don't need to waste time trying to recalculate it from scratch. In most cases, it would be much faster to check these conditions than evaluate all this stuff

Code (C++)
Select All
    // Handle dynamic zones.
    ALevelInfo* Level = Zone->Level;
    if (!GIsEditor && Level && Level->DynamicZonesList)
    {
        for (ADynamicZoneInfo* DZ = Level->DynamicZonesList; DZ; DZ = DZ->NextDynamicZone)
        {
            if (!DZ->bDeleteMe && DZ->ShouldFilterZone(Location, ZoneActor, Result.Zone))
            {
                Result.Zone = DZ;
                break;
            }
        }
    } 


in UModel::PointRegion.

2) dynamic zones quickly discard the updates of actors performed by previously handled dynamic zones. All this hard work

Code (C++)
Select All
        for (int i = 0; i < XLevel->Actors.Num(); i++)
        {
            AActor* A = XLevel->Actors(i);
            if (!A || A == this || !A->Region.Zone)
                continue;
            XLevel->SetActorZone(A);
        } 


becomes irrelevant after we execute this code again for the next dynamic zone during the same game tick. This also explains why I observed the quadratic time complexity on the number of dynamic zones when I tested dynamic zones with disabled bQuickFilter: we have an internal engine's loop which calls Tick for each such a dynamic zone; on each iteration, the nested loop iterates through all actors; and for every actor, UModel::PointRegion executes another nested loop which may iterate through all dynamic zones in the worst case. So, this is O(U × A × D), where U is the number of updated dynamic zones, A is the number of actors, and D is the total number of dynamic zones.

Hence, the alternative (and most likely less optimal) approach to reducing the number of evaluations could be performing this loop

[method #2]
Code (C++)
Select All
        for (int i = 0; i < XLevel->Actors.Num(); i++)
        {
            AActor* A = XLevel->Actors(i);
            if (!A || A == this || !A->Region.Zone)
                continue;
            XLevel->SetActorZone(A);
        } 


only once per the whole engine tick when any dynamic zone determined that actors need to be updated. This would reduce time complexity to O(A × D). However, I presume that a reasonable usage of dynamic zones would imply that the average percent of actors which really need to be altered should be very low, except the case when dynamic zones use MatchOnlyZone (which I consider below). Then method #1 (calling XLevel->SetActorZone(A) under conditions #2 and #3 as described above) could let us achieve a better time complexity - O(U × A).

Of course, method #1 is still not ideal, because it may lead to redundant evaluations of

Code (C++)
Select All
*A && (*A)->ShouldUpdateZoneInRelationTo(*this) 


possibly followed by

Code (C++)
Select All
XLevel->SetActorZone(*A) 


when a previous dynamic zone already determined that A needs an update. Again, this redundancy can be eliminated by moving such checks into a loop that would handle all actors and dynamic zones once per engine tick:

[method #3]
Code (C++)
Select All
    // Making the list of all dynamic zones that should be checked
    ADynamicZoneInfo *FirstUpdatedZone = nullptr;

    for (ADynamicZoneInfo *Zone = Level.DynamicZonesList; Zone; Zone = Zone->NextDynamicZone)
        if (Zone->bUpdateTouchers || Zone->bMovesForceTouchUpdate && Zone->Location != Zone->OldPos)
        {
            Zone->bUpdateTouchers = false;
            Zone->OldPos = Zone->Location;
            Zone->NextUpdatedDynamicZone = FirstUpdatedZone;
            FirstUpdatedZone = Zone;
        }

    if (!FirstUpdatedZone)
        return;

    int ActorsNum = XLevel->Actors.Num();
    for (int i = 0; i < ActorsNum; ++i)
        if (AActor *A = XLevel->Actors(i))
            for (ADynamicZoneInfo *Zone = FirstUpdatedZone; Zone; Zone = Zone->NextUpdatedDynamicZone)
                if ((*A)->ShouldUpdateZoneInRelationTo(*Zone))
                {
                    XLevel->SetActorZone(*A);
                    break; // We're done with A, no need to check other dynamic zones
                } 


The benefits of this optimization may be hard to notice if XLevel->SetActorZone(*A) is not called for the vast majority of actors. If moving dynamic zones overlap, method #1 may become ineffective and we return back to the O(U × A × D) time complexity. Method #3 reduces max time complexity to O(A × D) for the worst case.

Now it's time to consider what we can do in case if an evaluation of this loop

Code (C++)
Select All
    for (int i = 0; i < ActorsNum; ++i)
        if (AActor *A = XLevel->Actors(i))
            for (ADynamicZoneInfo *Zone = FirstUpdatedZone; Zone; Zone = Zone->NextUpdatedDynamicZone)
                if ((*A)->ShouldUpdateZoneInRelationTo(*Zone))
                {
                    XLevel->SetActorZone(*A);
                    break; // We're done with A, no need to check other dynamic zones
                } 


would take too much time. I don't like the idea to use random filters like bCollideActors in order to reduce the number of handled actors. This is just a cheating that may lead to weird results. In particular, actors of type UnrealShare.Bubble are normally non-colliding, but nevertheless they rely on ZoneChange and should self-destruct outside of water zones:

Code
Select All
simulated function ZoneChange( ZoneInfo NewZone )
{
	if ( !NewZone.bWaterZone )
	{
		Destroy();
		PlaySound (EffectSound1);
	}
} 


If we have something like a moving air-filled cabin inside a water zone, then non-colliding actors should be handled.

In general, we don't know whether it's important to update a particular actor or not, but I think that updating actors ASAP is usually not required. For example, if a bubble disappears in 0.1 seconds instead of 0.017 seconds (6 ticks instead of 1 tick with 60 FPS), this is much better than if it never detected that it should disappear.

So, we could try to implement something like a throttling mechanism that would break an execution of the outer loop when a certain time limit is exceeded and then continue to handle the remaining actors on the next tick:

[method #3.1]
Code (C++)
Select All
    unsigned long const TimeLimit = 8; // Max allowed time in milliseconds before throttling

    // Making the list of all dynamic zones that should be checked
    ADynamicZoneInfo *FirstUpdatedZone = nullptr;

    for (ADynamicZoneInfo *Zone = Level.DynamicZonesList; Zone; Zone = Zone->NextDynamicZone)
    {
        if (!bUpdateOnlyRemainingActors)
            Zone->bUpdateRemainingActors = false;

        if (Zone->bUpdateRemainingActors ||
            Zone->bUpdateTouchers ||
            Zone->bMovesForceTouchUpdate && Zone->Location != Zone->OldPos)
        {
            if (!bUpdateOnlyRemainingActors)
            {
                Zone->bUpdateTouchers = false;
                Zone->OldPos = Zone->Location;
            }

            Zone->NextUpdatedDynamicZone = FirstUpdatedZone;
            FirstUpdatedZone = Zone;
        }
    }

    if (!FirstUpdatedZone)
        return;

    auto BeginTimestamp = timeGetTime();
    int ActorsNum = XLevel->Actors.Num();

    for (int i = 0; i < ActorsNum; ++i)
        if (AActor *A = XLevel->Actors(i))
        {
            if (bUpdateOnlyRemainingActors)
                if (!A->bPendingZoneUpdate)
                    continue;
                else
                    A->bPendingZoneUpdate = false;

            for (ADynamicZoneInfo *Zone = FirstUpdatedZone; Zone; Zone = Zone->NextUpdatedDynamicZone)
                if (A->ShouldUpdateZoneInRelationTo(*Zone))
                {
                    XLevel->SetActorZone(A);
                    break; // We're done with A, no need to check other dynamic zones
                }

            if (!(i & 15) && timeGetTime() - BeginTimestamp > TimeLimit)
            {
                // Marking the remaining actors as unhandled.
                // Note: when bUpdateOnlyRemainingActors is true, this part shouldn't be skipped,
                // because otherwise newly spawned actors won't have a chance to be handled until
                // the end of the next traversal through all actors.
                for (++i; i < ActorsNum; ++i)
                    if ((A = XLevel->Actors(i)))
                        A->bPendingZoneUpdate = true;

                if (!bUpdateOnlyRemainingActors)
                    for (ADynamicZoneInfo *Zone = FirstUpdatedZone; Zone; Zone = Zone->NextUpdatedDynamicZone)
                        Zone->bUpdateRemainingActors = true;

                bUpdateOnlyRemainingActors = true;
                return;
            }
        }

    // If we get here, then all actors have been handled,
    // so the next time we should continue from the beginning
    bUpdateOnlyRemainingActors = false; 


In order to reduce the maximal latency of handling actors in relation to dynamic zones whose (bUpdateTouchers || Zone->bMovesForceTouchUpdate && Zone->Location != Zone->OldPos) become true after throttling has been started, I had to allow considering such dynamic zones and newly spawned actors before finishing the current traversal through all actors. This solution implies that for a particular dynamic zone actors can be handled starting from the middle of the array of actors, and after reaching the end of it the process will be started again from the begin.

If the index of an actor in XLevel->Actors always remains the same for every tick, then the second half of this code can be simplified to

[method #3.2]
Code (C++)
Select All
    ....
    for (int i = FirstUpdActorIndex; i < ActorsNum; ++i)
        if (AActor *A = XLevel->Actors(i))
        {
            for (ADynamicZoneInfo *Zone = FirstUpdatedZone; Zone; Zone = Zone->NextUpdatedDynamicZone)
                if (A->ShouldUpdateZoneInRelationTo(*Zone))
                {
                    XLevel->SetActorZone(A);
                    break; // We're done with A, no need to check other dynamic zones
                }

            if (!(i & 15) && timeGetTime() - BeginTimestamp > TimeLimit)
            {
                if (!FirstUpdActorIndex)
                    for (ADynamicZoneInfo *Zone = FirstUpdatedZone; Zone; Zone = Zone->NextUpdatedDynamicZone)
                        Zone->bUpdateRemainingActors = true;

                FirstUpdActorIndex = i + 1;
                return;
            }
        }

    // If we get here, then all actors have been handled,
    // so the next time we should continue from the beginning
    FirstUpdActorIndex = 0; 


Such an implementation should provide a decent quality of handling for any reasonable number of dynamic zones.

I'll describe the detailed proposal with method #3.1 later.
« Last Edit: Aug 29th, 2018 at 3:36pm by Masterkent »  
Back to top
 
IP Logged
 
Masterkent
Developer Team
Offline



Posts: 1133
Location: Russia
Joined: Apr 5th, 2013
Gender: Male
Re: Issue #65. DynamicZoneInfo does not update Region of actors which initially reside in its volume
Reply #8 - Aug 29th, 2018 at 4:57pm
Print Post  
Suggested changes:

1. In DynamicZoneInfo.uc:

Code
Select All
	Class DynamicZoneInfo extends ZoneInfo
		native;

	var DynamicZoneInfo NextDynamicZone;

	var() enum EDynZoneInfoType
	{
		DZONE_Cube, // Use cube shape (using BoxMin and BoxMax)
-		DZONE_Sphere, // Sphere shape (using SphereSize)
+		DZONE_Sphere, // Sphere shape (using SphereSize as radius)
-		DZONE_Cylinder, // Cylinder shape (using SphereSize as width, CylinderSize as height)
+		DZONE_Cylinder, // Cylinder shape (using SphereSize as radius, CylinderSize as half-height)
	} ZoneAreaType;
	var() vector BoxMin,BoxMax; // Bounding box of the zone when DZONE_Cube
	var() float CylinderSize,SphereSize; // Cylinder/sphere bounds of the zone when DZONE_Sphere or DZONE_Cylinder.
-	var() ZoneInfo MatchOnlyZone; // Only return this result when that actor is within this zone.
+	var() ZoneInfo MatchOnlyZone; // Associate a location with this zone only if the location would match the zone pointed to by MatchOnlyZone
+								  // when considering only non-dynamic zones. If MatchOnlyZone is none, then this filter is not applied.
	var() bool bUseRelativeToRotation; // Result checks should be done relative to rotation.
-	var() bool bMovesForceTouchUpdate; // Force to update touchers when this actor moves.
+	var() bool bMovesForceTouchUpdate; // Schedule updating the regions of all actors that enter or leave this zone when its Location is changed.
+									   // (See also bUpdateTouchers).
-	var() bool bQuickFilter; // Use collision hash to check for initial actors inside volume instead (for performance gain).
-	var bool bScriptEvent; // Call script event before actually applying this zone.
+	var bool bUseScriptedMatching; // Call event function MatchesThisZone to determine if a location should belong to this zone.
-	var transient bool bUpdateTouchers; // Force to update zone touchers next tick (auto-resets to false after its done).
+	var transient bool bUpdateTouchers; // Set this flag to true in order to schedule updates of such properties as
+										// Region/FootRegion/HeadRegion of the set of affected actors for the nearest game ticks.
+										// The native implementation automatically resets this flag to false. The set of affected
+										// actors consists of all actors that entered or left this zone since the last updates of
+										// their regions and before the native implementation reset this flag to false.
+										// If you need such a scheduling whenever Location of this zone is changed, use
+										// bMovesForceTouchUpdate.
+
+	var private bool bUpdateRemainingActors;
-	var transient vector OldPose;
+	var private vector OldPos;
+	var transient private pointer NextUpdatedDynamicZone;

+	// Sets bUpdateTouchers to false and then updates such properties as Region/FootRegion/HeadRegion
+	// of all actors that entered or left this zone since the last updates of their regions
+	native function UpdateActorZones();
+
	simulated function PostBeginPlay()
	{
		if( Role==ROLE_None )
			Role = ROLE_Authority; // Give client authority.

		// Add to the list
		NextDynamicZone = Level.DynamicZonesList;
		Level.DynamicZonesList = Self;
+		UpdateActorZones();
		Super.PostBeginPlay();
-		bUpdateTouchers = true;
	}

	simulated function Destroyed()
	{
		local DynamicZoneInfo D;

		// Remove from the list.
		if ( Level.DynamicZonesList==Self )
		{
			Level.DynamicZonesList = NextDynamicZone;
			Return;
		}
		For( D=Level.DynamicZonesList; D!=None; D=D.NextDynamicZone )
		{
			if ( D.NextDynamicZone==Self )
			{
				D.NextDynamicZone = D.NextDynamicZone.NextDynamicZone;
				Return;
			}
		}
	}

-	// Called if bScriptEvent and point test is within this zone bounds.
-	// Return true to skip this dynamic zone.
-	simulated event bool FilterZone( vector Position, Actor ZoneActor );
+	// Called if bUseScriptedMatching and the pair (Position, ZoneActor) matches this zone according to other filters
+	// (in particular, Position should be within the bounds specified by ZoneAreaType and the corresponding parameters).
+	// Return true if Position is supposed to match this zone and false otherwise.
+	simulated event bool MatchesThisZone(vector Position, Actor ZoneActor);

	defaultproperties
	{
		bStatic=False
		bNoDelete=True
		RemoteRole=ROLE_None
		bAlwaysRelevant=False
		BoxMin=(X=-200,Y=-200,Z=-200)
		BoxMax=(X=200,Y=200,Z=200)
		SphereSize=250
		CylinderSize=250
-		bQuickFilter=True
	} 


Modified DynamicZoneInfo.uc

Comments:

1.1. Variables CylinderSize, SphereSize, bMovesForceTouchUpdate, bScriptEvent, bUpdateTouchers, and OldPose could have been named in a better way:

CylinderSize -> HalfHeight
SphereSize -> Radius
bMovesForceTouchUpdate -> bUpdateZoneActorsWhenMoving
bScriptEvent -> bUseScriptedMatching
bUpdateTouchers -> bScheduleUpdatingOfZoneActors
OldPose -> OldPos

At this moment, however, some maps and mods may already depend on the current names, so I'm suggesting to change only bScriptEvent and OldPose and provide a better documentation for all public properties.

1.2. Internal variables bUpdateRemainingActors, OldPos, and NextUpdatedDynamicZone have been declared with the private keyword - no reason to access them in UScript.

1.3. For OldPos, the transient keyword was removed intentionally. It should have the actual value when the game is loaded.

1.4. New native function UpdateActorZones should make it possible for modders to update the relevant zone actors immediately (when the latencies implied by using bUpdateTouchers are not acceptable). It is also called from DynamicZoneInfo.PostBeginPlay to update all zone actors.

1.5. Function FilterZone was replaced with MatchesThisZone (which is supposed to perform positive matching instead of negative matching).

2. In Actor.uc, add new boolean variable (somewhere in the section with bool variables):

Code
Select All
var private   bool	bPendingZoneUpdate; // Whether the actor is scheduled to be updated by dynamic zones 


3. In class AActor, add new static member function:

Code (C++)
Select All
static bool ShouldUpdateZoneInRelationTo(
    AActor *InActor,
    const FVector &Point,
    const ADynamicZoneInfo &CurrentZone,
    ADynamicZoneInfo &Zone); 


and define it as follows:

Code (C++)
Select All
bool AActor::ShouldUpdateZoneInRelationTo(
    AActor *InActor,
    const FVector &Point,
    const ADynamicZoneInfo &CurrentZone,
    ADynamicZoneInfo &Zone)
{
    return (!Zone.bDeleteMe && Zone.ContainsLocation(Point, InActor)) != (CurrentZone == &Zone);
} 


3.1. In class AActor, add new virtual member function:

Code (C++)
Select All
virtual bool ShouldUpdateZoneInRelationTo(ADynamicZoneInfo &Zone); 


and define it as follows:

Code (C++)
Select All
bool AActor::ShouldUpdateZoneInRelationTo(ADynamicZoneInfo &Zone)
{
    return Region.Zone && ShouldUpdateZoneInRelationTo(this, Location, Region.Zone, Zone);
} 


3.2. In class APawn, add the overrider for AActor::ShouldUpdateZoneInRelationTo:

Code (C++)
Select All
virtual bool ShouldUpdateZoneInRelationTo(ADynamicZoneInfo &Zone) override; 


and define it like this:

Code (C++)
Select All
bool APawn::ShouldUpdateZoneInRelationTo(ADynamicZoneInfo &Zone)
{
    if (!Region.Zone)
        return false;
    return
        AActor::ShouldUpdateZoneInRelationTo(Zone) ||
        AActor::ShouldUpdateZoneInRelationTo(this, GetFootLocation(), FootRegion.Zone, Zone) ||
        AActor::ShouldUpdateZoneInRelationTo(this, GetHeadLocation(), HeadRegion.Zone, Zone);
} 


Function calls GetFootLocation() and GetHeadLocation() are placeholders for actual ways to retrieve foot location and head location of a particular pawn. BTW, such functions could be useful in UScript too, so you should consider a possibility to add them.

3.3. In class APlayerPawn, add the overrider for AActor::ShouldUpdateZoneInRelationTo and define it as follows:

Code (C++)
Select All
bool APlayerPawn::ShouldUpdateZoneInRelationTo(ADynamicZoneInfo &Zone)
{
    if (!Region.Zone)
        return false;
    return
        APawn::ShouldUpdateZoneInRelationTo(Zone) ||
        ShouldUpdateZoneInRelationTo(this, CalcCameraLocation, CameraRegion.Zone, Zone);
} 


I don't know if SetActorZone actually updates CalcCameraLocation. If it doesn't, then this overrider for PlayerPawn would be useless and therefore should not be added.

4. In class ADynamicZone:

4.1. Add the overrider for AActor::ShouldUpdateZoneInRelationTo:

Code (C++)
Select All
virtual bool ShouldUpdateZoneInRelationTo(ADynamicZoneInfo &Zone) override; 


and define it as follows:

Code (C++)
Select All
bool ADynamicZoneInfo::ShouldUpdateZoneInRelationTo(ADynamicZoneInfo &Zone)
{
    return this != &Zone && AActor::ShouldUpdateZoneInRelationTo(Zone);
} 


4.2. Define ADynamicZoneInfo::Tick as follows:

Code (C++)
Select All
UBOOL ADynamicZoneInfo::Tick(FLOAT DeltaTime, enum ELevelTick TickType)
{
    UBOOL Result = Super::Tick(DeltaTime, TickType);
    if (!GIsEditor && Level && Level->DynamicZonesList == this)
        DoScheduledUpdates(*Level, DeltaTime);
    return Result;
} 


4.3. Remove the in-class declaration and the definition of ADynamicZoneInfo::Spawned entirely.

Comments:

This definition

Code (C++)
Select All
void ADynamicZoneInfo::Spawned()
{
    Super::Spawned();
    OldPose = Location;
} 


is not needed, because OldPos is updated in UpdateActorZones, which is called from PostBeginPlay (see pp. 1, 1.4, and 4.5).

4.4. Remove the in-class declaration and the definition of ADynamicZoneInfo::Serialize entirely.

Comments:

Zone's Location change is not guaranteed to be handled before saving the game. If it's not handled, such an assignment

Code (C++)
Select All
void ADynamicZoneInfo::Serialize(FArchive& Ar)
{
    Super::Serialize(Ar);
    if (Ar.IsLoading())
        OldPose = Location;
} 


may prevent handling it later. This is why OldPos should be a non-transient UScript property and use the general serialization mechanism for such properties instead.

4.5. Define ADynamicZoneInfo::UpdateActorZones as follows:

Code (C++)
Select All
void ADynamicZoneInfo::UpdateActorZones()
{
    if (!XLevel || XLevel->Actors.Num() <= 0)
        return;

    bUpdateTouchers = false;
    OldPos = Location;

    AActor **A = &XLevel->Actors(0);
    AActor **End = A + XLevel->Actors.Num();
    for (; A != End; ++A)
        if (*A && (*A)->ShouldUpdateZoneInRelationTo(*this))
            XLevel->SetActorZone(*A);
} 


4.6. Add static data member ADynamicZoneInfo::bUpdateOnlyRemainingActors:

Code (C++)
Select All
inline static bool bUpdateOnlyRemainingActors; 


Note: old compilers may not recognize inline definitions of static data members, then the inline keyword should be removed and the variable definition should be provided outside of the definition of the enclosing class.

4.7. Add static member function ADynamicZoneInfo::DoScheduledUpdates:

Code (C++)
Select All
static void DoScheduledUpdates(LevelInfo &Level); 


and define it as follows:

Code (C++)
Select All
void ADynamicZoneInfo::DoScheduledUpdates(LevelInfo &Level, FLOAT DeltaTime)
{
    // Max allowed time in milliseconds before throttling
    unsigned long const TimeLimit =
        Clamp(static_cast<unsigned long>(DeltaTime * 500), 5UL, Level.NetMode == NM_Standalone ? 8UL : 20UL);

    auto XLevel = Level.XLevel;
    if (!XLevel)
        return;

    // Making the list of all dynamic zones that should be checked
    ADynamicZoneInfo *FirstUpdatedZone = nullptr;

    for (ADynamicZoneInfo *Zone = Level.DynamicZonesList; Zone; Zone = Zone->NextDynamicZone)
    {
        if (!bUpdateOnlyRemainingActors)
            Zone->bUpdateRemainingActors = false;

        if (Zone->bUpdateRemainingActors ||
            Zone->bUpdateTouchers ||
            Zone->bMovesForceTouchUpdate && Zone->Location != Zone->OldPos)
        {
            if (!bUpdateOnlyRemainingActors)
            {
                Zone->bUpdateTouchers = false;
                Zone->OldPos = Zone->Location;
            }

            Zone->NextUpdatedDynamicZone = FirstUpdatedZone;
            FirstUpdatedZone = Zone;
        }
    }

    if (!FirstUpdatedZone)
        return;

    auto BeginTimestamp = timeGetTime();
    int ActorsNum = XLevel->Actors.Num();

    for (int i = 0; i < ActorsNum; ++i)
        if (AActor *A = XLevel->Actors(i))
        {
            if (bUpdateOnlyRemainingActors)
                if (!A->bPendingZoneUpdate)
                    continue;
                else
                    A->bPendingZoneUpdate = false;

            for (ADynamicZoneInfo *Zone = FirstUpdatedZone; Zone; Zone = static_cast<ADynamicZoneInfo *>(Zone->NextUpdatedDynamicZone))
                if (A->ShouldUpdateZoneInRelationTo(*Zone))
                {
                    XLevel->SetActorZone(A);
                    break; // We're done with A, no need to check other dynamic zones
                }

            if (!(i & 15) && timeGetTime() - BeginTimestamp > TimeLimit)
            {
                // Marking the remaining actors as unhandled.
                // Note: when bUpdateOnlyRemainingActors is true, this part shouldn't be skipped,
                // because otherwise newly spawned actors won't have a chance to be handled until
                // the end of the next traversal through all actors.
                for (++i; i < ActorsNum; ++i)
                    if ((A = XLevel->Actors(i)))
                        A->bPendingZoneUpdate = true;

                if (!bUpdateOnlyRemainingActors)
                    for (ADynamicZoneInfo *Zone = FirstUpdatedZone; Zone; Zone = static_cast<ADynamicZoneInfo *>(Zone->NextUpdatedDynamicZone))
                        Zone->bUpdateRemainingActors = true;

                bUpdateOnlyRemainingActors = true;
                return;
            }
        }

    // If we get here, then all actors have been handled,
    // so the next time we should continue from the beginning
    bUpdateOnlyRemainingActors = false;
} 


4.8. Replace ADynamicZoneInfo::ShouldFilterZone with ADynamicZoneInfo::ContainsLocation:

Code (C++)
Select All
bool ContainsLocation(const FVector &Point, AActor *InActor, const AZoneInfo *BoundingZone = nullptr); 


and define it like this:

Code (C++)
Select All
bool ADynamicZoneInfo::ContainsLocation(const FVector &Point, AActor *InActor, const AZoneInfo *BoundingZone)
{
    if (MatchOnlyZone)
    {
        if (!BoundingZone)
        {
            if (!XLevel || !XLevel->Model || !Level)
                return false;
            ADynamicZoneInfo *DynamicZonesList = Level->DynamicZonesList;
            Level->DynamicZonesList = nullptr;
            BoundingZone = XLevel->Model->PointRegion(Level, Point).Zone;
            Level->DynamicZonesList = DynamicZonesList;
        }

        if (BoundingZone != MatchOnlyZone)
            return false;
    }

    FVector CheckP = (Point - Location);
    if (bUseRelativeToRotation)
        CheckP = CheckP.TransformVectorBy(GMath.UnitCoords / Rotation);

    bool bWithinBounds = false;

    switch (ZoneAreaType)
    {
    case DZONE_Cube:
        bWithinBounds = CheckP.X >= BoxMin.X && CheckP.Y >= BoxMin.Y && CheckP.Z >= BoxMin.Z
            && CheckP.X <= BoxMax.X && CheckP.Y <= BoxMax.Y && CheckP.Z <= BoxMax.Z;
        break;
    case DZONE_Sphere:
        bWithinBounds = CheckP.SizeSquared() <= Square(SphereSize);
        break;
    case DZONE_Cylinder:
        bWithinBounds = Abs(CheckP.Z) <= CylinderSize && CheckP.SizeSquared2D() <= Square(SphereSize);
        break;
    }

    if (!bWithinBounds)
        return false;
    if (bUseScriptedMatching && !eventMatchesThisZone(Point, InActor))
        return false;

    return true;
} 


Comments:

4.8.1. I'm not sure if I'm using PointRegion correctly, because I don't have any documentation for this function and the meaning of its first parameter is unclear for me, so I just hoped that passing Level as the first argument would make it return the correct region.

[I didn't ever write any native mods for Unreal/UT, and my knowledge about native coding for Unreal is very limited, so I can misunderstand some things].

4.8.2. For DZONE_Sphere and DZONE_Cylinder, I removed slow evaluations of the square root (evaluation and comparison of two squares should work faster).
  
Back to top
 
IP Logged
 
.:..:
Board Moderator
Developer Team
*****
Offline



Posts: 1446
Location: Finland
Joined: Aug 16th, 2005
Gender: Male
Re: Issue #65. DynamicZoneInfo does not update Region of actors which initially reside in its volume
Reply #9 - Sep 1st, 2018 at 7:54am
Print Post  
I think you are a little overthinking this, we aren't making a new UnrealEngine...

Dynamic zones aren't meant to be spawned in masses either, maybe just use 1 or 2 in level for some movable water or whatnot, and even then you might not even need to update initial actors to that zone.

I do get it where you are coming with this but sometimes it not worth clogging up the code just to take every single situation into account.

Also do not make references to OS functions like timeGetTime, as that can provide portability problems to Linux (or Mac), especially when UE has own internal functions for that (appSeconds or appCycles).
  

Shivaxi wrote on Jul 25th, 2013 at 12:50pm:
...and now im stuck trying to fix everything you broke for the next 227 release xD Tongue

(ಠ_ಠ)
Back to top
ICQYIM  
IP Logged
 
Masterkent
Developer Team
Offline



Posts: 1133
Location: Russia
Joined: Apr 5th, 2013
Gender: Male
Re: Issue #65. DynamicZoneInfo does not update Region of actors which initially reside in its volume
Reply #10 - Sep 1st, 2018 at 7:37pm
Print Post  
.:..: wrote on Sep 1st, 2018 at 7:54am:
I think you are a little overthinking this, we aren't making a new UnrealEngine...

But since this feature became a part of the game patch (rather than an amateur mod), it should have as less flaws as possible, right?

.:..: wrote on Sep 1st, 2018 at 7:54am:
Dynamic zones aren't meant to be spawned in masses either, maybe just use 1 or 2 in level for some movable water or whatnot

I don't see any guidelines that would describe how exactly dynamic zones are meant to be used. What if mappers/modders find an application that doesn't fit in your mental model? Should that mean that they might spend hours just to figure out that dynamic zones can't be used as expected because the implementation of such zones isn't as good as it potentially could be?

.:..: wrote on Sep 1st, 2018 at 7:54am:
and even then you might not even need to update initial actors to that zone.

But what if someone does need to update them? The Bleeder's code demonstrates that even the moment of evaluation of DynamicZoneInfo.PostBeginPlay may be too late for updating the relevant zone actors.

.:..: wrote on Sep 1st, 2018 at 7:54am:
I do get it where you are coming with this but sometimes it not worth clogging up the code just to take every single situation into account.

You already made your code relatively complicated with that hashing stuff. For 1 - 2 zones, this optimization makes no any noticeable difference even if you handle 10000 actors with scripted determination of zone bounds.

And let's be honest, mappers don't need a short and "beautiful" code, most of them won't even understand it, they need a correctly working thing in the first place. And yes, making a correctly working implementation sometimes implies addressing particular cases.

.:..: wrote on Sep 1st, 2018 at 7:54am:
Also do not make references to OS functions like timeGetTime, as that can provide portability problems to Linux (or Mac), especially when UE has own internal functions for that (appSeconds or appCycles).

Such "problems" aren't dangerous, because a code with calls to non-existing functions won't be silently compiled into an incorrectly working program.

I didn't know if there is something like appTimeGetTime that would serve as a portable wrapper for timeGetTime, so I just wrote timeGetTime (but forgot to say "good luck with fixing compile-time errors when building the binaries for Linux").

Of course, a portable version should offer a sufficient precision. In particular, measuring time intervals in seconds represented as floats would not suffice - float has too low precision.
  
Back to top
 
IP Logged
 
Smirftsch
Forum Administrator
*****
Offline



Posts: 7790
Location: at home
Joined: Apr 30th, 1998
Gender: Male
Re: Issue #65. DynamicZoneInfo does not update Region of actors which initially reside in its volume
Reply #11 - Sep 2nd, 2018 at 10:49am
Print Post  
yuk!
Sorry for my absence, but I had to take care about some RL stuff- and still have to, unfortunately. I was hoping you 2 could sort it out meanwhile Smiley

I need to read me really into all the code, but I quickly read about what you wrote.

First a note to TimeGetTime - in current build I use this already for appSeconds in Windows (and an equal precise function for Linux), so this should be safe to be used and I won't run into any OS trouble here.

As for the amount of DynamicZones- I think MK is right here, iirc Krull0r is using them more than a few times per map already for his project - and it seems the usage of them has already gone beyond the basic idea Smiley

I hope to get my stuff done this week, so I can have a real look into this, please don't stop working on it.
  

Sometimes you have to lose a fight to win the war.
Back to top
WWWICQ  
IP Logged
 
Krull0r
Global Moderator
Betatester
Developer Team
*****
Offline


227 Emitter Expert

Posts: 392
Location: Germany
Joined: Jul 1st, 2007
Gender: Male
Re: Issue #65. DynamicZoneInfo does not update Region of actors which initially reside in its volume
Reply #12 - Sep 2nd, 2018 at 6:59pm
Print Post  
Smirftsch wrote on Sep 2nd, 2018 at 10:49am:

As for the amount of DynamicZones- I think MK is right here, iirc Krull0r is using them more than a few times per map already for his project - and it seems the usage of them has already gone beyond the basic idea Smiley


Smirftsch is right. Since the level geometry becomes more complex I really need a lot of dynamic ZoneInfos
In some levels the zone portals for water does not build right and it leaks somewhere. So I’m using a dynamic ZoneInfo.

I’m also using them for separate OpenAL reverb settings in very small areas like small metal boxes or what ever.
  

Back to top
 
IP Logged
 
Masterkent
Developer Team
Offline



Posts: 1133
Location: Russia
Joined: Apr 5th, 2013
Gender: Male
Re: Issue #65. DynamicZoneInfo does not update Region of actors which initially reside in its volume
Reply #13 - Sep 2nd, 2018 at 8:18pm
Print Post  
Smirftsch wrote on Sep 2nd, 2018 at 10:49am:
As for the amount of DynamicZones- I think MK is right here, iirc Krull0r is using them more than a few times per map already for his project - and it seems the usage of them has already gone beyond the basic idea

As long as there no any dynamic zones that need to continuously update actors, the overall number of dynamic zones doesn't matter. But adding just one "volatile" dynamic zone may lead to comparing all actors against nearly all dynamic zones, and this process is not efficient. [This is how the current implementation works]

With the basic optimization (update only potentially affected actors), the total number of dynamic zones shouldn't matter in most cases (since the most time consuming operation would be comparing all actors vs all "volatile" dynamic zones.

According to my rough estimations, comparing 5000 actors vs 50 "volatile" non-scripted dynamic zones should take less than 5 ms and can work well enough without using any throttling mechanism.

Calling a UScript function makes a single comparison about 8 times slower. But normally modders should use a proper bounding shape in order to exclude most of the actors before applying the scripted filter. So it may be possible to use up to 5 - 10 scripted dynamic zones without big performance troubles.

Most maps I've checked have only up to 2500 actors, and for such maps the values above can be proportionally higher. So, I wouldn't insist regarding the throttling mechanism. But the basic optimization should be implemented at least.

Another thing I don't like in the current implementation is the late initialization of dynamic zones and delayed initial update of zone actors. Some mods may try to obtain actor's Region.Zone from GameInfo.InitGame (directly or indirectly - e.g. in mutator's BeginPlay/PostBeginPlay) and they won't get the actual actor's zone if it's a dynamic zone.
  
Back to top
 
IP Logged
 
Page Index Toggle Pages: 1
Send TopicPrint
Bookmarks: del.icio.us Digg Facebook Google Google+ Linked in reddit StumbleUpon Twitter Yahoo