Sunday, July 12, 2015

Prototyping: Mother Brain II


So, in looking at the sprites for Mother Brain, I notice that she is put together very modularly. What I want to do as a sort of experiment is apply the various states of her animations in segments as opposed to blitting the whole sprite directly on. What I mean by this is instead of having frames for each permutation of her eye opened and closed, I just have a separate part for her eyes. Which if it goes well, I will separate her out entirely. So, updating the sheet leaves me with this:

In code, I'll work with the existing sheet plus this one. The change is pretty minor. We need to store the location of the eye in relation to the sprite, which is stored as the (col, row) location in the room. We need to store the eye images. I chose a dictionary so that the named states can be referenced. And then we store the state of Mother Brain, which I called awake. If she's awake, the eye is opened. Else it's closed.

        self.eye_coord = (5,7)
        self.eye = {
            'opened': pygame.Surface((TILE_SIZE, TILE_SIZE)).convert(),
            'closed': pygame.Surface((TILE_SIZE, TILE_SIZE)).convert()
        }
        self.eye['opened'].blit(self.eye_sheet, (0,0), (0, 0, 16, 16))
        self.eye['closed'].blit(self.eye_sheet, (0,0), (16,0, 16, 16))
        
        # When awake, eye is opened. Else, closed.
        self.awake = False

After that, we simply draw based on her alertness

    def draw(self, surface):
        # -- snip prior drawing steps --
            
        x, y = pixel_from_tile(self.eye_coord[0], self.eye_coord[1])
        if self.awake:
            surface.blit(self.eye['opened'], (x, y))
        else:
            surface.blit(self.eye['closed'], (x, y))

And her being awake or asleep is simply changed on a toggle method at the moment.

Prototyping: Mother Brain I


In this update, I am adding Mother Brain.


This is pretty much the same process as with the food tubes. The first thing I do is just get the first image from the sheet on screen. So, there you go:


The next thing I do is get her glass tube set up. Here is the back added:


Now, the front glass is a little more complicated. First, I have to copy the section on the sheet which contains the open door. Then, I copy the closed tile and the opened tile. Then, blit the original seection to two new surfaces. One gets the closed part blitted black, and the other gets the opened part blitted closed. In the end, it will probably be tiles just like the base image, but I'm not entirely sure. I'm just trying a different approach to see how things work out.

Here is what it looks like with just the base image copied directly


Now, this image won't actually ever be seen in game. It's only an in-between state. So, we copy the tile from the original and create a new surface for the closed glass. After blitting that together, it looks like it should:

That takes care of closed glass state. Now just take the original glass, and fill a single tile with black to open the glass up.

With both states completed, create a flag on the Mother Brain object which draws the correct glass.

class MotherBrain(object):
    '''
    
    '''
    def __init__(self, col=3, row=5):
        '''
        OK. So we really don't need col, row passed in but I want to make it similar 
        interface to the existing Food class, so I can refactor easier into a more 
        abstract object later.
        '''
        self.sheet, self.rect = load_image(join("assets","motherbrain_sheet.png"))
        self.image = pygame.Surface((TILE_SIZE*5, TILE_SIZE*4)).convert()
        self.image.fill((0,0,0))
        
        self.image.blit(self.sheet, (0,0), (0, 0, 48, 64))
        
        self.top = (col, row)
        
        self.back_glass = pygame.Surface((TILE_SIZE*2, TILE_SIZE*4)).convert()
        self.back_glass.blit(self.sheet, (0,0), (528,0, 16, 64))
        
        # copy the raw image from the sprite sheet.
        self.front_glass_raw = pygame.Surface((TILE_SIZE*2, TILE_SIZE*4)).convert()
        self.front_glass_raw.blit(self.sheet, (0,0), (544, 0, 16, 64))
        
        self.front_glass_closed = pygame.Surface((TILE_SIZE*2, TILE_SIZE*4)).convert()
        self.front_glass_closed.blit(self.front_glass_raw, (0,0)) # blit original glass
        self.front_glass_closed.blit(self.front_glass_raw, (0, TILE_SIZE*2), (0, TILE_SIZE*1, 16, 16))
        
        self.front_glass_opened = pygame.Surface((TILE_SIZE*2, TILE_SIZE*4)).convert()
        self.front_glass_opened.blit(self.front_glass_raw, (0,0)) # blit original glass
        self.front_glass_opened.fill((0,0,0), (0, TILE_SIZE*1, 16, 16))
        
        self.opened = False
        
    def transform(self):
        '''
        Technically, once the glass opens it shouldn't close again, but it is nice to have 
        versatility in code which can be later removed once closer to completion.
        '''
        self.opened = not self.opened
        
        
    def draw(self, surface):
        x, y = pixel_from_tile(2, 5) # back glass (col, row)
        surface.blit(self.back_glass, (x, y))
        x, y = pixel_from_tile(self.top[0], self.top[1])
        surface.blit(self.image, (x, y))
        x, y = pixel_from_tile(6, 5) # front glass (col, row)
        if self.opened:
            surface.blit(self.front_glass_opened, (x, y))
        else:
            surface.blit(self.front_glass_closed, (x, y))

