Sporg Postmortem

October 11th, 2010

My Ludum Dare entry, Sporg, isn’t very good.

I had a lot of fun writing an entire game in 48 hours. I chose to start from scratch (rather than take the option of working with an established self-created codebase or ‘game maker’ program), because I wanted to make the process as challenging as possible, and challenging it was.

I spent a good fifteen minutes designing the game, and the rest of my work time creating the media assets and writing the game. Fifteen minutes seemed like plenty of time, I had a reasonably good idea and figured I could work out the rest of the details as I wrote. This is perhaps best exemplified by me ‘finishing’ the game at around 9:30PM Sunday night, and then realising I hadn’t created any levels to actually play. So the game had no levels and had not been playtested. I threw a couple of maps together in bed, and then a few more on my morning commute, and submitted the game from work.

The levels are uninteresting. This is half lack of careful level design, half lack of assets to actually include in the levels. Some decorative tiles or animation would have compensated a little, but the limited gameplay mechanic really limited what I could do with the levels. Ultimately you’re either trying to time a dash past an enemy, or trying to have one enemy shoot another, which doesn’t create a great range of level design possibilities. Different enemy types (with different rotation speeds or patterns, or perhaps even movement or multiple hit-points) would have added a lot of gameplay options.

The game is slow. I didn’t really appreciate this until I saw someone else play the game and look away from the screen to talk to someone, without taking their hand off of the movement controls. The game is so slow that the player could have a conversation without having to worry about getting shot or reaching their destination. I could try and blame this on Pygame, (the development library used for Sporg), but it’s really an artifact of rushed development without adequate planning. It also didn’t help that I developed the game on my i7 920, and most people seem to still be running single core netbooks or something equally underpowered. I did actually do some of the work on an eee901 netbook, but obviously didn’t playtest the game enough to really notice.

Dodgy collision detection! Getting stuck on the walls, getting hit by shots that you might argue should have missed, having the enemy blast you through supposedly solid corners.. I was aware of these issues through development but postponed dealing with them. To be fair, I feel I made the right choice in prioritising the other issues which did get fixed, such as the horrible flickering issue and stuttering sound which were absent from the released version.

Next Ludum Dare, which I hope to participate in, will be different:

  • DESIGN – I will give over a much greater portion of time to designing the game and it’s gameplay. I think I need to dedicate at least two hours, though perhaps not in a single chunk, in order to produce something that I actually want to play after I’ve written it.
  • TESTING – Must test frequently! It’s better to produce a game with only 50% of your intended features working 100% than to implement 100% of features but have them work only 50% of the time, no?
  • LEVELS – Or maps or whatever is appropriate. The game engine needs to be completed with plenty of time left to develop an interesting gameplay experience to play through.
  • SCOPE – All of the above points are going to require time, which has to come from somewhere. I think perhaps the most important aspect for anyone developing a Ludum Dare game is to keep the scope as tight as possible. Focus on a small set of mechanics, features etc and do them well in a way that allows the player to experience what it is you dreamed up in that design period, rather than be distracted by incompleteness and bugs.

Ludum Dare: Sporg

August 23rd, 2010

Spent majority of the weekend participating in Ludum Dare, the theme for which was “Enemies as Weapons”

More details later, but for now, a download link:

Ludum Dare entry page: Sporg

sporg_ld48_release.zip (4407KB).

Reverse Pong Game Snippet

April 29th, 2010

In reverse pong, you score points by allowing the ‘ball’ to pass into your own goal, rather than defending your goal as in traditional pong. The score value of the ball is doubled each time it is reflected by a player, and the scores of both players diminish by a percentage each time at each reflection.

Rather than focusing on traditional game skills such as reflex and dexterity, Reverse Pong is instead a bluffing game of mental misdirection and brinksmanship which attempts to pit the personalities, rather than the skills, of the two players into conflict.

import random
import pygame

#revpong
#pong clone
#players score points by allowing the pong ball into their own goal
#every paddle deflection doubles the current ball value

