Yeah, the title says it all really. There's a game I've been wanting to make for a while, only I haven't managed to get the hang of graphics using C++, which had been my language of choice, so I decided to learn python while I was at it. I've been surprised at how quickly it's moving along; what I've got so far is about three days worth of work. This is the first big thing I've ever done in python, so it probably isn't the best or neatest code, but it works. The point of this thread is to get people's opinions about what I've done so far and get suggestions for what to do next.
So far, my effort has been concentrated on the overworld/map part of the game. In addition to the below, I've got some rudimentary menu setups with buttons and a few nondescript informative display objects.
Implemented features (* is recently implemented features):
Can control player using arrow keys
Collision detection with map edge
Can set player speed
Objects can sit on the map, blocking or allowing player motion, and be moved/removed/added
Objects can be interacted with using spacebar, causing a preset effect
Objects (including player) can have animations set to play on command, which will either loop, pause, or play a different animation once run through. Animations can be paused and added to existing objects, but cannot be removed. Player must have four 'idle' animations (one in each direction) and four 'moving' animations (again, one in each direction) as well as a default animation to handle errors. Maps can be animated as well, though they can only have one constantly looping animation.
Portals between maps can be added to maps. A portal is set to open either from all sides or from a particular side, can be open or shut, and must have four animations corresponding to closed, open, closing, and opening states. (note animations do not need to be more than one frame)
*Map will scroll around the player as they move, keeping the player within a specified rectangle on the actual screen.
Planned features (meaning I know how I'm going to do them and just haven't finished them yet):
Player motion will have the option of being a little more detailed, with running added
Portals will be able to move a player to a different spot on the same map.
Objects on the map will be able to follow simple behaviors (move back and forth, block the player from entering a well, etc)
Sound
Areas on the map will cause effects when moved through, such as random encounters, slowed player movement, etc
Popup menu related to game
Hoped for features (meaning I haven't the slightest idea how to do them, but want to):
Distortion/particle effects to be applied to sprites/maps
Transition effects between maps/scenes (fadeout, wipe, etc)
AI for objects on the map to respond to player motion and actions appropriately, or interact with the player itself
Support for multiple player characters at the same time (one using arrow keys, one using wasd, one using a joystick... however many control schemes I can think up)
And of course, I've still got to build a battle system and character stats and stuff. One nice thing about it though is that it's incredibly easy to render cutscenes using the game engine, so there's one fewer thing to worry about.
Now some questions:
What kinds of NPC interactions are there? Some obvious ones are teleporting the player, starting a cutscene, opening/closing a portal, moving around, adding or removing map objects or bringing up a textbox. Anything important I'm missing?
Second, this being more a programming question: Does it make more sense to handle all the things going on in the main program, or to set up a mapHandler class to deal with all the events related to maps going on? I can see some organizational advantages to the second, but it seems like it would take a fair amount of extra work to pass the relevant data back and forth between the main program and the class. On the other hand, it seems like it might make transitioning between moving around on the map and fighting monsters and whatever else it is you end up doing easier.
For those interested, poorly-commented source code:
import pygame
import sys
from pygame.locals import *
class mapObject(object):
def __init__(self, xx, yy, img, f = 'png', s = 1, layer = 0):
self.x = xx
self.y = yy
self.image = pygame.image.load(img + '.' + f).convert_alpha()
self.renderLayer = layer
self.alpha = 100
self.rect = self.image.get_rect()
self.mask = pygame.mask.from_surface(self.image)
self.mask.fill() #see if you can find what's up with from_surface; this shouldn't be necessary.
self.solid = s
def getRect(self):
return pygame.Rect(self.rect.x + self.x, self.rect.y + self.y, self.rect.width, self.rect.height)
def blank(self, img, color):
return img
def flicker(self, color, frame): #returns sprite with 'frame' frame of flicker to color effect. 'color < 0' means transparent
if frame == 1:
return image
else:
return blank(image, color)
#add more of these as you think of them/can
'''
interacts are implemented like buttons. Only work when active (players are always inactive, to avoid problems.)
'''
class Interactive(mapObject):
def __init__(self, xx, yy, img, zrect, act, f = 'png', a = 1, adir = 0, s = 1, layer = 0):
mapObject.__init__(self, xx, yy, img, f, s, layer)
self.action = act
self.zone = zrect
self.active = a
self.actdir = adir # direction in which actions are possible. If 0, no restrictions. 1 from south, 2 from west, -1 from north, -2 from east.
def interact(self):
return self.action(self)
def getZone(self):
return pygame.Rect(self.zone.x + self.x, self.zone.y + self.y, self.zone.width, self.zone.height)
def drawZone(self, surf, color):
pygame.draw.rect(surf, color, self.getZone(), 0)
'''
WRT Animations, any animate object has at least one animation which has some number of frames.
0th animation is idle, and there is an animation manager class which keeps track of the frames gone by and repeats and stuff.
'name' is a single image which exists to handle animation glitches (in case of glitch, the image shows up rather than an animation).
each animation is a name and a number n, corresponding to the image and the total number of frames, and a string corresponing to end behavior. Images are loaded in the form 'name_x' from x = 0 to n. End behavior is either 'pause' or the next animation to play.
'''
class Animate(Interactive):
def __init__(self, xx, yy, name, anims, zrect, act, f = 'png', a = 1, adir = 0, s = 1, layer = 0):
Interactive.__init__(self, xx, yy, name, zrect, act, f, a, adir, s, layer)
self.frame = 0
self.animations = {}
self.endbehavior = {}
self.mode = 'idle'
self.paused = 0
self.format = f
for anim in anims:
self.animations[anim[0]] = []
self.endbehavior[anim[0]] = anim[2]
for i in range(0, anim[1]):
self.animations[anim[0]].append(pygame.image.load(name + '_' + anim[0] + '_' + str(i) + '.' + self.format).convert_alpha())
def play(self, anim): #anim is a string referring to a specific animation
if anim in self.animations:
self.mode = anim
self.paused = 0
else:
play('idle')
def pause(self):
self.paused = 1
def unpause(self):
self.paused = 0
def update(self):
if self.paused == 0 and self.mode in self.animations:
self.frame = self.frame + 1
if self.frame >= len(self.animations[self.mode]):
self.frame = 0
if self.endbehavior[self.mode] == 'pause':
self.paused = 1
else:
self.play(self.endbehavior[self.mode])
self.image = self.animations[self.mode][self.frame]
else:
self.image = self.animations[self.mode][self.frame]
def addAnim(self, name, length, end):
self.animations[name] = []
for i in range(0, length):
self.animations[name].append(pygame.image.load(name + '_' + i + '.' + self.format).convert_alpha())
class Player(Animate):
def __init__(self, xx, yy, name, dir, spd, f='png', iln=[1,1,1,1,1], im = 1):
anims = []
anims.append(['idle', iln[0], 'idle']) #iln is length of idle animation
anims.append(['idles', iln[1], 'idles'])
anims.append(['gos', spd, 'idles'])
anims.append(['idlew', iln[2], 'idlew'])
anims.append(['gow', spd, 'idlew'])
anims.append(['idlee', iln[3], 'idlee'])
anims.append(['goe', spd, 'idlee'])
anims.append(['idlen', iln[4], 'idlen'])
anims.append(['gon', spd, 'idlen'])
Animate.__init__(self, xx, yy, name, anims, 0, 0, f, 0, 0, 0)
self.direction = dir #nwse, appended to name to get idle/walking/whatever in different directions.
self.speed = spd #length of walking animation.
self.input = im #method of input. 1-arrow keys, 2-wasd, 3-numpad, more as needed
self.moving = 0
def update(self):
Animate.update(self)
if self.moving:
self.play('go' + self.direction)
class Portal(Animate):
def __init__(self, xx, yy, name, dest, i, zrect, f = 'png', dir = 0, opn = 1, act = 0, a = 0, s = 1, layer = 0):
anims = []
anims.append(['idle', i[0], 'idle']) #idle open
anims.append(['idlec', i[1], 'idlec']) #idle closed
anims.append(['open', i[2], 'idle']) # open
anims.append(['close', i[3], 'idlec']) #close
Animate.__init__(self, xx, yy, name, anims, zrect, act, f, a, dir, s, layer)
self.direction = dir
self.open = opn
self.destination = dest
def Open(self):
self.play('open')
self.open = 1
def Close(self):
self.play('close')
self.open = 0
class Map(object):
def __init__(self, name, limg, himg, bimg, fs=1, f='png'):
self.name = name
self.frame = 0
self.lframes = []
self.hframes = []
for i in range(0, fs):
self.lframes.append(pygame.image.load(limg + '_' + str(i) + '.' + f).convert())
self.hframes.append(pygame.image.load(himg + '_' + str(i) + '.' + f).convert())
self.lowimage = self.lframes[0]
self.highimage = self.hframes[0]
self.mask = pygame.mask.from_threshold(pygame.image.load(bimg).convert(), (0,0,0), (10,10,10,255))
self.offx = 0
self.offy = 0
self.objects = []
self.interacts = []
self.animates = []
self.portals = []
self.frame = 0
self.paused = 0
def addObject(self, thing):
self.objects.append(thing)
if thing.solid:
self.mask.draw(thing.mask, (thing.x, thing.y))
def addInteract(self, thing):
self.interacts.append(thing)
self.addObject(thing)
def addAnimate(self, thing):
self.animates.append(thing)
if thing.active:
self.addInteract(thing)
else:
self.addObject(thing)
def addPortal(self, thing):
self.portals.append(thing)
self.addAnimate(thing)
def removeObject(self, thing):#note that removing an object will destroy any part of the wall behind it.
self.objects.remove(thing)
if thing.solid:
self.mask.erase(thing.amask, (thing.x, thing.y))
def move(self, player):
for portal in self.portals:
if portal.open:
if portal.direction == player.direction or portal.direction == 0:
if portal.getZone().colliderect(player.getRect()):
return portal.destination
xs = 0
ys = 0
if player.direction == 'n':
ys = -player.speed
elif player.direction == 'w':
xs = -player.speed
elif player.direction == 'e':
xs = player.speed
elif player.direction == 's':
ys = player.speed
if str(self.mask.overlap(player.mask, (player.x + xs, player.y + ys))) == 'None':
player.x = player.x + xs
player.y = player.y + ys
return 0
def draw(self, surf):
surf.blit(self.lowimage, self.lowimage.get_rect(x = self.offx, y = self.offy))
for thing in self.objects:
surf.blit(thing.image, thing.image.get_rect(x = thing.x + self.offx, y = thing.y + self.offy))
def update(self):
if self.paused == 0:
self.frame = self.frame + 1
if self.frame >= len(self.lframes):
self.frame = 0
self.lowimage = self.lframes[self.frame]
self.highimage = self.hframes[self.frame]
for thing in self.animates:
thing.update()
def pause(self):
for thing in self.animates:
thing.pause()
def unpause(self):
for thing in self.animates:
thing.unpause()
def resetMask(self):
self.mask = pygame.mask.from_threshold(pygame.image.load(bimg).convert(), (0,0,0), (10,10,10,255))
for thing in self.objects:
if thing.solid:
self.mask.draw(thing.mask, (thing.x, thing.y))
and
import pygame
import sys
from pygame.locals import *
import mapObjects
def main():
pygame.init()
screen = pygame.display.set_mode((1024,768))
pygame.display.set_caption('Test')
background = pygame.Surface(screen.get_size())
background = background.convert()
background.fill((250,250,250))
clock = pygame.time.Clock()
map = mapObjects.Map('hi', 'maptest', 'maptest', 'maptest_0.bmp', 1, 'bmp')
iln = [5,1,1,1,1]
player = mapObjects.Player(100, 100, 'test', 's', 5, 'png', iln)
map.addAnimate(player)
while 1:
background.fill((250,250,250))
clock.tick(30)
if player.moving:
temp = map.move(player)
if temp:
map = temp
map.update()
map.draw(background)
screen.blit(background, (0,0))
pygame.display.flip()
for event in pygame.event.get():
if event.type == QUIT:
return
elif event.type == KEYDOWN:
if event.key == K_ESCAPE:
return
elif event.key == K_LEFT:
player.direction = 'w'
player.moving = 1
elif event.key == K_RIGHT:
player.direction = 'e'
player.moving = 1
elif event.key == K_UP:
player.direction = 'n'
player.moving = 1
elif event.key == K_DOWN:
player.direction = 's'
player.moving = 1
elif event.key == K_SPACE:
for i in map.interacts:
if i.active:
if player.getRect().colliderect(i.getZone()):
i.interact() #add player.dir check here when I get around to using an actual player.
elif event.type == KEYUP:
if event.key == K_LEFT and player.direction == 'w':
player.moving = 0
elif event.key == K_RIGHT and player.direction == 'e':
player.moving = 0
elif event.key == K_UP and player.direction == 'n':
player.moving = 0
elif event.key == K_DOWN and player.direction == 's':
player.moving = 0
if __name__ == '__main__': main()
The main code here is fairly skeletal and won't work unless you give it some images to pull the map and player sprite from. It's straightforward to add things.