I will definitely be doing this with the tiles themselves. In reality, the open/closed glass should be treated as a door because that's what it is. However, I haven't written any code about doors yet. So, we'll see how that goes. At any rate, that's the update for now.

Friday, July 10, 2015

Prototyping - Food Tube

Anyway... so I've decided to go in a different direction because it is a bit foolish to spend too much time on any one aspect when entering into a new project. It's just hard to know what you need or will need. By jumping around, you end up with the likelihood of being able to use previously created things in different areas -- kind of like getting a power-up in a metroidvania game that allows you to progress further. Weird.

With that said, I started creating a little bit of functionality for the final boss. It's a good idea to do as little as possible at a time to get a result, and then build on the results. For example, the very first thing I did was display the ripped screen to the window. All this is is the regular image file.


Then, using the ripped tiles from earlier exercises, I created the mapping data, and loaded the images by tiles instead. It resulted in the exact same image as above except now it was drawn tile by tile instead. From here, it was time to add the food tubes that block the way. First, I simply added the sprite to the map.


If you've ever played Metroid before and made it here, you know that when Samus damages these things, they shrink down. If she cannot kill them in a timely manner, they will regenerate. Well, the first thing I did was order the various sprites from healthiest to weakest in a sheet:


So, now we can say that at the most healthy, we want the sprite at (0,0). When it is a little damaged, we want the next healthiest at (16, 0). And so on. And since they are in order, we can create an attribute on the Food object called health which goes from 0 to 4. If we take the health, and multiply it by the tile size, we can get the correct x coordinate to start cropping from. In code, it looks like this:

class Food(object):
    '''
    This is the food tube that supports Mother Brain. It starts fully replenished, 
    and when damaged, goes through 3 additional states of decay. If Samus does not 
    completely destroy it in a set time, it will regenerate [not implemented].
    
    The health of the food tube determines where the spritesheet should be copied from. 
        self.health * TILE_SIZE
    At full health, health is 0 and so = 0
    At full health, health is 1 and so = 16
    At next health, health is 2 and so = 32
    At next health, health is 3 and so = 48
    And when dead, do not draw at all.
    
    All food tubes occupy two tiles in the room. Provide the top col and row, and 
    the bottom is calculated.
    
    10, 6 is where demo tube is located on room.
    '''
    def __init__(self, col, row):
        self.sheet, self.rect = load_image(join("assets","food_sheet.png"))
        self.image = pygame.Surface((TILE_SIZE, TILE_SIZE)).convert()
        self.image.blit(self.sheet, (0,0), (0, 0, 16, 16))
        
        self.top = (col, row)
        
        self.health = 0
        self.is_alive = True
    
    def transform(self, change=1):
        '''
        Food tube will change based on damage. Once dead, cannot transform.
        '''
        if not self.is_alive: return
        
        self.health += change
        self.health = max(0, min(self.health,4))
        
        if self.health >= 4:
            self.is_alive = False
        self.image.blit(self.sheet, (0,0), (self.health * TILE_SIZE, 0, TILE_SIZE, TILE_SIZE))
        
    def draw(self, surface):
        print("self.health={0}".format(self.health))
        if not self.is_alive: return
    
        x, y = pixel_from_tile(self.top[0], self.top[1])
        surface.blit(self.image, (x, y))
        surface.blit(self.image, (x, y+TILE_SIZE))

