0
\$\begingroup\$

I never understood how would you elegantly describe two people talking in OO. I think this question extends to how similar objects communicate in general. I am designing a game now and Units have to attack each other.

My first idea was something along the lines:

public class Unit {
   public void attack(Unit targetUnit) {
      //calculates damage base on this unit stats
      targetUnit.takeDamage(damage);
  }

  public void takeDamage(int damage) {
    this.health = health - damage;
  }

}

And then later:

Unit whiteKnight = new Unit();
Unit blackKnight = new Unit();

//a possible combat interaction
whiteKnight.attack(blackKnight);
blackKnight.attack(whiteKnight);

Is this acceptable? Is this the standard?

\$\endgroup\$
  • 1
    \$\begingroup\$ I don't think there is a standard. But your system is probably the easiest and is totally ok, especially if it works for you. \$\endgroup\$ – ElDuderino Nov 25 '15 at 10:36
  • \$\begingroup\$ There is elegance in simplicity. The way you have it isn't wrong at all. There are thousands of ways to implement it. The way you've modeled it is pretty good, because you're not modifying the class's health from the outside directly. Having a separate attack and reaction function allows you to compartmentalize the Unit's attacking functions and the other Unit's response to being attacked. Your method makes it easy to track down when the health is being modified in your code. The only drawback would be if you didn't want each object to know about the other object, and instead wanted a manager \$\endgroup\$ – Premier Bromanov May 23 '17 at 16:55
1
\$\begingroup\$

I highly suggest to stick to the entity/component system. To give you an example with your mentioned design.

You would create 2 components

  1. AttackComponent
  2. HealthComponent

The AttackComponent would have a method to attack an entity.

The HealthComponent would have a method to modify the health property.

Then you attach those 2 components to every entity that you want to be able to fight or only the health component if you want the entity to be able to take damage.

Then in the AttackComponent method attack, you would take the entity as target and just take theirs healthcomponent and call the modify method.

It is maybe a bit more work to have a working entity/component design, and it is maybe sometimes a bite overkill to generate an own component for such an easy task like health, but believe me, your game will be way more easier to create/manage/handle and of course you will always have the possibility to convert such a system, easy into an editor.

| improve this answer | |
\$\endgroup\$
0
\$\begingroup\$

Mediator design pattern is what you want to do eventually. Links to resources at bottom of post, and link to an amazing book from 1994 that I highly suggest to anyone who does programming.

There is no one way to program this design pattern properly. You can make the mediator do all of the work, and just update the objects, or have the mediator not care about any passed messages, and have the game objects handle the messages themselves.

I apologize if my java code is incorrect. I haven't played with Java for a while. Here is a quick example that fits your code:

CUnit.java

import IMediator;
public class CUnit {
    private IMedator m_Mediator;
    // You will want this to be overrided by the WhiteKnight/BlackKnight classes.
    public void ReceiveMessageFromMediator(NameValuePair message)
    {
        if ( message[0] == "attack" ) 
        {
            takeDamage(message[1]);
        }
    }
}

CUniManager.java

