Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Queue Refactor #95

Open
wants to merge 18 commits into
base: dev
Choose a base branch
from
Open

Queue Refactor #95

wants to merge 18 commits into from

Conversation

Packetdancer
Copy link
Collaborator

@Packetdancer Packetdancer commented Sep 30, 2024

This is a fairly significant set of changes, reworking how the ability queue works and how the effect application works.

The core of this is the creation of a templated TGMASBoundOperationQueue where each TGMASBoundOperation represents a single action (add/remove, whatever) to a queue. The queue can be replicated client-to-server via GMC moves (as with the ability queue), server-to-client via RPC (as with the ServerAuth effect queue type), or used as a holding pen (as with the PredictedQueued effect queue type). In addition, each operation has a non-templated form which can be passed via RPC and then restored to a full operation in the queue.

Atop that change, the following additional changes were made.

  • The ability queue was converted to using a bound operation queue.
  • Effect application was converted to using bound operation queues.
  • New effect queue types were defined: Predicted, PredictedQueued, and ServerAuth are exposed to blueprint. ClientAuth and ServerAuthMove exist for the sake of completeness, but are hidden in the UENUM.
  • New API was added to determine how an effect operation (add/remove) is queued.
  • Old API was converted to use the new queues; the old API will use Predicted if bOutside is false, and ServerAuth if true.
  • The new API accepts "effect handles" -- these are effect identifiers which are only relevant/valid locally, but which can be used to refer to an effect immediately after it is queued for application, whether or not a valid effect ID was available yet.

For the sake of testing, there is a matching queue-refactor branch of the GMASExTemplate project which pulls this branch as the GMAS submodule, and where the BP_EffectBox has been converted to using the new API. In addition, BP_EffectBox has been set up so that the method by which the effect is queued can be changed easily in the blueprint (so that I didn't go insane while testing every queue type on every network scenario, repeatedly).

(However, despite the changes, previous code should Just Work seamlessly, though the older legacy API will have deprecation warnings.)

The bound operations queue functionality is meant to also support gameplay cues, as my next large task.

@Packetdancer Packetdancer added enhancement New feature or request maintenance Code cleanup, portability considerations, and such labels Sep 30, 2024
Copy link
Collaborator

@Aherys Aherys Sep 30, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion :

  • Backward incrementation removal instead of dual loop.
  • int reference as primitive type is less performant than copy (the more you know)

@Aherys
Copy link
Collaborator

Aherys commented Sep 30, 2024

First fast review, nothing to report, good job.

  • elegant use of template for the bound queue, i like it.

@petegilb
Copy link
Collaborator

petegilb commented Oct 2, 2024

This is super cool! I looked through it and there's nothing that I disagree with so next step would be some testing and we'll see where we're at!

@rafadsm
Copy link

rafadsm commented Oct 3, 2024

I am experiencing a small rollback when using sprint in the demo project described (Playing as Client)
When using sprint, my character accelerates for a few milliseconds, then returns to normal speed and accelerates again
The same when sprint is turned off
The character slows down, accelerates and slows down again
I tried to bind to MaxDesiredSpeed, but that wasn't the problem
I don't know if it could be something with the ability system or the demo

Here is a video, it doesn't always happen, but at some point you notice the character doing a slight rollback

video: https://streamable.com/if7uk4

Although I don't understand much about the gmc, I believe you did a great job with this rework

@petegilb
Copy link
Collaborator

petegilb commented Oct 3, 2024

I am experiencing a small rollback when using sprint in the demo project described When using sprint, my character accelerates for a few milliseconds, then returns to normal speed and accelerates again The same when sprint is turned off The character slows down, accelerates and slows down again I tried to bind to MaxDesiredSpeed, but that wasn't the problem I don't know if it could be something with the ability system or the demo

Here is a video, it doesn't always happen, but at some point you notice the character doing a slight rollback

video: https://streamable.com/if7uk4

Although I don't understand much about the gmc, I believe you did a great job with this rework

I had the same issue. Was getting replays constantly when starting

@Packetdancer
Copy link
Collaborator Author

@rafadsm -- Do the most recent changes to the branch fix your desync/replay? I know they did for @petegilb, but I want to double-check.

@rafadsm
Copy link

rafadsm commented Oct 8, 2024

@rafadsm -- Do the most recent changes to the branch fix your desync/replay? I know they did for @petegilb, but I want to double-check.

I did some tests and the desync/replay is fixed, thanks!

FInstancedStruct InstancedPayload {};

UPROPERTY()
float RPCGracePeriodSeconds { 1.f };
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This default should be editable on the ASC somehow

Also it would be nice to be able to set this at the Effect level as well. Per @Aherys comment in Discord, some things absolutely need to be resolved sooner with little to no grace period (which will of course cause corrections).

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion : AdvancedDisplay for this one,
Value below some reasonable amount must not be possible and we must warn the user when used/avoid using them (or why using this queue mode then ?) OR warn the user, and switch/force the queue mode if this is the case.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would also suggest making the grace time a lot lower by default. If you use the client's move timestamp to determine if the grace time is over, you're effectively only waiting for a one-way trip (server to client RPC) and not RTT.

This means that having 1000 ms of grace time by default is a bit much. A grace period of 150 ms or 250 ms would account for most scenarios (technically double those numbers).

And high ping would result in rubberbanding anyway. If the server hitches causing a ping spike in players, they'll also feel it, so a lower grace period shouldn't affect players at all, and make it harder for cheaters to game the system.

Additionally, the header might be better holding the player's latest move timestamp (double) instead of the remaining period seconds. Seems a little more readable, you'd check against the current move timestamp you're handling from the player when checking if the grace period is over.

Copy link

@xCynDev xCynDev left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A few things I've spotted reading this PR. Some of these could be some pretty critical issues, others are just ideas.

*GetNetRoleAsString(GetOwnerRole()), *GetOwner()->GetName())
}

