00
Architecture technique · Unity · PurrNet

PLAYER ACTIONS
SCALABLES

Moteur Unity
Online PurrNet
Prediction PurrDiction
Pattern Brain + command

L'architecture joueur separe l'intention, la simulation et la presentation. Le brain produit des commandes, le joueur predit applique les regles, les couches de states se combinent sans explosion de classes, et la vue reste libre pour animation, camera et feedbacks.

Principes

CE QUI DOIT RESTER SIMPLE

La maintenabilite vient surtout de frontieres nettes : une classe ne doit pas lire l'input, appliquer les regles, bouger le joueur et declencher les VFX en meme temps.

01 · Brain
Source d'intention
Le brain ne controle pas le personnage. Il produit un PlayerInput par tick : move, aim, sprint, action demandee.
02 · Simulation
Regles autoritaires
Le PredictedPlayer valide les commandes, met a jour les states et applique le mouvement deterministe.
03 · States
Couches orthogonales
Courir avec le ballon n'est pas une classe dediee : c'est Locomotion = Run + Possession = WithBall.
04 · View
Presentation separee
Animation, camera, sons et VFX lisent le state ou les events de simulation, mais ne decident pas du gameplay.
Diagramme de classes

VUE GLOBALE NAVIGABLE

Zoom molette ou boutons, deplacement au drag. Le diagramme se concentre sur les frontieres : input, commandes, slots joueurs, prediction, simulation et presentation.

PURRDICTION / SHARED GAMEPLAY CORE

PurrDiction orchestre prediction, rollback et reconciliation. Le gameplay football reste dans un core deterministe partage par le local, l'online, l'IA et les replays.