pygame.init()
pygame.display.set_caption("Revpong")
screen = pygame.display.set_mode((800, 480), pygame.FULLSCREEN)
pygame.mouse.set_visible(False)
#screen = pygame.display.set_mode((800, 480))
windowsize = screen.get_size()

gameContinue = True

game_colour = (255, 255, 255)
clock = pygame.time.Clock()

font = pygame.font.SysFont(pygame.font.get_default_font(), 36)

paddle_dimensions = (24, 96)
paddle = pygame.surface.Surface(paddle_dimensions)
paddle.fill(game_colour)

ball = pygame.surface.Surface((24, 24))
ball.fill(game_colour)

ball_position = (windowsize[0]/2, windowsize[1]/2)
ball_velocity = (-4, 4)

#x positions of paddles
paddle_x_1 = paddle_dimensions[0] * 3
paddle_x_2 = windowsize[0] - paddle_dimensions[0] * 4

#y positions of paddles
paddle_y_1 = windowsize[1] / 2 - paddle_dimensions[1] / 2
paddle_y_2 = windowsize[1] / 2 - paddle_dimensions[1] / 2

#deltas of paddles
paddle_d_1 = 0
paddle_d_2 = 0

#scores
score_ball = 1
score_1 = 0
score_2 = 0

def reset_ball(ball_position, ball_velocity):
	random_y = int(random.random() * 5) - int(random.random() * 5)
	random_x = 2 + int(random.random() * 2)
	ball_position = windowsize[0]/2, windowsize[1]/2
	if score_1 > score_2:
		ball_velocity = random_x, random_y
	elif score_2 > score_1:
		ball_velocity = -random_x, random_y
	else:
		if random.random() > 0.5:
			ball_velocity = random_x, random_y
		else:
			ball_velocity = -random_x, random_y
	return ball_position, ball_velocity

ball_position, ball_velocity = reset_ball(ball_position, ball_velocity)

