#Item abilities in a multiplayer setting

1 messages · Page 1 of 1 (latest)

rancid pendant
#

I've made an item and inventory system that works as expected in a multiplayer setting.
The issue I'm facing is implementing using / doing actions / modifying items; more specifically, serializing specific parameters.

The system is pretty standard;

  • ItemData defines the static parts of an item (sprite, name, Id, stats, etc)
  • ItemInstance is a struct that is serialized over the network and describes a single item / stack
  • Stats are just byte values with a unique identifier for dynamic information like ammo, condition, etc

The goal is 'translating' the data into an actual physical object so that I can implement actions like:

  • Firing a weapon
  • Consuming an item
  • Food spoiling

I initially thought about creating an abstract class that describes an action, and adding a list of actions to the ItemData that receive the ItemInstance and player object
But I quickly ran into two issues:

  • Since the actions are pure data, where do I call my Rpc to execute the action on the server?

  • Since the actions are executed on the server, it would need to receive dynamic data from the player :
    For example, if I fired a gun, the server wants to spawn the bullet where the player was when clicked the fire button (as opposed to where the player was when the Rpc was received)
    This means that the arguments I want to send to the Rpcs are dynamic (consuming food doesn't require the player's position when the action was executed, but firing a gun does)

#

I'm not sure how to design this system to support these features. The only realistic solution I could come up with was creating NetworkBehaviours for each different action that requires world context (I.e having a 'fire' script that calls it's own RPCs on the weapon's model) which seems like poor architecture since I'm mixing item functionality between the data and prefabs.

I tried searching for information / articles on solving these types of problems but nothing I found was relevant, if there's any source you could direct me to regarding a fitting architecture, it'd be highly appericiated :)

strong tundra
#

I'm not exactly sure what you're describing as your set up. But if you're saying you want to dynamically define actions an inventory item can have taken on it, like this

{
   instanceId: 123, 
   (Other item stat values)...
   Actions: [
      "Shoot", "Throw", "Reload"
   ]
}

I would suggest thinking from the player control standpoint instead of the behavior stand point. As in players are always limited to "left click", "right click", "e to interact", "r to reload" etc. Every item can have rpcs for these generic actions, and then define behavior downstream to that

#

And for anything that shouldn't have behaviors for certain actions, those rpcs can be left as empty stubs that do nothing, and in turn shouldn't be called

#

Something like this:

Class Item {
   Public void OnInteract() {
       // this can be an rpc, or the rpc can be earlier in the callstack (as in at the player control level)
      // do any on interact stuff every item needs
      OnInteractInternal();
   } 
   
   \\ override for specific item behaviours
   Protected OnInteractInternal() {}
}

Or if you want to make different MonoBehaviour scripts for each specific item behavior

Interface IInteractBehaviour {
   // implement specific behavior here
   Public void Run();
}

Class Item {
   Public OnInteract() {
       \\ you can pre-cache the behaviors on instantiation
       Foreach (behavior in GetComponents<IInteractBehaviour>()) {
         Behavior.Run();
      }
   }
}
strong tundra
#

For getting arguments to each behavior you can have a base struct that's used that contains some non descript fields for whatever behavior wants to use them. But rpcs should be tick aligned (unless im misremembering network for gameobjects) so if you're synchronizing important game state you shouldn't have to worry about positions being different like in your bullet spawning example. Especially on the server who should have authority anyway.

runic geyser
# rancid pendant I'm not sure how to design this system to support these features. The only reali...

Assuming what you're asking is a possible way of sending/receiving arbitrary data that follows a schema.

An approach I use is defining structs to act as schemas (using your own data structure would be the same, serialization steps could be totally defined outside of them).
You fill that up with the data you want to pass around and then you serialize/deserialize it to send/receive it.
To distinguish between different schemas, attach an identifier to the sent data.

For example:

public ref struct GunShotEvent
{
  public readonly Vector3 Position;
}

