Use palace for positional audio rendering
This fixes GH-15. Sources doesn't seem to be cleaned up properly though.
This commit is contained in:
parent
c326f93bbb
commit
600c72d0d4
|
@ -25,8 +25,8 @@ from sys import modules
|
|||
|
||||
from .constants import (
|
||||
TANGO, HERO_HP, SFX_HEART, HEAL_SPEED, MIN_BEAT, ATTACK_SPEED, ENEMY,
|
||||
ENEMY_SPEED, ENEMY_HP, SFX_SLASH_HERO, MIDDLE, WALL, FIRANGE, AROUND_HERO,
|
||||
ADJACENTS, EMPTY, SQRT2, ENEMIES)
|
||||
ENEMY_SPEED, ENEMY_HP, SFX_SPAWN, SFX_SLASH_HERO, MIDDLE, WALL, FIRANGE,
|
||||
AROUND_HERO, ADJACENTS, EMPTY, SQRT2, ENEMIES)
|
||||
from .misc import sign, randsign, regpoly, fill_aapolygon, play
|
||||
from .weapons import Bullet
|
||||
|
||||
|
@ -51,7 +51,6 @@ class Hero:
|
|||
spin_queue (float): frames left to finish spinning
|
||||
wound (float): amount of wound
|
||||
wounds (deque of float): wounds in time of an attack (ATTACK_SPEED)
|
||||
sfx_heart (pygame.mixer.Sound): heart beat sound effect
|
||||
"""
|
||||
def __init__(self, surface, fps, maze_size):
|
||||
self.surface = surface
|
||||
|
@ -68,8 +67,6 @@ class Hero:
|
|||
self.spin_queue = self.wound = 0.0
|
||||
self.wounds = deque([0.0])
|
||||
|
||||
self.sfx_heart = SFX_HEART
|
||||
|
||||
def update(self, fps):
|
||||
"""Update the hero."""
|
||||
if self.dead:
|
||||
|
@ -87,7 +84,7 @@ class Hero:
|
|||
if self.wound < 0: self.wound = 0.0
|
||||
self.wounds.append(0.0)
|
||||
if self.next_beat <= 0:
|
||||
play(self.sfx_heart)
|
||||
play(SFX_HEART)
|
||||
self.next_beat = MIN_BEAT*(2 - self.wound/HERO_HP)
|
||||
else:
|
||||
self.next_beat -= 1000 / fps
|
||||
|
@ -168,7 +165,6 @@ 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 (pygame.mixer.Sound): sound effect of slashed hero
|
||||
"""
|
||||
def __init__(self, maze, x, y, color):
|
||||
self.maze = maze
|
||||
|
@ -182,8 +178,6 @@ class Enemy:
|
|||
self.spin_speed = self.maze.fps / ENEMY_HP
|
||||
self.spin_queue = self.wound = 0.0
|
||||
|
||||
self.sfx_slash = SFX_SLASH_HERO
|
||||
|
||||
@property
|
||||
def pos(self):
|
||||
"""Coordinates (in pixels) of the center of the enemy."""
|
||||
|
@ -227,7 +221,7 @@ class Enemy:
|
|||
if self.maze.map[srcx+i//w][srcy+i//u] == WALL: return False
|
||||
self.awake = True
|
||||
self.maze.map[self.x][self.y] = ENEMY
|
||||
play(self.maze.sfx_spawn, self.spawn_volume, self.get_angle()+pi)
|
||||
play(SFX_SPAWN, self.x, self.y)
|
||||
return True
|
||||
|
||||
def fire(self):
|
||||
|
@ -317,7 +311,7 @@ class Enemy:
|
|||
if not self.spin_queue and not self.fire() and not self.move():
|
||||
self.spin_queue = randsign() * self.spin_speed
|
||||
if not self.maze.hero.dead:
|
||||
play(self.sfx_slash, self.get_slash(), self.get_angle())
|
||||
play(SFX_SLASH_HERO, self.x, self.y, self.get_slash())
|
||||
if round(self.spin_queue) != 0:
|
||||
self.angle += sign(self.spin_queue) * pi / 2 / self.spin_speed
|
||||
self.spin_queue -= sign(self.spin_queue)
|
||||
|
|
|
@ -20,25 +20,23 @@ __doc__ = 'Brutal Maze module for shared constants'
|
|||
|
||||
from string import ascii_lowercase
|
||||
|
||||
from pkg_resources import resource_filename as pkg_file
|
||||
import pygame
|
||||
from pygame.mixer import Sound
|
||||
from pkg_resources import resource_filename as pkg_file
|
||||
|
||||
SETTINGS = pkg_file('brutalmaze', 'settings.ini')
|
||||
ICON = pygame.image.load(pkg_file('brutalmaze', 'icon.png'))
|
||||
NOISE = pkg_file('brutalmaze', 'soundfx/noise.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()
|
||||
SFX_NOISE = pkg_file('brutalmaze', 'soundfx/noise.ogg')
|
||||
SFX_SPAWN = pkg_file('brutalmaze', 'soundfx/spawn.ogg')
|
||||
SFX_SLASH_ENEMY = pkg_file('brutalmaze', 'soundfx/slash-enemy.ogg')
|
||||
SFX_SLASH_HERO = pkg_file('brutalmaze', 'soundfx/slash-hero.ogg')
|
||||
SFX_SHOT_ENEMY = pkg_file('brutalmaze', 'soundfx/shot-enemy.ogg')
|
||||
SFX_SHOT_HERO = pkg_file('brutalmaze', 'soundfx/shot-hero.ogg')
|
||||
SFX_MISSED = pkg_file('brutalmaze', 'soundfx/missed.ogg')
|
||||
SFX_HEART = pkg_file('brutalmaze', 'soundfx/heart.ogg')
|
||||
SFX_LOSE = pkg_file('brutalmaze', 'soundfx/lose.ogg')
|
||||
SFX = (SFX_NOISE, SFX_SPAWN, SFX_SLASH_ENEMY, SFX_SLASH_HERO,
|
||||
SFX_SHOT_ENEMY, SFX_SHOT_HERO, SFX_MISSED, SFX_HEART, SFX_LOSE)
|
||||
|
||||
SQRT2 = 2 ** 0.5
|
||||
INIT_SCORE = 2
|
||||
|
|
|
@ -32,11 +32,12 @@ from threading import Thread
|
|||
with redirect_stdout(StringIO()): import pygame
|
||||
from pygame import KEYDOWN, MOUSEBUTTONUP, QUIT, VIDEORESIZE
|
||||
from pygame.time import Clock, get_ticks
|
||||
from palace import free, use_context, Device, Context
|
||||
from appdirs import AppDirs
|
||||
|
||||
from .constants import SETTINGS, ICON, NOISE, HERO_SPEED, MIDDLE
|
||||
from .constants import SETTINGS, ICON, SFX, SFX_NOISE, HERO_SPEED, MIDDLE
|
||||
from .maze import Maze
|
||||
from .misc import sign, deg, join
|
||||
from .misc import sign, deg, join, play, clean_sources
|
||||
|
||||
|
||||
class ConfigReader:
|
||||
|
@ -104,17 +105,12 @@ class ConfigReader:
|
|||
|
||||
class Game:
|
||||
"""Object handling main loop and IO."""
|
||||
def __init__(self, config):
|
||||
pygame.mixer.pre_init(frequency=44100)
|
||||
def __init__(self, config: ConfigReader):
|
||||
pygame.init()
|
||||
self.headless = config.headless and config.server
|
||||
if config.muted or self.headless:
|
||||
pygame.mixer.quit()
|
||||
else:
|
||||
pygame.mixer.music.load(NOISE)
|
||||
pygame.mixer.music.set_volume(config.musicvol)
|
||||
pygame.mixer.music.play(-1)
|
||||
pygame.display.set_icon(ICON)
|
||||
if not self.headless: pygame.display.set_icon(ICON)
|
||||
self.actx = None if self.headless else Context(Device())
|
||||
self._mute = config.muted
|
||||
|
||||
if config.server:
|
||||
self.server = socket()
|
||||
|
@ -128,8 +124,7 @@ class Game:
|
|||
else:
|
||||
self.server = self.sockinp = None
|
||||
|
||||
# self.fps is a float to make sure floordiv won't be used in Python 2
|
||||
self.max_fps, self.fps = config.max_fps, float(config.max_fps)
|
||||
self.max_fps, self.fps = config.max_fps, config.max_fps
|
||||
self.musicvol = config.musicvol
|
||||
self.touch = config.touch
|
||||
self.key, self.mouse = config.key, config.mouse
|
||||
|
@ -138,7 +133,35 @@ class Game:
|
|||
self.hero = self.maze.hero
|
||||
self.clock, self.paused = Clock(), False
|
||||
|
||||
def __enter__(self): return self
|
||||
def __enter__(self):
|
||||
if self.actx is not None:
|
||||
use_context(self.actx)
|
||||
self.actx.listener.position = MIDDLE, 0, MIDDLE
|
||||
self.actx.listener.gain = not self._mute
|
||||
play(SFX_NOISE)
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_value, traceback):
|
||||
if self.server is not None: self.server.close()
|
||||
if not self.hero.dead: self.maze.dump_records()
|
||||
if self.actx is not None:
|
||||
free(SFX)
|
||||
clean_sources()
|
||||
use_context(None)
|
||||
self.actx.destroy()
|
||||
self.actx.device.close()
|
||||
pygame.quit()
|
||||
|
||||
@property
|
||||
def mute(self):
|
||||
"""Mute state."""
|
||||
return getattr(self, '_mute', 1)
|
||||
|
||||
@mute.setter
|
||||
def mute(self, value):
|
||||
"""Mute state."""
|
||||
self._mute = int(bool(value))
|
||||
self.actx.listener.gain = not self._mute
|
||||
|
||||
def export_txt(self):
|
||||
"""Export maze data to string."""
|
||||
|
@ -161,13 +184,7 @@ class Game:
|
|||
self.maze.resize((event.w, event.h))
|
||||
elif event.type == KEYDOWN:
|
||||
if event.key == self.key['mute']:
|
||||
if pygame.mixer.get_init() is None:
|
||||
pygame.mixer.init(frequency=44100)
|
||||
pygame.mixer.music.load(NOISE)
|
||||
pygame.mixer.music.set_volume(self.musicvol)
|
||||
pygame.mixer.music.play(-1)
|
||||
else:
|
||||
pygame.mixer.quit()
|
||||
self.mute ^= 1
|
||||
elif not self.server:
|
||||
if event.key == self.key['new']:
|
||||
self.maze.reinit()
|
||||
|
@ -196,6 +213,7 @@ class Game:
|
|||
if not self.paused: self.maze.update(self.fps)
|
||||
if not self.headless: self.maze.draw()
|
||||
self.clock.tick(self.fps)
|
||||
clean_sources()
|
||||
return True
|
||||
|
||||
def move(self, x=0, y=0):
|
||||
|
@ -308,11 +326,6 @@ class Game:
|
|||
slashing = buttons[self.mouse['slash']]
|
||||
self.control(right, down, angle, firing, slashing)
|
||||
|
||||
def __exit__(self, exc_type, exc_value, traceback):
|
||||
if self.server is not None: self.server.close()
|
||||
if not self.hero.dead: self.maze.dump_records()
|
||||
pygame.quit()
|
||||
|
||||
|
||||
def main():
|
||||
"""Start game and main loop."""
|
||||
|
|
|
@ -29,7 +29,7 @@ import pygame
|
|||
from .characters import Hero, new_enemy
|
||||
from .constants import (
|
||||
EMPTY, WALL, HERO, ENEMY, ROAD_WIDTH, WALL_WIDTH, CELL_WIDTH, CELL_NODES,
|
||||
MAZE_SIZE, MIDDLE, INIT_SCORE, ENEMIES, SQRT2, SFX_SPAWN,
|
||||
MAZE_SIZE, MIDDLE, INIT_SCORE, ENEMIES, SQRT2, SFX_SPAWN, SFX_MISSED,
|
||||
SFX_SLASH_ENEMY, SFX_LOSE, ADJACENTS, TANGO_VALUES, BG_COLOR, FG_COLOR,
|
||||
COLORS, HERO_HP, ENEMY_HP, ATTACK_SPEED, MAX_WOUND, HERO_SPEED,
|
||||
BULLET_LIFETIME, JSON_SEPARATORS)
|
||||
|
@ -63,8 +63,6 @@ class Maze:
|
|||
glitch (float): time that the maze remain flashing colors (in ms)
|
||||
next_slashfx (float): time until next slash effect of the hero (in ms)
|
||||
slashd (float): minimum distance for slashes to be effective
|
||||
sfx_slash (pygame.mixer.Sound): sound effect of slashed enemy
|
||||
sfx_lose (pygame.mixer.Sound): sound effect to be played when you lose
|
||||
export (list of defaultdict): records of game states
|
||||
export_dir (str): directory containing records of game states
|
||||
export_rate (float): milliseconds per snapshot
|
||||
|
@ -293,7 +291,7 @@ class Maze:
|
|||
if d > 0:
|
||||
wound = d * SQRT2 / self.distance
|
||||
if self.next_slashfx <= 0:
|
||||
play(self.sfx_slash, wound, enemy.get_angle())
|
||||
play(SFX_SLASH_ENEMY, enemy.x, enemy.y, wound)
|
||||
self.next_slashfx = ATTACK_SPEED
|
||||
enemy.hit(wound / self.hero.spin_speed)
|
||||
if enemy.wound >= ENEMY_HP:
|
||||
|
@ -323,7 +321,7 @@ class Maze:
|
|||
enemy = new_enemy(self, gridx, gridy)
|
||||
enemy.awake = True
|
||||
self.map[gridx][gridy] = ENEMY
|
||||
play(self.sfx_spawn, enemy.spawn_volume, enemy.get_angle())
|
||||
play(SFX_SPAWN, enemy.x, enemy.y)
|
||||
enemy.hit(wound)
|
||||
self.enemies.append(enemy)
|
||||
continue
|
||||
|
@ -334,17 +332,17 @@ class Maze:
|
|||
self.score += enemy.wound
|
||||
enemy.die()
|
||||
self.add_enemy()
|
||||
play(bullet.sfx_hit, wound, bullet.angle)
|
||||
play(bullet.sfx_hit, gridx, gridy, wound)
|
||||
fallen.append(i)
|
||||
break
|
||||
elif bullet.get_distance(self.x, self.y) < self.distance:
|
||||
if block:
|
||||
self.hero.next_strike = (abs(self.hero.spin_queue/self.fps)
|
||||
+ ATTACK_SPEED)
|
||||
play(bullet.sfx_missed, wound, bullet.angle + pi)
|
||||
play(SFX_MISSED, gain=wound)
|
||||
else:
|
||||
self.hit_hero(wound, bullet.color)
|
||||
play(bullet.sfx_hit, wound, bullet.angle + pi)
|
||||
play(bullet.sfx_hit, gain=wound)
|
||||
fallen.append(i)
|
||||
for i in reversed(fallen): self.bullets.pop(i)
|
||||
|
||||
|
@ -510,7 +508,7 @@ class Maze:
|
|||
self.destx = self.desty = MIDDLE
|
||||
self.stepx = self.stepy = 0
|
||||
self.vx = self.vy = 0.0
|
||||
play(self.sfx_lose)
|
||||
play(SFX_LOSE)
|
||||
self.dump_records()
|
||||
|
||||
def reinit(self):
|
||||
|
|
|
@ -26,8 +26,9 @@ from random import shuffle
|
|||
|
||||
import pygame
|
||||
from pygame.gfxdraw import filled_polygon, aapolygon
|
||||
from palace import Buffer, Source
|
||||
|
||||
from .constants import ADJACENTS, CORNERS
|
||||
from .constants import ADJACENTS, CORNERS, MIDDLE
|
||||
|
||||
|
||||
def randsign():
|
||||
|
@ -81,29 +82,34 @@ def around(x, y):
|
|||
return chain(a, c)
|
||||
|
||||
|
||||
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)
|
||||
|
||||
channel = sound.play()
|
||||
if angle is None:
|
||||
channel.set_volume(volume)
|
||||
else:
|
||||
delta = cos(angle)
|
||||
volumes = [volume * (1-delta), volume * (1+delta)]
|
||||
for i, v in enumerate(volumes):
|
||||
if v > 1:
|
||||
volumes[i - 1] += v - 1
|
||||
volumes[i] = 1.0
|
||||
sound.set_volume(1.0)
|
||||
channel.set_volume(*volumes)
|
||||
|
||||
|
||||
def json_rec(directory):
|
||||
"""Return path to JSON file to be created inside the given directory
|
||||
based on current time local to timezone in ISO 8601 format.
|
||||
"""
|
||||
return path.join(
|
||||
directory, '{}.json'.format(datetime.now().isoformat()[:19]))
|
||||
|
||||
|
||||
def play(sound: str, x: float = MIDDLE, y: float = MIDDLE,
|
||||
gain: float = 1.0) -> None:
|
||||
"""Play a sound at the given position."""
|
||||
buffer = Buffer(sound)
|
||||
source = buffer.play()
|
||||
source.spatialize = True
|
||||
source.position = x, 0, y
|
||||
source.gain = gain
|
||||
sources.append(source)
|
||||
|
||||
|
||||
def clean_sources() -> None:
|
||||
"""Destroyed stopped sources."""
|
||||
global sources
|
||||
sources, tmp = [], sources
|
||||
for source in tmp:
|
||||
if source.playing:
|
||||
sources.append(source)
|
||||
else:
|
||||
source.destroy()
|
||||
|
||||
|
||||
sources = []
|
||||
|
|
|
@ -21,7 +21,7 @@ __doc__ = 'Brutal Maze module for weapon classes'
|
|||
from math import cos, sin
|
||||
|
||||
from .constants import (BULLET_LIFETIME, SFX_SHOT_ENEMY, SFX_SHOT_HERO,
|
||||
SFX_MISSED, BULLET_SPEED, ENEMY_HP, TANGO, BG_COLOR)
|
||||
BULLET_SPEED, ENEMY_HP, TANGO, BG_COLOR)
|
||||
from .misc import regpoly, fill_aapolygon
|
||||
|
||||
|
||||
|
@ -34,8 +34,7 @@ class Bullet:
|
|||
angle (float): angle of the direction the bullet pointing (in radians)
|
||||
color (str): bullet's color name
|
||||
fall_time (int): time until the bullet fall down
|
||||
sfx_hit (pygame.mixer.Sound): sound effect indicating target was hit
|
||||
sfx_missed (pygame.mixer.Sound): sound effect indicating a miss shot
|
||||
sfx_hit (str): sound effect indicating target was hit
|
||||
"""
|
||||
def __init__(self, surface, x, y, angle, color):
|
||||
self.surface = surface
|
||||
|
@ -45,7 +44,6 @@ class Bullet:
|
|||
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."""
|
||||
|
|
|
@ -7,7 +7,7 @@ module = 'brutalmaze'
|
|||
author = 'Nguyễn Gia Phong'
|
||||
author-email = 'mcsinyx@disroot.org'
|
||||
home-page = 'https://github.com/McSinyx/brutalmaze'
|
||||
requires = ['appdirs', 'pygame>=1.9', 'setuptools']
|
||||
requires = ['appdirs', 'palace', 'pygame>=1.9', 'setuptools']
|
||||
description-file = 'README.rst'
|
||||
classifiers = [
|
||||
'Development Status :: 4 - Beta',
|
||||
|
@ -28,5 +28,4 @@ license = 'AGPLv3+'
|
|||
brutalmaze = "brutalmaze.game:main"
|
||||
|
||||
[tool.flit.sdist]
|
||||
include = ['wiki']
|
||||
exclude = ['docs']
|
||||
|
|
Loading…
Reference in New Issue