Mercurial > touhou
changeset 456:cae1ae9de430
Add native text support, MSG instructions 3 and 8, and text at the beginning of a stage.
author | Emmanuel Gil Peyrot <linkmauve@linkmauve.fr> |
---|---|
date | Tue, 16 Jul 2013 21:11:40 +0200 |
parents | 6864a38b2413 |
children | 4ccc47828002 |
files | README pytouhou/game/game.pyx pytouhou/game/text.py pytouhou/games/eosd.py pytouhou/lib/_sdl.pxd pytouhou/lib/sdl.pxd pytouhou/lib/sdl.pyx pytouhou/ui/gamerunner.pyx pytouhou/ui/renderer.pxd pytouhou/ui/renderer.pyx pytouhou/ui/texture.pyx pytouhou/ui/window.pyx pytouhou/vm/msgrunner.py setup.py |
diffstat | 14 files changed, 312 insertions(+), 24 deletions(-) [+] |
line wrap: on
line diff
--- a/README +++ b/README @@ -16,7 +16,8 @@ Running: * Cython * A working OpenGL driver * SDL2 - * SDL2_image, SDL2_mixer + * SDL2_image, SDL2_mixer, SDL2_ttf + * A TTF font file, placed as “font.ttf” in the game directory. Building sample data:
--- a/pytouhou/game/game.pyx +++ b/pytouhou/game/game.pyx @@ -19,7 +19,7 @@ from pytouhou.game.bullet cimport Bullet from pytouhou.game.enemy cimport Enemy from pytouhou.game.item cimport Item from pytouhou.game.effect cimport Particle -from pytouhou.game.text import Text +from pytouhou.game.text import Text, NativeText from pytouhou.game.face import Face @@ -46,6 +46,7 @@ cdef class Game: self.items = [] self.labels = [] self.faces = [None, None] + self.texts = [None, None, None, None] self.interface = interface self.hints = hints @@ -209,6 +210,11 @@ cdef class Game: return label + def new_native_text(self, pos, text, align='left'): + label = NativeText(pos, text, shadow=True, align=align) + return label + + def new_hint(self, hint): pos = hint['Pos'] #TODO: Scale @@ -264,6 +270,12 @@ cdef class Game: self.update_hints() # Not from this game, so unknown. for label in self.labels: #TODO: what priority is it? label.update() + for text in self.texts: #TODO: what priority is it? + if text is not None: + text.update() + for text in self.native_texts: #TODO: what priority is it? + if text is not None: + text.update() self.update_faces() # Pri XXX # 5. Clean up @@ -487,6 +499,11 @@ cdef class Game: self.effects = filter_removed(self.effects) self.labels = filter_removed(self.labels) + #self.native_texts = filter_removed(self.native_texts) + + for i, text in enumerate(self.texts): + if text is not None and text.removed: + self.texts[i] = None # Disable boss mode if it is dead/it has timeout if self.boss and self.boss._enemy.removed:
--- a/pytouhou/game/text.py +++ b/pytouhou/game/text.py @@ -135,29 +135,24 @@ class Text(GlyphCollection): def move_timeout_update(self): - GlyphCollection.update(self) if self.frame % 2: for glyph in self.glyphes: glyph.y -= 1 - if self.frame == self.timeout: - self.removed = True + self.timeout_update() def fadeout_timeout_update(self): - GlyphCollection.update(self) if self.frame >= self.start: if self.frame == self.start: self.fade(self.duration, 255, lambda x: x) elif self.frame == self.timeout - self.duration: self.fade(self.duration, 0, lambda x: x) - if self.fade_interpolator: - self.fade_interpolator.update(self.frame) - self.alpha = int(self.fade_interpolator.values[0]) - for glyph in self.glyphes: - glyph.sprite.alpha = self.alpha - glyph.sprite.changed = True - if self.frame == self.timeout: - self.removed = True + self.fade_interpolator.update(self.frame) + self.alpha = int(self.fade_interpolator.values[0]) + for glyph in self.glyphes: + glyph.sprite.alpha = self.alpha + glyph.sprite.changed = True + self.timeout_update() def fade(self, duration, alpha, formula): @@ -167,9 +162,9 @@ class Text(GlyphCollection): def set_timeout(self, timeout, effect=None, duration=0, start=0): + self.timeout = timeout + start if effect == 'move': self.update = self.move_timeout_update - self.timeout = timeout + start elif effect == 'fadeout': self.alpha = 0 for glyph in self.glyphes: @@ -177,10 +172,8 @@ class Text(GlyphCollection): self.update = self.fadeout_timeout_update self.duration = duration self.start = start - self.timeout = timeout + start else: self.update = self.timeout_update - self.timeout = timeout + start @@ -236,3 +229,88 @@ class Gauge(Element): self.anmrunner = None + +class NativeText(object): + def __init__(self, pos, text, gradient=None, alpha=255, shadow=False, align='left'): + self.removed = False + self.x, self.y = pos + self.text = text + self.alpha = alpha + self.shadow = shadow + self.align = align + self.frame = 0 + + self.gradient = gradient or [(255, 255, 255), (255, 255, 255), + (128, 128, 255), (128, 128, 255)] + + self.update = self.normal_update + + + def normal_update(self): + self.frame += 1 + + + def timeout_update(self): + self.normal_update() + if self.frame == self.timeout: + self.removed = True + + + def move_timeout_update(self): + if self.frame % 2: + self.y -= 1 + self.timeout_update() + + + def move_ex_timeout_update(self): + if self.frame >= self.start: + if self.frame == self.start: + self.move_in(self.duration, self.to[0], self.to[1], lambda x: x) + elif self.frame == self.timeout - self.duration: + self.move_in(self.duration, self.end[0], self.end[1], lambda x: x) + if self.offset_interpolator: + self.offset_interpolator.update(self.frame) + self.x, self.y = self.offset_interpolator.values + self.timeout_update() + + + def fadeout_timeout_update(self): + if self.frame >= self.start: + if self.frame == self.start: + self.fade(self.duration, 255, lambda x: x) + elif self.frame == self.timeout - self.duration: + self.fade(self.duration, 0, lambda x: x) + self.fade_interpolator.update(self.frame) + self.alpha = int(self.fade_interpolator.values[0]) + self.timeout_update() + + + def fade(self, duration, alpha, formula): + self.fade_interpolator = Interpolator((self.alpha,), self.frame, + (alpha,), self.frame + duration, + formula) + + + def move_in(self, duration, x, y, formula): + self.offset_interpolator = Interpolator((self.x, self.y), self.frame, + (x, y), self.frame + duration, + formula) + + + def set_timeout(self, timeout, effect=None, duration=0, start=0, to=None, end=None): + self.timeout = timeout + start + if effect == 'move': + self.update = self.move_timeout_update + elif effect == 'move_ex': + self.update = self.move_ex_timeout_update + self.duration = duration + self.start = start + self.to = to + self.end = end + elif effect == 'fadeout': + self.alpha = 0 + self.update = self.fadeout_timeout_update + self.duration = duration + self.start = start + else: + self.update = self.timeout_update
--- a/pytouhou/games/eosd.py +++ b/pytouhou/games/eosd.py @@ -21,7 +21,7 @@ from pytouhou.game.itemtype import ItemT from pytouhou.game.player import Player from pytouhou.game.orb import Orb from pytouhou.game.effect import Effect -from pytouhou.game.text import Text, Counter, Gauge +from pytouhou.game.text import Text, Counter, Gauge, NativeText from pytouhou.game.background import Background from pytouhou.vm.eclrunner import ECLMainRunner @@ -110,14 +110,15 @@ class EoSDGame(Game): players = [EoSDPlayer(state, self, resource_loader, common.characters[state.character]) for state in player_states] - common.interface.start_stage(self, stage) - # Load stage data self.std = resource_loader.get_stage('stage%d.std' % stage) background_anm = resource_loader.get_single_anm('stg%dbg.anm' % stage) self.background = Background(self.std, background_anm) + common.interface.start_stage(self, stage) + self.native_texts = [common.interface.stage_name, common.interface.song_name] + self.resource_loader = resource_loader #XXX: currently used for texture preload in pytouhou.ui.gamerunner. Wipe it! Game.__init__(self, players, stage, rank, difficulty, @@ -183,10 +184,21 @@ class EoSDInterface(object): text = 'FINAL STAGE' elif stage == 7: text = 'EXTRA STAGE' + + self.stage_name = NativeText((192, 200), unicode(game.std.name), shadow=True, align='center') + self.stage_name.set_timeout(240, effect='fadeout', duration=60, start=120) + + self.set_song_name(game.std.bgms[0][0]) + self.level_start = [Text((16+384/2, 200), self.ascii_anm, text=text, align='center')] #TODO: find the exact location. self.level_start[0].set_timeout(240, effect='fadeout', duration=60, start=120) self.level_start[0].set_color('yellow') - #TODO: use the system text for the stage name, and the song name. + + + def set_song_name(self, name): + #TODO: use the correct animation. + self.song_name = NativeText((384, 432), u'♪ ' + name, shadow=True, align='right') + self.song_name.set_timeout(240, effect='fadeout', duration=60, start=120) def set_boss_life(self):
--- a/pytouhou/lib/_sdl.pxd +++ b/pytouhou/lib/_sdl.pxd @@ -164,3 +164,19 @@ cdef extern from "SDL_mixer.h" nogil: int Mix_VolumeMusic(int volume) int Mix_PlayChannel(int channel, Mix_Chunk *chunk, int loops) + + +cdef extern from "SDL_pixels.h" nogil: + ctypedef struct SDL_Color: + Uint8 r, g, b, a + + +cdef extern from "SDL_ttf.h" nogil: + ctypedef struct TTF_Font: + pass + + int TTF_Init() + void TTF_Quit() + TTF_Font *TTF_OpenFont(const char *filename, int ptsize) + void TTF_CloseFont(TTF_Font *font) + SDL_Surface *TTF_RenderUTF8_Blended(TTF_Font *font, const char *text, SDL_Color fg)
--- a/pytouhou/lib/sdl.pxd +++ b/pytouhou/lib/sdl.pxd @@ -74,9 +74,16 @@ cdef class Chunk: cdef void set_volume(self, float volume) nogil +cdef class Font: + cdef TTF_Font *font + + cdef Surface render(self, unicode text) + + cdef void init(Uint32 flags) except * cdef void img_init(Uint32 flags) except * cdef void mix_init(int flags) except * +cdef void ttf_init() except * IF UNAME_SYSNAME == "Windows": cdef void set_main_ready() @@ -84,6 +91,7 @@ IF UNAME_SYSNAME == "Windows": cdef void quit() nogil cdef void img_quit() nogil cdef void mix_quit() nogil +cdef void ttf_quit() nogil cdef void gl_set_attribute(SDL_GLattr attr, int value) except * cdef list poll_events() cdef const Uint8* get_keyboard_state() nogil
--- a/pytouhou/lib/sdl.pyx +++ b/pytouhou/lib/sdl.pyx @@ -116,6 +116,27 @@ cdef class Chunk: Mix_VolumeChunk(self.chunk, int(volume * 128)) +cdef class Font: + def __init__(self, const char *filename, int ptsize): + self.font = TTF_OpenFont(filename, ptsize) + if self.font == NULL: + raise SDLError(SDL_GetError()) + + def __dealloc__(self): + if self.font != NULL: + TTF_CloseFont(self.font) + + cdef Surface render(self, unicode text): + cdef SDL_Color white + white = SDL_Color(255, 255, 255, 255) + surface = Surface() + string = text.encode('utf-8') + surface.surface = TTF_RenderUTF8_Blended(self.font, string, white) + if surface.surface == NULL: + raise SDLError(SDL_GetError()) + return surface + + cdef void init(Uint32 flags) except *: if SDL_Init(flags) < 0: raise SDLError(SDL_GetError()) @@ -131,6 +152,11 @@ cdef void mix_init(int flags) except *: raise SDLError(SDL_GetError()) +cdef void ttf_init() except *: + if TTF_Init() < 0: + raise SDLError(SDL_GetError()) + + IF UNAME_SYSNAME == "Windows": cdef void set_main_ready(): SDL_SetMainReady() @@ -148,6 +174,10 @@ cdef void mix_quit() nogil: Mix_Quit() +cdef void ttf_quit() nogil: + TTF_Quit() + + cdef void gl_set_attribute(SDL_GLattr attr, int value) except *: if SDL_GL_SetAttribute(attr, value) < 0: raise SDLError(SDL_GetError())
--- a/pytouhou/ui/gamerunner.pyx +++ b/pytouhou/ui/gamerunner.pyx @@ -28,6 +28,9 @@ from .background import BackgroundRender from .music import MusicPlayer, SFXPlayer, NullPlayer from .shaders.eosd import GameShader, BackgroundShader +from collections import namedtuple +Rect = namedtuple('Rect', 'x y w h') +Color = namedtuple('Color', 'r g b a') logger = get_logger(__name__) @@ -158,6 +161,7 @@ class GameRunner(GameRenderer): self.game.run_iter(keystate) if not self.skip: self.render_game() + self.render_text() self.render_interface() return True @@ -175,6 +179,36 @@ class GameRunner(GameRenderer): glDisable(GL_SCISSOR_TEST) + if self.game.msg_runner: + rect = Rect(48, 368, 288, 48) + color1 = Color(0, 0, 0, 192) + color2 = Color(0, 0, 0, 128) + self.render_quads([rect], [(color1, color1, color2, color2)], 0) + + + def render_text(self): + if self.font_manager is None: + return + + labels = [label for label in self.game.texts + self.game.native_texts if label is not None] + self.font_manager.load(labels) + + black = Color(0, 0, 0, 255) + + for label in labels: + if label is None: + continue + + rect = Rect(label.x, label.y, label.width, label.height) + gradient = [Color(*color, a=label.alpha) for color in label.gradient] + + if label.shadow: + shadow_rect = Rect(label.x + 1, label.y + 1, label.width, label.height) + shadow = [black._replace(a=label.alpha)] * 4 + self.render_quads([shadow_rect, rect], [shadow, gradient], label.texture) + else: + self.render_quads([rect], [gradient], label.texture) + def render_interface(self): elements = []
--- a/pytouhou/ui/renderer.pxd +++ b/pytouhou/ui/renderer.pxd @@ -16,3 +16,4 @@ cdef class Renderer: cdef PyObject *elements[640*3] cpdef render_elements(self, elements) + cpdef render_quads(self, rects, colors, texture)
--- a/pytouhou/ui/renderer.pyx +++ b/pytouhou/ui/renderer.pyx @@ -14,6 +14,7 @@ from libc.stdlib cimport malloc, free from libc.string cimport memset +from os.path import join from pytouhou.lib.opengl cimport \ (glVertexPointer, glTexCoordPointer, glColorPointer, @@ -24,9 +25,15 @@ from pytouhou.lib.opengl cimport \ GL_ONE_MINUS_SRC_ALPHA, GL_ONE, GL_TEXTURE_2D, GL_TRIANGLES, glGenBuffers) +from pytouhou.lib.sdl import SDLError + from pytouhou.game.element cimport Element from .sprite cimport get_sprite_rendering_data -from .texture import TextureManager +from .texture import TextureManager, FontManager + +from pytouhou.utils.helpers import get_logger + +logger = get_logger(__name__) DEF MAX_ELEMENTS = 640*4*3 @@ -59,6 +66,12 @@ cdef class Renderer: def __init__(self, resource_loader): self.texture_manager = TextureManager(resource_loader, self) + font_name = join(resource_loader.game_dir, 'font.ttf') + try: + self.font_manager = FontManager(font_name, 16, self) + except SDLError: + self.font_manager = None + logger.error('Font file “%s” not found, disabling text rendering altogether.', font_name) if not self.use_fixed_pipeline: glGenBuffers(1, &self.vbo) @@ -149,3 +162,36 @@ cdef class Renderer: if not self.use_fixed_pipeline: glBindBuffer(GL_ARRAY_BUFFER, 0) + + + cpdef render_quads(self, rects, colors, texture): + # There is nothing that batch more than two quads on the same texture, currently. + cdef Vertex buf[8] + cdef unsigned short indices[12] + indices[:] = [0, 1, 2, 2, 3, 0, 4, 5, 6, 6, 7, 4] + + length = len(rects) + assert length == len(colors) + + glBindBuffer(GL_ARRAY_BUFFER, self.vbo) + + #TODO: find a way to use offsetof() instead of those ugly hardcoded values. + glVertexAttribPointer(0, 3, GL_INT, False, sizeof(Vertex), <void*>0) + glEnableVertexAttribArray(0) + glVertexAttribPointer(1, 2, GL_FLOAT, False, sizeof(Vertex), <void*>12) + glEnableVertexAttribArray(1) + glVertexAttribPointer(2, 4, GL_UNSIGNED_BYTE, True, sizeof(Vertex), <void*>20) + glEnableVertexAttribArray(2) + + for i, r in enumerate(rects): + c1, c2, c3, c4 = colors[i] + + buf[4*i] = Vertex(r.x, r.y, 0, 0, 0, c1.r, c1.g, c1.b, c1.a) + buf[4*i+1] = Vertex(r.x + r.w, r.y, 0, 1, 0, c2.r, c2.g, c2.b, c2.a) + buf[4*i+2] = Vertex(r.x + r.w, r.y + r.h, 0, 1, 1, c3.r, c3.g, c3.b, c3.a) + buf[4*i+3] = Vertex(r.x, r.y + r.h, 0, 0, 1, c4.r, c4.g, c4.b, c4.a) + + glBufferData(GL_ARRAY_BUFFER, 4 * length * sizeof(Vertex), buf, GL_DYNAMIC_DRAW) + glBindTexture(GL_TEXTURE_2D, texture) + glDrawElements(GL_TRIANGLES, 6 * length, GL_UNSIGNED_SHORT, indices) + glBindBuffer(GL_ARRAY_BUFFER, 0)
--- a/pytouhou/ui/texture.pyx +++ b/pytouhou/ui/texture.pyx @@ -19,7 +19,7 @@ from pytouhou.lib.opengl cimport \ glGenTextures, glBindTexture, glTexImage2D, GL_TEXTURE_2D, GLuint, glDeleteTextures) -from pytouhou.lib.sdl cimport load_png, create_rgb_surface +from pytouhou.lib.sdl cimport load_png, create_rgb_surface, Font from pytouhou.formats.thtx import Texture #TODO: perhaps define that elsewhere? import os @@ -50,6 +50,33 @@ class TextureManager(object): entry.texture.renderer = self.renderer +cdef class FontManager: + cdef Font font + cdef object renderer + + def __init__(self, fontname, fontsize=16, renderer=None): + self.font = Font(fontname, fontsize) + self.renderer = renderer + + + def load(self, label_list): + for label in label_list: + if not hasattr(label, 'texture'): + surface = self.font.render(label.text) + label.width, label.height = surface.surface.w, surface.surface.h + + if label.align == 'center': + label.x -= label.width // 2 + elif label.align == 'right': + label.x -= label.width + else: + assert label.align == 'left' + + texture = Texture(label.width, label.height, -4, surface.pixels) + label.texture = load_texture(texture) + label.texture.renderer = self.renderer + + cdef decode_png(loader, first_name, secondary_name): image_file = load_png(loader.get_file(os.path.basename(first_name))) width, height = image_file.surface.w, image_file.surface.h
--- a/pytouhou/ui/window.pyx +++ b/pytouhou/ui/window.pyx @@ -97,6 +97,7 @@ cdef class Window: sdl.set_main_ready() sdl.init(sdl.INIT_VIDEO) sdl.img_init(sdl.INIT_PNG) + sdl.ttf_init() if sound: sdl.mix_init(0) @@ -169,5 +170,6 @@ cdef class Window: def __dealloc__(self): sdl.mix_close_audio() sdl.mix_quit() + sdl.ttf_quit() sdl.img_quit() sdl.quit()
--- a/pytouhou/vm/msgrunner.py +++ b/pytouhou/vm/msgrunner.py @@ -85,6 +85,7 @@ class MSGRunner(object): self._game.msg_runner = None self._game.msg_wait = False self.ended = True + self._game.texts = [None] * 4 + self._game.texts[4:] @instruction(0) @@ -106,6 +107,15 @@ class MSGRunner(object): face.load(index) + @instruction(3) + def display_text(self, side, index, text): + if index == 0: + self._game.texts[0] = None + self._game.texts[1] = None + self._game.texts[index] = self._game.new_native_text((64, 372 + index * 24), text) + self._game.texts[index].set_timeout(-1, effect='fadeout', duration=15) + + @instruction(4) def pause(self, duration): if not (self.skipping and self.allow_skip): @@ -129,6 +139,12 @@ class MSGRunner(object): self._game.music.play(track) + @instruction(8) + def display_description(self, side, index, text): + assert side == 1 # It shouldn’t crash when it’s something else. + self._game.texts[2 + index] = self._game.new_native_text((336, 320 + index * 18), text, align='right') + + @instruction(10) def freeze(self): self.frozen = True