I Done Did It
During a boring on-line meeting, I went ahead and put in the visibility logic. Let’s see how things look now.
As I think I mentioned yesterday, one of the issues with my first cut at the AppearingContent was that it initialized on being placed, and that the Dungeon places things before the DungeonView even exists, so the view wasn’t even there yet, much less subscribed to the necessary message.
So I changed the rules a bit. The dungeon still places things in its own memory as they are defined. Then, when the DungeonView is ready to run the game, it calls Dungeon.run, which in turn calls item.run for every item in the dungeon. Most of them are indifferent to this call, but AppearingContent takes the occasion to initialize itself to invisible, publishing the necessary event. So the code is like this:
class DungeonView(arcade.View):
def __init__(self, dungeon, testing=False):
if not testing:
super().__init__()
self.dungeon = dungeon
self.subscribe_to_remove(dungeon)
self.subscribe_to_visibility(dungeon)
... more init stuff
def subscribe_to_visibility(self, dungeon):
def callback(event, item, a_boolean):
try:
view = self.content_views.get(item)
view.set_visibility(a_boolean)
except KeyError:
pass
dungeon.subscribe('item_visibility', callback)
def run(self):
self.setup()
self.dungeon.run()
self.window.show_view(self)
arcade.run()
class Dungeon:
def run(self):
for content in self.contents.values():
for item in content:
item.run(self)
class AppearingContent:
def run(self, dungeon):
self.set_visibility(dungeon, False)
def callback(event):
dungeon.announce(f'{self.name} has appeared!')
self.set_visibility(dungeon, True)
dungeon.subscribe_once(self.name + ' button_pressed', callback)
class ContentView:
error_texture = ':resources:images/items/star.png'
def __init__(self, content_item, resource, scale=0.5):
self.item = content_item
try:
texture = arcade.load_texture(resource)
except (FileNotFoundError, AttributeError):
texture = arcade.load_texture(self.error_texture)
self.sprite = Sprite(texture)
self.sprite.scale = scale_texture(self.sprite.texture, scale)
def set_visibility(self, a_boolean):
self.sprite.visible = a_boolean
All that happens, just so that we can set sprite.visible. But I believe it’s the right way to do things:
-
The only model-view connection is via PubSub events.
DungeonView subscribes to events from Dungeon.
ContentView subscribes to events from content. -
Dungeon knows content; content does not know dungeon.
DungeonView knows Dungeon; Dungeon does not know the view.
ContentViews know their content; content does not know the view. -
Content can be passed a pointer to the dungeon when needed, such as to publish or subscribe.
In short, I think all the connections are righteous, no pointers from child to parent, no pointers from model to view.
Other tidying transpired as well.
The abstract class for Content now includes only two methods, interact_with_player, and run. Three of the four concrete classes just ignore run. Only two implement interact_with_player. One, ApperaingContent, has an additional method for its own convenience:
class AppearingContent:
def __init__(self, name, resource, scale=0.5):
self.name = name
self.resource = resource
self.scale = scale
def interact_with_player(self, dungeon):
if self.visible:
dungeon.receive_item(self)
dungeon.announce(f'You have received {self.name}!')
def run(self, dungeon):
self.set_visibility(dungeon, False)
def callback(event):
dungeon.announce(f'{self.name} has appeared!')
self.set_visibility(dungeon, True)
dungeon.subscribe_once(self.name + ' button_pressed', callback)
def set_visibility(self, dungeon, a_boolean):
dungeon.publish('item_visibility', self, a_boolean)
self.visible = a_boolean
There is only one caller of set_visibility, so it could be inlined, if one were so inclined. One is not.
The interact_with_player method is disturbingly similar to the one in ReceivableContent, which just lacks the if statement. I think we could apply Strategy or Method Object and reduce the number of concrete classes, probably all the way down to just one, with pluggable methods for both interact and run. Whether that would be more clear is doubtful, but there would be fewer classes. Maybe we’ll try that one of these days.
Musing …
There are still things not to like. While the AppearingContent object is a bit more general now, allowing for more than one kind of thing appearing, it’s not really as general as it might be.
Views of content are going to need more than one texture. Our next move might be to add a down state to the button. The Button itself is too specialized, probably calling for some kind of multi-state content. Presumably the content would announce its state via PubSub, as is the new convention, and the view would select the corresponding image.
There is work to be done on image assets in general. As things stand, we associate the resource with the content item, and the view looks at the item to get what it needs:
main.py
item = DrawableContent("skel", my_resources + 'Skeleton2.png', scale=0.5)
dungeon.place_content_at(Cell(30, 26), item)
class DungeonView:
def create_content_lists(self):
self.content_views = dict()
self.content_sprite_list = arcade.SpriteList()
for cell, content in self.dungeon.contents.items():
for item in content:
resource = item.resource
scale = item.scale
view = ContentView(item, resource, scale)
view.sprite.position = cell.center_position(cell_size)
self.content_views[item] = view
self.content_sprite_list.append(view.sprite)
class ContentView:
error_texture = ':resources:images/items/star.png'
def __init__(self, content_item, resource, scale=0.5):
self.item = content_item
try:
texture = arcade.load_texture(resource)
except (FileNotFoundError, AttributeError):
texture = arcade.load_texture(self.error_texture)
self.sprite = Sprite(texture)
self.sprite.scale = scale_texture(self.sprite.texture, scale)
I think we need some kind of asset directory that is keyed by a name that the content knows, and when the views can look up the asset there. We’ll need to support multiple textures. I’m thinking that an initial simple approach would be just to return a list of resources and have a general PubSub message about content state(0-N), which the view will use to select and set the texture.
We’ll do Button by hand, and that should give us some understanding and will probably get at least some of the structure in place.
And that’s my report. See you next time!