2

Hello again Stack Overflow. you probably remember me from my unit spawning problem in my pygame program, "Table Wars." I decided to change the scope of my game to a real-time strategy rather than a turn-based game. I want the game to play along the lines of top Flash game: "Age of War." Almost everything works in the game: spawning units, the HUD for the game, and even base health. Unfortunately, I can't seem to figure out how to implement the ability for units to attack enemies or the enemy base. Here is the concepts going on for the units themselves:

  • The unit spawns on a keypress around the team's base: K_1 spawns a sprite from the Red_Infantry class
  • The unit, when spawned, is added to a Group class. There are two Groups, one for each team.
  • The unit moves via a move_ip call within a def update until it reaches a point close to the enemy base, where it stops.

Here is how I want combat to go for the different units:

  • The unit stops whenever it spots an enemy within it's attack range. Different units have different attack ranges
  • The unit then attacks in one-second intervals.
  • If the unit sucessfully reduces the enemy unit's health to 0, the enemy dies, and the other may continue
  • This cycle repeats until the unit reaches the enemy base, where it will then attack the enemy base on one-second intervals. One of the units will be able to deal triple the normal damage to the base.

Here is a sample of my code, showing the Red_Infantry class:

class Red_Infantry(pygame.sprite.Sprite):
def __init__(self, screen):
    pygame.sprite.Sprite.__init__(self)
    self.image, self.rect = load_image('Soldier_red.png', -1)
    self.rect.move_ip(random.randint(75, 100), random.randint(275, 325))
    self.selected = 0
    self.area = screen.get_rect()
    self.health = 100 #Soldiers are have mediocre toughness.
    self.attack_damage = 25 #The amount of damage it deals
    self.range = 20 #The attack range of the unit.
    self.update()
def update(self):
    self.rect.move_ip(1, 0)
    if self.rect.right >= 725: #This position is close to the enemy base...
        self.rect.right = 725 #...where it will then stop
    if self.health <= 0:  
        self.kill() #a simple code that kills the sprite if his health reaches 0

The main loop only contains the ability to spawn each of the units.

Oventoaster
  • 299
  • 1
  • 5
  • 13
  • 1
    Can you elaborate a bit what your `Group` class look like? `if unit.attack = any(unit.in_range(baddie) for group in all_enemy_groups for baddie in group.units)` Will yield `True` if in any of the groups in `all_enemy_groups` there is at least one unit `baddie` which is in range of `unit`. – Manuel Ebert May 09 '12 at 15:51

2 Answers2

3

It's not entirely clear from your question how groups interact with each for spotting. In the following, I'm going to assume that Group A "spots" Group B if any member a of A is within some specified range of any member b of B.

The simplest way to do this would be to just iterate over all (a,b) pairs. To do this, you can use the itertools library, something like...

spotted = False
for a, b in itertools.product(A.sprites( ), B.sprites( )):
    if is_in_range(a, b):
        spotted = True
        break

The problem with this approach is that the computational cost is rather high. (It's n**2 in complexity.) Furthermore, without some kind of pruning and optimization, you have to run this block of code for each pair of friendly/enemy group, too. Now, if each group is guaranteed to have some constant geometry, then we can make the cost of computing MUCH cheaper. However, assuming that groups have no fixed geometry, then I'd recommend looking into using a geometry package to do a lot of the work for you. These packages are very powerful and very efficient...and a lot of them are also surprisingly easy to use. There may be geometry packages specific to PyGame...I can't think of any at the moment.

I often use the shapely package. If we use shapely, then the problem of determining groups that are within detection range is something more like...

import shapely
import shapely.geometry

#-- Build a polygon envelope around the members of the group.
pointsA = shapely.geometry.MultiPoint(map(lambda r: r.center, A.sprites( )))
pointsB = shapely.geometry.MultiPoint(map(lambda r: r.center, B.sprites( )))

#-- Ask shapely to give the minimum distance between the hulls that
#-- encompass the two groups.
distance = pointsA.convex_hull.distance(pointsB.convex_hull)

if distance < RANGE_OF_DETECTION:
   detected = True
else:
   detected = False

NOTE that I haven't tested the code above...it's only to demonstrate the general idea of using the shapely library to help with the geometry computations.

If you're new to game programming, you might also want to look into using quadtrees as a means of pruning how much geometry computation needs to be done.

parselmouth
  • 1,598
  • 8
  • 8
  • My program uses a single group. If you think having two seperate groups for the teams will help, then I will try that. If it helps, there are four unique units per team, and you can spawn as many as you want – Oventoaster May 10 '12 at 01:52
  • I'm not sure what you mean by quadtrees. Can you explain in detail how those work? Perhaps you can offer an example? – Oventoaster May 11 '12 at 22:26
  • quadtrees are a 2-D spatial data structure that index a plane. The idea being that you can query the quadtree to see what's near by point (or object) p, without having to compute the distance between p and EVERY object that's in the plane. Take a look at the wikipedia page for quadtrees: http://en.wikipedia.org/wiki/Quadtree – parselmouth May 11 '12 at 22:29
  • I am now using the itertools library and two separate groups. However, I want the units to be able to individually attack each other. what should I do? – Oventoaster May 16 '12 at 21:24
1

Here's a starting point

class RedInfantry(pygame.sprite.Sprite):
    def __init__(self):
        self.screen = pygame.display.get_surface()
        self.image, self.rect = load_image('Soldier_red.png', -1)    
        self.rect.move_ip(random.randint(75, 100), random.randint(275, 325))
        self.target = None
        self.range = 20
        self.attack_cooldown = 200
        self.attack_last = 0

    def move_toward(self, target):
        # move toward players click destination, if one is set.
        # else toward my attack target

    def update(self):
        # move...
        self.find_target()
        self.move_toward(self.target)
        self.attack()
        # check HP
        if self.health <= 0:
            self.kill()

    def find_target(self):
        """finds new targets in range:
        for speed: only call this once every 200ms."""
        if self.target is not None: return
        for enemy in B.sprites():
            if distance(self.rect.center, enemy.rect.center) <= self.range:
                self.target = enemy
                return
        # else no targets in range
        self.target = None  


    def attack(self):
        """attack, if able.
        target exists? still alive? gun cooldown good?"""
        if self.target is None: return
        if self.target.health <= 0: return
        if not self.cooldown_ready(): return

        # all good, fire. could verify still in range. 
        self.target.damage(self.attack_damage)

    def cooldown_ready(self):
        # gun ready to fire? has cooldown in MS elapsed.
        now = pygame.time.get_ticks()
        if now - self.attack_last >= self.attack_cooldown:
            self.attack_last = now
            return True
        return False
ninMonkey
  • 7,211
  • 8
  • 37
  • 66
  • Where do I call the `distance` variable? I get a "global name distance is not defined" error when I use this block. Also, for future reference, the names of my groups are RedTeam and BluTeam, not A and B. – Oventoaster May 22 '12 at 17:40
  • Disregard my earlier comment, I figured it out. Thanks a lot! your code works wonderfully! – Oventoaster May 22 '12 at 21:31