while gameContinue:
	clock.tick(50)
	for event in pygame.event.get():
		if event.type == pygame.QUIT:
			gameContinue = False

	#input
	keys = pygame.key.get_pressed()

	paddle_d_1 = 0
	paddle_d_2 = 0

	if keys[pygame.K_ESCAPE]:
		gameContinue = False

	if keys[pygame.K_w]:
		paddle_y_1 -= 4
		paddle_d_1 -= 1
	if keys[pygame.K_s]:
		paddle_y_1 += 4
		paddle_d_1 += 1
	if keys[pygame.K_o]:
		paddle_y_2 -= 4
		paddle_d_2 -= 1
	if keys[pygame.K_l]:
		paddle_y_2 += 4
		paddle_d_1 += 1

	ball_old_position = ball_position
	ball_position = ball_position[0] + ball_velocity[0], ball_position[1] + ball_velocity[1]

	#check new ball_position for collision with top and bottom edges
	if ball_position[1] < 12 or ball_position[1] > windowsize[1] - 12:
		#bounce off top or bottom edge
		ball_position = ball_old_position
		ball_velocity = (ball_velocity[0], -ball_velocity[1])

	#check new ball_position for collision with score zones
	if ball_position[0] < paddle_dimensions[0]:
		#player 1 scores
		ball_position, ball_velocity = reset_ball(ball_position, ball_velocity)
		score_1 += score_ball
		score_ball = 1
	if ball_position[0] > windowsize[0] - paddle_dimensions[0]:
		#player 2 scores
		score_2 += score_ball
		score_ball = 1
		ball_position, ball_velocity = reset_ball(ball_position, ball_velocity)

	ball_rect = pygame.Rect((ball_position[0] - 12, ball_position[1] - 12), (24, 24))
	#check new ball_position for collision with paddle_1
	paddle_rect = pygame.Rect((paddle_x_1, paddle_y_1), paddle_dimensions)
	if ball_rect.colliderect(paddle_rect):
		#if the ball has passed the paddle x middle
		if ball_position[0] < paddle_x_1 + paddle_dimensions[0]/2:
			#test if ball is up or down
			if ball_position[1] < paddle_y_1 + paddle_dimensions[1]:
				#ball is up
				ball_velocity = ball_velocity[0], ball_velocity[1] - 4
			else:
				#ball is down
				ball_velocity = ball_velocity[0], ball_velocity[1] + 4
		else:
			ball_velocity = ball_velocity[0] * -1 + 0.1, ball_velocity[1] + paddle_d_1
			ball_position = paddle_x_1 + paddle_dimensions[0] + 12 + ball_velocity[0], ball_position[1] + ball_velocity[1]
			score_ball = score_ball * 2
			score_1 = int(score_1 * 0.90)
			score_2 = int(score_2 * 0.90)

	paddle_rect = pygame.Rect((paddle_x_2, paddle_y_2), paddle_dimensions)
	if ball_rect.colliderect(paddle_rect):
		#if the ball has passed the paddle x middle
		if ball_position[0] > paddle_x_2 + paddle_dimensions[0]:
			#test if ball is up or down
			if ball_position[1] < paddle_y_2 + paddle_dimensions[1] / 2:
				#ball is up
				ball_velocity = ball_velocity[0], ball_velocity[1] - 4
			else:
				#ball is down
				ball_velocity = ball_velocity[0], ball_velocity[1] + 4
		else:
			ball_velocity = ball_velocity[0] * -1 - 0.1, ball_velocity[1] + paddle_d_2
			ball_position = paddle_x_2 - 12 + ball_velocity[0], ball_position[1] + ball_velocity[1]

			score_ball = score_ball * 2
			score_1 = int(score_1 * 0.90)
			score_2 = int(score_2 * 0.90)

	#draw
	screen.fill((0,0,0))

	screen.blit(paddle, (paddle_x_1, paddle_y_1))
	screen.blit(paddle, (paddle_x_2, paddle_y_2))
	screen.blit(ball, (ball_position[0] - 12, ball_position[1] - 12))

	screen.blit(font.render(str(score_1), False, game_colour), (0,0))
	offset = font.size(str(score_2))
	screen.blit(font.render(str(score_2), False, game_colour), (windowsize[0]-offset[0],0))
	offset = font.size(str(score_ball))
	screen.blit(font.render(str(score_ball), False, game_colour), (windowsize[0]/2 - offset[0]/2, windowsize[1]-offset[1]))
	pygame.display.flip()

Ninjar game snippet

April 27th, 2010

Ninjar below birds Ninjar destroying birds

Our Ninjar exists in perfect Ninjar tranquility, upon a finite plane of austere Zen contemplation. Only the incessant, infinite flights of confetti filled combustible birds mar this ultimate Ninjar existence. Armed only with his flying kick of compassion, our Ninjar may attempt to rid the sky of these airborne abominations, but ultimately must come to realise that external actions are meaningless and true Zen is found only by acting on the world within.

(Unoptimised PyGame example)

Download Ninjar (requires Python, Pygame)

Python source:

#Ninjar 11:04 AM 19/03/2010

import sys, random
import pygame
from pygame.locals import *