classDiagram
    direction LR

    class MatchSession {
        +GameMode mode
        +LockSlotsForMatch()
        +StartLocal()
        +StartOnline()
        +StartReplay()
    }

    class LocalJoinLobby {
        +Open()
        +Close()
        +LockSlotsForMatch()
    }

    class InputDeviceRegistry {
        +DetectDevices()
        +GetUnassignedDevices()
    }

    class JoinInputListener {
        +ListenForJoin()
        +ListenForLeave()
        +ListenForReady()
    }

    class PlayerSlotAllocator {
        +CreateSlot(device)
        +AssignDevice(slot, device)
        +RemoveSlot(slot)
        +SwitchTeam(slot)
    }

    class InputDeviceRouter {
        +ReadUnityInput()
        +RouteToSlot()
    }

    class PlayerSlot {
        +PlayerSlotId id
        +InputDevice device
        +TeamId team
        +bool isReady
        +bool isLocal
        +PlayerID networkOwner
    }

    class IInputBrain {
        <<interface>>
        +BuildInput(tick) PlayerInput
    }

    class LocalInputBrain {
        +BuildInput(tick) PlayerInput
    }

    class AIBrain {
        +BuildInput(tick) PlayerInput
    }

    class ReplayBrain {
        +BuildInput(tick) PlayerInput
    }

    class PlayerInput {
        <<struct>>
        +int tick
        +Vector2 move
        +Vector2 aim
        +bool sprint
        +ActionCommand action
    }

    class PlayerState {
        <<struct>>
        +Vector3 position
        +Vector3 velocity
        +Quaternion rotation
        +LocomotionState locomotion
        +PossessionState possession
        +ActionState action
        +float actionTimer
    }

    class BallState {
        <<struct>>
        +Vector3 position
        +Vector3 velocity
        +BallMode mode
        +PlayerSlotId owner
        +PlayerSlotId lastTouch
    }

    class MatchState {
        <<struct>>
        +int scoreA
        +int scoreB
        +float matchTimer
        +RunModifierState modifiers
    }

    class MatchSimulation {
        +SimulateTick(inputs, ref match, ref players, ref ball, dt)
    }

    class PlayerRulesResolver {
        +SanitizeInput(ref input)
        +ResolveAction(input, ref state)
    }

    class PlayerMovementMotor {
        +Simulate(input, ref state, dt)
    }

    class BallSimulation {
        +Simulate(ref ball, players, dt)
        +ApplyKick()
        +ResolveContest()
    }

    class PurrPredictedPlayer {
        PredictedIdentity INPUT STATE
        +GetFinalInput(ref input)
        +SanitizeInput(ref input)
        +Simulate(input, ref state, dt)
        +UpdateView(viewState, verified)
    }

    class PurrPredictedBall {
        PredictedIdentity STATE
        +Simulate(ref state, dt)
        +UpdateView(viewState, verified)
    }

    class LocalMatchRunner {
        +Tick()
        +RunWithoutNetwork()
    }

    class GameplayView {
        +ApplyPlayerView()
        +ApplyBallView()
        +PlayVFX()
        +DriveCamera()
    }

    class UXHUDPresenter {
        +ShowScore()
        +ShowPossession()
        +ShowLatencyFeedback()
    }

    LocalJoinLobby --> InputDeviceRegistry
    LocalJoinLobby --> JoinInputListener
    LocalJoinLobby --> PlayerSlotAllocator
    LocalJoinLobby --> MatchSession : locks slots

    InputDeviceRegistry --> JoinInputListener
    JoinInputListener --> PlayerSlotAllocator
    PlayerSlotAllocator --> PlayerSlot
    PlayerSlotAllocator --> InputDeviceRouter : active bindings

    MatchSession --> InputDeviceRouter
    MatchSession --> LocalMatchRunner : local
    MatchSession --> PurrPredictedPlayer : online
    MatchSession --> PurrPredictedBall : online

    InputDeviceRouter --> PlayerSlot
    PlayerSlot --> IInputBrain
    IInputBrain <|.. LocalInputBrain
    IInputBrain <|.. AIBrain
    IInputBrain <|.. ReplayBrain
    IInputBrain --> PlayerInput

    LocalMatchRunner --> IInputBrain
    LocalMatchRunner --> MatchSimulation

    PurrPredictedPlayer --> PlayerInput
    PurrPredictedPlayer --> PlayerState
    PurrPredictedPlayer --> MatchSimulation

    PurrPredictedBall --> BallState
    PurrPredictedBall --> BallSimulation

    MatchSimulation --> MatchState
    MatchSimulation --> PlayerState
    MatchSimulation --> BallState
    MatchSimulation --> PlayerRulesResolver
    MatchSimulation --> PlayerMovementMotor
    MatchSimulation --> BallSimulation

    BallSimulation --> BallState
    BallSimulation --> PlayerState

    GameplayView --> PurrPredictedPlayer
    GameplayView --> PurrPredictedBall
    GameplayView --> UXHUDPresenter
    UXHUDPresenter --> MatchState
            
UML de flux

ORDRE DES ETAPES

Le mode de jeu est choisi avant la composition. En local, les joueurs peuvent choisir leurs cotes. En online, le joueur choisit son equipe/roster, mais l'adversaire est fourni par le matchmaking ou le serveur.

GAME FLOW / LOCAL VS ONLINE TEAM RULES

Cette vue protege la logique UX : on ne manipule pas les memes choix de team en local et en online. Le verrouillage des slots arrive seulement quand toutes les contraintes du mode sont satisfaites.

