For direct access use https://forums.oldunreal.com
It's been quite a while since oldunreal had an overhaul, but we are moving to another server which require some updates and changes. The biggest change is the migration of our old reliable YaBB forum to phpBB. This system expects you to login with your username and old password known from YaBB.
If you experience any problems there is also the usual "password forgotten" function. Don't forget to clear your browser cache!
If you have any further concerns feel free to contact me: Smirftsch@oldunreal.com

Issue #65. DynamicZoneInfo does not update Region of actors which initially reside in its volume

Report bugs, read about fixes, new features and ask questions about the Unreal 227 patch here. Place comments and commit suggestions.
Post Reply
User avatar
Masterkent
OldUnreal Member
Posts: 1469
Joined: Fri Apr 05, 2013 12:41 pm

Issue #65. DynamicZoneInfo does not update Region of actors which initially reside in its volume

Post by Masterkent »

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).
Last edited by Masterkent on Thu Jun 21, 2018 2:44 pm, edited 1 time in total.
User avatar
Masterkent
OldUnreal Member
Posts: 1469
Joined: Fri Apr 05, 2013 12:41 pm

Re: Issue #65. DynamicZoneInfo does not update Region of actors which initially reside in its volume

Post by Masterkent »

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 ... 704437/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?
User avatar
.:..:
OldUnreal Member
Posts: 1634
Joined: Tue Aug 16, 2005 4:35 am

Re: Issue #65. DynamicZoneInfo does not update Region of actors which initially reside in its volume

Post by .:..: »

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.
1823223D2A33224B0 wrote:...and now im stuck trying to fix everything you broke for the next 227 release xD :P
(ಠ_ಠ)
User avatar
Masterkent
OldUnreal Member
Posts: 1469
Joined: Fri Apr 05, 2013 12:41 pm

Re: Issue #65. DynamicZoneInfo does not update Region of actors which initially reside in its volume

Post by Masterkent »

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) 
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).
Last edited by Masterkent on Tue Jul 24, 2018 9:05 pm, edited 1 time in total.
User avatar
Masterkent
OldUnreal Member
Posts: 1469
Joined: Fri Apr 05, 2013 12:41 pm

Re: Issue #65. DynamicZoneInfo does not update Region of actors which initially reside in its volume

Post by Masterkent »

Is there a possibility to get the relevant source code for DynamicZoneInfo? Maybe I could think about how to tweak it.
User avatar
.:..:
OldUnreal Member
Posts: 1634
Joined: Tue Aug 16, 2005 4:35 am

Re: Issue #65. DynamicZoneInfo does not update Region of actors which initially reside in its volume

Post by .:..: »

Sure since I wrote it, it shouldn't be a problem:
https://oldunreal.com/cgi-bin/nopaste/?152
1823223D2A33224B0 wrote:...and now im stuck trying to fix everything you broke for the next 227 release xD :P
(ಠ_ಠ)
User avatar
.:..:
OldUnreal Member
Posts: 1634
Joined: Tue Aug 16, 2005 4:35 am

Re: Issue #65. DynamicZoneInfo does not update Region of actors which initially reside in its volume

Post by .:..: »

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.
1823223D2A33224B0 wrote:...and now im stuck trying to fix everything you broke for the next 227 release xD :P
(ಠ_ಠ)
User avatar
Masterkent
OldUnreal Member
Posts: 1469
Joined: Fri Apr 05, 2013 12:41 pm

Re: Issue #65. DynamicZoneInfo does not update Region of actors which initially reside in its volume

Post by Masterkent »

Thanks, this code clarifies many things.

Regarding ADynamicZoneInfo::UpdateActorZones, this part

[code]
    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);
        }
    }[/code]
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]
        AActor **A = &XLevel->Actors(0);
        AActor **End = A + XLevel->Actors.Num();
        for (; A != End; ++A)
            if (*A && (*A)->ShouldUpdateZoneInRelationTo(*this))
                XLevel->SetActorZone(*A);
[/code]
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]
    // 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;
            }
        }
    }[/code]
in UModel::PointRegion.

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

[code]
        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);
        }[/code]
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]
        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);
        }[/code]
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]*A && (*A)->ShouldUpdateZoneInRelationTo(*this)[/code]
possibly followed by

[code]XLevel->SetActorZone(*A)[/code]
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]
    // 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
                }[/code]
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]
    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
                }[/code]
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]
    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;[/code]
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]
    ....
    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;[/code]
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 edited by Masterkent on Wed Aug 29, 2018 3:36 pm, edited 1 time in total.
User avatar
Masterkent
OldUnreal Member
Posts: 1469
Joined: Fri Apr 05, 2013 12:41 pm

Re: Issue #65. DynamicZoneInfo does not update Region of actors which initially reside in its volume

Post by Masterkent »

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]static bool ShouldUpdateZoneInRelationTo(
AActor *InActor,
const FVector &Point,
const ADynamicZoneInfo &CurrentZone,
ADynamicZoneInfo &Zone);[/code]
and define it as follows:

