def makeFullscreen(self): # self.window = sdl2.ext.Window(self.title, size=(self.res_x, self.res_y), flags=sdl2.SDL_WINDOW_FULLSCREEN) current_display = sdl2.SDL_GetWindowDisplayIndex(self.window.window) current_display_mode = sdl2.SDL_DisplayMode() res = sdl2.SDL_GetCurrentDisplayMode(current_display,current_display_mode) px_log.log(f"Current display mode: {current_display_mode}") display_modes = [] display_mode_index = 0 # sdl2.SDL_GetWindowDisplayIndex(self.window.window) for mode_index in range(current_display, sdl2.SDL_GetNumDisplayModes(display_mode_index)): disp_mode = sdl2.SDL_DisplayMode() ret = sdl2.SDL_GetDisplayMode( display_mode_index, mode_index, disp_mode ) display_modes.append(disp_mode) px_log.log(f"mode:{mode_index} info:{display_modes[mode_index]}") # todo: something more clever than this after testing on more monitors # prob just make a better guess on the window size as a whole fraction of the native display size # without getting too large or too small # e.g. for MacBook Pro, 960 is too small; 1280 isn't great and is more fuzzy than 1920 - which looks fine; # 2880 looks even sharper than as 1920, but will prob be pushing too many pixels # Prob should allow it to be overridden via options/config # sdl2.SDL_SetWindowSize(self.window.window,1280,720) # sdl2.SDL_SetWindowSize(self.window.window,1440,810) # sdl2.SDL_SetWindowSize(self.window.window,1920,1080) sdl2.SDL_SetWindowSize(self.window.window, self.res_x*self.zoom, self.res_y*self.zoom) sdl2.SDL_SetWindowFullscreen(self.window.window, sdl2.SDL_WINDOW_FULLSCREEN) return
def run(tests=False): game = PacBun() if tests: game.runTests() game.run() px_log.log("Exiting...") px_log.flushToFile() return 0
def makeTemplates(self, templates_data): for name, template in templates_data.items(): px_log.log(f"Making {name} template.") self.entity_manager.makeEntityTemplate(name, controller=template['controller'](self.controller_manager) if 'controller' in template else None, collider=template['collider'](self.controller_manager) if 'collider' in template else None, graphics=self.graphics_manager.makeTemplate(template['graphics']['component'], {'RenderLayer': self.render_layers[template['graphics']['render layer']]}) if 'graphics' in template else None )
def renderTextToSurfaceAndSave(self, file, string, font=False, color=px_graphics.Color(1, 1, 1, 1)): surf = sdl2.sdlttf.TTF_RenderUTF8_Solid(self.sdl_fonts[font].font, string.encode('utf-8'), color) if surf is None: log(f"TTF_RenderText failed: {string}") width = surf.contents.w height = surf.contents.h return width, height
def initEntity(self, entity, data=False): entity.message = False if data: entity.pos = data['pos'] entity.parent = entity.game.getEntityByName(f'bunny choice {0}') entity.bun_num = data['bun num'] bunnies = entity.game.game_data['bunnies'] entity.message_color = bunnies.index(entity.bun_num).color entity.bun_name = data['bun name'] else: px_log.log( "*** Warning: Entity {entity.name} with BunnyChooseController component missing data." )
def update(self, entity, time): if entity.new_state: entity.new_state = False if entity.state in self.anims: entity.current_anim = entity.state entity.current_state = entity.state self.anims[entity.current_anim].startAnim( entity ) # TODO allow some anims to begin from different frame else: log(f"Warning: {entity.name} animation doesn't exist for requested state {entity.state}" ) self.anims[entity.current_anim].advanceAnim(entity, time)
def makeAtlas(self, start_size=256): # tries this size and increases dimensions until the images all fit atlas_dim = start_size while not self._renderAtlas(atlas_dim, dry_run=True): atlas_dim = int(atlas_dim + 64) if atlas_dim > 4096: log("WARNING: a texture atlas dimension bigger than 4K may not work on some machines" ) # actually render atlas - do again with rendering and updating the image records self._renderAtlas(atlas_dim, dry_run=False) self.atlas_dim = atlas_dim log(f"Graphics|RenderLayer:Atlas created at size: {self.atlas_dim}")
def _renderAtlas(self, atlas_dim, dry_run, gap=0): if not dry_run: # make the atlas texture self.TA = sdl2.SDL_CreateTexture(self.ren.renderer, sdl2.SDL_PIXELFORMAT_ABGR8888, sdl2.SDL_TEXTUREACCESS_TARGET, atlas_dim, atlas_dim) # point drawing at the atlas and clear it sdl2.SDL_SetRenderTarget(self.ren.renderer, self.TA) sdl2.SDL_SetRenderDrawColor(self.ren.renderer, 0, 0, 0, 0) sdl2.SDL_RenderClear(self.ren.renderer) sdl2.SDL_SetRenderDrawColor(self.ren.renderer, 255, 255, 255, 255) # draws textures in rows, left to right, then top to bottom accum_x = 0 # where we've got to across TA max_y = 0 # how tall the row is so we know where to start the next one accum_y = 0 # how far down the current row is for image in sorted(self.images, key=Image.getHeight, reverse=True): if accum_x + image.width + gap > atlas_dim: # will this image go off right of atlas? accum_x = 0 # push down accum_y += max( max_y, image.height) + gap # in case this is the tallest image max_y = 0 if not dry_run: # draw into atlas and kill old texture image.draw(accum_x, accum_y) sdl2.SDL_DestroyTexture(image.texture) # and update the image to point to where it went in the atlas # update in place to avoid re-doing indexes image.texture = self.TA image.src = sdl2.SDL_Rect(accum_x, accum_y, image.width, image.height) accum_x += image.width + gap max_y = max(max_y, image.height) if accum_y + max_y > atlas_dim: log(f"Atlas overflow at size {atlas_dim}") return False if not dry_run: # point drawing at screen again sdl2.SDL_SetRenderTarget(self.ren.renderer, None) sdl2.SDL_SetTextureBlendMode(self.TA, sdl2.SDL_BLENDMODE_BLEND) return True
def addImageFromFile(self, file, trim=False): # find if this file has been loaded before and return that if so # also check if there's an index that is empty and can be re-used filepath = os.path.abspath(file) for index, image in enumerate(self.images): if image: if image.file == filepath: image.ref_count += 1 return index, image.trim_x, image.trim_y # otherwise find next empty slot (or append) index = self._getNextEmptySlot() try: self.images[index] = Image.fromFile(self.ren, filepath, width=None, height=None, trim=trim) return index, self.images[index].trim_x, self.images[index].trim_y except Exception as e: log("Problem loading image for frame: " + str(e) + " file:" + filepath)
def renderText(self, string, font=False, color=px_graphics.Color(1, 1, 1, 1)): #We need to first render to a surface as that's what TTF_RenderText #returns, then load that surface into a texture surf = sdl2.sdlttf.TTF_RenderUTF8_Solid(self.sdl_fonts[font].font, string.encode('utf-8'), color.toSDLColor()) if surf is None: log(f"TTF_RenderText failed: {string}") return None texture = sdl2.SDL_CreateTextureFromSurface(self.ren.sdlrenderer, surf) if texture is None: print("CreateTexture") #Clean up the surface and font width = surf.contents.w height = surf.contents.h sdl2.SDL_FreeSurface(surf) # sdl2.sdlttf.TTF_CloseFont(font) return texture, width, height
def checkCollide(self,A,B): # progressive bounding box # check x first Apos = A.getPos() Adim = A.dim Aorig = A.orig Bpos = B.getPos() Bdim = B.dim Borig = B.orig if (Apos.x - Aorig.x + Adim.x)> (Bpos.x -Borig.x): # Aright > Bleft if (Bpos.x - Borig.x + Bdim.x) > (Apos.x - Aorig.x): # Bright < Aleft if (Apos.z - Aorig.z + Adim.z) > (Bpos.z - Borig.z): if (Bpos.z - Borig.z + Bdim.z) > (Apos.z - Aorig.z): if (Apos.y - Aorig.y + Adim.y) > (Bpos.y - Borig.y): if (Bpos.y - Borig.y + Bdim.y) > (Apos.y - Aorig.y): # we have a collision if collision_debug: log(f"Collision - A: {A.name} B: {B.name}") return True
def __init__(self): px_log.log("Getting game data...") self.game_data = px_utility.getDataFromFile('game.config')['game'] px_log.log("Getting user data...") self.user_data = px_utility.getDataFromFile('user.config')['user'] px_log.log("Setting up window...") # Initialize the video system - this implicitly initializes some # necessary parts within the SDL2 DLL used by the video module. # # You SHOULD call this before using any video related methods or # classes. sdl2.ext.init() self.title = self.game_data['title'] self.res_x = self.game_data['res_x'] self.res_y = self.game_data['res_y'] self.zoom = self.user_data['zoom'] self.fullscreen = self.user_data['fullscreen'] self.clear_color = self.game_data['clear_color'] sdl2.SDL_Init(sdl2.SDL_INIT_VIDEO | sdl2.SDL_INIT_JOYSTICK | sdl2.SDL_INIT_GAMECONTROLLER) # INITIALISE GRAPHICS & THE SCREEN # Create a new window (like your browser window or editor window, # etc.) and give it a meaningful title and size. We definitely need # this, if we want to present something to the user. # todo: calculate resolution more carefully esp for fullscreen # create the window (without showing it) self.windowed_width = self.res_x*self.zoom # keep these for later self.windowed_height = self.res_y*self.zoom self.window = sdl2.ext.Window(self.title, size=(self.windowed_width, self.windowed_height)) if(self.fullscreen): self.makeFullscreen() # set up a renderer to draw stuff. This is a HW accelerated one. # Switch on VSync to avoid running too fast # and wasting power and keep graphics nice and clean self.ren = sdl2.ext.Renderer(self.window, flags=sdl2.SDL_RENDERER_ACCELERATED | sdl2.SDL_RENDERER_PRESENTVSYNC) # makes zoomed graphics blocky (for retro effect) sdl2.SDL_SetHint(sdl2.SDL_HINT_RENDER_SCALE_QUALITY, b"nearest") # makes the graphics look and act like the desired screen size, even though they may be rendered at a different one sdl2.SDL_RenderSetLogicalSize(self.ren.renderer, self.res_x, self.res_y) self.running = True self.game_mode = eGameModes.title self.input = px_game_pad.Input(self) self.render_layers = {} self.scroll = False self.flags={} self.drawables = px_entity.EntityList() self.audibles = px_entity.EntityList() self.updatables = px_entity.EntityList() self.graphics_manager = px_entity.ComponentManager(game=self) self.controller_manager = px_entity.ComponentManager(game=self) self.entity_manager = px_entity.EntityManager(game=self) self.sound_manager = px_entity.ComponentManager(game=self) self.sound_mixer = px_sound.SoundMixer(self) self.collision_manager = px_collision.CollisionManager(game=self) # By default, every Window is hidden, not shown on the screen right # after creation. Thus we need to tell it to be shown now. self.window.show()
def nextScene(self, next_scene=-1, mode=False): # kill gc.enable() gc.collect() # clear all the flags self.flags={} if mode: if mode!=self.current_mode: px_log.log(f"Switching to mode: {mode}") self.current_mode=mode self.mode_data = self.game_data['modes'][self.current_mode] # convenience # kill old mode self.killEntitiesExceptDicts( [ self.game_data['entities'], ] ) # set up next mode px_log.log(f"Making {mode} mode templates.") if 'templates' in self.mode_data: self.makeTemplates(self.mode_data['templates']) px_log.log(f"Making {mode} mode entities.") if 'entities' in self.mode_data: self.makeEntities(self.mode_data['entities']) if next_scene<0: # no scene specified so revert to 0 next_scene = 0 px_log.flushToFile() if next_scene>=0: # specified scene rather than following pre-defined order self.current_scene = next_scene specified = "specified " else: self.current_scene+=1 if self.current_scene>=len(self.mode_data['scenes']): # todo add check for completing the game instead of just looping? self.current_scene=0 specified = "" next_scene = self.mode_data['scenes'][self.current_scene] px_log.log( f"Switching to {specified}scene [{self.current_mode},{self.current_scene}]: {self.mode_data['scenes'][self.current_scene]}") # kill old scene self.killEntitiesExceptDicts( [ self.game_data['entities'], self.mode_data['entities'] if 'entities' in self.mode_data else {} ] ) ################### # init next scene # ################### self.scene_data = self.scenes_data['scenes'][self.mode_data['scenes'][self.current_scene]] # initialise map todo: make another entity instead of special # if "Map" in self.scene_data: # self.level = map.Map(self, self.scene_data, self.templates['tile']) if 'templates' in self.scene_data: self.makeTemplates(self.scene_data['templates']) if 'entities' in self.scene_data: self.makeEntities(self.scene_data['entities']) px_log.flushToFile() gc.collect() gc.disable() if len(gc.garbage)>0: px_log.log(gc.garbage)
def delete(self, data): px_log.log(f"Missing delete function for Component:{type(self)}")
def __init__(self): super(PacBun, self).__init__() sdl2.mouse.SDL_ShowCursor(False) px_log.log("Window set up.") px_log.log("Setting up render layers...") for rl_name,rl_data in self.game_data['render layers'].items(): rl = px_graphics.RenderLayer(self.ren) self.render_layers[rl_name]=rl if 'fonts' in rl_data: px_log.log(f" Getting fonts in Render Layer {rl_name}...") for font_name, font_data in rl_data['fonts'].items(): # todo keep a handle on the fonts returned px_log.log(f" Getting font: {font_name}...") rl.addFont(font_data['file'],font_data['size']) px_log.log("Render Layers set up.") # todo: consider moving this into px_game px_log.log("Making game scope templates...") self.templates = self.makeTemplates(self.game_data['templates']) # self.templates.update(self.makeTemplates(overlay_templates_data, self.overlay_renlayer)) px_log.log("Templates made.") px_log.log("Making game scope entities...") self.makeEntities(self.game_data['entities']) px_log.log("Game scope entities made.") px_log.log("Getting scenes...") self.getScenesData('PB_scenes.config') # put all separate images into texture atlasses for (more) efficient rendering px_log.log("Making texture atlases...") for rl_name, rl_instance in self.render_layers.items(): if 'texture atlas' in self.game_data['render layers'][rl_name]: # make texture atlas # todo: do stuff with texture atlas files if 'size hint' in self.game_data['render layers'][rl_name]: rl_instance.makeAtlas(self.game_data['render layers'][rl_name]['size hint']) else: rl_instance.makeAtlas() rl_instance.dumpAtlasToFiles(f"{rl_name}.png",f"{rl_name}.json") # self.renlayer.dumpAtlasToFiles("TA.png", "TA.json") px_log.log("### Startup complete ###") px_log.flushToFile() self.current_mode=-1 self.nextScene(next_scene=0, mode=self.game_data['init_mode'])
def initEntity(self, entity, data=False): entity.won = False # keeps track of if the map is complete or not impassables = ['H', '#', '<', '>'] entity.map = copy.deepcopy( data) # deepcopy since we're going to flip it upside down # flip since we draw from bottom left for i in range(0, 9): entity.map[i], entity.map[16 - i] = entity.map[16 - i], entity.map[i] entity.tiles = [None] * 17 for i in range(0, 17): entity.tiles[i] = [None] * 30 entity.holes = [] entity.bunnies = [] entity.fox_starts = [] entity.num_spaces = 0 entity.num_poos = 0 bunny_to_place = 0 for y in range(0, 17): for x in range(0, 30): if entity.map[y][x] == '0': continue # create tile for each square with anything in it this_tile = entity.game.requestNewEntity('tile', pos=Vec3( 8 + x * 16, 8 + y * 16, 1), parent=self, name=f"back{x},{y}") entity.tiles[y][x] = this_tile # set up each tile from what the map says if entity.map[y][x] == "H": this_tile.setState(tile.eTileStates.hedge) elif entity.map[y][x] == "#": this_tile.setState(tile.eTileStates.void) else: # work out ways out of the space exit_map_value = 0 if y + 1 < 17: if entity.map[y + 1][x] not in impassables: this_tile.components['controller'].addExit( this_tile, px_entity.eDirections.up) exit_coord = Vec3(x * 16 + 8, y * 16 + 10, 0) exit_direction = px_entity.eDirections.up exit_map_value += 1 if y - 1 > 0: if entity.map[y - 1][x] not in impassables: this_tile.components['controller'].addExit( this_tile, px_entity.eDirections.down) exit_coord = Vec3(x * 16 + 8, y * 16 + 6, 0) exit_direction = px_entity.eDirections.down exit_map_value += 2 if x - 1 > 0: if entity.map[y][x - 1] not in impassables: this_tile.components['controller'].addExit( this_tile, px_entity.eDirections.left) exit_coord = Vec3(x * 16 + 6, y * 16 + 8, 0) exit_direction = px_entity.eDirections.left exit_map_value += 4 if x + 1 < 30: if entity.map[y][x + 1] not in impassables: this_tile.components['controller'].addExit( this_tile, px_entity.eDirections.right) exit_coord = Vec3(x * 16 + 10, y * 16 + 8, 0) exit_direction = px_entity.eDirections.right exit_map_value += 8 if entity.map[y][x] == "o": this_tile.components['controller'].setState( this_tile, tile.eTileStates.hole) entity.holes.append( Hole(pos=Vec3(x, y, 0), exit=exit_coord, direction=exit_direction)) elif entity.map[y][x] == "O": this_tile.components['controller'].setState( this_tile, tile.eTileStates.cutscene_hole) elif entity.map[y][x] == "T": # map to which tunnel graphic to use based on exits # which_tunnel = [ # tile.eTileStates.tunnel_no_exit, # 0000 # tile.eTileStates.tunnel_up, # 0001 # tile.eTileStates.tunnel_down, # 0010, 2 # tile.eTileStates.tunnel_up_down, # 0011, 3 # tile.eTileStates.tunnel_left, # 0100, 4 # tile.eTileStates.tunnel_up_left, # 0101, 5 # tile.eTileStates.tunnel_down_left, # 0110, 6 # tile.eTileStates.tunnel_up_down_left, # 0111, 7 # tile.eTileStates.tunnel_right, # 1000, 8 # tile.eTileStates.tunnel_up_right, # 1001, 9 # tile.eTileStates.tunnel_down_right, # 1010,10 # tile.eTileStates.tunnel_up_down_right, # 1011,11 # tile.eTileStates.tunnel_left_right, # 1100,12 # tile.eTileStates.tunnel_up_left_right, # 1101,13 # tile.eTileStates.tunnel_down_left_right, # 1110,14 # tile.eTileStates.tunnel_up_down_left_right, # 1111,15 # ][exit_map_value] if exit_map_value > 15: px_log.log( f"warning: exit map value boo boo {exit_map_value}" ) this_tile.components['controller'].setState( this_tile, tile.eTileStates.tunnel_no_exit + exit_map_value) else: if entity.map[y][x] == "B": if bunny_to_place < entity.game.getNumBunnies(): current_bunny_data = entity.game.getCurrentBunnyData( bunny_to_place) entity.bunnies.append( entity.game.requestNewEntity( current_bunny_data.handle, Vec3(x * 16 + 8, y * 16 + 8, 0), parent=entity, name=current_bunny_data.handle, data={ 'game_pad': entity.game.getGamePad( bunny_to_place), })) bunny_to_place += 1 elif entity.map[y][x] == "1": # entity.fox_starts.append(Fox(pos=Vec3(x * 16 + 8, y * 16 + 8, 0),type=fox.eFoxTypes.direct)) entity.game.requestNewEntity( 'fox', pos=Vec3(x * 16 + 8, y * 16 + 8, 0), parent=entity, name= 'fox 1', # todo: consider numbering per fox not just per type data={ 'type': fox.eFoxTypes.direct, }) elif entity.map[y][x] == "2": # entity.fox_starts.append(Fox(Vec3(x * 16 + 8, y * 16 + 8, 0),fox.eFoxTypes.axis_swap)) entity.game.requestNewEntity( 'fox', pos=Vec3(x * 16 + 8, y * 16 + 8, 0), parent=entity, name= 'fox 2', # todo: consider numbering per fox not just per type data={ 'type': fox.eFoxTypes.ahead, }) elif entity.map[y][x] == "3": # entity.fox_starts.append(Fox(Vec3(x * 16 + 8, y * 16 + 8, 0),fox.eFoxTypes.ahead)) entity.game.requestNewEntity( 'fox', pos=Vec3(x * 16 + 8, y * 16 + 8, 0), parent=entity, name= 'fox 3', # todo: consider numbering per fox not just per type data={ 'type': fox.eFoxTypes.axis_swap, }) elif entity.map[y][x] == "4": # entity.fox_starts.append(Fox(Vec3(x * 16 + 8, y * 16 + 8, 0),fox.eFoxTypes.cowardly)) entity.game.requestNewEntity( 'fox', pos=Vec3(x * 16 + 8, y * 16 + 8, 0), parent=entity, name= 'fox 4', # todo: consider numbering per fox not just per type data={ 'type': fox.eFoxTypes.cowardly, }) # blank space entity.num_spaces += 1 this_tile.setState(tile.eTileStates.path)