Post-mortem (Jjp-pyweek17)

I did mention when I announced that my game has been uploaded that I wasn't as satisfied with this entry compared to my previous one, so I was pleasantly surprised by the majority of the feedback that I got. Firstly, I'm really glad that many of you found it enjoyable to play. That's probably the most important part of a game, in my opinion. :)

I was a bit disappointed to read that the game crashed for at least two people, but that's what happens when I implement most of the actual gameplay in the last two days of PyWeek. In particular, I didn't even get the shop done until about four hours before the coding deadline, and I just finished adding sounds about an hour before it, too. If it does crash for anyone in the future, feel free to post a traceback in the comments.

In the end, this PyWeek was even more fun and interesting than the last one, particularly since I felt that I learned even more from this one. I'll go over a bunch of thoughts and lessons that I have collected now that the judging period is over.

The parts that were never realized

I think the main reason that I was initially not as satisfied with the result was because there were so many things that I had to remove due to time constraints. Now that I look at how many .py files there are in the source code folder, I kinda surprised myself with how much I actually implemented during that week. Despite that, there are a lot of unimplemented things that could have made the game better.

The pacing of the game, which several of the comments alluded to, did suffer partially because of a “scripted” event that I did not have time to implement. It was going to occur on the third level, and it would have spawned a few enemies that you had to run away from after you got the coordinate data on that level. In retrospect, however, I should probably just cut that level out altogether since people probably understood the excavation mechanics after the second level, if not the first one. If I had done that, the player would have seen the enemies a bit sooner, which would help the game's pacing.

Related to that is that I was also going to put some story elements in the game. I was going to have log files (that were buried for some reason) that the player can read, an introductory wall of text when the game started for the first time, and an ending wall of text that explains just why that mysterious red crystal was so bad. This does sound ambitious, which was probably why it never made it into the game.

I was also going to make the first level be the tutorial, but I settled for writing a manual instead. I'm glad that the manual helped people understand the gameplay mechanics. A tutorial would still be ideal, though, since playing is more fun than reading instructions.

Many people also pointed out the lack of music, and that's one aspect of the game that I simply did not have time for, unfortunately. I agree that it would really help the game out a lot.

There were a lot of other things that I wanted to include, such as optional levels, but I can't have everything. Well...I can always work on this game more now that this PyWeek is essentially over, right? :)

The learning process

I implemented many new things for the first time during this project, and some of this stuff turned out to be tricky. I think I spent the first day or so just implementing a scrolling camera. Some stuff turned out to be much easier than I thought, however, especially save games. The pickle module pretty much did the work for me in that case, although I'm a bit aware of its drawbacks. For something like PyWeek, though, it's fine :p

Also, my collision detection code is awful and inefficient. Just take a look at this snippet of code from terrain.py:

  def process_bullets(self):
    for bullet in self.bullets:
      bullet.update()
      bullet_rect = bullet.get_pos_rect()
      player_hitbox = self.player_vehicle.get_hitbox()
      
      if not bullet.friendly and player_hitbox.colliderect(bullet_rect):
        self.player_vehicle.on_damage(bullet.damage)
        bullet.hit = True
      
      elif bullet.friendly:
        for enemy in self.enemies:
          enemy_hitbox = enemy.get_hitbox()
          if bullet.friendly and enemy_hitbox.colliderect(bullet_rect):
            enemy.on_damage(bullet.damage, self.player_vehicle)
            bullet.hit = True
            self.enemy_hit_sound_play = True
            break # Only one enemy should be hit per bullet
         
        if bullet.hit:    
          alert_rect = pygame.Rect(0, 0, 500, 500)
          alert_rect.center = bullet_rect.center
          
          for enemy in self.enemies:
            enemy_rect = enemy.get_pos_rect()
            
            if alert_rect.colliderect(enemy_rect):
              enemy.aware = True
              enemy.aware_timeout = 5 * FPS

This code was actually a bit problematic towards the end of the week. During the second-to-last day, I started creating the levels, and the level sizes were so big that stray bullets were inevitably everywhere, slowing the game down to as low as 40 FPS instead of the 60 that I was aiming for. As soon as I realized this, I got a bit discouraged. Reducing the level sizes did help to cloak this problem, and it also made it easier to populate the levels with enough enemies instead of having vast, empty spaces, but it was still annoying to deal with. If I ever make another game that depends heavily on collision detection, I will need to implement some more efficient techniques.

The menus were also painful to write, although remembering that functions can be assigned to variables really helped a lot. The shop system is just...bad. Please don't look at it :p

Finally, unlike the last entry, I wanted to have some graphics other than rectangles, and so working with those graphics was something that was new to me, too.

For next time...

I think I should probably try to have an idea that doesn't depend too much on level data, since I'm kinda tired of making one-off level formats. I did that during the last entry too, and it was annoying to have to write code to parse my hand-rolled level format and to make sure that the levels were actually how I intended them to be. In fact, during the hour or so I spent playtesting, I found a bug where one of the weapon blueprints didn't spawn because I typo'd the item id number.

Either that, or I should use formats that already exist. I saw that one entry used JSON(?) or something like that.

Also, as mentioned above, I definitely will have to research better methods of collision detection and practice implementing them. I can't be using the naive and inefficient method forever :p Quadtrees, anyone? (or anything else?)

And I should put some music in already...

In conclusion

This was a fun PyWeek, and I enjoyed creating something for it. I feel bad admitting that I only played roughly one-fourth of the other entries, but many of them were quite fantastic, and I still have no idea how some of you manage to make 3D-based games in a week!

Also, I might work more on this game and on my last entry as well, so I might announce updated versions (and hopefully Windows builds) later on. I can't promise anything though since I am a college student, after all :p

Finally, thanks goes to the PyWeek organizers and to everyone that played and gave feedback on my game. :)

(log in to comment)

Comments

Ah, collision detection is a fun subject. :D

Quad-trees and BSP trees are fun -- I've always been fond of the latter -- but for a quick practical solution, one good approach is to just split the map into an even grid, keep track of which objects are in which grid box, and do naive collision detection in each box. You could probably get away with a fairly small grid box size in 2d, and building the sets of objects in each grid box on every frame would probably still be way cheaper than naive O(n^2) collision detection. :)

Quad/BSP-trees built from the map use the same basic idea, but using the leafs of the tree instead of boxes of a grid. Bit more expensive to build the set of objects for each leaf, but massive space savings if you have a lot of 'dead' space in your map (you can't really cover any significant 3d volume with a fine grid...).

You could also build quad/BSP-trees from the objects themselves, or use segment/interval trees on the bounding boxes. I'm not thrilled at the idea of dynamically building these trees on every frame in python, though, and they're messy to update. Probably only worth it if you need to deal with really massive volumes of space, with very high local density of objects.


Finally, you can do some neat things just by sorting the lists of items in one dimension. E.g. on the x axis, build a list of objects sorted by left edge (lower x value) of the bounding box, and create an (initially empty) heap of active objects keyed by right edge coordinate. Iterate over the list and for each object, remove any objects on the heap with right edge coordinate less than the object's left edge coordinate, then add the new object to the heap and check for collisions between it and all objects currently in the heap. To avoid catastrophic behavior if all objects are on a vertical line, split it into intervals (1d, so can go very fine without space problems) and keep a heap for each one, or (bonus points) use an interval tree for quick checks along the other dimension.


But again, for a one-week project, a 2d grid is straightforward to implement and would probably work fine. :)