Light Lessons
Hello, loves!
With a reasonable design, reasonable changes are generally reasonably easy. So keep the design reasonable.
Anyone in the dungeon game business knows that the “right light thing” is ray-tracing, so that your torch only casts light on tiles that a straight ray of light can reach. Some corners and crevices won’t be illuminated until you get positioned correctly. That scheme also allows one to hide behind things, avoiding ranged attacks, etc.
Yesterday’s little experiment took a few lines of code to illuminate a room as soon as Dot enters it, and it leaves illuminated any room that has ever been illuminated. My Product Definition Personality (PDP she/her) wanted more of a discovery experience, not a full-on lighting experience. But even if they wanted ray-casting or shadows or whatever they’d call it, I might still do the simple thing we did yesterday.
Why? Because it would show progress, it would give us all a sense of the effect, and it would very likely take us to most of the places we’d have to visit with the full ray-casting feature, if it were to be done, but without the mental complexity of dealing with ray casting at the same time as adjusting the code to accept illumination.
Today, we have a fairly decent visual effect and a fairly decent implementation that could support something more sophisticated if we wanted it. No one knows at this point whether we’ll do ray casting or not. Depends on whether my Primary Personality (PP/Ron he/him) thinks it would be interesting.
The approach, however, is one that I often use. Sometimes we call it a Spike, sometimes it’s Wishful Thinking, sometimes it’s Cast a Line Over tis Chasm, sometimes it’s Hey, Hold My Chai, sometimes it’s just what we do that day. I used to call it Do The Simplest Thing That Could Possibly Work, but for some reason I’ve gone off that phrase.
But it’s always the same: find a simple short path from where we are to some sensible subset of what we need, get that in place, learn from it, then proceed.
Is that a lesson? I don’t know. It’s just what I do, because it seems to mostly work.
Next Steps
Today, I have an idea about the light. A couple of nights ago I dreamed up the notion of illuminating a room at a time, as we did yesterday. Last night, I dreamed up the idea of illuminating only part of the room, in a “radius” around Dot, a small radius normally, a larger one if she has the brilliant torch.
So let’s see how we might do that. We’ll have to review yesterday’s code, since I have no particular recollection of how we did it.
PyCharm happens to be open and this method is centered:
class DungeonView:
def illuminate(self):
room = self.dungeon.player_cell.room
view = self.room_views[room]
view.illuminate()
for content_view in self.content_views_by_room[room]:
content_view.illuminate()
Looks like we’ll be working below that method, but as a reminder, who’s sending it?
class DungeonView:
def on_draw(self):
self.clear()
self.scroll_dungeon_camera()
self.illuminate()
with self.dungeon_camera.activate():
self.room_sprite_list.draw()
self.draw_contents()
self.draw_passages()
with self.scroller_camera.activate():
self.scroller.draw()
Right, the drawing event. We could be more efficient about this but that’s for another day.
So how about the RoomView’s illuminate?
class RoomView:
def illuminate(self):
for sprite in self.sprites:
sprite.visible = True
Ah. That’s unfortunate. To deal with illumination around Dot, we’ll need to know the cell coordinates, not just the sprite.
We’ll have to do some work. But that’s not a bad thing. We build what we need, when we need it, and we try not to build things before we need them, because we (Ron) have a tendency to get speculative things, how shall we put it, Not Quite Right.
OK, how does this RoomView work? This is the bit we have to change:
class RoomView:
def __init__(self, dungeon, room):
self.dungeon = dungeon
self.layout = dungeon.layout
self.room = room
self.sprites = []
self.texture_finder = TextureFinder()
def create_floor(self, shape_list):
cells = set(self.room.cells)
for cell in cells:
self.choose_flooring(cell, shape_list)
def choose_flooring(self, cell, shape_list):
texture = self.choose_flooring_texture(cell)
sprite = self.make_adjusted_sprite(cell, texture)
self.sprites.append(sprite)
shape_list.append(sprite)
Let’s just build a dictionary from Cell to sprite, like this:
class RoomView:
def __init__(self, dungeon, room):
self.dungeon = dungeon
self.layout = dungeon.layout
self.room = room
self.sprites = []
self.cell_sprites = dict()
self.texture_finder = TextureFinder()
def choose_flooring(self, cell, shape_list):
texture = self.choose_flooring_texture(cell)
sprite = self.make_adjusted_sprite(cell, texture)
self.cell_sprites[cell] = sprite
self.sprites.append(sprite)
shape_list.append(sprite)
And now illuminate:
def illuminate(self):
dot = self.dungeon.player_cell
for cell, sprite in self.cell_sprites.items():
if dot.manhattan_distance(cell) < 4:
sprite.visible = True
I just picked 4 as the light range. We’ll deal with that later. Before I make any pictures or videos, let’s do the content. As things stand, all the content in the room illuminates at once. We need to make similar changes there.
class ContentView:
error_texture = ':resources:images/items/star.png'
def __init__(self, content_item, resources, scale=0.5):
self.item = content_item
self.sprite = Sprite()
for resource in resources:
try:
texture = arcade.load_texture(resource)
except (FileNotFoundError, AttributeError):
texture = arcade.load_texture(self.error_texture)
self.sprite.append_texture(texture)
self.sprite.set_texture(0)
self.sprite.visible = False
self.sprite.scale = scale_texture(self.sprite.textures[-1], scale)
def illuminate(self):
self.sprite.visible = True
Here, the content view doesn’t even know the cell. Where is it created? DungeonView, of course:
class DungeonView:
def create_content_lists(self):
for room in self.dungeon.rooms:
self.content_views_by_room[room] = []
self.content_views = dict()
self.content_sprite_list = arcade.SpriteList()
for cell, content in self.dungeon.contents.items():
for item in content:
resources = item.resources
scale = item.scale
view = ContentView(item, resources, scale)
view.sprite.position = cell.center_position(cell_size)
self.content_views[item] = view
self.content_views_by_room[cell.room].append(view)
self.content_sprite_list.append(view.sprite)
Change signature to provide the cell. We’re going to need access to the dungeon as well, but let’s try something else. First change signature:
...
for item in content:
resources = item.resources
scale = item.scale
view = ContentView(cell, item, resources, scale) # CHANGED
view.sprite.position = cell.center_position(cell_size)
self.content_views[item] = view
self.content_views_by_room[cell.room].append(view)
self.content_sprite_list.append(view.sprite)
And let’s pass some details to illuminate:
class DungeonView:
def illuminate(self):
dot = self.dungeon.player_cell
room = dot.room
view = self.room_views[room]
view.illuminate()
for content_view in self.content_views_by_room[room]:
content_view.illuminate(dot, range=4)
class ContentView:
def illuminate(self, dot, range):
if dot.manhattan_distance(self.cell) < range:
self.sprite.visible = True
I think that should do the trick. And it does:
Reflection
I noticed that the videos seem kind of jerky. When actually playing the game, that jerkiness isn’t so noticeable. I think it has to do with how one focuses during play. Interesting phenomenon for sure. Not sure what, if anything, to do about it.
A couple of very simple changes, and now Dot can only see what she has visited before, and in a small area around her. Clearly we can arrange to increase the range if she has the brilliant torch, etc etc.
We turned up at least one “design error” today, the fact that the ContentView didn’t have the cell. But until today it didn’t NEED the cell, and providing it was one Change Signature away. So was that an Egregious Design Error, or a Triumph of Lean Design? You decide. To me it’s just a standard thing, discover that we need something, and provide it. If it is difficult to provide, that’s time to look at the design in detail, as our needs have deviated far enough from what they were to suggest that some revisions would be useful.
Overall, I find the illumination effect to be pretty reasonable. Rarely, it will illuminate something that is clearly not in Dot’s view, but even then it’s not troubling. When we increase the light range, we might want to revisit whether we need something better. And we are in an improved if not perfect position to do ray-casting now, if we need to.
I’m going to commit this as it is: illumination limited to distance 3 around Dot But that’s not to say that this is perfect even for what we have. We need to pull the desired range up to Dungeon, which will know what Dot has and holds, and we need to pass the range down to the RoomView as we did to the ContentView. We’ll probably find some other improvements as well, when we go looking. That will be in some future session. We’ve done what we set out to do here today.
Summary
Something like:
With a reasonable design, reasonable changes are generally reasonably easy. So keep the design reasonable.
See you next time!