Add sound options and semi-separate front-end from engine

This commit is contained in:
Nguyễn Gia Phong 2018-02-19 16:55:55 +07:00
parent 8852a9f678
commit bc47fb3f30
8 changed files with 107 additions and 67 deletions

View File

@ -24,7 +24,6 @@ from random import choice, randrange, shuffle
from sys import modules
import pygame
from pygame.mixer import Sound
from pygame.time import get_ticks
from .constants import *
@ -50,7 +49,7 @@ class Hero:
spin_speed (float): speed of spinning (in frames per slash)
spin_queue (float): frames left to finish spinning
wound (float): amount of wound
sfx_heart (Sound): heart beat sound effect
sfx_heart (pygame.mixer.Sound): heart beat sound effect
"""
def __init__(self, surface, fps):
self.surface = surface
@ -64,7 +63,7 @@ class Hero:
self.spin_speed = fps / HERO_HP
self.spin_queue = self.wound = 0.0
self.sfx_heart = Sound(SFX_HEART)
self.sfx_heart = SFX_HEART
def update(self, fps):
"""Update the hero."""
@ -92,6 +91,9 @@ class Hero:
x, y = pygame.mouse.get_pos()
self.angle = atan2(y - self.y, x - self.x)
self.spin_queue = 0.0
def draw(self):
"""Draw the hero."""
trigon = regpoly(3, self.R, self.angle, self.x, self.y)
fill_aapolygon(self.surface, trigon, self.color[int(self.wound)])
@ -117,7 +119,7 @@ class Enemy:
spin_speed (float): speed of spinning (in frames per slash)
spin_queue (float): frames left to finish spinning
wound (float): amount of wound
sfx_slash (Sound): sound effect indicating close-range attack damage
sfx_slash (pygame.mixer.Sound): sound effect of slashed hero
"""
def __init__(self, maze, x, y, color):
self.maze = maze
@ -132,7 +134,7 @@ class Enemy:
self.spin_speed = self.maze.fps / ENEMY_HP
self.spin_queue = self.wound = 0.0
self.sfx_slash = Sound(SFX_SLASH_HERO)
self.sfx_slash = SFX_SLASH_HERO
def get_pos(self):
"""Return coordinate of the center of the enemy."""
@ -238,6 +240,7 @@ class Enemy:
def draw(self):
"""Draw the enemy."""
if get_ticks() < self.maze.next_move and not self.awake: return
radious = self.maze.distance/SQRT2 - self.awake*2
square = regpoly(4, radious, self.angle, *self.get_pos())
color = TANGO[self.color][int(self.wound)] if self.awake else FG_COLOR
@ -257,7 +260,6 @@ class Enemy:
self.spin_queue -= sign(self.spin_queue)
else:
self.angle, self.spin_queue = pi / 4, 0.0
if self.awake or get_ticks() >= self.maze.next_move: self.draw()
def hit(self, wound):
"""Handle the enemy when it's attacked."""
@ -290,8 +292,7 @@ class Chameleon(Enemy):
def draw(self):
"""Draw the Chameleon."""
if (not self.awake or self.spin_queue
or get_ticks() < max(self.visible, self.maze.next_move)):
if not self.awake or get_ticks() < self.visible or self.spin_queue:
Enemy.draw(self)
def hit(self, wound):

View File

