This blog is about video games and the artistic development process behind them. It will focus on my work and growth as an artist in the game development pipeline. The majority of content will be more "technical Art", modeling, texturing, and maybe rigging. There should also be a good amount of traditional art, and cake
Tuesday, December 28, 2021
Python Projects Table of Contents
Python: Wallpaper-scroller - Entry 1 - Rough Gif Support
Python Side Project
-Wallpaper Scroller-
I am still working on my RPG inventory system. But I've been too tired to think about it. So I was dinking around with a wallpaper scrolling script.
Here is a video of it working.
I show the gif playing function and the base function to set the wallpaper image. Gifs do not play true to duration / fps. Since the OS method I call to set the background appears to have a limit low tolerance for quick changes. A gif I tested at with 79 frames and a duration of 50 milliseconds didn't play nice with the script.
Brief:
- support for higher frame rate gifs
- altering stretch, tile, fit settings
- add a tkinter tray interface
Code Snippet:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 | import os # To create folders and save gif frames import ctypes # To set the windows background image import glob # To pull file contents of a directory from PIL import Image # To parse gif data def import_images( direct:str, fext: [ str ] ) -> { str : [ str ] }: """Takes a directory and a list of files with extension to include. Returns a dictionary with the extension as a key and a array of image names.""" """Prime the dictionary with the { key(extension) : value(empty array) } """ img_dict = { } for extension in fext: img_dict.update( { extension : [] } ) """Use glob() to pull in all files of extension at path, clean the name string and append it to appropriate key : array.""" for extension in fext: for filename in glob.glob( f'{ direct }\\*.{ extension }' ): file = filename.replace( f"{ direct }\\", "" ) img_dict[extension].append( file ) return img_dict def set_background( path:str ) -> None: """Takes in a absolute path and call the OS to set background image.""" ctypes.windll.user32.SystemParametersInfoW( 20, # System wallpaper index 0, # Buffer f"{ path }", # File Absolute Path 0 # Final Buffer ) def unpack_gif( directory:str, image:str, temp:str="temp_frames" ) -> None: """Takes in a directory path, an image name.gif, and a new temp folder name. Then parses out the gif's frames into the temp folder at directory. Once a valid path is available set that background to path.""" image_path = os.path.join(directory, image) with Image.open( image_path ) as im: # Open the gif with pillow temp_folder = temp # Name of temporary gif frames folder folder_path = os.path.join(directory, temp_folder) try: os.mkdir( folder_path ) # Create the temporary folder to hold the gifs frames except: pass frame_duration = im.info['duration'] # Get gif run length in milliseconds img_fps = ( ( frame_duration / im.n_frames ) / 1000 ) # Get the time per frame in milliseconds and convert it to seconds. for frame in range(im.n_frames): # Get the number of frames in the gif im.seek( frame ) # The current frame in for loop 0 - end_frame name = f'temp_gif_{frame}.png' # Create a name for the frame image path = os.path.join(folder_path, name) im.save( path, 'GIF' ) # Save the frame image to the temporary folder set_background( path ) # Now we have a path to set the background, call the set function time.sleep( img_fps * 10 ) # slow down to not congest the wallpaper assignment |
credits and resources:
<!-- https://stackoverflow.com/questions/679189/formatting-code-snippets-for-blogging-on-blogger --!>
<!-- --!>
<!-- http://www.craftyfella.com/2010/01/syntax-highlighting-with-blogger-engine.html --!>
<!-- https://gogopdf.com/blog/Alex-Gorbatchev-And-gogopdf-Turn-Raw-Javascript-To-A-PDF-Service --!>
-END-
Friday, December 24, 2021
Python: Text RPG - Entry 2.1 - Inventory
Python Text RPG
-Inventory-
In today's sprint I was changing gears to let some ideas ruminate about the Maps and organization problem. So I started tackling items and an inventory system. You can see the use example at the bottom of the article.
Sprint method: For my sprints I set a block of time to work and when it expires that's what I have for the day.
class Item():
# class variable:gentype = "consumable"
subtype = "potion"
variant_type = "elixir"
name_ = "elixir of healing"
desc = "restores () health over () seconds"
value = 5 #recomended value; value assigned per shop
stack_size = 1
quantity = 1
weight_class = 0#, 1, 2, 3, 4 # none, light, medium, heavy, very heavy
visable = True
def __init__( self, name:str, qty:int, stack:int=1, **keywargs ):
# instance variable:
# Info
self.gentype = "general category".lower()
self.subtype = "local category".lower()
self.variant_type = "specific category".lower()
self.name_ = str( name ).lower()
self.description = "does things".lower()
# Inventory
self.stack_size = stack
self.quantity = qty if qty <= stack else stack
self.weight_class = 0#, 1, 2, 3, 4 # none, light, medium, heavy, very heavy
# Shop
self.value = 5 #recomended value; value assigned per shop
self.visable = True
def __str__( self ):
return f"{ self.name().title() }: { self.qty()[0] }/{ self.stack() }"
def __repr__( self ):
return f"{ self.name() }: { self.qty()[0] }/{ self.stack() }"
# Public facing calls to set and get object string variables.
def gen_type( self, new_gentype:str=None ) -> str:
return self._set_string( "gentype", new_gentype )
def sub_type( self, new_subtype:str=None ) -> str:
return self._set_string( "subtype", new_subtype )
def type( self, new_type:str=None ) -> str:
return self._set_string( "variant_type", new_type )
def name( self, new_name:str=None ) -> str:
return self._set_string( "name_", new_name )
def desc( self, new_desc:str=None ) -> str:
return self._set_string( "description", new_desc )
# The utility function for setting string variables
def _set_string( self, attr, string ):
if string:
setattr(self, attr, str( string ).lower() )
return getattr( self, attr )
# Public facing calls to set and get object boolean variables.
def show( self, new_state=None ):
return self._toggle_bool( "visable", new_state )
# The utility function for setting boolean variables
def _toggle_bool( self, attr, state ):
if None != state:
setattr( self, attr, state )
return getattr( self, attr )
# Public facing calls to set and get object numeric variables.
def price( self, new_price:int=None ) -> int:
return self._set_digit( "value", new_price )
def weight( self, new_weight:int=None ) -> int:
return self._set_digit( "weight_class", new_weight )
def stack( self, new_stack:int=None ) -> int:
return self._set_digit( "stack_size", new_stack )
def qty( self, new_qty:int=None ) -> ( int, int ):
remainder = 0
if not new_qty: pass
elif 1 < ( new_qty / self.stack() ):
remainder = new_qty - self.stack()
new_qty = self.stack()
return self._set_digit( "quantity", new_qty ), remainder
# The utility function for setting numeric variables
def _set_digit( self, attr:str, number:int ) -> int:
if str( number ).isdigit():
setattr( self, attr, number )
return getattr( self, attr )
# Get the current capacity for the item stack
def capacity( self ) -> int:
out = self.stack_size - self.quantity
return out if out <= self.stack_size else 0
class Container():
items = []
size = 3
# width x height ala diablo
def __init__( self, size:int ):
self.items = []
self.size = size
def item( self, item:Item=None ) -> Item or [ Item ]:
"""Takes in an item; If successful returns added item. If failed returns held items."""
out = self.items
if item:
self._add_item( item )
out = item
return out
def remove_item( self, item:Item ) -> bool:
out = False
if 1 not in self._validate_insert( item ):
self.items.pop( self.items.index( item ) )
out = True
return out
def _full( self ) -> int:
return self.size - len( self.items )
def _contains_obj( self, item:Item ) -> bool:
return item in self.items
def _contains_name( self, item:Item ) -> bool:
return item.name() in ''.join( str( self.items ) )
def _validate_insert( self, item:Item ) -> { int, str }:
"""returns a dictionary of { outcome_code : note, } "0, and -4 to -6" are invalid insertions. -2 to -3 denote the insertion is a duplicate. Return value of 1 indicates item is not present."""
out = { }
if not item:
out.update( { 0 : "empty argument" } )
if Item is not type(item):
out.update( { -6 : "not an item" } )
if 0 >= self._full():
out.update( { -5 : "inventory full" } )
if 0 >= item.stack():
out.update( { -4 : "empty item stack" } )
if self._contains_obj( item ):
out.update( { -3 : "item duplicate" } )
if self._contains_name( item ):
out.update( { -2 : "name duplicate" } )
else:
out.update( { 1 : 'valid insert' } )
return out
def _add_item( self, item:Item ) -> bool:
out = False
if 1 == list( self._validate_insert( item ) )[0]:
self.items.append( item )
out = True
return out
def _item_to_index( self, item:Item ) -> [ int ] or None:
out = None
catch = self._validate_insert( item )
if -3 in catch:
# Index of objects matches
out = self.items.index( i )
if -2 in catch:
# Index of Items with name matches
out = [self.items.index( i ) for i in self.items if item.name() == i.name() ]
return out
def _index_to_item( self, index:int) -> Item:
return self.items[ index ]
def _qty( self, item:Item, new_qty:int=None ) -> Item:
out = item
catch = self._validate_insert( item )
if -2 in catch:
# Case: item name exists in list
index = self._item_to_index( item )[0]
held_item = self._index_to_item(index)
# add the current qty to the new qty and return the remainder
updated_stock = held_item.qty( held_item.qty()[0] + item.qty()[0] )[1]
catch = item.qty( updated_stock )
out = item
return out
# USE EXAMPLE:
# Change the bag size to exclude items below
inv = Container( 6 )
# Try it yourself: Item( name, qty, stack_size )
# 'Pickup' various items until inventory is full
inv.item( Item( "health potion", 1 ) ) # 1
inv.item( Item( "raw steak flank", 1 ) ) # 2
inv.item( Item( "rusty sword", 1 ) ) # 3
inv.item( Item( "mirror shield", 1 ) ) # 4
inv.item( Item( "iron greaves", 1 ) ) # 5
ps = Item( "platinum sword", 1, 4 ) #6
inv.item( ps )
#---- Inventory at capacity
inv.item( Item( "obsidian helm", 1 ) ) #7
inv.item( Item( "golden armor", 1 ) ) #8
print( inv.item( ) )
print()
# Use Scenario: top off an item you already have from one in a locker
world_item = Item( "platinum sword", 4, 4 ) # Item in the locker
# world_item = inv._qty( world_item ) # finding similar item in inventory and stacking to full
print( inv._qty( world_item ) ) # show new qty for locker item
print( inv.item( ) )
-END-
Thursday, December 23, 2021
Python: Text RPG - Entry 1.1 - Map Movement Trial
Python Text RPG
-Map Movement-
Was trying out a different solution for controlling the connections and organization of locations in the game. I got some ideas from this trial. But! The solution requires really heavy lifting and is very verbose. Neither of which I'm fond of dealing with.
I'm going for clean and easy to implement.
Things that worked:
I did appreciate having more explicit function calls, but also I'm not a fan of having a bunch of them because I'm lazy. But in the long run having a catch all function like 'children' could be bad idea for separation of responsibilities. I also liked the look and readability of the type hinting in the function signature.
I like the idea that the game manager knows about all the locations in the game and they are not nested in multiple layers of location object. But that also presents a name space difficulty. I can't have two northshire abbey's. One in Northshire, and one in Westfall. It also makes pretenses a data grouping difficulty. How does the game_manager know that 'floors_0-4' belong to the lions pride inn in goldshire and not some other structure or map? if I get into explicit naming like expansion_lordaron_goldshire_lions_pride_Inn_floor_1, then it is a pain if I change or move something.
Things that didn't work:
Too explicite for connections I want to take groupings of data for granted.
I didn't think through the granularity and relationships between. They currently only work at the 'zone' category level. Anything 'below' that and the child relationships are broken. A fixable logic error but this day sprint is out of time.
Things I would do differently:
define everything important all at once Location(zone type, name, visible, and lock)
I like the simplicity of everything be a general area 'zone' or a specific facility or place 'site' and nothing else. Elwynn Forest is a zone, goldshire is a site. Deadmine in Westfall would also be a site
_______________________________________________________________
# Stub
class Container(): pass
# Stub
class Grid(): pass
_______________________________________________________________
class Location(): pass # Ugly code for Location parameter signature
class Location():
# Initialize class variables
name_ = ""
notes = ""
locked = True
# sublocations = [] # TBD: Should the Game_Manager own this data or the parent Location?
play_grid = None
contents = None
options = []
intro = ""
def __init__( self, name, locked, notes="", intro="", options=None, play_grid:Grid=None, contents:Container=None ) -> None:
# Initialize object's variables
self.name_ = name
self.notes = notes
self.locked = locked
# self.sublocations = sublocations if sublocations else [] # TBD
self.play_grid = play_grid
self.contents = contents
self.options = options
self.intro = intro
def __str__( self ):
return f"{self.name_}"
def __repr__( self ):
# TODO: make a list comprehension that spits out a formatted string of all object variables.
return f"<{self.name_}: {self.locked}, {self.play_grid}, {self.contents} >"
def set_intro( self, msg ):
self.intro = str( msg )
def fetch_intro( self ):
return self.intro
def set_options( self, *options ):
self.options = [ str( o ) for o in options ]
def fetch_options( self ):
return self.options
_______________________________________________________________
class Game_Manger():
previous_location = None
current_location = None
locations = []
travel_paths = []
available = [] # optimization? or iterate through locations.locked.
location_type = { "zone" : [], "subzone" : [], "site" : [], "section" : [] }
# TBD this is a heavy lifting solution
# Create Locations
def register_location( self, location:Location ) -> Location:
"""Appends the provided location to the games locations and create a travel_paths array"""
if location not in self.locations and location.name_ not in ''.join( str( self.locations ) ):
self.locations.append( location )
self.travel_paths.append( [] )
self.update_travel_locations()
return location
def register_location_to_category( self, location:Location, category:str ) -> None:
"""Appends the provided location to the specified category key for indexing"""
# Should be more explicit? add_zone, add_subzone?
if str(category).lower() in self.location_type:
if location not in self.location_type[ category ] or location.name_ not in ''.join( str( self.location_type[ category ] ) ):
self.location_type[category].append( location )
# Create Connections between Locations
def register_path_to_locations( self, parent:Location, *children:Location ) -> [ Location ]:
"""Returns a list of children added to the parents travel_paths list"""
catch = []
index = self.locations.index( parent )
for child in children:
if child not in self.travel_paths[ index ] or child.name_ not in ''.join( str( self.travel_paths[ index ] ) ):
self.travel_paths[index].append( child ) # Todo: Store index numbers rather than object reference?
catch.append( child )
return catch
# Enable travel to a location
def unlock_location( self, location:Location ) -> bool:
"""Unlock the provided location and add it to the available"""
self.location.locked = False
self.available.append( location )
return location.locked
def lock_location( self, location:Location ) -> bool:
"""Lock the provided location and remove it from the available"""
self.location.locked = True
self.available.remove( self.available.index( location ) )
return location.locked
def fetch_category( self, locale:Location ) -> str:
for category in self.location_type.keys():
if locale in self.location_type[category]:
return category
def fetch_index_to_location( self, index:int ) -> Location:
return self.locations[int(index)]
def fetch_location_to_index( self, locale ) -> int:
return self.locations.index(locale)
def update_travel_locations( self ) -> None:
self.available = [ (idx, locale) for idx, locale in enumerate(self.locations) if locale.locked == False ]
def travel_locations( self ) -> [ Location ]:
"""Get list of unlocked locations"""
return self.available
def travel_neighbors( self, locale:Location ) -> [ Location ]:
return [ p for p in self.travel_paths[ self.fetch_location_to_index( locale ) ] if p in self.location_type[ self.fetch_category( locale ) ] ]
def travel_children( self, locale:Location ) -> [ Location ]:
return [ p for p in self.travel_paths[ self.fetch_location_to_index( locale ) ] if p in self.location_type[ 'subzone' ] or p in self.location_type[ 'site' ] or p in self.location_type[ 'section' ] ]
def fetch_current_location( self ) -> Location:
return self.current_location
def travel_to_location( self, new_location:Location ) -> Location:
"""Travel from current location to the provided one"""
self.previous_location = self.current_location
self.current_location = new_location
return self.current_location
def travel_return_to_previous( self ) -> Location:
"""Travel from the current location to the previous one"""
self.current_location = self.previous_location
return self.current_location
Use Example:
gm = Game_Manger()
zo1 = gm.register_location( Location( "Stormwind", False, "Human capital" ) )
gm.register_location_to_category( zo1, "zone" )
zo2 = gm.register_location( Location( "Westfall", False, "low level grassland farms" ) )
gm.register_location_to_category( zo2, "zone" )
zo3 = gm.register_location( Location( "Duskwood", False, "mid level dangerous woods" ) )
gm.register_location_to_category( zo3, "zone" )
zo4 = gm.register_location( Location( "Redridge Mountains", False, "mid level mountains" ) )
gm.register_location_to_category( zo4,"zone" )
zo5 = gm.register_location( Location( "Burning Steppes", False, "high level lava flows" ) )
gm.register_location_to_category( zo5, "zone" )
zo6 = gm.register_location( Location( "Elwynn Forest", False, "Lowbie starting region" ) )
gm.register_location_to_category( zo6, "zone" )
a1 = gm.register_location( Location( "Northshire Valley", False, "Welcome and start of new game tutorial" ) )
gm.register_location_to_category( a1, "subzone" )
a2 = gm.register_location( Location( "Goldshire", False, "first lowbie town" ) )
gm.register_location_to_category( a2, "subzone" )
gm.register_path_to_locations( zo6, a1, a2 )
gm.register_path_to_locations( zo6, zo1, zo2, zo3, zo4, zo5 )
print( "CHILDREN:", zo6, gm.travel_children( zo6 ) )
print( "NEIGHBORS:", zo6, gm.travel_neighbors( zo6 ) )
print()
b2 = gm.register_location( Location( "Echo Ridge Mine", False, "The first dungeon" ) )
gm.register_location_to_category( b2, "site" )
c3 = gm.register_location( Location( "Northshire vineyards", False, "A small grape farm" ) )
gm.register_location_to_category( c3, "site" )
d4 = gm.register_location( Location( "Northshire river", False, "a river cutting through the northshire zone" ) )
gm.register_location_to_category( d4, "site" )
b = gm.register_location( Location( "Northshire Abbey", False, "The building" ) )
gm.register_location_to_category( b, "site" )
gm.register_path_to_locations(a1, a2)
gm.register_path_to_locations(a1, b, b2, c3, d4)
print( "CHILDREN:", a1, gm.travel_children(a1) )
print( "NEIGHBORS:", a1, gm.travel_neighbors(a1) )
print()
c = gm.register_location( Location( "Northshire Abbey Exit", False, "General starting spot" ) )
gm.register_location_to_category( c, "section" )
d = gm.register_location( Location( "Main Hall", False, "zone exit > trainers and quest givers" ) )
gm.register_location_to_category( d, "section" )
e = gm.register_location( Location( "Hall of Arms", False, "Get more quests" ) )
gm.register_location_to_category( e, "section" )
f = gm.register_location( Location( "Library Wing", False, "Get training" ) )
gm.register_location_to_category( f, "section" )
gm.register_path_to_locations( a1, c, d, e, f )
print( "CHILDREN:", a1, gm.travel_children(a1) )
print( "NEIGHBORS:", a1, gm.travel_neighbors(a1) )
-END-
Sunday, December 19, 2021
Python: Text RPG - Entry 1 - Map Grid and Movement
Python Text RPG
-Map Grid and Movement-
SIMPLE DEMO:
Monday, December 13, 2021
Python: Text RPG - Intro
Python Text RPG
-An Adventure-
This will be an ongoing series for a pet project. Feel free to follow along!
If you have any feedback, suggestions, or requests please leave me a message.
About The Author:
I am not new to Python or game development but I am far from an expert. So I will be spending time researching and making mistakes as I develop this project. If you are an advanced+ python user I doubt you will get much from following the progress of this project. If you are brand new or have dabbled a bit but never built anything then this may be of interest to you.
About The Project:
This will be a text based Python 3.9+ adventure game in the command console. My current approach is to start with a brute force monolith. Meaning a linear all in one file mess of code. Once the base is ready and all the features are established and implemented. I'll start breaking the parts out into their own classes and get more into an object oriented approach.
If this sounds interesting please continue on to part 1!
Documentation Conventions:
At the top of each page I'll have a "updated" string to keep track of things I've touched. As well as a "status" to denote if I plan to work on it any time soon.
Example:
Updated: 2021/12/11 : 01:03
Status: Work In Progress
At the Top of each page will be a forward and back button that will simply navigate you to the next or previous section of the course.I expect the documentation to be a little haphazard as I figure out how I'm going to organize. I do apologize for the mess!
All headers will end with a colon(:), Each Section: will be bold-underlined, Main Groups: will be bold, and sub-headings: will be underline.