# Interaction, carrying and throwing Owns the one system through which a player acts on the world: what can be touched, how it is targeted, what the prompt says, how the request reaches the server, and what happens to things a player picks up. It does **not** own any prop's behaviour: a bench belongs to [Crafting.md](Crafting.md), a downed teammate to [Combat.md](Combat.md). This document says how a player acts on a thing; the feature docs say what the thing does. Read [Architecture.md](Architecture.md) first. Built in step 6, before combat, because reviving a teammate and inserting a piece into a bench are the same act on different props and neither should invent its own mechanism. ## One system Before the earlier projects had this, their design docs described four implicit interaction models: a raycast "use", a proximity push, a trigger volume you stood on, and an undefined "confirm". In a game where the world is the interface that becomes a dozen mechanisms that each feel slightly different. One system instead, and one interface, which is justified by the count: the two source projects between them named a bench, a station, a container, a ticket, a contract card, a counter, a locker, a plinth, a lever, a cart's handles, a cart being righted, and a downed player. Twelve implementations is not speculative abstraction. **The boundary that keeps it one system:** an interaction is a **discrete act on a fixed prop**. Carrying is **continuous possession of a moving object**. Pushing is continuous force and is neither. Picking something up therefore goes through the carry component, not the interaction service, even though both are on the same key; the interaction component routes the press. ## Layout ``` Source//Interaction/ ├── Interactable.h // IInteractable, the UInterface every prop implements (on an actor or a component) ├── InteractionTypes.h // FInteractionPrompt, FInteractionQuery, the reason tags ├── InteractionSubsystem.h // UWorldSubsystem: registry, server-side validation, the one dispatch path ├── InteractionComponent.h // on the player: targeting, prompt, the request RPC, hold-to-confirm ├── InteractionHighlightComponent.h // on a prop: custom depth outline, in-range and targeted states ├── Carryable.h // ICarryable and UCarryableComponent: what can be held, and by whom └── CarryComponent.h // on the player: hands, pick up, drop, throw, hand over Content/Interaction/ ├── M_Outline_PP // the post-process outline material reading custom stencil └── WBP_InteractionPrompt // one prompt widget, see UI.md ``` Trace channel `Interactable` is `ECC_GameTraceChannel1`, declared in `DefaultEngine.ini`; every interactable's collider responds to it and nothing else does. Carryables additionally sit on `Carryable` (`ECC_GameTraceChannel2`). ## Types at a glance | Type | Lifetime | Notes | | --- | --- | --- | | `IInteractable` | interface | Implemented by a prop actor, or by a component when one actor holds several props | | `FInteractionPrompt`, `FInteractionQuery` | value | The prompt is the output of one function, on both peers | | `UInteractionSubsystem` | world | Registry and the server-side funnel | | `UInteractionComponent` | per player body | Owner only; the only thing that sends the request | | `UInteractionHighlightComponent` | per prop | Presentation | | `ICarryable`, `UCarryableComponent` | per carryable actor | Replicated `CarriedBy` | | `UCarryComponent` | per player body | Two hands | ## The interactable contract ```cpp USTRUCT(BlueprintType) struct FInteractionQuery { GENERATED_BODY() TObjectPtr Instigator; // who is asking TObjectPtr Body; // their body, for range and carried state FGameplayTagContainer BodyTags; // State.Carrying, State.Downed, ... from the ability system }; USTRUCT(BlueprintType) struct FInteractionPrompt { GENERATED_BODY() FGameplayTag Verb; // Interact.Verb.Open, .Take, .Insert, .Revive, .Pull ...; localised by the widget FText TargetName; // "Assembly bench", "Rainer" (a downed player's name is a name, not a key) bool bEnabled = true; FGameplayTag Reason; // Interact.Reason.HandsFull, .Occupied, .NotYours, .OutOfReach ... shown when disabled float HoldSeconds = 0.f; // > 0 for irreversible verbs: scrap, cancel, leave }; UINTERFACE(BlueprintType) class UInteractable : public UInterface { GENERATED_BODY() }; /** * Anything a player can touch. The prompt and the permission come from ONE function so what the prompt says is * possible and what the server allows can never disagree. Interact() runs on the authority only. */ class IInteractable { GENERATED_BODY() public: virtual FInteractionPrompt GetPrompt(const FInteractionQuery& Query) const = 0; // pure, called every frame locally virtual void Interact(const FInteractionQuery& Query) = 0; // SERVER ONLY, after re-validation virtual FVector GetInteractionLocation() const = 0; // what the server measures reach against virtual bool IsLocalOnly() const { return false; } // true: acts on this machine's own state, never forwarded virtual float GetPriorityBias() const { return 1.f; } // a person outranks the furniture they stand at virtual FGameplayTag GetPropTag() const = 0; // stable identity for telemetry: Prop.Bench, Prop.ReviveTarget }; ``` `IsLocalOnly` exists for props like a personal locker or a settings surface: things that only change this player's own local state. They run their `Interact` on the client without a round trip and are never sent to the server. Everything touching shared state stays forwarded. Without the flag, an earlier project had a guest's locker setting the host's class. ## The subsystem: one funnel ```cpp UCLASS() class UInteractionSubsystem : public UWorldSubsystem { GENERATED_BODY() public: // Reach. Tokens, not literals, so they are tuned in one place and the prompt and the server agree. static constexpr float ReachCm = 180.f; static constexpr float HighlightCm = 350.f; static constexpr float ReachToleranceCm = 50.f; // the client's frame and the server's are a tick apart bool IsInReach(const FVector& BodyLocation, const IInteractable& Target, float Tolerance = 0.f) const; /** * SERVER ONLY. The only path an interaction takes. Never trusts the client's claim that it was close enough * or that the prompt was enabled: re-checks reach with tolerance, re-runs GetPrompt for permission, then calls * Interact. Emits prop_interacted. One distance comparison is the difference between a convenience and an * exploit. */ bool ServerTryInteract(const FInteractionQuery& Query, TScriptInterface Target); // Registry, so the highlight sweep and later systems can ask "what is near" without a world search. void Register(TScriptInterface Prop); void Unregister(TScriptInterface Prop); void QueryNear(const FVector& Point, float Radius, TArray>& Out) const; }; ``` Props register in `BeginPlay` on every peer and unregister in `EndPlay`. The RPC carries the prop as an object reference: map-placed actors and replicated actors both resolve on the server through their network identity, which is why there is no hand-rolled handle table. A prop that is neither placed nor replicated cannot be interacted with over the network, by construction, which is the correct failure. ## The player's component ```cpp /** * The targeting, the prompt, the press. Owner only. Finds what you are looking at, offers it, and on the press * either routes to the carry component (a carryable with free hands) or sends the request to the server. */ UCLASS() class UInteractionComponent : public UActorComponent { GENERATED_BODY() public: TScriptInterface GetCurrentTarget() const; FInteractionPrompt GetCurrentPrompt() const; // the HUD reads this; it never computes its own void OnInteractPressed(); void OnInteractReleased(); // released matters for hold-to-confirm protected: // TickComponent, owner only: // 1. Sweep a 15 cm sphere from the camera along its forward for ReachCm on the Interactable channel. // What you LOOK at wins; the earlier overlap-and-angle-score model is used only for the highlight set. // 2. If the hit resolves to an IInteractable (on the hit component first, then its owner) and it is in // reach per the subsystem, it is the target; GetPrompt(query) is the prompt. Otherwise no target, no prompt. // 3. Every 0.25 s, QueryNear(HighlightCm) drives the in-range outline set; the target gets the strong outline. // Press: // target is ICarryable and hands are free -> Carry->RequestPickUp(target) (not an interaction) // carrying and target accepts an insert -> ServerRequestInteract(target) (Insert is an interaction) // target.IsLocalOnly() -> target->Interact(query) locally // prompt.HoldSeconds > 0 -> start the hold; send on completion; release cancels silently // otherwise -> ServerRequestInteract(target) UFUNCTION(Server, Reliable) void ServerRequestInteract(UObject* Target); // builds the query from the owning controller and calls the subsystem's ServerTryInteract. // The client sends the target and nothing else; the server reads the body's position itself. }; ``` Two players interacting with one prop in the same frame resolve host-side by arrival order: the second fails the re-run `GetPrompt` if the first changed the prop's state. Contested props that need a soft lock ("someone is using this", with a name) get it in their own `GetPrompt`; the system does not know what contention means. ## Carrying ``` [SALVAGED] Two hands, and things you carry are real. Most objects take one hand; big ones take both and, in first person, sit in the middle of your view. Carrying something two-handed blocks Interact, Insert and attacking until you put it down. Drop is always available and instant. Nothing you carry is ever destroyed by dropping, throwing or bumping into a wall with it. ``` ```cpp UINTERFACE() class UCarryable : public UInterface { GENERATED_BODY() }; class ICarryable { GENERATED_BODY() public: virtual UCarryableComponent* GetCarryable() = 0; }; /** On any actor that can be held. The actor is the object; this is its carry state. */ UCLASS(meta = (BlueprintSpawnableComponent)) class UCarryableComponent : public UActorComponent { GENERATED_BODY() public: UPROPERTY(EditDefaultsOnly) bool bTwoHanded = false; UPROPERTY(EditDefaultsOnly) float CarryWeight = 1.f; // against the carrier's CarryCapacity, see Stats.md UPROPERTY(EditDefaultsOnly) bool bBlocksAttack = false; // one-handed loot does not; a crate does UPROPERTY(EditDefaultsOnly) FName AttachSocket = TEXT("carry_r"); // or carry_both UPROPERTY(ReplicatedUsing = OnRep_CarriedBy) TObjectPtr CarriedBy; // null on the ground bool IsCarried() const { return CarriedBy != nullptr; } // Server: attach to the carrier's mesh socket, physics off, collision to Ignore, dormancy off. // Release: detach, physics on, collision back, an impulse if thrown, dormancy back on when at rest. // OnRep_CarriedBy does the same attach/detach on clients so the carried object rides the body without // per-frame movement replication. }; /** On the player. Two hands, the verbs, and the encumbrance effect that is the only way weight slows a body. */ UCLASS() class UCarryComponent : public UActorComponent { GENERATED_BODY() public: UPROPERTY(Replicated) TArray> Hands; // 0, 1 or 2 entries; two-handed fills both bool HasFreeHandsFor(const UCarryableComponent& Object) const; UCarryableComponent* GetHeld() const; // the one-handed object, or the two-handed one void RequestPickUp(UCarryableComponent* Object); // owner -> ServerPickUp void RequestDrop(); // G tap void RequestThrow(float ChargeSeconds); // G held then released; the interaction component tracks the hold UFUNCTION(Server, Reliable) void ServerPickUp(UCarryableComponent* Object); // Validate: in reach (subsystem, with tolerance), not carried by anyone, not State.Downed, hands free. // Object->AttachTo(this). ApplyStatus(GE_Encumbered, CarryMath::SpeedFactor(weight, CarryCapacity), infinite): // weight is an effect on MoveSpeed like mud or a slow (Stats.md). Grant State.Carrying(.TwoHanded). // Emit object_picked_up. UFUNCTION(Server, Reliable) void ServerDrop(); // Always legal while carrying. Release at the hands with no velocity. Remove GE_Encumbered and the tags. Emit object_dropped. UFUNCTION(Server, Reliable) void ServerThrow(FVector_NetQuantizeNormal Aim, float ChargeSeconds); // Two-handed objects cannot be thrown (reason: TooHeavy). power = clamp(charge / MaxCharge) * ThrowForce(mass). // Release with impulse along Aim. It lands where it lands. Fragile things take a Mark (Crafting.md), never damage. // Emit object_thrown. UFUNCTION(Server, Reliable) void ServerHandOver(UCarryComponent* Other); // Both in reach, other has free hands. The co-op verb. Emit object_handed_over. // A down drops what you hold (Combat.md calls RequestDrop on the server when State.Downed is granted). }; ``` The attach-not-simulate approach is the whole reason carrying feels instant: while held, the object is a child of the carrier's mesh and rides its predicted movement; there is no physics to predict and nothing to reconcile. The moment it leaves the hands it is a server-simulated physics body again, replicated with the engine's predictive interpolation mode, and a thrown object arriving a tick late on a remote screen reads as weight, not lag. **Throwing is in, and it is allowed to be scrappy.** It is the cheapest physical comedy available and it serves coordination: tossing an ingot across a room beats walking it, at the cost of accuracy. Rules that keep it fun: objects never break from a throw; a thrown object that comes to rest is an ordinary floor object; thrown objects collide with the world but never knock something out of another player's hands; catching is not a mechanic, aim at the feet. **Blocking is deliberate.** A two-handed object commits your hands and, in first person, your eyes. The deadzone widening while carrying ([Movement.md](Movement.md)) is the safety valve: you can peek round your load without turning your body. If playtests read the block as friction rather than weight, the valve is per-object (`bTwoHanded` is data) and never a global rule change. ## Highlighting Custom depth with stencil. `UInteractionHighlightComponent` sets `bRenderCustomDepth` and a stencil value on the prop's primitives: 1 for in-range, 2 for targeted, 0 otherwise. One post-process material in the player camera's volume draws the outline from the stencil, reading its two colours from the theme ([UI.md](UI.md)). The prompt says *what*; the outline says *which*, so two ingots lying on each other show which one the press takes. No prop draws its own outline and no prop chooses a colour. ## Networking | State | Authority | Mechanism | | --- | --- | --- | | Targeting and the prompt | Client, owner | Local every frame; cosmetic; never replicated | | The request | Client asks, server decides | `ServerRequestInteract` on the player's own component | | Reach and permission | Server | Re-checked with tolerance; the client's claim is never trusted | | The prop's effect | Server | Whatever the owning feature does; replicated by that feature | | Local-only props | Client | Never sent | | Held objects | Server | `CarriedBy` replicated; attachment mirrored in `OnRep` | | Thrown and dropped objects | Server | Physics simulated on the server, replicated movement | ## Telemetry | Event | When | Payload | | --- | --- | --- | | `prop_interacted` | A successful interaction | `prop`, `verb`, `held_seconds`, `player_class` | | `interaction_refused` | The server refused a request | `prop`, `reason` | | `object_picked_up` | Carry begins | `object`, `two_handed`, `weight` | | `object_dropped` | Drop or forced drop | `object`, `forced` | | `object_thrown` | Throw | `object`, `charge`, `distance_landed` | | `object_handed_over` | Hand over | `object` | `prop_interacted` earns its place because a world-as-interface game has a failure mode a menu game does not: a player who cannot find the bench has nothing to fall back on. If a prop exists and nobody in a session ever touches it, it is not discoverable, and this event is the only way to learn that before someone says so. `interaction_refused` by reason is the cheapest possible measure of whether prompts explain themselves. ## Tests - Automation: `IsInReach` with and without tolerance; the prompt of a test prop is disabled with `HandsFull` when the query carries `State.Carrying.TwoHanded`. - Functional: two clients and a server; client A picks up a crate, client B sees it on A's body; A throws it, both see it land within tolerance of the server's resting place; a request from beyond reach plus tolerance is refused and `interaction_refused` carries `OutOfReach`. ## Open questions - **Q1. Contested props.** Arrival order is the rule. Whether a bench or a station wants a visible soft lock with a name is each prop's call in its own `GetPrompt`; the first prop that needs one sets the pattern. - **Q2. A greyed prompt or no prompt for an unusable prop.** Greyed, with the reason: more discoverable and more honest. Revisit if prompts clutter a crowded room. - **Q3. Hold-to-interact as an accessibility option.** The hold mechanism exists for irreversible verbs; making it optional for every verb is a settings toggle over the same code. With the settings pass, not before. - **Q4. Which objects are two-handed.** Data per object. The first crate, the first log and the first assembled greatsword decide the initial list; the earlier projects chose crates, logs, ore chunks and packed stations. - **Q5. A satchel.** The earlier smithing design added purchasable pocket slots later in progression. Not now; parked in [`../Ideas.md`](../Ideas.md). Hands only until something needs more.