-
-
Notifications
You must be signed in to change notification settings - Fork 15
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
base: dev
Are you sure you want to change the base?
Queue Refactor #95
Conversation
Note that this requires adding an SV_PreRemoteMoveExecution to your movement component, and calling into GMAS's PreRemoteMove call in that.
New queue types (ClientAuth, PredictedQueued) added.
There was a problem hiding this comment.
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)
First fast review, nothing to report, good job.
|
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! |
I am experiencing a small rollback when using sprint in the demo project described (Playing as Client) 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 |
FInstancedStruct InstancedPayload {}; | ||
|
||
UPROPERTY() | ||
float RPCGracePeriodSeconds { 1.f }; |
There was a problem hiding this comment.
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).
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
…erverAuth queued.
There was a problem hiding this 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); |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 }; |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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 overrideUObject::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.
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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()
.
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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(); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Reasonable change!
There was a problem hiding this 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); |
There was a problem hiding this comment.
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.
Replicable in fresh project. An issue occurs when nested gameplay tags are granted and removed with effects. |
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 eachTGMASBoundOperation
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 theServerAuth
effect queue type), or used as a holding pen (as with thePredictedQueued
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.
Predicted
,PredictedQueued
, and ServerAuth are exposed to blueprint. ClientAuth and ServerAuthMove exist for the sake of completeness, but are hidden in the UENUM.Predicted
ifbOutside
is false, andServerAuth
if true.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 theBP_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.