Game organization in CloudBehaviours
Split your game state across CloudBehaviours, keep domain logic private, and call between behaviours when needed.
Read time 3 minutesLast updated 15 days ago
CloudBehaviours carries expectations about how you structure your game's objects. A game with players, hands, and cards doesn't map to a single CloudBehaviour class the way a MonoBehaviour hierarchy might. Decide what becomes its own CloudBehaviour, and put game logic that doesn't cross the client boundary in the right place.
Decide what becomes its own CloudBehaviour
Give a piece of state its own CloudBehaviour when it has its own owner or lifetime. State that belongs to one player and outlasts any single match, such as a profile or an inventory, is a -scoped CloudBehaviour. State that belongs to a match and ends with it lives on a -scoped CloudBehaviour.
PlayerMultiplayerSessionInside a CloudBehaviour, what clients need to see decides how you declare each piece of state:
- State that every player sees the same way is a public field or property, which synchronizes.
- State that looks different to each player is exposed through a per-player state view.
- State that clients never see is a private field.
For example, in a card game with players, hands, and cards:
- The match belongs to the session and ends with it. It's a -scoped CloudBehaviour.
MultiplayerSession - Each player's hand also ends with the match, so it stays on the match CloudBehaviour. Each player sees only their own hand, so the match exposes it through a per-player state view.
- A single card has no owner or lifetime of its own. It stays a plain C# class referenced by the match CloudBehaviour.
Where real game logic lives
A shared model type crossing the client boundary is data-only: it carries public fields and properties, and nothing else. Methods on a model type never regenerate on the client, and a public property that can be read but not publicly assigned, such as a computed property, causes a compile-time error.
Keep your actual game logic in a private domain layer inside the CloudBehaviour, where ordinary object-oriented code works normally. Expose only data-only projections and events to the client:
[StateScope(Scope.MultiplayerSession)]public class CardGame : CloudBehaviour{ [CloudCodeSerializeProperty] private Deck _deck = new(); // Real logic: shuffling, dealing [CloudCodeSerializeProperty] private Dictionary<string, List<Card>> _hands = new(); // Private: only the server knows each hand [SyncView("Hand")] internal List<Card> ProjectHand(string playerId) => // Each player receives only their own hand _hands.TryGetValue(playerId, out var hand) ? hand : new List<Card>(); public int Round; // Data-only, synchronized public event Action<string> CardDrawnBy; // Push channel: says who drew, never which card public void DrawCard() { var playerId = Context.PlayerId; // The caller, never a client-supplied ID if (!_hands.TryGetValue(playerId, out var hand)) _hands[playerId] = hand = new List<Card>(); hand.Add(_deck.Draw()); // Domain logic runs here CardDrawnBy?.Invoke(playerId); }}
[CloudCodeSerializeProperty]_deckDeck[CloudCodeSerializeProperty]Persistence rules apply inside too: on saves the deck object, but only its public members and the private ones that also carry . Mark 's private card list with the attribute, or its cards reset after the behaviour is reloaded. Refer to Player and match persistence in CloudBehaviours.
Deck[CloudCodeSerializeProperty]_deck[CloudCodeSerializeProperty]DeckThis shape works for a turn-based game with moderately complex mechanics: private domain logic, data-only projections, and an event stream for presentation.
Coordinate between CloudBehaviours
When one behaviour needs to call another, such as a match behaviour awarding a reward from a player's inventory behaviour, refer to Cross-behaviour calls in CloudBehaviours.