[code]bool AActor::ShouldUpdateZoneInRelationTo(
AActor *InActor,
const FVector &Point,
const ADynamicZoneInfo &CurrentZone,
ADynamicZoneInfo &Zone)
{
return (!Zone.bDeleteMe && Zone.ContainsLocation(Point, InActor)) != (CurrentZone == &Zone);
}[/code]
3.1. In class AActor, add new virtual member function:

[code]virtual bool ShouldUpdateZoneInRelationTo(ADynamicZoneInfo &Zone);[/code]
and define it as follows:

[code]bool AActor::ShouldUpdateZoneInRelationTo(ADynamicZoneInfo &Zone)
{
return Region.Zone && ShouldUpdateZoneInRelationTo(this, Location, Region.Zone, Zone);
}[/code]
3.2. In class APawn, add the overrider for AActor::ShouldUpdateZoneInRelationTo:

[code]virtual bool ShouldUpdateZoneInRelationTo(ADynamicZoneInfo &Zone) override;[/code]
and define it like this:

[code]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);
}[/code]
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]bool APlayerPawn::ShouldUpdateZoneInRelationTo(ADynamicZoneInfo &Zone)
{
if (!Region.Zone)
return false;
return
APawn::ShouldUpdateZoneInRelationTo(Zone) ||
ShouldUpdateZoneInRelationTo(this, CalcCameraLocation, CameraRegion.Zone, Zone);
}[/code]
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]virtual bool ShouldUpdateZoneInRelationTo(ADynamicZoneInfo &Zone) override;[/code]
and define it as follows:

[code]bool ADynamicZoneInfo::ShouldUpdateZoneInRelationTo(ADynamicZoneInfo &Zone)
{
return this != &Zone && AActor::ShouldUpdateZoneInRelationTo(Zone);
}[/code]
4.2. Define ADynamicZoneInfo::Tick as follows:

[code]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;
}[/code]
4.3. Remove the in-class declaration and the definition of ADynamicZoneInfo::Spawned entirely.

Comments:

This definition

[code]void ADynamicZoneInfo::Spawned()
{
Super::Spawned();
OldPose = Location;
}[/code]
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]void ADynamicZoneInfo::Serialize(FArchive& Ar)
{
Super::Serialize(Ar);
if (Ar.IsLoading())
OldPose = Location;
}[/code]
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]void ADynamicZoneInfo::UpdateActorZones()
{
if (!XLevel || XLevel->Actors.Num() 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);
}[/code]
4.6. Add static data member ADynamicZoneInfo::bUpdateOnlyRemainingActors:

[code]inline static bool bUpdateOnlyRemainingActors;[/code]
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]static void DoScheduledUpdates(LevelInfo &Level);[/code]
and define it as follows:

[code]
void ADynamicZoneInfo::DoScheduledUpdates(LevelInfo &Level, FLOAT DeltaTime)
{
// Max allowed time in milliseconds before throttling
unsigned long const TimeLimit =
Clamp(static_cast(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(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(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;
}[/code]
4.8. Replace ADynamicZoneInfo::ShouldFilterZone with ADynamicZoneInfo::ContainsLocation:

[code]bool ContainsLocation(const FVector &Point, AActor *InActor, const AZoneInfo *BoundingZone = nullptr);[/code]
and define it like this:

[code]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 break;
case DZONE_Sphere:
bWithinBounds = CheckP.SizeSquared() break;
case DZONE_Cylinder:
bWithinBounds = Abs(CheckP.Z) break;
}

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

return true;
}[/code]
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).
User avatar
.:..:
OldUnreal Member
Posts: 1634
Joined: Tue Aug 16, 2005 4:35 am

Re: Issue #65. DynamicZoneInfo does not update Region of actors which initially reside in its volume

Post by .:..: »

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).
1823223D2A33224B0 wrote:...and now im stuck trying to fix everything you broke for the next 227 release xD :P
(ಠ_ಠ)
User avatar
Masterkent
OldUnreal Member
Posts: 1469
Joined: Fri Apr 05, 2013 12:41 pm

Re: Issue #65. DynamicZoneInfo does not update Region of actors which initially reside in its volume

Post by Masterkent »

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?
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?
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.
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.
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.
Last edited by Masterkent on Sat Sep 01, 2018 7:50 pm, edited 1 time in total.
User avatar
Smirftsch
Administrator
Posts: 8999
Joined: Wed Apr 29, 1998 10:00 pm
Location: NaPali
Contact:

Re: Issue #65. DynamicZoneInfo does not update Region of actors which initially reside in its volume

Post by Smirftsch »

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 :)

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 :)

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.
User avatar
Krull0r
Global Moderator
Posts: 543
Joined: Sun Jul 01, 2007 4:07 pm

Re: Issue #65. DynamicZoneInfo does not update Region of actors which initially reside in its volume

Post by Krull0r »


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 :)
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.
Image
User avatar
Masterkent
OldUnreal Member
Posts: 1469
Joined: Fri Apr 05, 2013 12:41 pm

Re: Issue #65. DynamicZoneInfo does not update Region of actors which initially reside in its volume

Post by Masterkent »

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.
Last edited by Masterkent on Sun Sep 02, 2018 8:34 pm, edited 1 time in total.
Post Reply

Return to “Unreal 227”