Oh... one particular thing to note about this code that someone reading might find interesting is that to force a value to be between two values (ex: 0 <= x <= 4), then you can call min() on the value to bring it into high bounds, and then max() on that to bring it into bounds low bounds. The reason I bring this up is because it seems a little counter-intuitive. But, it does work.

When I want to test some aspect like this without having to noodle around with a bunch of additional details, I just set the various states to a key event. So, pressing left damages the tube and pressing right heals it. Once the health fully expires, pressing left and right does nothing.

    while running:
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                running = False
                
            if event.type == pygame.KEYDOWN:
                if event.key == pygame.K_LEFT:
                    food.transform()
                elif event.key == pygame.K_RIGHT:
                    food.transform(-1)

The next thing to do is start getting Motherbrain set up. I also just realized that I could have ordered those food sprites in reverse so that the health actually makes sense in context to a higher number being more healthy instead of how it is now where zero is most healthy. I'll actually probably wait until I'm making the real thing to make the change.

Friday, June 26, 2015

The Map III

Unfortunately, I was a bit ignorant about approaching the map. In saving the files, I saved them as the default .xcf file type that gimp uses. As it turned out, what I really needed was .png files. Any time you have 100+ of something, it's generally a really good idea to take a second and see if you can automate whatever you're going to have to do.

So, I wrote a program that went over the map, and copied each individual unique room to file. It was compared against using python's ImageChops and the PIL.

My next step is now to go through the saved rooms, and store the unique tiles using essentially the same code. Now, I know that some tiles will be junk, such as the word tiles, monsters, powerups, and such. So, once that's done, I'll put good tiles into one directory, and bad into another. Then, I'll go back over the saved rooms. If the tile is in the good tiles, I will draw it. If it's in the bad tiles, I'll draw a black box (because everything is black) to erase the offending tile.

This will clean all of my rooms up to be blank. I have to think more about further steps though.

Monday, June 22, 2015

The Map II

I'm currently taking a small break from working with the map image, and what I've been doing so far is changing all blank areas (rooms that are completely black) to white areas, which will mean it is totally empty when I read the file programatically. Unfortunately, the creator of this map used the same color as what is in the tiles (black), so it was not a simple matter of just bucketfill or what have you. I endured the tedium of doing this manually.

And with that done, I have also started going over the map and saving the rooms themselves to files. Anyone who has played Metroid would agree with me that the game is visually repetitive. It's because they reuse rooms often. I save one and delete the rest. Thankfully the differences are fairly easy to spot, and a room is usually placed only within proximity of each other of the same kind. Regardless, it is mind-numbingly boring.

So, the map itself is comprised of 30x30 room sections or 900 rooms, however, many rooms are not used. In fact, 387 rooms are empty. This gives us 513 rooms at face value, but many rooms are solid blocks, which serve to end a room pattern. There are 58 of these rooms. The total explorable rooms is now 455.

That is all for now.

Sunday, June 21, 2015

Déjà vu

I recently started working on this project again, having forgotten all about having kept record of what little work I had previously done on this blog. As I began searching for a site to blog my project, this came up and thanks to google's auto-login features, I was reminded of this blog's existence.

This makes me quite happy because it saves me some work.

At any rate, I have decided to start with creating a clone of classic NES Metroid in python using pygame. Sure, using Super Metroid tiles for the finished product would be great, but realistically, it's a pretty small change. What needs to happen right now is simply to create the clone to the point of not being able to discern which version of the game it is without some careful analysis, such as jump heights, timing, etc -- which will probably be very difficult to get just right.

Obviously, this is not as interesting as the initial project idea because hey, if you want to play Metroid, just go play it on NES.

Regardless, for the sake of memory and moments like now, I will blog my progress.

Stay tuned.