Eschaton: 7DRL Development Process - Part 2
Eschaton: noun, the final event in the divine plan; the end of the world.
Last time I talked about approaching the 7DRL and creating this post apocalyptic ASCII roguelike, Eschaton. This time, I want to just quickly document some of the development process, and where I got to at the end of the 7 days.
Classes
I told myself I'd keep as much code in pure C# as possible - essentially sitting in a .dll within Unity, and the monobehaviours in the engine itself being relatively thin frontends that just present the data to Unity's renderer (passing it to PhiOS) and collecting input from Unity's input system (the legacy system will be fine). So I've already got my own GridPosition (similar to Unity's Vector2), a class for random numbers, an RGB class and operators, and then code for the game - GridRect, GridMap, and GridEntity - the latter being the base representation for 'game things' like items, monsters, and the player - which can go in the positions of the GridMap.
But I also knew that most likely things could end up in the same position, piled up on the same square - for instance if you come across a pile of items, or possibly a slain monster's corpse and all its items dumped on the same tile. I'm going to need to be able to store piles of things - which gave birth to the GridLayeredMap, which at each location stores a list of entities on that spot. And needed a way to remember which locations on a map have changed since last time they were checked - so we have the GridRenderableCollection, which can be checked to see if it is 'Dirty', provide a list of GridPositions that have changed (via the risque method GetDirtyPositions()), and clear the changes via Clean(). This meant that each turn I could just render the changes to the GridMap with something like:
foreach (GridPosition pos in map.GetDirtyPositions() ) { diaplay.Render( pos, map.At( pos ).GetRenderData() ); }
Which brings me to RenderData - this was a small class which is attached to anything which is renderable on the grid, called GridRenderables. RenderData just stores the foreground and background RGB data, and the character to render. Every GridEntity has one, so you can call GetRenderData on a GridEntity to pass its RenderData on to the display class (back in Unityland). And GridMap.At() returns the topmost GridEntity at an x, y GridPosition. So it's all rather neat and tidy.
Except that it's absolutely not.
Clean Code?
You see, I also told myself that on this project I'd try to keep my code clean, use SOLID principles etc. I mostly did this - except that I was going fast. Seven days is not a lot of time and I was also working a full time job, so I'm grabbing an hour or two here and there to work on this project when I can. What I am doing though, is trying to make sure that there are good interfaces for classes so that I can swap out the concrete implementations for things later if I need to, or at least keep the architecture flexible. So GridEntitys implement the IGridEntity and IGridRenderable classes. LayeredGridMaps implement the IGridMap and IRenderableCollection interfaces. It starts off fine, and things feel neat and organised.
Here's IGridRenderableCollection. Now, how it turned out isn't perfect, but it does have its utility - a renderable collection was used as the basis for maps of entities in the game (GridLayeredMap and GridMap), as well as UI stuff. The StringMessage was my first attempt at some messages at the bottom of the screen. Initially this was just one line, but later I extended it so that you could have message history. The other branch of the family tree was when I started trying to organise UI a bit more for the Info Bar. This was something I realised I'd need to show the player information about their surroundings, their inventory, and their equipment.
And then you add a weird thing and you need to fudge the interfaces a little, or introduce a new interface, or do some suspect inheritance - not because it's needed, but because you don't have time to think it through. On the entity side of the family tree - the IGridRenderable items that can occupy GridMaps and StringMessages,...well, I may have let it get away from me:
It's not as bad as it looks - a lot of the stuff at the bottom of the diagram is stuff I made for the game which derives from the early interfaces and classes, things like Ground and Grass, items like Bread and Candle and Knife, and some different types of wall. Very exciting. And you can see the beginnings of RLActor - the things that can act in this game. But you can also see that this is kind of...complex. Maybe needlessly so - maybe not - but definitely not fun to work with. I often found myself forgetting which interface held which function(s), which may be partly bad naming, and partly a violation of the Single Responsibility Principle (perhaps some of my objects are doing too much?). So it was unwieldy, but I feel it worked. It worked because I had enough abstraction to be able to start putting lighting into the game:
Because I had different kinds of renderable collections - maps - and because they could track what was dirty and clean, and because I had RGB values I could multiply together, I could create a composite collections of RenderData containing lighting information and render that to the screen. But my guys needed to be able to do stuff.
Action
I made an ActionQueue - characters wanting to act could submit an action to the queue, and the queue could order itself by the 'speed of actions' if required. It could then run through and process all the actions - say, the player's action as well as the monsters move/attack actions. So when the player moves there's a simple pre-test to make sure it can move into a valid spot (not a wall, not off screen). Then we test if the spot you tried to move to was a monster: if so. you're attacking it, not moving. Once we have a valid Action for the player it's submitted to the ActionQueue, and then all the monsters get a chance to consider actions and submit them (I use the term monsters for anything that isn't the player). Actually they're all Actors - in that they derive from the RLActor class - entities that can submit actions. The actors in the game all have inventories and equipment too. So in theory, I can actually switch my 'controlled entity' to any RLActor. That is, I should be able to control any monster in the game, use its items, etc. I haven't actually tested this, but perhaps down the track I will! I made a quick stress test for my action system, making sure that lots of entities could move around at once.
Interface
I needed to work a bit on the interface for the game - now that we have Inventory and Equipment, I needed to show them. And in a similar vein to Brogue, I wanted to show items / actors that are close to the player in the UI so that I can see what is interactable or interesting. So I created the InfoBar mentioned earlier. Additionally you could now pick up items and put them in your inventory, but for simplicity of user interface it would just pick up the top item from the tile you were on. I wasn't entirely lazy - I implemented the two step process for dropping items, where if you're holding multiple items you get a second prompt to ask for the number of the item you want to drop, as displayed in your inventory. There was nothing but time stopping me from applying this to the GET command as well (the inconsistency is annoying), but time was fast running out. I hadn't even implemented combat yet: if you walk into a monster, the message console just says 'Uh Oh it was an actor'
Combat
At this stage I'm in the last few days of the 7DRL, and there's no way I'm going to finish. I've got placeholder items and enemies - although I will say all the items had values for being able to do damage or how much they'd feed you. So you could attack with the candle, or try to eat the knife, if you wanted. Except you couldn't use anything - I had run out of time to implement it. You also couldn't equip anything: I had the inventory, but the equipment code was pretty much a class with a bunch of empty methods (but of course, it had a nice IEquipment interface defined!). I agreed with Beetlefeet (who made the excellent Pepperwood Golf and finished within 7 days) that for it to be a game you at least had to be able to die, so I'd better implement combat. I was pretty much on my last day at this point, and I probably wasn't going to have enough time to implement Equipment, so I implemented some very basic combat - you walk into an enemy, you attack with your bare hands (which is an item that just gets made on the fly for you, when you attack). The monster has a normal behaviour of just walking around but if you attack it, it hates you (yes, there's an IActorDisposition interface - hit a guy and your reputation with him goes down a whole lot!). At this point, the monster will just attack you every turn until one of you dies - I didn't have time for anything more complex. The bug is, to save time the monster just assumes that you're next to it: so even if the player moves away, the monster is still hitting you. These are the pitfalls of developing games in 7 days.
It's done. Ship it.
So I did. It was important to me throughout the project that I could run this in a browser - so you can play it on the Eschaton itch.io page. It's very possible I'll be uploading more versions, so I might make the humble webgl010.zip available for download - for posterity!
One Last Thing
It was nagging me that the lights were just lighting up areas wholesale on every cell - they didn't get blocked by vision blocking entities, for example. And I did have a pretty sweet system in place for storing properties of entities that are on a tile - in a bitfield that I could query with a nice set of functions. So I really needed to put that to use so the lighting could check for props.Isnt( EntityPropertiesType.BLOCKSVISION ).
Enter Bresenham's Algorithm. I took my light rendering code (which incidentally just presents an enumerator of GridPositions within its circle) and ran each point through a Bresenham's algorithm to see if it got blocked by one of those BLOCKSVISION properties on a cell. If it didn't, render light there. If it did, stop rendering light there. And so we ended up with a posthumous version of the game which respects lines of sight. I haven't uploaded it - lights aren't dynamic (so they can't move) and I'd rather refactor the whole project before I do another release. But here's a screenshot.
The End?
And so ended my 7DRL development experience. It's not a full game - in fact I'd say at this point I'm close to maybe having something I could use as the basis for a game the next time 7DRL rolls around. I didn't even upload the game as a submission for 7DRL - the submission criteria is that the game you make is finished and playable. This game is not finished and barely playable.
But that's ok. I see enough here to go ahead with that refactor, sort out that tangle of interfaces, fix some of the things I wish I could have fixed as I was coding (I made a list!), and continue on with the project to make something more robust and easily extensible for future games. I'd like to see where this game goes too - what can we do with a post apocalyptic world with food shortages, radioactive items, mutations, fire, death, and destruction? I've achieved some of the things I wanted to - some (but not all) of the light and animation effects of Brogue, some of the retro aesthetic from Qud. The basic proof of concept architecture of actors and actions.
It's wide open for the next step.
Eschaton
An ASCII roguelike set in a futuristic, post apocalyptic world.
Status | In development |
Author | paradroid001 |
Genre | Simulation |
Tags | 2D, ascii, Roguelike |
More posts
- Eschaton: 7DRL Development Process - Part 1Mar 21, 2021
Leave a comment
Log in with itch.io to leave a comment.