class Ninja:
	def __init__(self, sprites):
		self.sprites = sprites #sprite array
		self.x = 400
		self.y_offset = 400 #height from floor. POSITIVE IS UP
		self.baseFrame = 0 #animation cycle baseframe
		self.f = 0 #frame to use to draw this char

		self.yImpulse = 0 #jump impulse
		self.health = 10

		self.vx = 0 #x momentum
		self.facing = True #True is right, False is left
		self.control_x = 0 #-1 = left, 0 = neutral, 1 = right
		self.control_jump = False #0 = not jumping, #1 = trying to jump
		self.control_attack = False
		self.control_duck = False
		self.timer_attack = 0 #time until can attack again
		self.attacking = False

	def tick(self):
		#See if we need to update the facing position and x velocity
		#when airborne we have less control over x
		if self.control_x == -1: #heading left
			self.facing = False
			if self.y_offset > 0:
				self.vx -= 1

			else:
				self.vx -= 3
				self.baseFrame += 1
		elif self.control_x == 1: #heading right
			self.facing = True
			if self.y_offset > 0:
				self.vx += 1
			else:
				self.vx += 3
				self.baseFrame += 1

		else: #not actively running, slow down a bit
			if self.vx > 0:
				self.vx = max(0, self.vx - 2)
			if self.vx < 0:
				self.vx = min(0, self.vx + 2)
			self.baseFrame = 0

		#Are we airborne?
		if self.y_offset > 0:

			if self.attacking:
				if self.facing:
					self.f = 1 #kick right
				else:
					self.f = 12 #kick left
			else:
				if self.facing:
					self.f = 0 #duck right
				else:
					self.f = 11 #duck left

				#Are we trying to attack?
				if self.control_attack and not self.timer_attack:
					self.timer_attack = 10
					self.attacking = True
					if self.facing:
						self.f = 1 #kick right
					else:
						self.f = 12 #kick left

			#Are we still going up?
			if self.yImpulse > 0:
				#slow down a bit
				self.yImpulse -= 1
				self.y_offset += self.yImpulse
			else: #Gravity time
				self.y_offset = max(self.y_offset - 8, 0)

		else: #we're not airborne, consider ground options
			self.attacking = False #we're not attacking if we're on the ground
			if self.control_duck: #if we're trying to duck
				if self.facing:
					self.f = 0 #duck right
				else:
					self.f = 11 #duck left

				#skid to a halt
				if self.vx > 0:
					self.vx = max(0, self.vx - 3)
				if self.vx < 0:
					self.vx = min(0, self.vx + 3)

			elif self.control_jump: #we're trying to jump?
				self.yImpulse = 24
				self.y_offset = 16

			else:
				self.f = self.baseFrame % 9 + 2
				if not self.facing:
					self.f += 11

		#attack timer countdown
		self.timer_attack = max(self.timer_attack - 1, 0)

		#impose maximum horizontal velocity
		if self.vx > 0:
			self.vx = min(9, self.vx)
		if self.vx < 0:
			self.vx = max(-9, self.vx)

		self.x += self.vx
		if self.x > 850:
			self.x = -25
		if self.x < -25:
			self.x = 825

	def draw(self, surface):
		surface.blit(self.sprites[self.f], (self.x, 364 - self.y_offset))