public enum EventTypes
{
  GunShot = 0
}

Define methods to serialize and deserialize that struct and attach an ID to the front.
Which in the end means serializing the data like so [ID][Bytes for the struct].

When you receive your data prefixed with the ID on the other size you know what to deserialize it into.

The message could be sent straight from whatever method performs the action for the player (which presumably knows the data you want passed around).
Wiring up the actual dispatch is an architecture problem that is specific to your project, so you'll have to figure that out unless you give us more info.

Maybe you'd want to treat this more like a request than a direct command to the server, also
If you want position to be exactly where the player fired without having to trust what they send, you'd need some sort of history to reference this to on the server side.

burnt tulip
#

I try to stick to just passing IDs around. Abilities would be predefined in some DB or List somewhere. So a player would PerformAbilityRPC(Ability id,Target id, params) and the server to validate ability costs, hit detection and all that, then spawn any effects.

rancid pendant
# strong tundra I'm not exactly sure what you're describing as your set up. But if you're saying...

The issue is that this makes adding new behaviours very limiting. I'm going for a survival-type project, and items could have different uses that don't necessarily adhere to any obvious schema. While I do agree that the vast majority of interactions could probably be reduced to one of a few schemas for actions.

I may be engaging a little over-generalization, but it seems like a more generic approach that supports dynamic data is more extensible. Then again, if it ends up becoming a headache for little gain, maybe your approach would be a better solution.

rancid pendant
rancid pendant
# runic geyser Assuming what you're asking is a possible way of sending/receiving arbitrary dat...

That actually seems like a great approach! I am using Fishnet, which thankfully should handle the serialization for me, but I assume I would actually need to create some database that figures out the data's type based on an ID. Still, that seems far better than having a god-class for all action Rpcs.

I admit I am somewhat unfamiliar with the serialization process since I've mostly used Unity / Fishnet's network systems as-is. I will try to learn about in a more in-depth manner.

rancid pendant
#

For reference, here is my code:
This is for actual instances of items / stacks (i.e items that exist in the world)

[Serializable]
public struct ItemInstanceStat
{
    [StatId] public byte StatId; 
    public float Value;
}

[Serializable]
public struct ItemInstance
{
    public ushort DataId;
    public uint InstanceId;
    public int Quantity; 

    public ItemInstanceStat[] _internalStats;

    public readonly System.Collections.Generic.IReadOnlyList<ItemInstanceStat> Stats => _internalStats;

    public ItemInstance(ushort dataId, uint instanceId, int quantity, ItemInstanceStat[] stats) // Constructor

    public readonly bool TryGetStat(ItemInstanceStatType stat, out float value) // Some stat-related logic
}

#
public class ItemData : ScriptableObject
{
    [field: Header("Identification")]
    [Tooltip("A string-based Id for visual clarity, that is then hashed into a ushort-based id for networking.")]
    [field: SerializeField] public string InternalId { get; private set; }
    [ReadOnly] public ushort Id;

    [field: Header("Item Definitions")]
    [SerializeField] private ItemInstanceStat[] _defaultStats;
    [SerializeField] private ItemAction[] _actions;

    public IReadOnlyCollection<ItemInstanceStat> DefaultStats => _defaultStats;
    public IReadOnlyCollection<ItemAction> Actions => _actions;

    public ItemInstance CreateInstance(int amount = 1) // Constructor

    public bool TryGetActions<T>(out List<(ItemAction action, T aspect)> results) where T : class // Returns every action of certain type

#if UNITY_EDITOR
    protected void OnValidate() // Assigns Ids using hashing

    private ushort GenerateStableHash(string str) // Hashing logic
#endif 
}
runic geyser
rancid pendant
#

I've removed / shortened code that was irrelevant

runic geyser
#

You wouldn't want to create buffers every time, so reusing the same makes this 0 heap alloc.
This is of course just one possible approach to the problem