UGMCAbilityEffect* Effect = DuplicateObject(Operation.ItemClass->GetDefaultObject<UGMCAbilityEffect>(), this);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any reason why DuplicateObject is used over NewObject in this case? Since the duplicated object is the CDO, it's basically the same as just calling NewObject with the item class no?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No reason really, other than "it was done that way in other bits of the code which I didn't write." I was just trying to match the existing behavior for effects in case there was a side effect I wasn't aware of. I have zero objection to switching it back to NewObject.

Copy link
Collaborator Author

@Packetdancer Packetdancer Oct 24, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Annoyingly it won't let me reply to the grace period comment, so I'm replying here.

I used 1 second because the existing "outside" behavior used that grace period; in case anyone was using that code, I didn't want to change the value on them by changing what was happening under the hood. (Same logic for doing the remaining time being deducted rather than a move timestamp.) Mostly, I just didn't want to change the implementation drastically and potentially change the behavior simultaneously when folks were testing it, as I wanted to have folks test the implementation sort of in isolation first; I figured behavior could be changed later.

I do think we should expose the grace period, and if no one is using the previous behavior I agree it would be good to have a shorter interval.

FInstancedStruct InstancedPayload {};

UPROPERTY()
float RPCGracePeriodSeconds { 1.f };
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would also suggest making the grace time a lot lower by default. If you use the client's move timestamp to determine if the grace time is over, you're effectively only waiting for a one-way trip (server to client RPC) and not RTT.

This means that having 1000 ms of grace time by default is a bit much. A grace period of 150 ms or 250 ms would account for most scenarios (technically double those numbers).

And high ping would result in rubberbanding anyway. If the server hitches causing a ping spike in players, they'll also feel it, so a lower grace period shouldn't affect players at all, and make it harder for cheaters to game the system.

Additionally, the header might be better holding the player's latest move timestamp (double) instead of the remaining period seconds. Seems a little more readable, you'd check against the current move timestamp you're handling from the player when checking if the grace period is over.


// The instanced struct representation of this payload, used to actually
// bind for replication.
FInstancedStruct InstancedPayload;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Keep in mind this is very unsafe as FInstancedStruct contains UObjects. The UScriptStruct of the wrapped struct, and the struct memory itself may contain UPROPERTY fields which the GC knows about through the AddStructReferencedObjects type trait.

I already talked to GRIM about implementing it for the GMC's bound variables since UObject* ones aren't referenced by the GC in some scenarios. You're meant to UPROPERTY instanced structs to prevent dangling pointers.

If there is no way to make it UPROPERTY, there are other solutions:

  • If this templated struct is held in a struct, you can implement the WithAddStructReferencedObjects type trait to manually add the instanced struct's references (and ItemClass as well) if they're not nullptr.
  • If this is held by a UObject, you can override UObject::AddReferencedObjects() and do the same.

I see this template is used by another template, so I would go up the chain until I find a UPROPERTY'd struct or UObject and handle it either way.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's worth noting that the payloads being used may have UObject* properties, but those are filled in on the side actually utilizing it, so I was not particularly concerned about dangling pointers in this case as opposed to an abstract way to sync the templated payload.

However, it's a valid concern if someone blithely used the bound queue for something else. I'm not going to be able to do a rewrite for the next couple of weeks, however, so if you'd like to see that change sooner, feel free to refactor and make a PR against the source branch!


FGameplayTag GetTag() const { return Header.Tag; }

bool GracePeriodExpired() const
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor pet peeve, mostly Epic's coding convention which has bool getters be a question, like HasGracePeriodExpired().

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fair, and easy enough to change.

// An actual class to be utilized with this, in case we need to instance it.
TSubclassOf<C> ItemClass { nullptr };

FInstancedStruct InstancedPayloadIds;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See comment on line 82.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

InstancedPayloadIds only ever contains an FGMASBoundQueueOperationIdSet, a struct which only ever contains a single property of type TArray; it's only an instanced struct because GMC lacks a way to bind arrays.

I don't consider this one a particular risk, as a result.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's fair. I'm mostly worried about the UScriptStruct but I suppose it shouldn't ever be GC'd for a C++ struct.

{
// Get a handle to our class, for instancing purposes.
TSoftClassPtr<C> ClassPtr = TSoftClassPtr<C>(FSoftObjectPath(Header.ItemClassName.ToString()));
ItemClass = ClassPtr.LoadSynchronous();
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can cause hitches as this flushes the async streaming queue. I would use the asset manager's FStreamableManager::RequestSyncLoad() as it instead puts the request at the top of the queue, potentially finishing faster.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reasonable change!

Copy link

@xCynDev xCynDev left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bonus

FGMCOuterApplicationWrapper Wrapper = FGMCOuterApplicationWrapper::Make<FGMCOuterEffectAdd>(Effect, InitializationData);
AddPendingEffectApplications(Wrapper);
QueuedEffectOperations.QueuePreparedOperation(Operation, false);
ClientQueueEffectOperation(Operation);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do wonder if we could instead have another queue on the server, so we don't spam RPCs in case of multiple effects. Minor optimization perhaps but we could have an array of operations and send them as one RPC instead.

@Crezetique
Copy link
Contributor

image
Bringing this over from discord to ensure vis and in case someone has the chance to look into it.

Replicable in fresh project. An issue occurs when nested gameplay tags are granted and removed with effects.
The effects are removed, but the granted tag is not removed correctly.
In the screenshot scenario, State.Test granted tag is never removed.
Having the effects added or removed in different order does not change the outcome.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request maintenance Code cleanup, portability considerations, and such
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants