When I originally started with the pyherc, I had plan to only support graphical tiles. Later on I decided to add support for curses interface and realized how the user interface was tangled with rest of the code. This blog post details how I organized the code in order to support two different user interfaces.
Core of the system is a class called Model. It represents the playing world and everything that happens there. It holds references to player character, first dungeon level and so on. Objects in the Model do have some logic in them, but not that much. Their main purpose is to represent objects in the game world and their relationship to each other.
Actual logic of moving, fighting and casting spells is located in a separate namespace Rules. I made the split because otherwise character class would have grown really huge and hard to maintain. Also, by having Rules in separate classes, I can more easily replace only the part I need to replace.
Third major portion of the game is the user interface (or rather two). The user interface connects directly to Model, because it needs to be able to display levels, monsters and such. In addition to holding reference to the Model, user interface also registers itself as a event listener of Model.
Being a listener means that the object will start receiving events from the model (it is also possible to register as a listener to a specific character). Events are simple classes that have enough attributes to capture information what is happening and one method that returns a string that can be displayed as a message.
Now, whenever something interesting happens, like a character moves or attacks, the Model will raise an event describing what just happened. These events are then routed to each listener that has been registered and thus arrive to the user interface too. User interface can then decide how to react to the event, usually updating the screen if the action is visible to the player. Because the Model is only concerned on raising and routing the events to subscribers, it is blissfully unaware of details of user interface and writing multiple of those is possible.
In curses interface, the user interface itself receives events and updates the screen accordingly. This is because characters that are displayed on the screen are not represented by objects, they’re just characters on screen buffer. Curses interface has to then figure out which tiles need to be updated and how.
Qt interface however, has each and every tile as a QGraphicsItem that has its own coordinates, image and other properties. When a tile object is created, it is actually registered as a listener to events of the specific thing (character usually) that it is displaying. When that character moves and raises an event, the tile receives it and automatically updates its coordinates to new ones. Qt then takes care of making sure the screen is updated if the action is within the players view.
When player wants to perform an action, flow of data is a bit different. Instead of modifying the Model directly or calling functions in Rules package, the user interface has access to something called ports. These ports define a nice, clean, interface for interfacing with Rules. Ports are supposed to form a stable interface that external systems can use to connect to the game and that the game can use when connecting to an external system. Usual examples are user interface and save files.
So, each user interface needs to know couple of things:
- How to display current state of Model
- How to subscribe to and receive events from Model and what to do with them
- How to interface with Ports
Because the Model or Rules have no hard dependency on User interface, it is possible to have multiple user interfaces that player can choose from. It is even possible to have multiple user interfaces active at the same time. Good example would be the normal user interface that is used for playing and a web page that is used for inspecting and changing internal state of the system for debugging purposes.