@ -19,21 +19,25 @@
__doc__ = 'brutalmaze module for shared constants'
from pkg_resources import resource_filename
from pygame import image
from pkg_resources import resource_filename as pkg_file
import pygame
from pygame.mixer import Sound
SETTINGS = resource_filename('brutalmaze', 'settings.ini')
ICON = image.load(resource_filename('brutalmaze', 'icon.png'))
MUSIC = resource_filename('brutalmaze', 'soundfx/music.ogg')
SFX_SPAWN = resource_filename('brutalmaze', 'soundfx/spawn.ogg')
SFX_SLASH_ENEMY = resource_filename('brutalmaze', 'soundfx/slash-enemy.ogg')
SFX_SLASH_HERO = resource_filename('brutalmaze', 'soundfx/slash-hero.ogg')
SFX_SHOT_ENEMY = resource_filename('brutalmaze', 'soundfx/shot-enemy.ogg')
SFX_SHOT_HERO = resource_filename('brutalmaze', 'soundfx/shot-hero.ogg')
SFX_MISSED = resource_filename('brutalmaze', 'soundfx/missed.ogg')
SFX_HEART = resource_filename('brutalmaze', 'soundfx/heart.ogg')
SFX_LOSE = resource_filename('brutalmaze', 'soundfx/lose.ogg')
SETTINGS = pkg_file('brutalmaze', 'settings.ini')
ICON = pygame.image.load(pkg_file('brutalmaze', 'icon.png'))
MUSIC = pkg_file('brutalmaze', 'soundfx/music.ogg')
mixer = pygame.mixer.get_init()
if mixer is None: pygame.mixer.init(frequency=44100)
SFX_SPAWN = Sound(pkg_file('brutalmaze', 'soundfx/spawn.ogg'))
SFX_SLASH_ENEMY = Sound(pkg_file('brutalmaze', 'soundfx/slash-enemy.ogg'))
SFX_SLASH_HERO = Sound(pkg_file('brutalmaze', 'soundfx/slash-hero.ogg'))
SFX_SHOT_ENEMY = Sound(pkg_file('brutalmaze', 'soundfx/shot-enemy.ogg'))
SFX_SHOT_HERO = Sound(pkg_file('brutalmaze', 'soundfx/shot-hero.ogg'))
SFX_MISSED = Sound(pkg_file('brutalmaze', 'soundfx/missed.ogg'))
SFX_HEART = Sound(pkg_file('brutalmaze', 'soundfx/heart.ogg'))
SFX_LOSE = Sound(pkg_file('brutalmaze', 'soundfx/lose.ogg'))
if mixer is None: pygame.mixer.quit()
SQRT2 = 2 ** 0.5
INIT_SCORE = 5**0.5/2 + 0.5 # golden mean

View File

