Hack & slash 'Attack' operation

mstultz
edited August 2010 in Photon Server
I'm currently trying to implement a hack & slash mechanic and am interested in how such an 'Attack' operation work. On a game level: a player would move around, and click the mouse to swing a sword. If an enemy or destructible object were in the attack range and direction of the player, it would get hit.

I'm light on the MMO/Proton-server API knowledge, so I had a basic idea of how I *thought* the action flow would work between client and server:

1. The Player would submit an Attack operation to the server, offering data such as possibly position, rotation (for direction), and an attack range. These values may already be stored on the corresponding Item class on the server; regardless.
2. I thought I would handle the Attack operation on the server by the following:

A. Get a list of items (other players, destructible objects, etc) in the Player's interest area (I don't think I can actually do this).
B. Check if items in said list are within attack radius.
C. Perform any 'OnAttack' operations (decrement health, destroy destructible-objects) and send an ItemAttacked event to represent the results on the client.

However, as 2A suggests, I don't think this flow actually works. So, how would you guys implement a hack & slash type Attack operation?

Mark

Comments

  • One thing you could do is look at InterestArea.OnItemSubscribed() and InterestArea.OnItemUnsubscribed() (on the server). You can override both of these methods and maintain your own list of items in that interest area. This would give you the ability to iterate all items in the player's interest area.

    Another idea is to create a 2nd interest area attached to the player that has a radius equal to the player's attack range. Any players in interest area are guaranteed to be hit, it's only a matter of direction at that point (Obviously units behind the player might not get hit, unless you're doing a full-circle sword swing attack or something).
  • You can override both of these methods and maintain your own list of items in that interest area. This would give you the ability to iterate all items in the player's interest area.
    That is a problem because each Item runs in it's own thread (fiber). When iterating you can't access the Items directly, that would not be thread safe.
    I have to think a bit more about this.
  • Another idea is to create a 2nd interest area attached to the player that has a radius equal to the player's attack range. Any players in interest area are guaranteed to be hit, it's only a matter of direction at that point (Obviously units behind the player might not get hit, unless you're doing a full-circle sword swing attack or something).
    Interest areas are a bit "fuzzy" - they subscribe all items in the overlapping regions. So you might end up hitting more people than you want.
  • How about this - based on void's post to keep track of items in a collection:
      private readonly Dictionary<Item, Vector> positions = new Dictionary<Item,Vector>();
    
            protected override void OnItemSubscribed(Item item, Vector itemPosition, Region itemWorldRegion, int propertiesRevision)
            {
                base.OnItemSubscribed(item, itemPosition, itemWorldRegion, propertiesRevision, itemCoordinate);
    
                item.PositionUpdateChannel.Subscribe(this.Peer.OperationRequestQueue.Fiber, this.OnItemPositionMessage);
                this.positions.Add(item, itemPosition);
    
                // etc
                ...
            }
    
            private void OnItemPositionMessage(ItemPositionMessage obj)
            {
                this.positions[obj.Source] = obj.Position;
            }
    
            protected override void OnItemUnsubscribed(Item item)
            {
                base.OnItemUnsubscribed(item);
    
                this.positions.Remove(item);
    
               // etc
                ...
            }
    
    Now you can iterate over all item positions without having to worry about thread safety.
  • Boris wrote:
    Another idea is to create a 2nd interest area attached to the player that has a radius equal to the player's attack range. Any players in interest area are guaranteed to be hit, it's only a matter of direction at that point (Obviously units behind the player might not get hit, unless you're doing a full-circle sword swing attack or something).
    Interest areas are a bit "fuzzy" - they subscribe all items in the overlapping regions. So you might end up hitting more people than you want.
    The docs for InterestArea made it sound like once an Item enters an overlapping region, a distance check is done to make sure the item is within the radius of the interest area. This seems like it would make sense to do. To me, interest areas can be more useful than just determining what items are visible. If they were more accurate, you could use them for situations like this.
  • Boris
    Boris
    edited August 2010
    Boris wrote:
    Another idea is to create a 2nd interest area attached to the player that has a radius equal to the player's attack range. Any players in interest area are guaranteed to be hit, it's only a matter of direction at that point (Obviously units behind the player might not get hit, unless you're doing a full-circle sword swing attack or something).
    Interest areas are a bit "fuzzy" - they subscribe all items in the overlapping regions. So you might end up hitting more people than you want.
    To solve it this way you would probably want to increase the region density.
    But then again interest areas have two thresholds: subscribe and unsubscribe. If they are too close to each other items on the border will subscribe and unsubscribe very often. Also if items move from one region to another they send a message into the region to tell potential subscribers (interest areas) that they are there. And when interest areas subscribe a new region they send a message to ask for items in it.
    Question is if all that is more load than the permanent position updating in the dictionary - I'm not really sure what's better, probably the other solution because it's just more work when there are other players near.
  • The docs for InterestArea made it sound like once an Item enters an overlapping region, a distance check is done to make sure the item is within the radius of the interest area. This seems like it would make sense to do. To me, interest areas can be more useful than just determining what items are visible. If they were more accurate, you could use them for situations like this.
    the distance check applies to the outer view area (the unsubscribe mechanism).
    subscribed are all items in the overlapping regions of the inner view area.
  • Another idea:
    1. Add an "Attack" channel to the Item
    2. Subscribe the channel in OnItemSubscribe
    3. When calling "Attack" publish a message with the avatar's attack channel including the avatar position.
    4. Potential victims (subscribers of your avatar item) receive the attack command (as long as the interest area is attached), evaluate the distance to the own avatar and reply if they are hit. (enqueue action on item operationQueue/fiber).
  • Possibly the best solution for this:

    Provide a way to sort Items in an interest area. For example, you could have a lambda/functor that sorts items by distance. When new items are subscribed/unsubscribed, the sort is re-evaluated. When a single item moves, it compares against its surrounding items in the list and adjusts its position in the container.

    It's definitely non-trivial, but having such a sorting mechanism means that we can process Items linearly (from shortest distance to longest) and stop processing the container of Items when we reach an item distance that is greater than the attack distance.

    I'm not sure how this conflicts with the multi-threaded portion of things, but to re-sort individual moving items in the world, you only need access to 3 elements at any position in the linear container. If internally a dictionary is being used for this, that would have to change, which might impact performance in other areas (such as item lookup by name/id).

    What do you think Boris?
  • Boris wrote:
    Another idea:
    1. Add an "Attack" channel to the Item
    2. Subscribe the channel in OnItemSubscribe
    3. When calling "Attack" publish a message with the avatar's attack channel including the avatar position.
    4. Potential victims (subscribers of your avatar item) receive the attack command (as long as the interest area is attached), evaluate the distance to the own avatar and reply if they are hit. (enqueue action on item operationQueue/fiber).
    For step 1, by "add a channel" are you referring to creating an ExitGames.Concurrency.Channels.Channel<T> as a member of MmoItem on the server? If not, please explain what you mean.

    For step 2, which InterestArea's OnItemSubscribe do I need to use? The standard one that determines which items come into view, or a second interest area that represents the attack range?

    For step 4, you've lost me completely. By publishing a message on the Attack Channel<T>, you are sending an attack message to all the clients. Are you implying that the clients perform the distance check and send an operation back if they were hit? This work needs to be done on the server. Please correct me if I'm wrong.

    Also I'm not really clear on what Fibers are. To me they seem like thread handles, but I could be wrong. Please explain what you mean by "enqueue action on item operationQueue/fiber".

    Thanks.
  • which might impact performance in other areas
    You said it: If we do that the performance will suffer.

    Right now the optimization is:
    - subscribe item when item region overlaps interest area's inner view area: that's just one message when an item enters a region or two messages when an interest area moves into a region where an item already exists
    - evaluate distance every 5 seconds (default) and unsubscribe if distance is too big: that's just one calculation every 5 seconds per item

    If the interest area provided a list as you suggest it means that there is not only one calculation per item movement, but also x calculations per interest area movement. So if you see 20 players and move you need to calculate 20 times the distance, and 20 others need to calculate 1 time the distance. That's 40 calculations for one movement update, and that happens 10 times per second for just 1 player.
    I don't see why the base class should do it for all games if just a few games need it.
    But there is no problem to add this feature it with that additional position list as posted above.
  • For step 1, by "add a channel" are you referring to creating an ExitGames.Concurrency.Channels.Channel<T> as a member of MmoItem on the server? If not, please explain what you mean.
    Yes, or MessageChannel which is just a subclass.
    For step 2, which InterestArea's OnItemSubscribe do I need to use? The standard one that determines which items come into view, or a second interest area that represents the attack range?
    If you use a second, then the second. But that is only a good idea if the attack range is much shorter and if the grid is granular enough.
    For step 4, you've lost me completely. By publishing a message on the Attack Channel<T>, you are sending an attack message to all the clients. Are you implying that the clients perform the distance check and send an operation back if they were hit? This work needs to be done on the server. Please correct me if I'm wrong.
    Not the clients should to the distance check, the opponent's interest area should.
    Also I'm not really clear on what Fibers are. To me they seem like thread handles, but I could be wrong. Please explain what you mean by "enqueue action on item operationQueue/fiber".
    An item has an operation queue. the operation queue has a fiber. actions enqueued on a fiber are executed one after the other, hence no threading issues between actions on a fiber.
  • Boris wrote:
    Another idea:
    1. Add an "Attack" channel to the Item
    2. Subscribe the channel in OnItemSubscribe
    3. When calling "Attack" publish a message with the avatar's attack channel including the avatar position.
    4. Potential victims (subscribers of your avatar item) receive the attack command (as long as the interest area is attached), evaluate the distance to the own avatar and reply if they are hit. (enqueue action on item operationQueue/fiber).

    I thought I was performing this correctly, but I appear to have lost my way. Perhaps you could shed some light.

    1. Every MmoItem contains a new MessageChannel< ItemEventMessage > for the 'Attack' channel, which gets instantiated inside MmoItem's constructor.
    2. Inside MmoClientInterestArea's OnItemSubscribe, I subscribe the incoming item (other players entering the interest area) to the interest area's owner's attack channel:
    // 'attachedItem' is the interest area's item, and 'item' is the item entering the interest area
    MmoItem attachedItem = (MmoItem)AttachedItem;
    attachedItem.AttackChannel.Subscribe( attachedItem.OperationQueue.Fiber, m =&gt; AttackChannel_ItemEventMessage( this, item, m ) );
    

    3. The server processes the 'Attack' operation, which is passed in an item id of the player attacking. On this item's attack channel, I publish an 'ItemAttacked' message.
    4. AttackChannel_ItemEventMessage() will get called for each enemy that gets attacked, with the attacker's interest area passed in. In here, I disregard the message passed in, and actually create a new 'ItemAttacked' message. I populate a Hashtable with some information, like the magnitude of the vector of the attacker to the potential attacked item, along with that candidates item id. I would have just stuffed this Hashtable into the original message, but this seems to crash sometimes (didn't investigate). I send the event via interestArea.Peer.ProtonPeer.SendEvent().

    So this does not work, and I have a few questions.
    1. The results I see are: the client who attacks will get an event "ItemAttacked" for each item it attacks. The items that got attacked receive no message. Alternatively, I reversed the subscription inside the OnItemSubscribed, and it obviously yields the exact opposite results (everyone gets a message EXCEPT the client that attacked). How can I send this event to all items in the interest area?
    2. What is the purpose of the last channel id parameter inside the GetEventData()? I thought to create one for my newly created AttackChannel, but I'm not sure where you would 'set' such an id for the channel itself.

    I'm obviously missing something with the concept of channels, and I'm sure I'm setting this stuff up wrong. I'd like to post another discussion on the flow of attacks, but I will wait until after I solve the above scenario.

    Thanks!
  • mstultz wrote:
    1. Every MmoItem contains a new MessageChannel< ItemEventMessage > for the 'Attack' channel, which gets instantiated inside MmoItem's constructor.
    correct
    mstultz wrote:
    2. Inside MmoClientInterestArea's OnItemSubscribe, I subscribe the incoming item (other players entering the interest area) to the interest area's owner's attack channel:
    // 'attachedItem' is the interest area's item, and 'item' is the item entering the interest area
    MmoItem attachedItem = (MmoItem)AttachedItem;
    attachedItem.AttackChannel.Subscribe( attachedItem.OperationQueue.Fiber, m =&gt; AttackChannel_ItemEventMessage( this, item, m ) );
    
    Here is a bug: You subscribed to the attached item. What you want to do is to subscribe to the newly subscribed item:
    MmoItem subscribedItem = (MmoItem)item;
    var unsubscriber = subscribedItem.AttackChannel.Subscribe( this.AttachedItem.OperationQueue.Fiber, this.AttackChannel_ItemEventMessage );
    
    don't forget to dispose the unsubscriber on unsubscribe :!:
    mstultz wrote:
    3. The server processes the 'Attack' operation, which is passed in an item id of the player attacking. On this item's attack channel, I publish an 'ItemAttacked' message.
    The idea was to publish Attack on your avatar Item's attack channel: The attacker publishes "I attack".
    The receiver can then answer to the sender (your avatar) "I was hit" - if you don't need any additional server logic that could be the event you send to the attacker item owner.
    attacker.OperationQueue.Fiber.Enqueue(() =&gt; { if (!attacker.Disposed) attacker.Owner.Peer.PublishEvent(eventData); });
    
    mstultz wrote:
    2. What is the purpose of the last channel id parameter inside the GetEventData()? I thought to create one for my newly created AttackChannel, but I'm not sure where you would 'set' such an id for the channel itself.
    this is the enet channel id and has nothing to do with the channels in your business logic.
    You can use different channels for different send priorities in the networking layer.
    Channel 0 is usually good enough.