CHANGELOG
Changelog
2.0 is a major release with breaking changes. Upgrading from 1.x: every change says who is affected by each change and what to do. Each entry below says what changed and why. “#1” to “#24” number the 1.x issues that the 2.0 code review listed; “N1” to “N6” number further issues found during the 2.0 work.
Version
Section titled “Version”- Plugin version 2.0.
- What changed:
CharacterInteractionSystem.upluginhas"Version": 2and"VersionName": "2.0"(1.x: 1 and “1.0”). - Why: 2.0 is a major release with breaking changes: assets are migrated when they load, and every behaviour change is described in Upgrading from 1.x: every change.
- What changed:
Execution model
Section titled “Execution model”- Custom version
CISVersion.- What changed: The plugin registers the custom version
CISVersion(GUID7C0D5A1E-4B2F-4E7A-9C3D-2A6B8F1E5D40, versionExecutionPolicies= 1; C++FCISCustomVersioninCommon/CISCustomVersion.h). Every Action, MoveHandler and Condition writes it when it is saved, so a package saved with 2.0 that contains one carries it. 1.x does not know it: when 1.x loads such a package it logs the warning “Package was saved with a custom version that is not present” and loads it without the new fields. A package without it was saved before 2.0 and is migrated when it is loaded (see “Migration on load”). - Why: Packages saved with 1.x must be told apart from new ones, so that old assets keep their 1.x behaviour and new assets get the 2.0 defaults.
- What to do: Keep the 1.x versions of your packages if you may need to go back to 1.x.
- What changed: The plugin registers the custom version
- New policies and settings.
- What changed: Actions and MoveHandlers (
UCISReplicatedObject) getInstancingPolicy(ECISInstancingPolicy:Default,SharedPerObject,InstancedPerExecution; defaultDefault) andNetExecutionPolicy(ECISNetExecutionPolicy:AllMachines,LocalPredicted,ServerOnly,LocalOnly; defaultAllMachines), Conditions getServerValidation(ECISConditionServerValidation:Default,Validate,DoNotValidate; defaultDefault).FCISInteractionObjectSettingsgetsbTransferOwnershipDuringUse(default true),bManageOwnerDormancy(default false) andMaxUseDuration(default 0, no limit). A class with its own RPCs or replicated properties (a Blueprint or C++ class below the plugin’s Action or MoveHandler classes; the plugin’s own fields do not count) always runs asSharedPerObject; choosingInstancedPerExecutionfor it (explicitly or throughDefaulton a Blueprint class) gives the data validation warning “Uses its own RPCs or replicated properties; runs as SharedPerObject”, which a Blueprint compile also shows. New C++ API:UCISReplicatedObject::GetEffectiveInstancingPolicy(the policy a use applies, neverDefault),UCISReplicatedObject::IsWithReplication,UCISInteractionObjectCondition::ShouldValidateOnServer,CIS::Migration::ClassHasCustomNetFields(Common/CISMigration.h), and the enumsECISUsePhaseandECISUseRejectReason. - Why: 2.0 takes the execution model of the Gameplay Ability System: a copy per use (
InstancedPerExecution) removes the shared state that simultaneous and repeated uses corrupted in 1.x, andNetExecutionPolicysays where an Action or MoveHandler runs and who leads it, so that the server can decide. A class with its own RPCs or replicated properties needs one replicated object, so it cannot run as a copy.
- What changed: Actions and MoveHandlers (
- C++ classes keep the 1.x behaviour by default.
- What changed:
Defaultis resolved by the object’s class: a C++ class keeps the 1.x behaviour (SharedPerObject, its Conditions are not validated on the server), a Blueprint class gets the 2.0 behaviour (InstancedPerExecution, validated on the server); an explicit value always wins. So objects of C++ classes that are never loaded from an old package (built withCreateDefaultSubobjectorNewObjectin C++, or spawned from C++ classes) behave as in 1.x. - Why: C++ code must not change its behaviour after the upgrade without any change of its own, while new Blueprint content gets the safer 2.0 defaults. A Condition that the server validates and that reads client-only state would reject every use of a remote client.
- What to do: A C++ class that wants the 2.0 behaviour sets
InstancingPolicyorServerValidationin its constructor.
- What changed:
- Migration on load.
- What changed: An Action, MoveHandler or Condition that its package’s linker loads from a package saved before
CISVersionkeeps its 1.x behaviour: it gets the value of its nearest Blueprint archetype (class default or template) saved with 2.0,Defaultincluded, and otherwise the explicit 1.x value:InstancingPolicySharedPerObject,NetExecutionPolicyAllMachines,ServerValidationDoNotValidate. ForNetExecutionPolicythe object’s ownbWithReplicationdecides in both directions, as in 1.x: off givesLocalOnly; on gives the inherited value, except that an inheritedLocalOnlybecomesAllMachines. A C++ archetype (a C++ class default or a C++ default subobject) passes nothing, and an archetype that was itself saved before 2.0 is looked through to its own archetype. So a pre-2.0 Blueprint class, its instances and new instances made from it keep the 1.x behaviour. The migration runs on every load of an old package, while its data is read (a class default at its PostLoad), and an object that a load builds from an old archetype before that archetype is migrated (for example a child Blueprint, a Blueprint instance or a level saved with 2.0 whose parent Blueprint was never re-saved) gets the archetype’s migrated values. A re-save writes a migrated value only where it differs from the object’s archetype, as for every property: a re-saved Blueprint class default holds explicitly each value that differs from its C++ parent class (InstancingPolicySharedPerObjectdiffers fromDefault), while an instance or a child that equals its archetype keeps inheriting from it, so a later change of the archetype reaches it. Copies made in memory are not migrated and keep the values they copied, the explicit values a user set on an object of an old package included: PIE copies, duplicates (Ctrl+D,DuplicateObject), copy and paste, and undo. New C++ API (Common/CISMigration.h):CIS::Migration::IsFromPackageBefore,NeedsLegacyMigration,GetNonNativeArchetype,ApplyLegacyDefaults,IsLoadingLegacyData,InheritPendingLegacyArchetype;UCISReplicatedObjectandUCISInteractionObjectConditionoverridePostInitProperties. - Why: Assets saved with 1.x keep their 1.x behaviour without a re-save, and a re-save keeps it; content built from an old archetype keeps it too.
- What changed: An Action, MoveHandler or Condition that its package’s linker loads from a package saved before
- Cooking after the upgrade.
- What changed: After upgrading to 2.0, make a full (non-iterative) cook. A cooked game does not migrate (it loads packages without the linker that the migration reads the version from); the cook migrates each package when it loads it in the editor and writes the result. An iterative cook can keep packages that 1.x cooked, which hold none of the new fields, so their Blueprint objects would resolve
Defaultto the 2.0 behaviour. - Why: A cooked game cannot migrate; only the editor (and so the cook) reads the version of a package.
- What changed: After upgrading to 2.0, make a full (non-iterative) cook. A cooked game does not migrate (it loads packages without the linker that the migration reads the version from); the cook migrates each package when it loads it in the editor and writes the result. An iterative cook can keep packages that 1.x cooked, which hold none of the new fields, so their Blueprint objects would resolve
- Blueprint variables with the new names.
- What changed:
UCISReplicatedObjectnow has properties namedInstancingPolicyandNetExecutionPolicy, andUCISInteractionObjectConditionone namedServerValidation. When a Blueprint below these classes has a variable of the same name, the Blueprint compiler renames that variable (toInstancingPolicy_0or similar) and logs the warning “Found a member variable with a conflicting name (…) - changed to …”; the Blueprint’s nodes follow the rename. Only a variable that is itself a property of the same kind, an enum of a C++enum classtype, is instead removed, and its nodes are pointed at the plugin’s property (a note, “replaced references with native parent class variable”); its nodes then fail to compile where the enum types differ. A 1.x Blueprint cannot have held a variable of the new enum types. - Why: The new properties of the base classes can have the names of variables that a Blueprint below them already has.
- What to do: Rename such variables.
- What changed:
- Live Coding and hot reload.
- What changed: The cache behind
CIS::Migration::ClassHasCustomNetFieldsis also cleared after every Live Coding patch or hot reload (FCoreUObjectDelegates::ReloadCompleteDelegate), as it already was when Blueprints are reinstanced in the editor, so a class that a patch gives its own RPCs or replicated properties runs asSharedPerObjectfrom then on. - Why: A stale cache would let a class that a patch gave its own RPCs or replicated properties run as a copy.
- What changed: The cache behind
- Executions.
- What changed: A use runs through an execution,
UCISUseExecution(Execution/CISUseExecution.h), whichUCISCharacterInteractionComponent::StartUseExecutioncreates when the defaultUCISInteractionDataObject::UseObjectruns. It runs the MoveHandler and the Action of the use side in the order of the side’sActionExecutionType, calls the data object’s events in the 1.x order (OnObjectUseStarted,OnMovementStarted,OnMovementEnded,OnObjectActionStarted,OnObjectUseFinished) and its virtual hooksOnMovementToObjectFinished,OnObjectActionUseFinishedandFinishUseObjectat the 1.x points, and ends the use.UseObject,InterruptUsingandFinishUseObjectstay the virtual entry points (start,UCISUseExecution::Interrupt,UCISUseExecution::End); the execution never callsUseObjectorInterruptUsing. The 1.x code ofInterruptUsing,StartObjectAction,OnMovementToObjectFinishedandFinishUseObjectstill runs a use that an override ofUseObjectruns itself. Every machine that takes part in a use runs its own execution (see “Use phases on the network”). New C++ API:UCISUseExecution,FCISExecutionContext,UCISCharacterInteractionComponent::StartUseExecutionandGetActiveUseExecution,UCISInteractionDataObject::GetCurrentExecution. - Why: One object runs a use on every machine, in the 1.x order of events, as an ability does in the Gameplay Ability System; the virtual functions that C++ code overrides stay where they were.
- What changed: A use runs through an execution,
- Per-execution instances.
- What changed:
InstancingPolicytakes effect.SharedPerObjectruns the Action or MoveHandler set on the interaction object itself, as in 1.x (its variables carry over from use to use).InstancedPerExecutionruns a copy made for the use (NewObjectwith the object component as the outer and the object set on it as the template,RF_Transient), released when the use ends, so its variables start from the object’s values at every use and never reach the next use. Each machine that runs the use makes its own copy; a copy is not replicated. A MoveHandler copy ticks while it moves; the object set on the interaction object does not tick for it. Content saved with 1.x and C++ classes keepSharedPerObject; new Blueprint classes getInstancedPerExecution(Default). - Why: With one shared object, overlapping or repeated uses of an object (several characters, a quick second use) changed each other’s variables.
- What to do: Keep state that must outlive a use on the object’s actor or component, or set the class to
SharedPerObject.
- What changed:
- Execution context.
- What changed: Actions and MoveHandlers get
GetExecutionContext(FCISExecutionContext:Character,CharacterComponent,ObjectComponent,SideIndex,ActionIndex,ExecutionId,bIsServer,bIsOwner,bIsSimulated,bRejectedByServer) and the Blueprint-pure gettersIsExecutingOnServer,IsLocallyPredicted,IsSimulatedExecution,WasRejectedByServer(category “CIS|Execution”). The context is set when the use starts, beforeOnObjectUseStarted, on every machine that runs the use; aSharedPerObjectobject gets a default context again when the use ends, a copy keeps its own. C++:SetExecutionContext,GetExecutionTemplate,IsPerExecutionInstance. - Why: Blueprints of Actions and MoveHandlers need to know for whom and where they run (server, predicting owner, a machine that only shows the use) without new parameters on the 1.x events.
- What changed: Actions and MoveHandlers get
- New object delegates.
- What changed:
UCISInteractionObjectComponentgets the Blueprint-assignableOnUseStartedDelegate(afterOnObjectUseStarted),OnActionStartedDelegate(afterOnObjectActionStarted, with the action that runs) andOnUseFinishedDelegate(afterOnObjectUseFinishedand the release of the object, before the character’sOnEndUseObjectDelegate), with the delegate types 1.x declared but never used. They fire on every machine that runs the use. They are paired (OnUseFinishedDelegateonly afterOnUseStartedDelegate) and not fired for a use that a listener ended before them. - Why: 1.x declared these delegate types but never fired them; listeners on the object (UI, AI, level scripts) had to bind to every character.
- What changed:
- The action is chosen when the use starts.
- What changed: The action of a use is chosen from the object state right before
OnObjectUseStarted(its index is in the execution context). 1.x looked it up right afterOnObjectUseStarted, so a state change made insideOnObjectUseStartedchose the action of the new state. - Why: A use has one action from its start, the one that the client’s request names and the server checks.
- What to do: Change the state before the use instead (for example in the character’s
OnStartUseObjectDelegate).
- What changed: The action of a use is chosen from the object state right before
- The use side is fixed for the whole use.
- What changed: A use keeps the side it started at for the movement, its abort, the action and the side’s
ActionExecutionType. 1.x read the data object’s current side at each step: a character that entered another side during the use (moved by its MoveHandler, for example) started the action with that side, and that side’sActionExecutionTypedecided whether the action started after the movement; a character that left every side got a default side, so aStartParallelWithMoveHandleruse started its action a second time when the movement finished. - Why: One use, one side: the server checks the side of the request, and a side change in the middle of a use started the action twice in 1.x.
- What changed: A use keeps the side it started at for the movement, its abort, the action and the side’s
- Ends inside a use’s own events.
- What changed: A use that a listener ends or interrupts inside
OnObjectUseStarted,OnMovementStarted,OnObjectActionStartedor the new object delegates stops there (1.x went on to start the movement and the action of the ended use, and could fail the check inFinishUseObject); an interruption or end requested while the use ends is ignored (1.x could callOnObjectUseFinishedtwice); a use whose character is gone when its action is due ends as interrupted, before the action events (1.x dereferenced the null character). An action that finishes before the action step ends the use, as in 1.x, and the use never reaches theActingphase; empty steps are completed at once. - Why: 1.x went on with a use that had already ended, and could end it twice or crash.
- What changed: A use that a listener ends or interrupts inside
- N4 kept: an interruption after the movement.
- What changed: A
SharedPerObjectMoveHandler, which is all 1.x content, is still aborted by an interruption after its movement has finished (MovementAbortedafterMovementFinished), as in 1.x. A MoveHandler copy (InstancedPerExecution) getsMovementAbortedonly while it moves. - Why: Changing it would change the events of every 1.x MoveHandler; a copy is new, so it gets the cleaner rule.
- What to do: In a MoveHandler copy, clean up in
MovementFinishedtoo.
- What changed: A
- The active use and its phase.
- What changed: The character interaction component keeps its hold and its use in one structure,
FCISActiveUse(the protectedUCISCharacterInteractionComponent::ActiveUse), which holds the runningUCISUseExecutionfrom its start until the use ends. New Blueprint-pureGetActiveUsePhase(ECISUsePhase):HoldingfromTryUseObjectuntil the use starts or the hold is cleared,Pendingon a remote client while it sends its request, then the execution’sMovingandActing,Endingonly inside the end,Noneotherwise and on a component without a character (#13).IsCharacterUseAnyObject,IsHasDelayedObjectDataandGetDelayedObjectProgresskeep their 1.x timing, so insideOnStartUseObjectDelegatethe phase is stillHolding. New C++ API:FCISActiveUse,UCISCharacterInteractionComponent::ActiveUse,NextPredictionKeyandEndActiveUse(protected),UCISUseExecution::OnPhaseChanged; the component overridesEndPlay(a subclass’s override must callSuper). - Why: One structure replaces the scattered 1.x flags of a use, so that a use and its prediction can be tracked on every machine, and the UI can read one phase.
- What changed: The character interaction component keeps its hold and its use in one structure,
- MaxUseDuration.
- What changed:
MaxUseDurationofFCISInteractionObjectSettingstakes effect. On the authority (standalone, the server, a listen host), a use that still runsMaxUseDurationseconds after it started (after the hold) ends as interrupted, through the data object’sInterruptUsingas withTryInterruptObjectUsing, whatever the object’sObjectInterruptionType(Noneincluded); the value is read when the use starts. 0, the default, is no limit, as in 1.x. Every end of the use and the component’sEndPlaystop the limit. The server also limits a remote client’s use, on its own clock; the client’s use then ends as interrupted through the server’s end (withoutInterruptUsingon the client). - Why: The authority needs a safety limit for uses that never end (a lost client, an Action that never finishes).
- What changed:
- A use that does not start ends at once.
- What changed: When a hold completes and the data object’s
UseObjectstarts no use (the data object is no longer usable, for example because a listener ofOnStartUseObjectDelegatemade it unusable, or a C++ override ofUseObjectstarts none), the announced use ends at once:OnClearDelayedObjectData, then oneOnEndUseObjectDelegatewith the object, andIsCharacterUseAnyObjectis false again. - Why: In 1.x the character stayed “using” with no end:
IsCharacterUseAnyObjectstayed true and it could not use any object again.
- What changed: When a hold completes and the data object’s
- N3 follow-up: a late FinishMovement is logged.
- What changed: A late
FinishMovementthat the plugin ignores (the movement is not running: never started, or already finished and invalidated) logs aVerboseline inCISLog. - Why: An ignored finish is easy to miss when you debug a MoveHandler.
- What changed: A late
- A new use from the end of the previous one.
- What changed: When a listener of the object’s
OnUseFinishedDelegate(or of the data object’s end) starts a new use of the same character, the first use’s tail runs first (itsMaxUseDurationtimer stops, its destruction binding goes, and a remote client sends its Ending to the server before the new request), and the end of the first use no longer resets the execution context that the new use gave a shared Action or MoveHandler. While the new use runs,IsCharacterUseAnyObjectreads true andGetCurrentUsingObjectnames its object, so no hold can start, and the character’sOnEndUseObjectDelegatefires at the end of each use. Every use that the character component starts sets these 1.x flags, also one that C++ starts without the hold (StartUseExecution, or the data object’sUseObject): it reads as in use while it runs and broadcasts the character’sOnEndUseObjectDelegateat its end (OnStartUseObjectDelegatestays an event of the hold). - Why: Since the object is free inside
OnUseFinishedDelegate, an AI can chain uses there; before this fix the first use’s limit could end the new use as interrupted, and the server ended its run of the first use as interrupted. - What to do: C++ that starts uses without the hold and listens to the character’s
OnEndUseObjectDelegate: expect an end without anOnStartUseObjectDelegatebefore it. Such a use (also 1.x content that calls the data object’sUseObjectdirectly) now reacts toTryInterruptObjectUsing: when the object’sObjectInterruptionTypematches, the call interrupts it (1.x did nothing); for a use whose data object the character does not track (one that C++ made itself), the call creates a new tracked data object instead and the use goes on.TryUseObjectstarts no hold while such a use runs.
- What changed: When a listener of the object’s
Networking: server authority
Section titled “Networking: server authority”- Server-authoritative use requests.
- What changed: A remote client’s use goes through the server. When the hold of an object with a use time above 0 starts, the client tells the server (
ServerNotifyHoldStarted); when the use starts, it sendsServerRequestUse(object, side index, action index, the use’sExecutionIdas prediction key) and runs the use at once (prediction;GetActiveUsePhaseisPendingwhile the request is sent). The server checks the request (see “Server validation and Project Settings”). Confirmed (ClientConfirmUse): the server sets the object’s use state,IsObjectUsing,CurrentUsingObjectand, withbTransferOwnershipDuringUse, the owner of the object’s actor, runs the use itself, and gives the previous owner back when the use ends. Rejected (ClientRejectUsewithECISUseRejectReason): the client’s use ends as interrupted withWasRejectedByServer()true on its Action and MoveHandler (the data object’sInterruptUsingis not called), and the client gives the object’s actor its previous owner back. Every rejection carries the object’s state and usable flag on the server, and the client’s copy of the object takes them, also when the rejected use has already ended there (for example an Action that finished at once and set itsNewStateAfterFinishActionbefore the answer arrived). A standalone game, a listen host’s own character and characters that the server runs are never checked. Stale or repeated keys are ignored on both sides. The server sets the user’sCurrentUsingObjectwhen the request arrives, after the client’sOnStartUseObjectDelegate(1.x: before it), so a Server RPC sent from that delegate reads null there. New C++ API: the protected RPCsServerNotifyHoldStarted,ServerRequestUse,ClientConfirmUse,ClientRejectUse(with the requested object and its state and usable flag on the server) andClientSyncExecutionKeys(see “Keys”) ofUCISCharacterInteractionComponent;FCISActiveUse::bRequestSent,bAwaitingServerAnswerandbRemoteUseOnServer. - Why: 1.x was client-authoritative: the server only relayed what a client decided. Now the server decides, while the client still starts the use at once, as with a predicted ability.
- What to do: Check the tolerances in Project Settings when your game lets characters use objects from farther away or right after a teleport.
- What changed: A remote client’s use goes through the server. When the hold of an object with a use time above 0 starts, the client tells the server (
- Server validation and Project Settings.
- What changed: The server checks a request with
CIS::Validation::ValidateUseRequest(FCISUseRequest) in the orderObjectInvalid,NotUsable,Occupied,NoActionForState,BadSide,OutOfRange,BadAngle,ConditionFailed,HoldTooShort. Before these checks, also withbServerValidationoff, a side index that the object on the server does not have is rejected asBadSide, and an action index that its side does not have asNoActionForState(the settings are not replicated, so a client’s copy can differ). Conditions that the server validates (ServerValidation) run there without a data object. The tolerances and a switch are the new project settingsUCISNetworkSettings(Project Settings > Plugins > Character Interaction System, saved inDefaultGame.ini):bServerValidationtrue,DistanceToleranceAbsolute100,DistanceToleranceScale0.5,AngleToleranceDegrees30,HoldTimeTolerance0.3. A rejection is logged inCISLogon the server (“ServerRequestUse from <character>: the use of <actor> is rejected (<reason>), key <key>.”) and on the client (“ClientRejectUse on <character>: the server rejected use <key> (<reason>).”). One case rejects what 1.x let the owner do: content that changes the object’s state tag only on a client (SetObjectStateon a client does not reach the server) gets its use rejected asNoActionForStatewhen the client’s state chooses another action than the server’s; the client’sSetObjectStatelogs a warning (#17). Such content already went out of sync in 1.x multiplayer. The server also checks with its own settings (use time, interaction angle, distance, sides), so values changed on the client only are rejected. New C++ API:UCISNetworkSettings,FCISUseRequest,CIS::Validation::ValidateUseRequest,CIS::Geometry::ResolveTargetComponent,GetTargetPointFromandGetTargetPoint(the point a character faces, shared by the data object and the server check). - Why: Server authority needs a check that is on for old content too; the tolerances are generous and the same for every asset, so that legitimate uses are not rejected, and one switch turns the check off.
- What to do: Change object states and usable flags on the server; set
DoNotValidateon Conditions that read client-only state; widen the tolerances where needed. See “When the server rejects a use” in Upgrading from 1.x: every change.
- What changed: The server checks a request with
- #1 The owner of the object’s actor is given back after a use.
- What changed: The owner of an object’s actor is given back at the end of a use, on every machine: the use remembers the previous owner and restores it, but only while the actor’s owner is still the controller that the use set (an owner that game code set during the use is kept;
FCISActiveUse::TransferredObjectOwner). C++SetOwnerOfActor(Actor, true)passesbCleartoServerSetOwnerOfActor. - Why: In 1.x the plugin cleared the owner locally and then called
ServerSetOwnerOfActorwithoutbClear, so on the server (and in a standalone game) the user’s controller stayed the owner after every use.
- What changed: The owner of an object’s actor is given back at the end of a use, on every machine: the use remembers the previous owner and restores it, but only while the actor’s owner is still the controller that the use set (an owner that game code set during the use is kept;
- #2 A client may change the owner only of the actor it uses.
- What changed:
ServerSetOwnerOfActorcalled by a client changes only the owner of the actor of the object that the server has confirmed for that character, while that use runs; for any other actor it logs a warning inCISLog(”… a client may only change the owner of that actor. Ignored.”) and changes nothing. A call on the server outside a received client RPC is not limited; the check isUE::Net::FNetContext::IsInsideNetRPC(), so server code that runs inside any received client RPC (aServer…_Implementation, an Action relayed from a client) is limited too.ServerSetCurrentUseObjectcalled by a client accepts only null or the object of the server’s active use of that character (else a warning; the sameIsInsideNetRPCrule); null from a client no longer ends the server’s run of that client’s use (its end isServerUsePhase). The plugin no longer callsServerSetOwnerOfActorfor its uses, norServerSetCurrentUseObjectwith an object; the data object’s 1.xFinishUseObjectfallback (a use without an execution) still callsSetOwnerOfActor(Actor, true). A C++ override ofUseObjectthat runs the use withoutSuperon a remote client (the 1.x fallback) sends no request, so the server refuses itsServerSetOwnerOfActorand never records the use. - Why: Through
ServerSetOwnerOfActorany client could make its controller the owner of any actor on the server. - What to do: C++ code that calls
ServerSetOwnerOfActororSetOwnerOfActoron a client for another actor is refused now; code that relied on the controller staying the owner after a use must set the owner itself on the server; a C++UseObjectoverride withoutSupershould callSuper(the plugin’s use, with a request) or run on the server.
- What changed:
- #17 Object state and usable flag set on a client.
- What changed:
SetObjectStateandSetUsableofUCISInteractionObjectComponentcalled on a client’s copy (a simulated proxy; an interactable stays one on a client also while that client’s controller owns its actor during a use) still change only that copy, as in 1.x (SetObjectStatealso broadcasts there), and now log once per component a warning inCISLog: “<function> on <actor> called on a client; the change is local only. Call it on the server.” The plugin’s own calls (the replicated state arriving, an Action’sNewStateAfterFinishAction) do not log. - Why: 1.x checked roles inconsistently, and such calls from a client silently never reached the server; the server state is the one that counts.
- What to do: Call
SetObjectStateandSetUsableon the server.
- What changed:
- #19 RPC validation.
- What changed: The new RPCs validate their input:
ServerRequestUse(a key below 1 or aboveCIS::MaxExecutionId, an object of another world),ServerUsePhase(a key outside 1..CIS::MaxExecutionId, a phase that a client never sends) andServerNotifyHoldStarted(an object of another world). A malformed call disconnects the client. A side or action index that the object on the server does not have is not malformed (the settings are not replicated, so a client’s copy can differ): the request is rejected (BadSide,NoActionForState), and a hold notice for such a side is recorded as any hold. Everything that depends on state or timing is a rejection (ClientRejectUse) or a warning, never a disconnect; a null object is not malformed (it may have been destroyed on the way). The 1.x RPCs keep accepting every call. - Why: Every
_Validateof 1.x returned true; only input that no correct client sends should disconnect.
- What changed: The new RPCs validate their input:
- The use state, and an object in use for a rejected client.
- What changed: New
FCISObjectUseStateandUCISInteractionObjectComponent::GetUseState(Blueprint-pure, replicated:User,ExecutionId,SideIndexandPhaseof the use the server knows),IsObjectOccupiedandSetUseState_Authority. A character’s data object treats an object as in use, on every machine, also while the object’s use state names another character, so a client whose use was rejected asOccupiedno longer sees the object as free while the other use runs (its ownIsObjectUsingcan still read false). - Why: Late joiners and dormant objects need the use in replicated state, and a rejected client must not see a used object as free.
- What to do: Read
GetUseStatetoo where content decides on a client that an object is free.
- What changed: New
- Use phases on the network.
- What changed: Every machine that takes part in a use runs it (the user’s machine, the server, and the clients that have the object’s actor, which show it), and the 1.x Server and Multicast relays of the Action and the MoveHandler are no longer sent. Each step has a leader by the
NetExecutionPolicyof its object (AllMachines,LocalOnlyand a step without an object: the user’s machine;LocalPredicted: the user’s machine and the server, whichever completes it first;ServerOnly: the server; a use of a character on the server leads every step); a machine that does not lead a step runs its own copy and waits for the leader’s phase. The user’s client sendsServerUsePhase(action started, ended), the server multicasts every phase through the object (MulticastUsePhasewithFCISUsePhaseEvent) and sends the user’s client the phases its run reached by itself (ClientUsePhase). A client that gets the object while a use runs shows nothing of it, and a player who joins during another player’s use does not run its Action or MoveHandler, as in 1.x. New C++ API:FCISUsePhaseEvent;UCISUseExecution::LeadsStep,LeadsOwnStep,OwnerLeadsStep,ApplyLeaderPhaseandIsEndInterrupted;UCISInteractionObjectComponent::MulticastUsePhase,SetObjectIsUsingForUse,TakeOwnershipForUseandReleaseOwnershipForUse(protected);UCISCharacterInteractionComponent::ServerUsePhaseandClientUsePhase(protected) andHandleSimulatedUsePhase;FCISActiveUse::UserController,ObjectActor,bSimulatedUseandbEndAppliedFromRemote. - Why: With server authority the server must run the use itself; the relays let each machine follow a client’s word, sent each finish a second time (#24) and needed ownership of the object.
- What to do: C++ overrides of the relays’
_Implementationfunctions are no longer called by the plugin’s uses: move such code to the Action’s or MoveHandler’s own events.
- What changed: Every machine that takes part in a use runs it (the user’s machine, the server, and the clients that have the object’s actor, which show it), and the 1.x Server and Multicast relays of the Action and the MoveHandler are no longer sent. Each step has a leader by the
- #24 An Action finishes once per use on every machine.
- What changed: An Action that finishes by itself on a machine that does not lead it (for example a Blueprint Action that calls
FinishActionon every machine) is finished there once per use instead of twice, so itsOnUseFinished,NewStateAfterFinishActionand, on a client,OnObjectStateChangedDelegaterun once there; it no longer ends the use there, the leader’s end does. - Why: On the server and the other clients 1.x finished such an Action a second time at
MulticastFinishAction, soOnUseFinishedfired twice.
- What changed: An Action that finishes by itself on a machine that does not lead it (for example a Blueprint Action that calls
- #10 A phase for a character that a client does not know.
- What changed: A phase whose character a client does not know (the character is not relevant to it) is skipped there, with a
Verboseline inCISLog, without a crash. The plugin no longer sends the 1.x multicast relays. The deprecated relays that code may still call (MulticastStartObjectActionof the Action,MulticastStartMovementandMulticastMovementAbortedof the MoveHandler) do nothing on a client that gets a null character (the character is not relevant to it). - Why: The 1.x multicasts dereferenced a null character on a client to which the character was not relevant.
- What changed: A phase whose character a client does not know (the character is not relevant to it) is skipped there, with a
- #18 An Action that finishes only on the server.
- What changed: With
ServerOnlyorLocalPredicted, aFinishActionon the server ends the step for every machine: the user’s client gets it throughClientUsePhase, the other clients through the object’s phases. WithAllMachines(all 1.x content) the user’s machine leads, so aFinishActionthat only the server calls finishes only the server’s copy, as in 1.x. - Why: In 1.x a
FinishActioncalled only on the server never reached the client, and the client’s use hung.AllMachineskeeps the 1.x rule so that old content does not change. - What to do: If your Action finishes only on the server, choose
ServerOnlyorLocalPredicted.
- What changed: With
- Ends that the server forces.
- What changed: The server’s run of a remote client’s use ends as interrupted when the client’s pawn is destroyed (also on disconnect), its controller changes (one tick after the change), the object’s actor is destroyed, the object’s
MaxUseDurationelapses (on the server’s clock) or a new request of the same client arrives. The use state,IsObjectUsing,CurrentUsingObjectand the previous owner come back, also when only the object component was destroyed during the use. The user’s client gets the server’s end while it still controls the pawn; after a pawn change the pawn’s new owner gets noClientUsePhaseof the previous owner’s use. A client that shows a use ends it with the user’s pawn, the object’s actor or its object component. On the user’s machine these ends interrupt the use’s execution (UCISUseExecution::Interrupt), as a rejection does: the data object’s events fire, but itsInterruptUsingis not called (a C++ override of it does not see them). - Why: Without them an object stayed in use on the server when its user’s client vanished or changed its pawn.
- What changed: The server’s run of a remote client’s use ends as interrupted when the client’s pawn is destroyed (also on disconnect), its controller changes (one tick after the change), the object’s actor is destroyed, the object’s
- Ownership and IsObjectUsing per use.
- What changed: Overlapping uses of one object (
bServerValidationoff) hold its actor’s owner per use, so the owner of the latest remaining use, and after all of them the owner before the uses, comes back. The plugin writesIsObjectUsingon each machine itself (SetObjectIsUsingForUse: noServerSetObjectIsUsingfrom the client, no false while the use state names another character). The user’s machine keeps its use’sIsObjectUsingand owner against the late server state of its previous use (a quick second use under network lag), so the Server RPCs of its Action or MoveHandler on the object’s actor still leave with the owner. - Why: With the server running the use, each machine owns its own value, and a late state of a previous use must not undo the current one.
- What changed: Overlapping uses of one object (
- Keys.
- What changed: One
ExecutionIdspace per character component. A use that the server runs itself for a remote player’s character takes an id after every key the server has seen, a request with a key that such a use took is rejected (Occupied), and the client continues after every id that the server’s phases showed it. When a remote player’s controller takes a character, the server sends that client the character’s last key (ClientSyncExecutionKeys, one tick after the change), so that its next use takes a newer key; a use of that client that still waits for an answer and has a key up to that one ends as rejected.CIS::MaxExecutionId(MAX_int32 / 2):ServerRequestUseandServerUsePhasewith a larger key are malformed and disconnect the client. - Why: The key of a predicted use must name one use on both machines, and must not overflow.
- What changed: One
- A pawn given to a client that shows its use; release before the object’s delegate.
- What changed: A client that shows a use of a pawn and is given that pawn ends the shown use (at the server’s phase of it, or at the latest one tick after the pawn change), so the new owner can use objects with the pawn at once. On the authority a use releases what it took (the use state; for the server’s run of a remote client’s use also
IsObjectUsing,CurrentUsingObjectand the owner of the object’s actor) before the object’sOnUseFinishedDelegatefires, so a server-side listener of that delegate finds the object free; on a client that shows the use,IsObjectUsingandGetUseStateare the replicated values inside it. New API:UCISUseExecution::OnReleasing,FCISActiveUse::bReleasedOnAuthority. - Why: Before, the shown use never ended on the new owner and the pawn could not use any object there; and a server-side listener (an AI that uses the object again) found the object still in use.
- What changed: A client that shows a use of a pawn and is given that pawn ends the shown use (at the server’s phase of it, or at the latest one tick after the pawn change), so the new owner can use objects with the pawn at once. On the authority a use releases what it took (the use state; for the server’s run of a remote client’s use also
- Known limitations of networked uses.
- What changed: The hold is measured on the server’s clock, and a hold notice sent again after packet loss shortens the hold that the server measures by about one round trip (
HoldTimeTolerancecovers about one frame plus jitter). During its own use, a client’s copy of the object ignores an owner that the server gives the object’s actor, and after the use keeps the owner from before the use until the server changes it again. A use that the server runs for a remote player’s character is not shown on that player’s machine, and a request sent before the server’s phases of such a use arrive can be rejected (Occupied) once. When the server changes an object right after it rejected a client’s use of it, and under packet loss that change reaches the client before the rejection, the client’s copy takes the older state and usable flag of the rejection until the server changes them again. Upgrading from 1.x: every change lists all of them. - Why: They are the edges of prediction over an unreliable network that 2.0 does not close; none of them affects a standalone game.
- What changed: The hold is measured on the server’s clock, and a hold notice sent again after packet loss shortens the hold that the server measures by about one round trip (
- Movement prediction.
- What changed: A remote client calls
FlushServerMovesof its character’sUCharacterMovementComponentright beforeServerRequestUse, as the Gameplay Ability System does before aLocalPredictedability, so the server no longer applies the root motion of a MoveHandler that runs on the server (AllMachines,LocalPredictedorServerOnly, for example throughUCISRootMotionMovementTask) to a movement that the client made before the use started. The client may send one more small movement RPC per use; the server sends nothing more. Measured in single runs on one machine, with 100 ms of packet lag each way and a 1 s root-motion movement at 200 cm/s: with the client’s movement send interval raised to 0.1 s so that a movement of about 0.06 to 0.09 s was held back at the press, 3 corrections per use without the flush (the same code with the flush removed) and 0 with it; with the engine’s default send interval no run held a movement at the press, and all had 0 corrections with and without the flush. - Why: When the client had held a movement back at the start of the use, the server applied the root motion to that movement as well, and corrected the client: a visible snap at the start of the movement.
- What changed: A remote client calls
Replication cost
Section titled “Replication cost”- #21 Templates replicate only when they must; N5.
- What changed: An Action or MoveHandler template of an interaction object is replicated only when it runs
SharedPerObject, hasbWithReplicationon and its class has RPCs or replicated properties of its own (CIS::Migration::ClassHasCustomNetFields; the plugin’s relays andbReplicatedBooldo not count). Every other template is no longer replicated: each machine uses its own copy, and the deprecated relays still reach the templates of class defaults, Blueprints and placed actors (they have stable names).UCISInteractionObjectComponent::ReplicateSubobjectsstarts fromSuperinstead of true, so an idle object no longer writes a bunch at every net update. When the object’s actor uses the registered subobject list, the component joins it atBeginPlayon the server and registers those templates (removed at the newEndPlay); the list is fixed then (a Blueprint recompiled during play, or a template that C++ puts into the settings afterBeginPlay, does not change it). N5: a template withbWithReplicationoff is never replicated. - Why: 1.x’s
ReplicateSubobjectsalways returned true and replicated every template, so hundreds of idle objects cost bandwidth at every net update; and the server could fail its checkIsSupportedForNetworkingfor a template withbWithReplicationoff (N5). - What to do: Give a class that must replicate a variable a replicated property or an RPC of its own; a C++ override of
ReplicateSubobjectscallsSuper.
- What changed: An Action or MoveHandler template of an interaction object is replicated only when it runs
- Push model.
- What changed:
ObjectState,bObjectIsUsingandUseStateofUCISInteractionObjectComponentare push-based (effective withNet.IsPushModelEnabled); every write of the plugin marks them dirty.bUsablestays without the push model (Blueprints write it directly). - Why: Push-based properties are not compared at every net update.
- What to do: C++ subclasses that write these protected fields directly call the setters or
MARK_PROPERTY_DIRTY_FROM_NAME.
- What changed:
- InteractionSettings is no longer replicated; N6.
- What changed:
InteractionSettingsofUCISInteractionObjectComponentis no longer replicated: every machine reads its own copy, built from the same class defaults, Blueprint or level data. A client reads its own copy for the data object (sides, angles, distances, use time, Conditions), the Actions and MoveHandler of the use andbTransferOwnershipDuringUse;MaxUseDurationandbManageOwnerDormancyare read on the server only. A change that the server makes at runtime no longer reaches the clients; a server-only change of what the server check uses (the sides,InteractionDistance) makes the client’s own check and the server’s disagree, and the request is rejected. N6: the Conditions of an object spawned at runtime are no longer null on the clients (1.x replicated them as null), so a client evaluates its own Conditions for its own character there too. - Why: The settings are static data of the asset, and replicating them cost bandwidth and delivered the Conditions of runtime-spawned objects as null (N6).
- What to do: Make runtime changes of
InteractionSettingson every machine; a Condition that must not run on a client checks where it runs.
- What changed:
- Owner dormancy.
- What changed:
bManageOwnerDormancytakes effect on the server, for an object’s actor that replicates, in a game that is not standalone. The object’s actor goesDORM_DormantAllatBeginPlay(an author’sDORM_Neveris kept,DORM_InitialandDORM_DormantPartialuntil the first use); a use wakes it before anything of the use replicates; 1 s after the use, with no use or hold left, it is dormant again. The plugin’s setters of the object state, the usable flag andIsObjectUsingflush the dormant actor (FlushNetDormancy), and a late joiner gets the current state. - Why: Hundreds of idle objects should cost nothing; dormancy is off by default because it stops the replicated variables and multicasts of the object’s own Blueprints.
- What to do: Do not turn it on for objects whose own Blueprint or C++ replicates variables or multicasts, whose Blueprints write
bUsabledirectly, whose dormancy other code sets, or whose Action or MoveHandler sends a Server RPC on the object’s actor at the start of a use.
- What changed:
- MoveHandler tick and CallRemoteFunction.
- What changed: A MoveHandler ticks only while its movement runs and never as a class default or an archetype (new overrides
IsTickableandGetTickableTickTypeofUCISInteractionObjectMoveHandler); a C++ subclass whoseTickacted beforeStartMovementor afterInvalidateHandlerno longer runs that code.UCISReplicatedObject::CallRemoteFunctionfinds the actor withGetTypedOuter<AActor>()and returns false without one. - Why: Every MoveHandler template and class default ticked in 1.x;
CallRemoteFunctionusedCastCheckedofGetOuter()->GetOuter(), a fatal error for an object without an actor there.
- What changed: A MoveHandler ticks only while its movement runs and never as a class default or an archetype (new overrides
- Measured cost.
- What changed: Single runs on one machine (editor build, about 45 to 60 server frames per second); the numbers vary with the frame rate and the machine, and the plugin’s tests check thresholds, not these values (at most 0.2 times the 1.x bunches and 0.5 times the 1.x bytes, per second and per server frame; with dormancy 0.1 times the bunches). 300 idle objects with two clients: 34.4 to 37.1 bunches/s and 3904 to 5053 bytes/s out of the server in two runs (1.x 22540.3 and 94751), the same as the scene without objects; with
bManageOwnerDormancy34.8 to 37.1 bunches/s. Per remote use: reliable out 7 and in 3 (1.x 10 and 5): the client sendsServerRequestUseand twoServerUsePhase, the server sendsClientConfirmUseand threeMulticastUsePhaseto each of the two clients; out bunches per use 7.53 to 13.25 (1.x 8.74) at 229 to 294 bytes (1.x 196), in two runs on the same server code. The use state adds an estimated 31 bytes of property data per client and use (three updates; without bunch and packet headers). - Why: The cost of idle objects was the main network problem of 1.x with hundreds of objects.
- What changed: Single runs on one machine (editor build, about 45 to 60 server frames per second); the numbers vary with the frame rate and the machine, and the plugin’s tests check thresholds, not these values (at most 0.2 times the 1.x bunches and 0.5 times the 1.x bytes, per second and per server frame; with dormancy 0.1 times the bunches). 300 idle objects with two clients: 34.4 to 37.1 bunches/s and 3904 to 5053 bytes/s out of the server in two runs (1.x 22540.3 and 94751), the same as the scene without objects; with
C++ API
Section titled “C++ API”- Deprecated functions.
- What changed:
SetOwnerOfActorandServerSetOwnerOfActorofUCISCharacterInteractionComponentare markedUE_DEPRECATED(2.0, …): a call gives a compiler warning; both still work (see #1, #2). The plugin’s own remaining calls (the 1.x path of a use without an execution, which ends throughFinishUseObjectandSetOwnerOfActor) are wrapped inPRAGMA_DISABLE_DEPRECATION_WARNINGS. Deprecated in doc comments only, kept and working: the relays ofUCISInteractionObjectAction(ServerStartObjectAction,MulticastStartObjectAction,ServerFinishAction,MulticastFinishAction) and ofUCISInteractionObjectMoveHandler(ServerandMulticastversions ofStartMovement,MovementAborted,FinishMovementandInvalidateHandler),UCISReplicatedObject::GetLegacyRelayObject, and the protectedServerSetCurrentUseObject. The plugin’s own path calls none of them.ServerSetCurrentUseObject(nullptr)from a client no longer ends the server’s run of its use. - Why: The server now sets the owner and the used object itself; the relays are replaced by the use phases. They stay for C++ code that calls them, as the compatibility rules of 2.0 require.
- What to do: Replace calls of
SetOwnerOfActorandServerSetOwnerOfActor(for example withAActor::SetOwneron the server), or wrap a call that must stay inPRAGMA_DISABLE_DEPRECATION_WARNINGS/PRAGMA_ENABLE_DEPRECATION_WARNINGS.
- What changed:
- Module dependencies.
- What changed: The plugin module depends on
DeveloperSettings(public) andNetCore(private). - Why: The project settings of the server check and the push model need them.
- What changed: The plugin module depends on
Component references and other fixes
Section titled “Component references and other fixes”- #16 A side is identified by its index.
- What changed: An interaction side is identified by its index in
InteractionSides, no longer by the equality of its two angles (operator==ofFCISInteractionObjectSidecompares only the angles, notbAngleSide). Two sides with the same angles now each use their own actions and their own move handler. Before, such a later side was never selected when it had the samebAngleSideas the earlier one; with the oppositebAngleSideit was selected but used the earlier side’s action and move handler. Moving between two such sides now hides and re-shows the object, as between any two sides. New C++ API:UCISInteractionObjectComponent::GetSideByIndex,FindSideIndex,GetActionIndexForSide,GetObjectActionForSideIndex;UCISInteractionDataObject::GetCurrentSideIndex,SetCurrentUseSideIndex.UCISInteractionDataObject::CurrentSideis deprecated: it is still kept in sync, but writes to it are ignored (the index selects the side). - Why: 1.x compared two sides by their two angles (
operator==), so a later side with the same angles was aliased to the earlier one; the server check of a use also needs a stable side index. - What to do: Content with two sides of identical angles now gets each side on its own (its own actions, its own move handler); if it relied on the old aliasing (the later side using the earlier side’s action and move handler), merge the two sides into one.
- What changed: An interaction side is identified by its index in
- N1 A side of half a turn.
- What changed: A side whose two angles are half a turn apart (within about 0.006° of 180°, e.g. −90..90) now covers the half circle around (
InteractionAngleMin+InteractionAngleMax) / 2, or the other half withbAngleSide. Before, such a side ignored where the character stood: withbAngleSideit was valid from every direction; without it, it was never valid, or valid from every direction at some object rotations, depending on float rounding. Content that relied on a 180° side being valid everywhere should use 0/0 withbAngleSide. Every other side gives the same result as before. The debug cone of such a side now shows its half circle (before, a cone with no direction); the cone of every other side is drawn as before. - Why: 1.x decided such a side by float rounding, so it ignored where the character stood.
- What changed: A side whose two angles are half a turn apart (within about 0.006° of 180°, e.g. −90..90) now covers the half circle around (
- Component references (
FComponentReference).- What changed:
FCISPrimitiveComponentsOnActorandFCISSceneComponentsOnActorkeep their names and store the component inComponent(FComponentReference, Blueprint read/write) next toSocketName. A 1.x name (SelectedPrimitiveComponentName,SelectedSceneComponentName) moves intoComponent.ComponentPropertywhen an asset is loaded; saving the asset writes only the new field. C++ code that used the old fields must useComponent: they areSelected*ComponentName_DEPRECATEDnow and only read on load. New C++ API:UCISFunctionLibrary::ResolveComponentReference,FCIS*ComponentsOnActor::IsSet. The lookup (used byGetPrimitiveComponentWithName,GetSceneComponentWithName, the trigger components and the angle detection component) tries the property name first, then the object name as 1.x did. So a native component whose object name differs from its property name is now found; 1.x found nothing for it and the reference silently did nothing (e.g.CapsuleComponentof a character, whose object isCollisionCylinder;Mesh, objectCharacterMesh0). Two more cases resolve differently from 1.x: a name that is an object-reference variable of component type (a Blueprint variable or C++ property) now gives the component that variable holds, when that component belongs to the same actor (1.x ignored variables and compared object names only); and when property X holds a different component than the one whose object name is X, the property’s component is now used instead of the 1.x object-name match. A name that matches no component still gives none, and now also logs one warning per actor class and name. Every reference in the demo content resolves to the same component as in 1.x. One rare case of the load migration: a 1.x instance (a placed actor, a child Blueprint) that explicitly overrode its archetype’s name with an empty one now keeps the archetype’s migrated reference, because the migration acts only on a non-empty name; to restore the empty reference, clearComponenton that instance. - Why: The 1.x picker stored property names while the runtime compared object names, so a C++ component whose object name differs from its property name (the capsule of a character) was never found.
- What changed:
- #3 The facing angle without an angle detection component.
- What changed: When
AngleDetectionComponentis empty or names no component, the facing angle is measured to the object’s root component (atSocketName); 1.x measured it to the world origin. The angle detection component is looked up once per character and object instead of every frame. - Why: 1.x left the point at (0,0,0) when it found no component and measured the angle to the world origin.
- What changed: When
- N2 The side’s MoveHandler and Action start as null.
- What changed:
FCISInteractionObjectSide::InteractionObjectSideMoveHandlerandFCISInteractionObjectActionData::InteractionObjectActionstart as null. No behaviour change; theLogClasserror “not initialized properly” at editor start is gone. - Why: 1.x left them uninitialised, which logged an error at editor start.
- What changed:
- The component picker.
- What changed: The editor picker of
FCISPrimitiveComponentsOnActorandFCISSceneComponentsOnActorwritesComponent. It lists the components of the actor class by the name the reference stores: C++ components by property name (e.g.CapsuleComponent,Mesh), Blueprint components by variable name, inherited ones included; for a Blueprint component overridden in a child Blueprint, the override is read. Every name that 1.x listed is still listed, except component properties that are empty on the class defaults (variables assigned at runtime; in the demo contentSelectedKeyof the code locks) and editor-only components. Editor-only components (e.g.ArrowComponentof a character,SpriteComponentof an info actor such as the game mode, a Blueprint component marked Editor Only) are not listed because they are stripped from a cooked build: a reference to one resolves in the editor and PIE and gives nothing in the game.RootComponentis now also listed for a primitive reference when the root is a primitive component. A pick replaces the whole reference (PathToComponentandOtherActorare cleared) in one undoable step, and resetsSocketNamewhen the picked component has no such socket. The first entry, None, clears the reference and the socket. With several objects selected, only the names that every selected object’s class has are listed; when their actor classes differ, a pick keepsSocketName(the component of that name can differ per class). The socket row keeps the 1.x name field and adds a list of None and the sockets of the selected component. A saved name that the list does not offer is shown as “<name> (not found)” (1.x showed “Select Component”). The picker no longer asserts when the outer chain of the edited object reaches a non-actor class first (for example, settings on a widget of a Widget Blueprint; 1.x calledUClass::GetDefaultObject<AActor>on that class). New editor C++ API (Customization/CISComponentPickerUtils.h):CIS::Editor::GatherComponentCandidates,GatherCommonComponentCandidates,GatherSocketNames,FindOwnerActorClass,PickComponent. - Why: The engine’s picker of
FComponentReferenceshows no components in the Blueprint editor in UE 5.6 and 5.7 and cannot pick a socket, so the plugin keeps its own picker, on top of the new storage.
- What changed: The editor picker of
- #6 Overlap per trigger and primitive.
- What changed: An interaction object stays overlapped while any of the character’s triggers still overlaps any primitive of the object’s actor. Before, the first end overlap of one (trigger, primitive) pair ended the overlap of the whole object (
OnOverlapped(false); the object was hidden and not usable, and the cleanup could remove its data) although another primitive of the actor, or another trigger, still overlapped it; it came back only with the next begin overlap. New protected C++ API:UCISCharacterInteractionComponent::IsActorOverlappingAnyTrigger. - Why: 1.x kept one overlap flag per actor, so the first end overlap of one pair ended the overlap of the whole object.
- What changed: An interaction object stays overlapped while any of the character’s triggers still overlaps any primitive of the object’s actor. Before, the first end overlap of one (trigger, primitive) pair ended the overlap of the whole object (
- #9 Null triggers.
- What changed:
InitInteractionTriggersskips null entries of its array and binds the triggers after them. Before, a null entry crashed (an assertion inAddDynamic). - Why: 1.x crashed on a null entry.
- What changed:
- #5 The cleanup no longer interrupts a use.
- What changed: The periodic cleanup (
TimeToClearInvalidObjectsData) no longer removes the data of an object the character is using. Before, when the character left the object’s overlap during a use (for example moved by a move handler or an action), the next cleanup interrupted the use: the move handler gotMovementAborted, the action finished withbInterrupted = true, andOnEndUseObjectDelegatefired. Now the use runs to its end, and the first cleanup after the end removes the data if the object is still not overlapped. - Why: 1.x’s periodic cleanup interrupted the object the character was using as soon as the character left its overlap.
- What to do: Content that relied on walking out of the trigger to end an action that never finishes by itself must now end the use itself, with
TryInterruptObjectUsing(with the object’sObjectInterruptionType) or by finishing the action (FinishAction).
- What changed: The periodic cleanup (
- #15 Data of destroyed object components.
- What changed: The cleanup also removes the data of an object whose interaction component was destroyed while its actor stayed (before, only a destroyed actor removed it). Before, that data stayed as long as the character lived, and every 0.1 s update of the best object logged the warning “NULL object”. The removal comes with the next cleanup run, so up to
TimeToClearInvalidObjectsData(90 s by default) after the destruction. If the character was using that object, the removal ends the use as an interruption; 1.x had no such end (it crashed inFinishUseObjectonce the component was gone). The action and the move handler are subobjects of the destroyed component: only if they have not been garbage collected yet does the move handler getMovementAbortedand the action its interrupted finish (OnUseFinished, both with a null object component;ServerFinishActionis not sent, because the component is invalid). If the action finishes by itself after the component is gone, the use ends there in the same way. On either end, the data object’sOnObjectUseFinishedruns,SetObjectIsUsing(false)is skipped (there is no component), the owner of the object’s actor is reset as on the normal path (the data object keeps the actor for that), and the character’sOnEndUseObjectDelegatefires with a null object component;IsCharacterUseAnyObjectis false again. The same null object reachesOnEndUseObjectDelegatein two more cases where user code destroys the object’s actor during the end of a use: when the actor goes after the use was marked finished, for example in the move handler’sMovementFinished, which the end runs for a move handler still running withStartParallelWithMoveHandler; and when the end comes from a removal that has already unbound the actor’sOnDestroyed, for example the removal of every object’s data when the character is destroyed (OnCharacterDestroyed). An actor destroyed earlier in the end (for example in the data object’sOnObjectUseFinished) first removes the object’s data through itsOnDestroyed, and that end is broadcast with the object, as in 1.x. One more case comes from the start of a use (#12): when a listener ofOnStartUseObjectDelegatedestroys the object’s actor, the announced use does not start, and its immediate end reachesOnEndUseObjectDelegatewith a null object component (the component is gone by then); 1.x crashed there. Every other end of a use broadcasts as before, with the object. When the cleanup removes an entry whose object component is gone, the data object’sOnVisibleChangedandOnUsableChangedof that removal (the object hidden and made unusable) also get a null object component: a Blueprint data object should checkIsValidon it there. New C++ API:UCISCharacterInteractionComponent::GetTrackedObjectDataCount. - Why: 1.x never removed the data of a destroyed component whose actor stayed, warned about it every 0.1 s, and crashed when such an object was in use.
- What changed: The cleanup also removes the data of an object whose interaction component was destroyed while its actor stayed (before, only a destroyed actor removed it). Before, that data stayed as long as the character lived, and every 0.1 s update of the best object logged the warning “NULL object”. The removal comes with the next cleanup run, so up to
- #14 Removing a tracked object during the tick.
- What changed: User code that removes a tracked object while the character ticks its data objects (for example a data object’s
OnVisibleChangedthat destroys the object’s actor) no longer raises the ensure “Container has changed during ranged-for iteration”: the tick walks over a copy of the tracked objects, in the same order as before. A removed object is not ticked any more; an object added during the walk is ticked from the next frame. - Why: User code that removed a tracked object during the walk over the tracked objects hit the engine’s ensure.
- What changed: User code that removes a tracked object while the character ticks its data objects (for example a data object’s
- N3 A late FinishMovement.
- What changed:
UCISInteractionObjectMoveHandler::FinishMovementdoes nothing when the movement is not running (never started, or already finished and invalidated by the plugin). Before, such a call crashed. It happened withStartParallelWithMoveHandlerwhen the action finished before the move handler: the plugin ended the use, finished and invalidated the handler, and the handler’s own later finish (a timer, a latent node) dereferenced the cleared side. Now that late finish is ignored: no secondMovementFinished,OnMovementEndedor end of the use. - Why: A late finish crashed (for example with
StartParallelWithMoveHandler).
- What changed:
- #4 GetCountOfUsableObjects.
- What changed:
GetCountOfUsableObjectsreturns the number of tracked objects the character can use now (those for whichIsCanUseObjectis true), so it is never negative. Before, it changed by +1/−1 on every usable change, and every new data object took it down by one first (theUsable(false)of the reset when the object’s first side becomes valid): one or two usable objects gave 0, and it went below zero when they stopped being usable. The data object’sOnUsableChangedevent still fires exactly as before, that reset included. - Why: 1.x counted +1/−1 on usable changes, so the count drifted and went below zero.
- What changed:
- #11 The hold progress.
- What changed:
GetDelayedObjectProgressstays in [0,1]: 0 without a pending hold, 1 during a hold whose use time is at mostKINDA_SMALL_NUMBER. Such a hold completes at once: a use time of 0 (TimeToUseObject0, the default, orOverrideObjectUseTime0) and a negative one included (only C++ can set a negative time; the editor clamps both settings to ≥ 0). Before, a zero use time gave NaN fromTryUseObjectuntil the hold was checked in the next tick: the UI and the listeners ofOnDelayedObjectDataSetandOnClearDelayedObjectDatacould see it. Inf existed only inside that check (CheckDelayedObjectTime), while the use started: the listeners ofOnStartUseObjectDelegateand the use’s own start events insideUseObject(the data object’sOnObjectUseStarted, the start of the action and of the move handler) could see it. A use time from 0 toKINDA_SMALL_NUMBERstill starts its use in the same tick as before. A negative use time is a change: 1.x kept its progress at 0 or below and never started the use; now the use starts in the next tick, as with 0. - Why: A use time of 0, the default, divided by zero, and the UI got NaN or Inf.
- What changed:
- #20 TryUseObject uses the highlighted object.
- What changed:
TryUseObjectuses the highlighted object (BestObjectToUse, the object of the lastOnBestObjectForUseChangedDelegate) while it is still usable, and only otherwise selects afresh. Before, it always selected afresh, so when the best object changed between two updates of the highlight (everyUpdateCurrentUseObjectInterval, 0.1 s by default), the character used another object than the highlighted one. The highlight is still updated only by that periodic update. An override ofGetUsableInteractObjectorIsCanUseObjectthat names an object the character does not track no longer crashesTryUseObject: a highlight that anIsCanUseObjectoverride accepts although the character does not track it falls back to the fresh selection (GetUsableInteractObject, the 1.x path), and an untracked fresh selection uses nothing. - Why: The object that the UI highlighted could differ from the one the character used.
- What to do: A C++ override of
GetUsableInteractObjectthat blocks a use at press time (returns null, or another object, whileTryUseObjectruns) is no longer consulted while a usable highlight exists; it still chooses the highlight in the periodic update. Put such checks intoIsCanUseObjector into the object’s usable conditions (UsableConditions), whichTryUseObjectstill honours.
- What changed:
- #12 The character component follows its controller.
- What changed: The character component follows its character’s controller. A controller change is evaluated one tick later, against the controller the character has then (so an unpossess and a possess of the character in the same call stack change nothing, for example a pawn taken over from another controller (
AController::OnPossessunpossesses the old controller first) orUnPossessfollowed byPossessof the same pawn, while such a pair split across tick groups may still shut the component down and initialise it again; and nothing of the plugin runs insidePossessorUnPossess): the component initialises (binds the triggers, starts the best-object update and the cleanup) when the character is locally controlled by a player controller (its controller is anAPlayerController; anAAIControllerdoes not count, even withbWantsPlayerState) and the component is not initialised, and shuts down when the character is no longer locally controlled. So it now initialises a pawn that a player possesses after it was spawned, a client pawn whose controller arrives after BeginPlay, and a pawn that the client knew before the server possessed it. An AI keeps the 1.x rule: it is initialised by BeginPlay when its controller possessed it before, and never later (an AI controller spawned after BeginPlay, for example bySpawnAIFromClassorSpawnDefaultController, leaves it untracked, as in 1.x). It shuts down onUnPossess, on the possession of another pawn, and onDetachFromControllerPendingDestroywhile the pawn is not destroyed yet (the usual death sequence): the triggers are unbound, the timers stop, the data of every tracked object is removed as byRemoveInteractionObjectData, a pending hold that has not started its use is cleared, and the highlight is cleared with oneOnBestObjectForUseChangedDelegate(nullptr). A running use is not interrupted: its action and move handler finish as they would have,OnEndUseObjectDelegatefires, and then the object’s data is removed. A later possession by a player initialises again (data still kept for a running use is reused, not duplicated); the data objects of the other tracked objects are new ones then, so Blueprint state stored on the old data objects (variables of aDataObjectClassBlueprint) is lost: keep such state on the object or on the character. Before, the component initialised only in BeginPlay and only when the character was locally controlled by then: a character possessed later never tracked any object, and an unpossessed character kept tracking, highlighting and using objects.InitCharacterInteractionComponentnow does nothing while the component is initialised (before, every call rebuilt the trigger set from the settings and restarted the timers), and also while it shuts down: a call from user code that the shutdown runs (a data object’s event during the removal, a listener of the highlight’sOnBestObjectForUseChangedDelegate(nullptr)) does nothing; if that user code possessed the character again, the evaluation of that controller change initialises the component one tick later. The destruction of the character and an end of play (the world is torn down, the level is unloaded) are handled as before. When a listener ofOnStartUseObjectDelegateclears the hold (TryInterruptObjectUsing) or removes the object (destroys its actor), the use does not start andOnEndUseObjectDelegatefollows at once; before, the component crashed there. New C++ API:UCISCharacterInteractionComponent::HandleCISCharacterControllerChanged(the handler ofReceiveControllerChangedDelegate; named so that a subclass’s ownOnCharacterControllerChangeddoes not clash with it),ShutdownCharacterInteractionComponent,bInteractionInitialized(protected). - Why: 1.x initialised the component only in
BeginPlayand only when the character was already locally controlled, so a respawn or a controller that came later broke interaction, and an unpossessed character kept interacting. - What to do: A project that called
InitCharacterInteractionComponentitself after a possession can drop the call (it is harmless); one that called it again to rebuild the triggers must callInitInteractionTriggersinstead. A C++ override ofInitCharacterInteractionComponent(a Blueprint-callable C++ virtual, which Blueprints cannot override) now also runs one tick after each controller change that makes a player-controlled character locally controlled while the component is not initialised;Superdoes nothing while the component is initialised, so code afterSuperthat must run once per init should testbInteractionInitializedbefore callingSuper. Code that reads the component’s state right afterPossessorUnPossessmust wait one tick. An AI possessed after BeginPlay that should interact still needs its component’sInitCharacterInteractionComponentcalled after the possession (from a component subclass), as in 1.x. Content that relied on an unpossessed or dead (detached) character still tracking or using objects must keep the character possessed.
- What changed: The character component follows its character’s controller. A controller change is evaluated one tick later, against the controller the character has then (so an unpossess and a possess of the character in the same call stack change nothing, for example a pawn taken over from another controller (
- #12 The trigger set across re-inits.
- What changed: The trigger set that user code passes to
InitInteractionTriggers(for example a Blueprint’s own triggers instead of the settings’TriggerComponents) stays in force when the component shuts down and initialises again: every laterInitCharacterInteractionComponentbinds the set of the last such call again (those of its triggers that still exist), instead of the settings’ set; without such a call it binds the settings’ set, as before. In 1.x nothing unbound the triggers, so that set stayed bound for good; without this, the init that #12 adds would replace it with the settings’ set, afterUnPossessand a laterPossess, and at the first possession by a player after BeginPlay. “The last call wins” as in 1.x, with two consequences: an empty array is a set too (nothing is bound after a re-init either), and a call made before the component’s own BeginPlay init now also survives that init (1.x replaced it with the settings’ set then).InitInteractionTriggersstill binds whether or not the character is locally controlled, as in 1.x. - Why: Without it, the new re-initialisation of #12 would replace a trigger set that user code passed after an unpossess and a possess.
- What to do: To go back to the settings’ triggers, pass them to
InitInteractionTriggers.
- What changed: The trigger set that user code passes to
- #13 A character component without a character.
- What changed: A
UCISCharacterInteractionComponenton an actor that is not anACharacterno longer crashes.InitializeComponentlogs the error “UCISCharacterInteractionComponent must be owned by an ACharacter (<owner>)” once, turns the component’s tick off, and every Blueprint function of the component does nothing and returns its default:TryUseObject,InitInteractionTriggersandInitCharacterInteractionComponentdo nothing;TryInterruptObjectUsing,IsCanUseObject,IsCharacterUseAnyObjectandIsHasDelayedObjectDatareturn false;GetCountOfUsableObjectsreturns 0,GetDelayedObjectProgress0,GetCurrentUsingObjectandGetUsableInteractObjectnull. Before, such an actor (the component in its class, or added at runtime) crashed inInitializeComponent. Nothing changes for a component on a character, also before itsInitializeComponent. - Why: 1.x crashed when the owner was not an
ACharacter.
- What changed: A
- #7 The attached mesh of UCISAnimNotifyStateAttachMesh.
- What changed:
UCISAnimNotifyStateAttachMeshkeeps the attached mesh per skeletal mesh. In 1.x one notify object, shared by every mesh that plays the animation, kept a singleSpawnedMesh. While two meshes were inside the notify window, the firstNotifyEnddestroyed the other mesh’s attachment and its own stayed for good. ANotifyEndwithout aNotifyBegin, or for a mesh without an owner, destroyed the mesh spawned last. A repeatedNotifyBeginleaked one mesh. Now eachNotifyEnddestroys only its own skeletal mesh’s attachment, and a repeatedNotifyBeginreplaces it. An attachment whose skeletal mesh was destroyed before itsNotifyEndis destroyed at the notify’s nextNotifyBegin/NotifyEnd.SpawnedMeshstays, deprecated and unused. - Why: 1.x kept one
SpawnedMeshin the notify object, which an animation asset shares between every mesh that plays it.
- What changed:
- #22 RootMotionMoveTo with no speed.
- What changed:
RootMotionMoveTowith no speed: 1.x divided by the speed, so a speed of 0 gave the root motion sourceDuration = +inf. Now a speed from 0 toKINDA_SMALL_NUMBERwith a distance aboveKINDA_SMALL_NUMBERgives a finiteDuration = Max(TimeOut, 0.05)s. Observably unchanged: the source still holds the character at its start, and the task still ends byTimeOut(OnRootMotionMovementFinished, thenOnTimeOut).OnTimeOutalso fires when that hold source ends by itself (itsDurationis theTimeOut, counted in world time), which can happen before the task’s own time-out check, for example with a time dilation above 1; the order,OnRootMotionMovementFinishedthenOnTimeOut, is the same. Kept from 1.x: any other speed, a negative one included, keepsMax(Dist / speed, 0.05); with no speed and a distance up toKINDA_SMALL_NUMBERthe duration is 0.05 s, as 1.x got at distance 0 fromMax(0/0 = NaN, 0.05). - Why: 1.x divided by the speed, so a speed of 0 gave an infinite duration.
- What changed:
- #23 MoveToDistance.
- What changed: The tooltip of
RootMotionMoveTonow says “MoveToDistance is not used; kept for compatibility”. The pin still has no effect, as in 1.x, which had no tooltip. - Why: Making the pin work would change the behaviour of content that filled it in.
- What changed: The tooltip of
- #8 Replication of bUsable.
- What changed:
bUsableofUCISInteractionObjectComponentis registered for replication (DOREPLIFETIME: to every client, no push model, so a direct Blueprint write on the server replicates too). 1.x marked itReplicatedbut left it out ofGetLifetimeReplicatedProps, so it replicated only through the engine’s fallbackNet.AutoRegisterReplicatedProperties(1 by default in UE 5.6), not at all in a project that sets that variable to 0; only a project that sets it to 0 and also setsNet.EnsureOnMissingReplicatedPropertiesRegister=1got an ensure for it. With the engine defaults nothing changes: the property replicated before exactly as it does now. WithNet.AutoRegisterReplicatedProperties=0: if the server callsSetUsable(false)(or setsbUsable), clients now see it, and their characters stop seeing and using the object; a client that receives the object’s actor gets the server’s value instead of the class default. Content that setsbUsableonly on a client (SetUsableon a client sendsServerSetUsableonly where the object’s actor is an autonomous proxy, which a replicated object is not unless the server made it one, so it usually changes only the local value; a direct write of the variable on a client) keeps that local value only until the server’s value changes, and a copy of the actor the client receives afresh (it joins, or the actor becomes relevant to it again) starts with the server’s value. - Why: 1.x marked
bUsableReplicatedbut never registered it. - What to do: Call
SetUsableon the server; for usability that differs per player, useUsableConditions. A C++ subclass ofUCISInteractionObjectComponentthat registeredbUsableitself in itsGetLifetimeReplicatedPropswith another condition now hits the engine’s checkf on the duplicate registration: remove its own registration.
- What changed:
- Build dependencies.
- What changed: The editor module
CharacterInteractionSystemEditorno longer listsUMG,UMGEditorandBlutilityin its public dependencies (it includes nothing from them). A project module that depends onCharacterInteractionSystemEditorand used their headers through it must add them to its own Build.cs. The runtime module no longer depends onBlutilityandUMGEditorin editor builds (they were private, so no project is affected), and the editor module is limited toWin64,MacandLinux, the platforms with an editor. - Why: The modules declared dependencies they did not use, and the editor module listed platforms that have no editor.
- What changed: The editor module
- Debug Lines Depth Priority.
- What changed: The editor label of
DebugLinesDephPriorityonUCISInteractionObjectComponentis now “Debug Lines Depth Priority”. The property name is unchanged, so saved values keep loading. - Why: The label showed the misspelt code name.
- What changed: The editor label of
Plugin and demo content
Section titled “Plugin and demo content”- The demo ships inside the plugin.
- What changed: The demo moved from the 1.x demo project (
/Game/CharacterInteraction) into the plugin, to/CharacterInteractionSystem/Demo, with the same folders and asset names. Its level is/CharacterInteractionSystem/Demo/Levels/CISExampleMapand ships its built lighting. The plugin declares the demo’s gameplay tags natively, in its runtime module.BP_CISCharacteruses Enhanced Input (IMC_CISDemowithIA_CISDemo_Move,IA_CISDemo_LookandIA_CISDemo_Jumpin/CharacterInteractionSystem/Demo/Input) instead of the legacy input mappings of the project, and the plugin depends on the Enhanced Input plugin. The level has an unbound Post Process Volume with a fixed exposure and no motion blur. The plugin package also contains Upgrading from 1.x: every change and Changelog. - Why: The demo works in any project without copying content, gameplay tags or input mappings into it.
- What to do: Open
/CharacterInteractionSystem/Demo/Levels/CISExampleMap. A project whose default input classes are the legacy ones cannot run the demo character. If your project has a copy of the 1.x demo in/Game/CharacterInteraction, see Upgrading from 1.x: every change, “An old copy of the demo in your project”.
- What changed: The demo moved from the 1.x demo project (
- The demo character is the UE5 mannequin.
- What changed:
BP_CISCharacteruses the UE5 mannequin Manny:SKM_Manny_Simpleon the UE5 skeletonSK_Mannequin, withPA_Mannequinand its materials and textures, in/CharacterInteractionSystem/Demo/Characters/Mannequins. The interaction animations of the demo were retargeted to the UE5 skeleton and keep their paths, names, notifies, slots and sections. Idle, walk and run play the 1D blend spaceBS_CISLocomotionwith the idle, walk and jog animations of the UE5 Third Person template, and the jump plays its jump, fall and land animations; walk and run speeds are unchanged.ABP_CIStargets the UE5 skeleton and keeps its variables and states.KeySocketis onhand_rof the UE5 skeleton. Removed: the UE4 mannequin of the 1.x demo (in 1.x/Game/CharacterInteraction/Meshes/Mannequin), the blend spaces BS_CISMovement and BS_CISMovementRun, the UE4 movement animations and FirstPerson_Idle. - Why: The demo shows the current UE5 character, and its interactions work with UE5 characters and animations.
- What to do: If you made Blueprints, animation Blueprints or animations from the demo character, see Upgrading from 1.x: every change, “The demo character”.
- What changed:
- The demo level’s sun casts dynamic shadows.
- What changed: The directional light of
CISExampleMap(Light Source) is movable instead of stationary. The other lights of the level stay stationary and use the level’s built lighting. - Why: The built lighting of the 1.x level was out of date for the sun, so in a project that allows static lighting neither the walls nor the character cast a shadow.
- What to do: Nothing.
- What changed: The directional light of
TPCharacterremoved fromWBP_InteractionObjectIcon.- What changed: The plugin’s widget
WBP_InteractionObjectIconno longer has the variableTPCharacter(of the demo’sBP_CISCharacterclass). Nothing in the plugin used it. - Why: The plugin’s own content no longer refers to the demo.
- What to do: A Blueprint of yours that reads or sets
TPCharacteron this widget or on a child of it no longer compiles: remove that use, or give your child widget a variable of its own.
- What changed: The plugin’s widget
- Content saved with 2.0.
- What changed: Every asset of the plugin (
/CharacterInteractionSystem) and of the demo (now/CharacterInteractionSystem/Demo; the 1.x demo project kept it in/Game/CharacterInteraction) is saved with 2.0. The Actions, MoveHandlers and Conditions, and the objects and the level that hold them, also carryCISVersion. - Why: The shipped content holds the 2.0 settings below instead of being migrated on every load.
- What to do: Nothing. To go back to 1.x, use the 1.x plugin with its own content.
- What changed: Every asset of the plugin (
- Execution policies of the demo Actions and MoveHandlers.
- What changed:
BP_InteractionObjectActionPlayMontage,BP_PlaySoundAction,BP_InteractionObjectActionPickUp,BP_CISNullObjectAction,BP_BaseMoveHandler,BP_MoveToComponentMoveHandlerandBP_MoveToDistanceMoveHandlerareInstancedPerExecutionwithAllMachines.BP_CodeLockActionisInstancedPerExecutionwithLocalOnly. - Why: None of them keeps state between uses, so each use gets its own copy. Animations, sounds and the move to the object play on every machine; the pick-up Action only plays an animation (the object handles the item); the code lock Action only changes the user’s camera, input and UI.
- What to do: A child Blueprint, a level, or an object of yours whose settings use these classes, saved with 1.x, takes these values (an Action with
bWithReplicationon, the code lock’s included, takesAllMachinesinstead ofLocalOnly). Set the values explicitly on the Action and MoveHandler templates in the Blueprint to keep the 1.x behaviour. See Upgrading from 1.x: every change, “The plugin’s and the demo’s content”.
- What changed:
- Server validation of the demo Conditions.
- What changed: The distance and trace Conditions of the demo and the plugin’s examples are
Validate; the two “is object rendered” Conditions stayDoNotValidate. - Why: The server can check distance and line of sight; only the client knows what it renders.
- What to do: The server applies no tolerance to a validated Condition and sees an approaching character about one network trip late, so a use at the edge of the demo’s
Distance(180) can be rejected asConditionFailed; a hold avoids most of this. To widen the margin, raiseDistanceon the Condition in the object’sUsableConditions, or set it toDoNotValidate. Your own 1.x objects that use these Conditions are validated too (see Upgrading from 1.x: every change, “The plugin’s and the demo’s content”).
- What changed: The distance and trace Conditions of the demo and the plugin’s examples are
- Owner dormancy on the demo objects.
- What changed:
bManageOwnerDormancyis on forBP_BaseInteractObject, its children exceptBP_CodeLock, andBP_PickUpInteractionObject. It stays off forBP_CodeLock,BP_Testand the plugin’sBP_CISBaseInteractableActor.NetUpdateFrequencyis unchanged. - Why: Idle demo objects should cost nothing on the network.
BP_CodeLockhas Server and Multicast events of its own and sends a Server RPC at the start of a use;BP_TestandBP_CISBaseInteractableActordo not replicate. - What to do: Turn
bManageOwnerDormancyoff in a child Blueprint of a demo object that replicates state of its own, writesbUsabledirectly, sets its dormancy, or whose Action or MoveHandler sends a Server RPC at the start of a use.
- What changed:
- The code lock opens on the server.
- What changed:
BP_CodeLocksetsCIS.CodeLock.Openedon the server, in itsServerPassMatchedevent, instead of on the user’s client inPassMatched.ServerPassMatchedandMulticastPassMatchedare now reliable, so the result cannot be lost on the way. - Why: The state set on the client never reached the server or the other clients, and 2.0 logs a warning for such a call.
- What to do: Nothing: the code lock now reads as opened on every machine.
- What changed:
- The demo level closes the locked door on the server only.
- What changed: The level Blueprint of
CISExampleMapsetsCIS.Object.Door.ClosedonBP_InteractionDoor_Lockedwhen the lever is pulled only whereIsServeris true. - Why: The lever’s state change also arrives on the clients, which then called
SetObjectStatethemselves (local only, with a warning in 2.0). - What to do: Nothing.
- What changed: The level Blueprint of