@ -35,7 +35,7 @@ from pygame import DOUBLEBUF, KEYDOWN, OPENGL, QUIT, RESIZABLE, VIDEORESIZE
from pygame.time import Clock, get_ticks
from appdirs import AppDirs
from .constants import *
from .constants import SETTINGS, ICON, MUSIC, HERO_SPEED
from .maze import Maze
from .misc import sign
@ -44,7 +44,8 @@ class ConfigReader:
"""Object reading and processing INI configuration file for
Brutal Maze.
"""
CONTROL_ALIASES = (('New game', 'new'), ('Pause', 'pause'),
CONTROL_ALIASES = (('New game', 'new'), ('Toggle pause', 'pause'),
('Toggle mute', 'mute'),
('Move left', 'left'), ('Move right', 'right'),
('Move up', 'up'), ('Move down', 'down'),
('Long-range attack', 'shot'),
@ -57,12 +58,14 @@ class ConfigReader:
self.config.read(SETTINGS) # default configuration
self.config.read(filenames)
def parse_graphics(self):
"""Parse graphics configurations."""
def parse_output(self):
"""Parse graphics and sound configurations."""
self.size = (self.config.getint('Graphics', 'Screen width'),
self.config.getint('Graphics', 'Screen height'))
self.opengl = self.config.getboolean('Graphics', 'OpenGL')
self.max_fps = self.config.getint('Graphics', 'Maximum FPS')
self.muted = self.config.getboolean('Sound', 'Muted')
self.musicvol = self.config.getfloat('Sound', 'Music volume')
def parse_control(self):
"""Parse control configurations."""
@ -84,27 +87,31 @@ class ConfigReader:
def read_args(self, arguments):
"""Read and parse a ArgumentParser.Namespace."""
if arguments.size is not None: self.size = arguments.size
if arguments.opengl is not None: self.opengl = arguments.opengl
if arguments.max_fps is not None: self.max_fps = arguments.max_fps
for option in 'size', 'opengl', 'max_fps', 'muted', 'musicvol':
value = getattr(arguments, option)
if value is not None: setattr(self, option, value)
class Game:
"""Object handling main loop and IO."""
def __init__(self, size, scrtype, max_fps, key, mouse):
def __init__(self, size, scrtype, max_fps, muted, musicvol, key, mouse):
pygame.mixer.pre_init(frequency=44100)
pygame.init()
pygame.mixer.music.load(MUSIC)
pygame.mixer.music.play(-1)
if muted:
pygame.mixer.quit()
else:
pygame.mixer.music.load(MUSIC)
pygame.mixer.music.set_volume(musicvol)
pygame.mixer.music.play(-1)
pygame.display.set_icon(ICON)
pygame.fastevent.init()
self.clock = Clock()
# self.fps is a float to make sure floordiv won't be used in Python 2
self.max_fps, self.fps = max_fps, float(max_fps)
self.musicvol = musicvol
self.key, self.mouse = key, mouse
self.maze = Maze(max_fps, size, scrtype)
self.hero = self.maze.hero
self.paused = False
self.clock, self.paused = Clock(), False
def __enter__(self): return self
@ -145,6 +152,14 @@ class Game:
self.maze.__init__(self.fps)
elif event.key == self.key['pause'] and not self.hero.dead:
self.paused ^= True
elif event.key == self.key['mute']:
if pygame.mixer.get_init() is None:
pygame.mixer.init(frequency=44100)
pygame.mixer.music.load(MUSIC)
pygame.mixer.music.set_volume(self.musicvol)
pygame.mixer.music.play(-1)
else:
pygame.mixer.quit()
if not self.hero.dead:
keys = pygame.key.get_pressed()
@ -181,7 +196,7 @@ def main():
parents.append(dirs.user_config_dir)
filenames = [join(parent, 'settings.ini') for parent in parents]
config = ConfigReader(filenames)
config.parse_graphics()
config.parse_output()
# Parse command-line arguments
parser = ArgumentParser(formatter_class=RawTextHelpFormatter)
@ -206,6 +221,14 @@ def main():
parser.add_argument(
'-f', '--max-fps', type=int, metavar='FPS',
help='the desired maximum FPS (fallback: {})'.format(config.max_fps))
parser.add_argument(
'--mute', '-m', action='store_true', default=None,
help='mute all sounds (fallback: {})'.format(config.muted))
parser.add_argument('--unmute', action='store_false', dest='muted',
help='unmute sound')
parser.add_argument(
'--music-volume', type=float, metavar='VOL', dest='musicvol',
help='between 0.0 and 1.0 (fallback: {})'.format(config.musicvol))
args = parser.parse_args()
if args.defaultcfg is not None:
with open(SETTINGS) as settings: args.defaultcfg.write(settings.read())
@ -215,10 +238,11 @@ def main():
# Manipulate config
if args.config: config.config.read(args.config)
config.read_args(args)
config.parse_graphics()
config.parse_output()
config.parse_control()
# Main loop
scrtype = (config.opengl and DOUBLEBUF|OPENGL) | RESIZABLE
with Game(config.size, scrtype, config.max_fps, config.key, config.mouse) as game:
with Game(config.size, scrtype, config.max_fps, config.muted,
config.musicvol, config.key, config.mouse) as game:
while game.loop(): pass

View File

@ -25,7 +25,6 @@ from random import choice, getrandbits, uniform
import pygame
from pygame import RESIZABLE
from pygame.mixer import Sound
from pygame.time import get_ticks
from .characters import Hero, new_enemy
@ -76,9 +75,8 @@ class Maze:
next_move (int): the tick that the hero gets mobilized
next_slashfx (int): the tick to play next slash effect of the hero
slashd (float): minimum distance for slashes to be effective
sfx_slash (Sound): sound effect indicating an enemy get slashed
sfx_shot (Sound): sound effect indicating an enemy get shot
sfx_lose (Sound): sound effect to be played when you lose
sfx_slash (pygame.mixer.Sound): sound effect of slashed enemy
sfx_lose (pygame.mixer.Sound): sound effect to be played when you lose
"""
def __init__(self, fps, size=None, scrtype=None):
self.fps = fps
@ -109,10 +107,9 @@ class Maze:
self.next_move = self.next_slashfx = 0
self.slashd = self.hero.R + self.distance/SQRT2
self.sfx_spawn = Sound(SFX_SPAWN)
self.sfx_slash = Sound(SFX_SLASH_ENEMY)
self.sfx_shot = Sound(SFX_SHOT_ENEMY)
self.sfx_lose = Sound(SFX_LOSE)
self.sfx_spawn = SFX_SPAWN
self.sfx_slash = SFX_SLASH_ENEMY
self.sfx_lose = SFX_LOSE
def add_enemy(self):
"""Add enough enemies."""
@ -140,13 +137,21 @@ class Maze:
def draw(self):
"""Draw the maze."""
self.surface.fill(BG_COLOR)
if get_ticks() < self.next_move: return
for i in self.rangex:
for j in self.rangey:
if self.map[i][j] != WALL: continue
x, y = self.get_pos(i, j)
square = regpoly(4, self.distance / SQRT2, pi / 4, x, y)
fill_aapolygon(self.surface, square, FG_COLOR)
if get_ticks() >= self.next_move:
for i in self.rangex:
for j in self.rangey:
if self.map[i][j] != WALL: continue
x, y = self.get_pos(i, j)
square = regpoly(4, self.distance / SQRT2, pi / 4, x, y)
fill_aapolygon(self.surface, square, FG_COLOR)
for enemy in self.enemies: enemy.draw()
self.hero.draw()
bullet_radius = self.distance / 4
for bullet in self.bullets: bullet.draw(bullet_radius)
pygame.display.flip()
pygame.display.set_caption('Brutal Maze - Score: {}'.format(
int(self.score - INIT_SCORE)))
def rotate(self):
"""Rotate the maze if needed."""
@ -266,7 +271,7 @@ class Maze:
self.score += enemy.wound
enemy.die()
self.enemies.pop(j)
play(self.sfx_shot, wound, bullet.angle)
play(bullet.sfx_hit, wound, bullet.angle)
fallen.append(i)
break
elif bullet.get_distance(self.x, self.y) < self.distance:
@ -310,15 +315,12 @@ class Maze:
for enemy in self.enemies: enemy.wake()
for bullet in self.bullets: bullet.place(dx, dy)
self.draw()
for enemy in self.enemies: enemy.update()
if not self.hero.dead:
self.hero.update(fps)
self.slash()
self.track_bullets()
pygame.display.flip()
pygame.display.set_caption('Brutal Maze - Score: {}'.format(
int(self.score - INIT_SCORE)))
self.draw()
def resize(self, size):
"""Resize the maze."""

View File

@ -76,6 +76,7 @@ def choices(d):
def play(sound, volume=1.0, angle=None):
"""Play a pygame.mixer.Sound at the given volume."""
if pygame.mixer.get_init() is None: return
if pygame.mixer.find_channel() is None:
pygame.mixer.set_num_channels(pygame.mixer.get_num_channels() + 1)

View File

@ -6,6 +6,11 @@ OpenGL: no
# FPS should not be greater than refresh rate.
Maximum FPS: 60
[Sound]
Muted: no
# Volume must be between 0.0 and 1.0
Music volume: 1.0
[Control]
# Input values should be either from Mouse1 to Mouse3 or a keyboard key
# and they are case-insensitively read.
@ -13,7 +18,8 @@ Maximum FPS: 60
# http://www.pygame.org/docs/ref/key.html
# Key combinations are not supported.
New game: F2
Pause: p
Toggle pause: p
Toggle mute: m
Move left: Left
Move right: Right
Move up: Up

View File

@ -22,7 +22,6 @@ __doc__ = 'brutalmaze module for weapon classes'
from math import cos, sin
from pygame.time import get_ticks
from pygame.mixer import Sound
from .constants import *
from .misc import regpoly, fill_aapolygon
@ -37,25 +36,28 @@ class Bullet:
angle (float): angle of the direction the bullet pointing (in radians)
color (str): bullet's color name
fall_time (int): the tick that the bullet will fall down
sfx_hit (Sound): sound effect indicating the bullet hits the target
sfx_missed (Sound): sound effect indicating the bullet hits the target
sfx_hit (pygame.mixer.Sound): sound effect indicating target was hit
sfx_missed (pygame.mixer.Sound): sound effect indicating a miss shot
"""
def __init__(self, surface, x, y, angle, color):
self.surface = surface
self.x, self.y, self.angle, self.color = x, y, angle, color
self.fall_time = get_ticks() + BULLET_LIFETIME
# Sound effects of bullets shot by hero are stored in Maze to avoid
# unnecessary duplication
if color != 'Aluminium':
self.sfx_hit = Sound(SFX_SHOT_HERO)
self.sfx_missed = Sound(SFX_MISSED)
if color == 'Aluminium':
self.sfx_hit = SFX_SHOT_ENEMY
else:
self.sfx_hit = SFX_SHOT_HERO
self.sfx_missed = SFX_MISSED
def update(self, fps, distance):
"""Update the bullet."""
s = distance * BULLET_SPEED / fps
self.x += s * cos(self.angle)
self.y += s * sin(self.angle)
pentagon = regpoly(5, distance // 4, self.angle, self.x, self.y)
def draw(self, radius):
"""Draw the bullet."""
pentagon = regpoly(5, radius, self.angle, self.x, self.y)
value = int((1-(self.fall_time-get_ticks())/BULLET_LIFETIME)*ENEMY_HP)
try:
fill_aapolygon(self.surface, pentagon, TANGO[self.color][value])

2
wiki

@ -1 +1 @@
Subproject commit 3b014564eb185bb525a59378fd2d9ede65bf0d33
Subproject commit 34af1cf8b3e3ea8272d6793a794484a239794d50