class Ninjar:
	def __init__(self):

		self.size = 800, 480 #set screen dimensions
		pygame.init() #start pygame
                pygame.mouse.set_visible(False)

		#Set up the screen
		pygame.display.set_caption("NINJAR 0")
		self.surface = pygame.display.set_mode(self.size, pygame.FULLSCREEN)
		self.background = pygame.image.load("sprites/background.png")

		#Set up game clock for framerate control
		self.clock = pygame.time.Clock()

		#Load sprites

		#Red sprites 0=duck right, 1=kick right 2->10=walk right, 11=duck left, 12 = kick right, 13->22=walk left
		self.redSprites = []
		duckSprite = pygame.image.load("sprites/ninja_red_r_duck.png").convert_alpha()
		kickSprite = pygame.image.load("sprites/ninja_red_r_kick.png").convert_alpha()
		self.redSprites.append(duckSprite)
		self.redSprites.append(kickSprite)

		for i in range(0,9):
			walkSprite = pygame.image.load("sprites/ninja_red_r_"+str(i)+".png").convert_alpha()
			self.redSprites.append(walkSprite)

		#Mirror loaded sprites for left frames
		for i in range(len(self.redSprites)):
			walkSprite = pygame.transform.flip(self.redSprites[i], True, False)
			self.redSprites.append(walkSprite)

		self.redNinja = Ninja(self.redSprites)
		self.ninjas = []
		self.ninjas.append(self.redNinja)

		#set us up the birdies
		self.birds = []
		self.birdFrames = []
		for i in range(6):
			frame = pygame.Surface((12, abs(3 - i)*3+1))
			frame.fill((200, 200, 200))
			self.birdFrames.append(frame)
		self.birdBlood = pygame.Surface((6,6))
		self.birdBlood.fill((255,64,64))

		self.gibs = []
		self.deathzone = pygame.Rect(0,0,0,0)

	def createBird(self):
		self.birds.append([-3.0,150.0+100.0*random.random(), int(random.random()*6)])

	def createGib(self, x, y):
		dx = random.random() * 20 - 10
		dy = random.random() * -10
		self.gibs.append((x,y,dx,dy))

	#Main gameloop
	def tick(self):
		self.clock.tick(30) #limit framerate to 30 fps
                dirtyRects = [] #list of dirty rect areas to update
		self.surface.blit(self.background, (0,0)) #redraw background

		for ninja in self.ninjas:
                        rect = (ninja.x, 364 - ninja.y_offset, 16, 64)
                        dirtyRects.append(rect)
			ninja.tick()
                        rect = (ninja.x, 364 - ninja.y_offset, 16, 64)
                        dirtyRects.append(rect)
			ninja.draw(self.surface)
			if ninja.attacking:
				xOffset = 24
				if not ninja.facing:
					xOffset = -3
				self.deathzone = pygame.Rect((ninja.x + xOffset,358 - (ninja.y_offset-56)), (20,20))

		#add more birds?
		if len(self.birds) < 100:
			if random.random() > 0.95:
				self.createBird()

		#Move and draw the birds
		for i in range(len(self.birds)):
			bird = self.birds[i]
                        rect = (bird[0], bird[1], 12, 12)
                        dirtyRects.append(rect)
			#Move bird
			x = bird[0] + random.random() * 6
			if x > self.size[0]:
				x = -20
			y = bird[1] + random.random()*5 - 2.5
			y = max( 110, min(350, y))
			f = (bird[2] + 1) % 6
                        rect = (bird[0], bird[1], 12, 12)
                        dirtyRects.append(rect)

			#test for death
			birdRect = pygame.Rect(x,y,12,12)
			if birdRect.colliderect(self.deathzone):
				gibCount = int(5 + random.random() * 5)
				for j in range(gibCount):
					self.createGib(x,y)

				x = -(10 + random.random()*100)

			self.birds[i] = (x, y, f)

			#Draw bird
			self.surface.blit(self.birdFrames[3], (x+4, y))
			self.surface.blit(self.birdFrames[4], (x-2, y))
			self.surface.blit(self.birdFrames[f], (x, y))

		#handle gibs
		for i in range(len(self.gibs)):
			gib = self.gibs[i]
                        rect = (gib[0], gib[1], 6, 6)
                        dirtyRects.append(rect)

			x = gib[0] + gib[2]
			y = gib[1] + gib[3]
			dx = gib[2] * 0.95
			dy = gib[3] + 0.5

			self.gibs[i] = (x,y,dx,dy)
                        rect = (gib[0], gib[1], 6, 6)
                        dirtyRects.append(rect)

			self.surface.blit(self.birdBlood, (x,y))

		for gib in self.gibs:
			if gib[1] > 420:
				self.background.blit(self.birdBlood, (gib[0], gib[1]))
				self.gibs.remove(gib)

		#handle pygame events
		for event in pygame.event.get():
			if event.type == pygame.QUIT:
				sys.exit()

			if event.type == pygame.KEYDOWN:
				#Up cursor
				if event.key == 273:
					self.redNinja.control_jump = True

				#Down cursor
				if event.key == 274:
					self.redNinja.control_duck = True

				#Right cursor
				if event.key == 275:
					self.redNinja.control_x = +1

				#Left cursor
				if event.key == 276:
					self.redNinja.control_x = -1

				#Space
				if event.key == 32:
					self.redNinja.control_attack = True

			if event.type == pygame.KEYUP:
				#Up cursor
				if event.key == 273:
					self.redNinja.control_jump = False

				#Down cursor
				if event.key == 274:
					self.redNinja.control_duck = False

				#Right cursor
				if event.key == 275:
					self.redNinja.control_x = 0

				#Left cursor
				if event.key == 276:
					self.redNinja.control_x = 0

				#Space
				if event.key == 32:
					self.redNinja.control_attack = False

		#Commit graphical changes to display
		#pygame.display.flip()
                pygame.display.update(dirtyRects)

app = Ninjar()
while True:
	app.tick()