stateDiagram-v2
    direction LR

    [*] --> MainMenu
    MainMenu --> ModeChoice : Play

    state ModeChoice <<choice>>
    ModeChoice --> LocalJoinLobby : Local game
    ModeChoice --> OnlineEntry : Online game

    state "Local join lobby" as LocalJoinLobby
    state "Local team setup" as LocalTeamSetup
    state "Local ready check" as LocalReadyCheck

    LocalJoinLobby : devices join / leave
    LocalJoinLobby : keyboard and gamepads
    LocalJoinLobby : create local player slots
    LocalJoinLobby --> LocalTeamSetup : players joined

    LocalTeamSetup : choose side A or B
    LocalTeamSetup : choose local teams
    LocalTeamSetup : allow same screen opposition
    LocalTeamSetup --> LocalReadyCheck : teams selected
    LocalReadyCheck --> LockMatchSlots : all local players ready

    state "Online entry" as OnlineEntry
    state "Party lobby" as OnlinePartyLobby
    state "Own team selection" as OnlineTeamSelection
    state "Online queue" as OnlineQueue
    state "Match found" as MatchFound
    state "Server opponent assignment" as ServerOpponent

    OnlineEntry : host / join / matchmaking
    OnlineEntry --> OnlinePartyLobby : connected

    OnlinePartyLobby : invite friends
    OnlinePartyLobby : manage same-side party
    OnlinePartyLobby : no adverse local team slot
    OnlinePartyLobby --> OnlineTeamSelection : party ready

    OnlineTeamSelection : choose own team / roster
    OnlineTeamSelection : choose captain / totems
    OnlineTeamSelection : cannot choose opponent side
    OnlineTeamSelection --> OnlineQueue : own team locked

    OnlineQueue --> MatchFound : opponent found
    MatchFound --> ServerOpponent : validate players
    ServerOpponent : server assigns opponent team
    ServerOpponent : server validates ownership
    ServerOpponent --> LockMatchSlots

    state "Lock match slots" as LockMatchSlots
    state "Create MatchSession" as CreateMatchSession
    state "Load arena" as LoadArena
    state "Countdown" as Countdown
    state "Match running" as MatchRunning
    state "Post match" as PostMatch

    LockMatchSlots : freeze devices / slots / ownership
    LockMatchSlots : build IInputBrain per slot
    LockMatchSlots --> CreateMatchSession
    CreateMatchSession --> LoadArena
    LoadArena --> Countdown
    Countdown --> MatchRunning
    MatchRunning --> PostMatch : match ends
    PostMatch --> MainMenu : exit
    PostMatch --> ModeChoice : rematch / new run
            
Couches joueur

COMBINER SANS EXPLOSER

Les etats combinables sont ranges par responsabilite. Ajouter une action ou un statut ne multiplie pas toutes les classes existantes.

Locomotion
Idle, Run, Sprint, Brake, Turn. Cette couche gere vitesse cible, acceleration, inertie, rotation et freinage.
Possession
WithoutBall, WithBall, ReceivingBall, LooseBallContest. Elle modifie les actions accessibles et certains parametres de controle.
Action
None, Pass, Shoot, Tackle, Dodge, Intercept. Cette couche gere priorites, durees, locks, cancels et couts.
Condition joueur
Normal, Slowed, Stunned, LockedDirection, SuperCharged, Invulnerable. Cette couche decrit l'etat temporaire du joueur sans dupliquer locomotion, possession ou action.
View state
Blend animation, camera feedback, VFX et audio. Cette couche peut interpoler, lisser et embellir sans casser la prediction.
PurrNet / PurrDiction

FLUX DE PREDICTION

Le joueur reseau doit rester une simulation reproductible. Les entrees passent par PurrDiction, le state est snapshot/reconcile, la vue se met a jour ensuite.

Brain
Local, IA, replay ou remote. Produit une intention brute sans toucher au Transform.
Input
Struct compacte et serialisable : move, aim, sprint, action, tick.
Sanitize
Clamp des axes, validation des actions, suppression des inputs impossibles.
Simulate
Mise a jour deterministe des layers et du motor avec delta de tick.
State
Position, velocity, rotation, locomotion, possession, action et timers.
View
Animation, camera, trails, sons et UI lisent le state final.
Banc de test

CYCLE DE VIE EN COULEUR

Cas volontairement simple : cliquer un scenario et suivre comment une intention traverse le brain, l'input, les state machines, la simulation et la vue.

IDLE
01 · Source
Brain
Neutral
Aucune intention forte. Le brain produit un input neutre.
02 · Command
Input
Move 0
Commande compacte prete a etre predite.
03 · State machine
Locomotion
Idle
Le joueur reste stable et pret a partir.
04 · State machine
Possession
WithoutBall
Aucune contrainte de dribble ou de tir.
05 · State machine
Action
None
Aucune action prioritaire ne verrouille le joueur.
06 · State machine
Condition
Normal
Pas de stun, slow ou lock temporaire.
07 · View layer
UX / Sound / Anim
Idle color
La vue affiche la couleur du state final sans decider du gameplay.
Trace du tick
tick 000 : LocalInputBrain -> PlayerInput neutral -> Locomotion Idle -> View idle.
Exemples