import IMediator;
import CUnit;
public class CUnitManager implements IMediator
{
    public void SendMessageToMediator(NameValuePair message)
    {
        if ( message[0] == "attack" )
        {
            CUnit curUnit = findUnitByName(message[1]);
            if ( curUnit != null )
            {
                curUnit .ReceiveMessageFromMediator(new NameValuePair(message[0], message[2]);
            }
        }
    }
}

After mediator design patterns there are other types out there. Such as Subscribe/Publish.

Links:

https://sourcemaking.com/design_patterns/mediator

http://www.amazon.ca/Design-Patterns-Elements-Reusable-Object-Oriented/dp/0201633612/ref=sr_1_1?ie=UTF8&qid=1448477793&sr=8-1&keywords=design+patterns

Publish/Subscribe https://en.wikipedia.org/wiki/Publish%E2%80%93subscribe_pattern

Related question https://stackoverflow.com/questions/3162291/mediator-pattern-vs-publish-subscribe

| improve this answer | |
\$\endgroup\$
0
\$\begingroup\$

The other two answers by LukGus and Yosh Synergi address one half of the problem: the entity design where the Attack() and TakeDamage() methods are defined, but I thought I'd address the other half, that is, this part:

Unit whiteKnight = new Unit();
Unit blackKnight = new Unit();

//a possible combat interaction
whiteKnight.attack(blackKnight);
blackKnight.attack(whiteKnight);

Generally speaking you won't actually do this. Mainly because this only allows for a finite number of units in your game and with a finite number of interactions. What if you wanted a third unit on say...Team Red? You can't just add another Unit RedKnight and have it attack/be attacked like the white and black ones here, afterall who's attacking whom and when? Is there a color-advantage? (i.e. white always attacks first, allowing white teams to slay black and red units before those units can deal damage to white).

So the general idea (and exact implementation will differ) is to store an array of units, possibly separated by team/faction/side/alliance or by type (e.g. for a Bullet Hell sort of game, you store Bullets in one array, Entities in another, and visual flair that doesn't interact with the previous two, in a third, etc.) and then you just loop through the arrays looking for overlaps (or other qualifiers of "do I attack now?").

e.g.

List<Unit> whiteTeam = new List<Unit>();
List<Unit> blackTeam = new List<Unit>();
...
foreach(Unit white in whiteTeam) {
    white.update(); //movement, etc
}
foreach(Unit black in blackTeam) {
    black.update();
}
foreach(Unit white in whiteTeam) {
    foreach(Unit black in blackTeam) {
        if(white.canAttack(black)) { //are they in range, etc?
            white.doAttackAndDamage(black); //deal damage
        }
        if(black.canAttack(white)) { //am I in range (etc) of them?
            black.doAttackAndDamage(white);
        }
    }
}

Notice that white units are never checked against other white units. Also note that both white and black attacks are processed at the same time. This may not always be possible. I did it here, because every attack is an A<->B comparison, it doesn't matter if we break it out into two nested loops (checking A->B, then again for B->A) or do it as a single nested loop (checking both at the same time) except that doing it this way is computationally faster.

The same would be true for games where bullets are in play: it is going to be very rare for bullets to need to check collisions against other bullets, so by separating out bullets and units by team means that each bullet makes fewer checks: it only needs to check against the enemy units. If a bullet does need to intercept (collide) with other bullets, it can be handled separately.

You have to watch out for handling ostensibly simultaneous events in a sequential manner. You don't want to give an edge to the white team by processing all white team attacks and removing black units from the array before processing the black team's attacks (unless the game is turn based and you do want that, but it you don't handle that in a white-then-black manner, but usually attacker-then-defender). But you might want to know that an enemy has been slain (or a bullet intercepted or whatever) while still processing the same side. E.g. if White Dude #17 killed Black Dude #12, White Dude #18 should look at Black Dude #12 and go "I don't need to attack him, he's already dead, but Black Dude #13...he's still alive and in range."

So your canAttack() method will probably end up being rather complicated (are they alive, are they in range, am I on cooldown, have I been stunned? etc). As well as your entity update loops. I know the one I have for a scrolling 2D shooter is: it has to process enemy bullets, player bullets, enemies, allies, particles, and bullet/bullet collisions (only across teams) and only after all of that is done, do things get removed for being dead/intercepted/old/off-screen. And things dying might spawn more bullets (e.g. an enemy that explodes on-death).

And that way everything happens in a way that looks fair to the player: if two bullets that both cause interception hit each other, then both should be intercepted (it shouldn't matter which one runs first).

But say I want to have a bullet that acts like an energy shield that intercepts, say 10 bullets? What happens if it intercepts another bullet that itself can intercept multiple bullets (say, 5)? What happens then? Well, in my case I said "ok, both of you subtract your intercept counts from each other, anyone at or below 0 remaining intercepts dies."

One of the problems I ended up having was dealing with bullets that move very very fast. It was easy for them to "skip over" a smaller, slower moving, interceptor on the other team. So I had to break down the movement into "move no farther than my own size, check collisions, repeat." So all of the collision code is actually inside the movement loop, so things got insanely convoluted quickly. For large battle zones with lots of units (or bullets), it would be completely infeasible to check against everything in the scene, at which point you have to break things down into quadtrees or similar.

By breaking things into quad trees, you break up your arrays, yeah these:

List<Unit> whiteTeam = new List<Unit>();
List<Unit> blackTeam = new List<Unit>();

So that they themselves are abstracted away. The TL;DR is that you break space into chunks and each entity/bullet only has to query what chunk its in (and the neighbors) and check against only the entities contained within that volume. Anything that's way over on the other side of the map isn't even considered. We've already pre-computed the distance check ("am I in range?") for a large majority of the battlefield. It still needs to be called, but it will only be queried for objects that might be in range. That is, if it's 1000 pixels away and no attack reaches farther than 50 pixels, we can break the battlefield down into 10 unit-wide volumes. No matter where inside that 50 by 50 pixel sized area a unit is, we know for a fact that it can only attack things in its volume or the 8 neighboring volumes. Anything farther away than that is Definitely Too Far Away and we don't even bother asking.

So instead of 10,000 bullets having to check collisions against 10,000 bullets, they instead check against only the few dozen in close proximity, saving even more CPU.

| improve this answer | |
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.