This is one of a few blog posts adapted from my Dissertation project about Rotatris, a tetris-like game I made in Swift and SpriteKit. In Rotatris, players try to build “shells” of squares around a central rotating block. You can read my full written dissertation here.
This post is, similar to the last, an excerpt from my dissertation designed to give some idea of how I deal with challenges and design decisions. It’s a bit dry, I’m afraid, since it’s all in the fancy academic format designed to make it dry and unentertaining.
Near the end of the allotted time for the project, I decided to add in animation for the shells falling down. I felt that this should be fairly simple: when each square was moved down, instead of setting its position directly and thus “teleporting” it downwards, I could run a simple moveTo animation on it over the course of 1/20th of a second or so. From the player’s perspective, each square would move quickly, one at a time, and it would be easy to spot the patterns of movement. This would help significantly with the more challenging aspects of the game, since players would eventually be able to predict accurately the exact movement that each square would make, and use these patterns to their own benefit. It would also help newer player learn the basic game mechanics, adding to the simplicity, and give players a brief break upon completing a shell – a useful bonus to high-level or marathon players.
However, this proved impossible to perform. SpriteKit animations work differently to the method predicted: instead of waiting until completion to run the next piece of code, they continue immediately, meaning that all squares animated simultaneously (or so close to simultaneously as to make no difference). This would have been acceptable as a solution, however the code that had already been implemented required each square to, before moving, check that the end position was not yet occupied by another square. Since other squares would not yet have reached their destination, and so would have a different position, it would always show as empty, causing multiple squares to move to the same place. This was not acceptable, since no two squares should be able to occupy the same space. One method suggested was having a check after movement for if squares were in the same place, in order that they should then adjust position to free spaces, but it was decided this was unintuitive and computationally inefficient.
SpriteKit animations do have an option to run code on completion, but this code is in a separate thread to the main code and references back cannot be made. This requires each square to be a nested function to the previous, requiring 12-200+ nested functions, each in a separate thread. For obvious reasons of efficiency this option was not used. In the end, it was decided to run a brief “wiggle” animation on each block as it moved, making it clear that something was happening, even if not explaining what was happening precisely.
This resolution was deeply unsatisfying. The clear transmission of information is a crucial part of any game with pretensions of competitiveness – players can play at drastically higher levels with full information than with guesswork. Without this, or a similar animation, Rotatris is notably worse than it could be. Given the opportunity to start again, this restriction could be designed around from the start, perhaps using the grid method suggested below in the “Collision detection” challenge – this would have allowed the blocks to be animated, with their “positions” being set entirely separately to where they actually were. In theory, this was the sort of difficulty that the refactoring discussed above in “Agile Methodology” was designed to resolve: in practice this code was written far too late to allow resolution.
An algorithm I was working on for some time before the project began was how to resolve the edge case where rotating the central block would cause it to occupy the same space as the falling tetromino, as in the image below. In the existing code, the next time the tetromino tried to move it would register as a collision and stop – with two squares in the same position, meaning that the calculation for when a shell was full would be incorrect.
It was felt that, as an edge-case bug, it was more worthwhile to work on other issues before resolving this, and it was left until a very late stage to do so. Early stages of player testing proved it to come up only rarely. In the end there was not time to implement a solution, but some possible options were considered.
As usual, the first recourse was intuitiveness: how would these blocks act if they were physical objects? In this case, the thing to do would be to push the tetromino
away as if it was being pushed by the rotating block: this would require detecting the point of contact and moving the tetromino in a particular direction based on that point, as in the image to the right, until the object was no longer colliding. This was not technically hard, but raised some other questions: why did the rotating block not push the tetromino when it would not collide with it at the end but would while moving? It was felt that players might also dislike how this would mean some collisions cause the blocks to connect together. (For example on the right, tetrominos are moved below the colliding blocks, whilst on the left they are moved above, immediately to fall onto the collision point and connect). It was also felt that it could cause some blocks to move a very long way, perhaps in a way players wouldn’t expect.
The second option was less intuitive but simpler: move the tetromino “outwards” until it no longer collided. This was easier both for a player to understand, and to implement from a coding perspective, but again had problems. Firstly it was unintuitive – while it was easier to understand once noticed, players would need to actively notice what was happening. Secondly, in some cases this would still lead to unexpected connections – when the tetromino was above the centre, for example, it would be pushed up onto the top of the central block. It would then attempt to fall down, and immediately get stuck: quite possibly causing a game over that the player would not be able to understand the cause of.
No solution to this problem was implemented. This is not thought to be a significant issue with the game –a game with this issue should not be shipped or sold, but given the time constraint the right decision was made to prioritise other issues.
As with rotation, the shell collapse algorithm was one that was under consideration for some time – in this case since before the project was even started. Details have been given of the final implementation in “Key Algorithms” above, but the algorithm proved to be unexpectedly challenging because of the animation (as above), and because of SpriteKit’s rule for how it selects children. What was wanted was an ironclad, easily understandable rule as to what order squares fall down in – for example “counter-clockwise from top left” or “corners, then sides, then top and bottom.” This would have required selecting the children in a specific order based on their position, which SpriteKit did not allow: instead when iterating through child nodes of a node, the list is called in the order in which they were added.
The program could have iterated through this list to find the child node which had the specific position wanted, then repeat, but this would have been drastically less efficient: O(nlogn) rather than O(n). It was felt that no such rule was needed, as long as the animation was suitable and showed clearly what was happening – as mentioned before, this was found not to be the case
Without animation, the rule was simplified. No two corner squares would ever fall down into the same place, so all corner squares could be run at once, first. Then squares could be run in columns and then in rows, again confident that in most cases squares would act in a logical manner. The algorithm was, in the end, dissatisfying, because it could and should be clearer. Given the opportunity, a better algorithm could have been found, or possibly the more inefficient method could have been used – Rotatris runs consistently at 60fps, the efficiency sacrifice could well have been made.
In my previous post, the final implementation for the rotation and the steps taken to design it are discussed. Here it should be added that it proved to be unexpectedly challenging to perform this design process. The original implementation – using the RotateBy action – was completely unsuitable since the collision detection method currently being used was unable to detect the changed positions of the objects. A quick implementation of the original algorithm, X->-Y, Y -> X, proved more helpful, and now objects were capable of collision, even if rotating in very strange ways.
This was the implementation used for a significant time through the testing process, delaying solving the problem for a long time. The algorithm details were only worked out once it became apparent that even the most primitive player testing would not get accurate results without this algorithm in place. It proved to be unexpectedly easy to solve once sketches were made and the table above was drawn up. (The above image is my original sketch: I make very many such sketches, and usually then have to remake them before someone else can read them). Once the final algorithm was written, rewriting it in pseudocode and then code was the work of a moment.
This was eventually satisfactorily resolved, but in retrospect it was unprofessional and bad practice for it to be left unresolved for so long. Failing to solve this caused no significant problems, but action should still have been taken to resolve the problem rather that avoiding it in favour of more flashy interesting puzzles.