INTEGRATION RETRACTABLE

Ouvrir seulement ce qui est utile. Les exemples montrent la structure, pas une API finale verrouillee.

Contrat brain interchangeable

Le brain est remplace rapidement selon le contexte. En solo il lit la manette, en online local il fournit aussi les inputs a PurrDiction, en remote il rejoue les commandes recues.

public interface IPlayerBrain
{
    PlayerInput GetInput(int tick);
}

public sealed class LocalInputBrain : IPlayerBrain
{
    public PlayerInput GetInput(int tick)
    {
        return new PlayerInput
        {
            tick = tick,
            move = ReadMoveStick(),
            aim = ReadAimStick(),
            sprint = ReadSprintButton(),
            action = ReadRequestedAction()
        };
    }
}
Input et state compatibles prediction

Les donnees simulees restent compactes. Les references Unity, les VFX, les sons et les objets non deterministes restent hors du state predit.

public struct PlayerInput : IPredictedData
{
    public int tick;
    public Vector2 move;
    public Vector2 aim;
    public bool sprint;
    public PlayerActionType action;
}

public struct PlayerState : IPredictedData<PlayerState>
{
    public Vector3 position;
    public Vector3 velocity;
    public Quaternion rotation;
    public LocomotionState locomotion;
    public PossessionState possession;
    public ActionState action;
    public float actionTimer;
}
PredictedPlayer avec couches de gameplay

Le PredictedPlayer orchestre les modules. Chaque couche est testable separement, mais la simulation finale reste centralisee pour la prediction et la reconciliation.

public sealed class PredictedPlayer
    : PredictedIdentity<PlayerInput, PlayerState>
{
    [SerializeField] private PlayerBrainProvider brainProvider;
    [SerializeField] private PlayerRulesResolver rules;
    [SerializeField] private PlayerMovementMotor motor;
    [SerializeField] private PlayerAnimationView animationView;

    protected override PlayerInput GetFinalInput()
    {
        return brainProvider.Current.GetInput(CurrentTick);
    }

    protected override void SanitizeInput(ref PlayerInput input)
    {
        input.move = Vector2.ClampMagnitude(input.move, 1f);
        input.aim = Vector2.ClampMagnitude(input.aim, 1f);
        rules.SanitizeAction(ref input);
    }

    protected override void Simulate(
        PlayerInput input,
        ref PlayerState state,
        float delta)
    {
        rules.Resolve(input, ref state);
        motor.Simulate(input, ref state, delta);
    }

    protected override void UpdateView(PlayerState state, float delta)
    {
        animationView.Apply(state, delta);
    }
}
Ajout d'une action scalable

Une nouvelle action ne doit pas modifier toutes les classes. Elle declare ses conditions, ses locks et ses effets, puis le resolver decide si elle peut demarrer.

[CreateAssetMenu(menuName = "ASU/Player Action")]
public sealed class PlayerActionDefinition : ScriptableObject
{
    public PlayerActionType type;
    public bool requiresBall;
    public bool locksDirection;
    public float duration;
    public float agilityMultiplier = 1f;
}

public bool CanStart(
    PlayerActionDefinition action,
    in PlayerState state)
{
    if (action.requiresBall && state.possession != PossessionState.WithBall)
        return false;

    if (state.action != ActionState.None)
        return false;

    return true;
}
Scalabilite

REGLES DE MAINTENANCE

Ces garde-fous evitent que le prototype online devienne un bloc fragile au moment d'ajouter pouvoirs, coequipiers, IA et variantes de run.

Input minimal
Envoyer des intentions compactes. Ne jamais envoyer des animations ou des decisions de camera dans l'input predit.
State explicite
Tout ce qui change le gameplay doit etre dans le state ou derivable du state. Le reste appartient a la view.
Layers separes
Locomotion, possession, action et modifiers evoluent separement, puis sont resolus ensemble par des regles courtes.
Data first
Les actions, stats et modifiers doivent etre parametrables via assets pour iterer sans reecrire le core.
Tests rapides
Chaque action doit pouvoir etre testee avec LocalInputBrain, AIBrain et RemoteBrain sans changer son code.