#4 - Analysis of API assumption problem
I want to discuss a problem I keep running into where I make false assumptions about the APIs I'm using. I'll start by giving a few examples of the problem, then I'll analyse them.
Examples
So I wanted to generalise enemies so that they could have a variety of attacks and movement patterns, and so that I could reuse one attack/movement for multiple enemies. So it made sense to me to structure the classes as follows:
I'm pretty sure this organisation makes sense, and would normally work fine, but the problems arose from the particulars of Unity.
1.
First, I wanted to make Enemy, Movement, and Attack either abstract classes or interfaces. But Unity's inspector can't serialize these (create a UI element in the inspector for the class) (see here). This would make sense, since this would be the equivalent of trying to instantiate them. But, this also means that you can't use generic classes such as lists, since those would be defined in terms of the interface or abstract class, in this case List<Attack> (Thinking about it now, maybe you could if you instantiated them before runtime?) But of course the inspector doesn't make this clear. It doesn't return an error if you define an abstract class as serializable. The class will appear in the inspector, and appear editable, but those changes won't be reflected in the code, and it will fall back on the default values. It should really give a warning or error message for this. It also doesn't mention any of this in the docs (here or here).
2.
Next, I wanted to be able to edit those classes in the inspector of course. But I also didn't want to have to manually create the classes for each new Enemy type, I also wanted to be able to do it programmatically, so I could have hard-coded definitions for enemies to pick from. But if I created them programmatically, they wouldn't appear in the editor until I ran the game, since it would be creating the classes at runtime. I needed to somehow create them at 'edit-time'. I found a few potential ways to do this, but they all had their issues;
- First, there was [InitializeOnLoad]. The problem with this was that it only gave you functions (with no parameters or return values) to run, meaning that I couldn't give it my input variables. I think this is more just meant for tracking than actually functionality.
- Next, [ExecuteInEditMode]. This didn't seem to work on launching or reloading the editor, only when a change was made, so it wouldn't work for my purposes.
- Finally, [ISerializationCallbackReceiver]. This is called any time an object needs to be serialized. In practice this means when the editor is launched/reloaded, as well as any time a change could be made, so it runs every frame. Firstly, I was uncomfortable using this because it was clearly not its intended purpose. And this was confirmed by the fact that Unity crashed many times while I was trying to get this to work. But if it works it works I guess. The real problems this posed were that I would be doing wasted duplicate work, and that I would overwrite changes to the inspector with the initial values of my code. The former could be somewhat fixed by checking one or many of the booleans Editor.isPlaying, Editor.isUpdating, and Editor.isCompiling (found here). I'm being vague here because I never really figured out which combination of them was correct for my purpose.
I eventually caved and gave up on creating enemies with defined attacks programmatically.
3.
For the following section I also want to mention a previous unrelated problem I ran into as well. I was programming the logic for enemies, and ran into a problem dealing with collisions. The enemies would collide into walls just fine, but when they collided with the player, they would take off at an angle – moving in accordance with physics. But I didn't want this, I just wanted them to stay still, or maybe move back a bit. I eventually realised that this was because I had their Rigidbody's on Dynamic mode. But once I set them to Kinematic, they no longer responded to collisions. It took me a strange amount of time and hacking before I realised that Kinematic objects just don't deal with collisions automatically, you have to program that yourself. This seemed strange to me, because up to this point Unity seemed to have pre-built all of the fundamentals of physics and collisions.
Analysis
I want to use these example to explore a more general topic. I think that the problem I kept running into at Amazon was that I was making assumptions as to the functionality of the APIs I was using, which turned out to be wrong. At the time I was unaware of this, so I spent a long time trying to find out what was wrong with my code, syntactically or logically; when the problem was actually the goal itself. And yet even now with this knowledge, I still fell into the same issue again here. Why?
1.
The first issue is that some assumptions must always be made, at least practically speaking. A simple example would be that an array works the same way in every language. I could spend the time to make sure that this is the case when using a new language, but having to do that for every data type you use would tank your work speed. Because of the number of assumptions required, so many are made that we usually don't realise we're making them. So the question then becomes: how do we determine when it’s ok to make such assumptions? The intuitive way I would do this would be through reason – why would anyone make a language which uses arrays differently when they're the same in every language I've ever used? But there's a few problems here;
- First, you could just be wrong. Take my assumption that kinematic objects would have built-in collision logic. How exactly would you implement this? For dynamic objects, you can just use physics. But kinematic objects don't use physics; their logic is defined wholly by the user. So do we stop on collision? Do we bounce back? Do we remove the velocity along the normal of the collision? There's no clear general case, so we would have to leave it up to the user to decide.
- Second, there could be some quirk of design that you're just not familiar with. The edit-time issue would be a good example of this. Having separate edit and run modes is unusual for me, my programs usually either run or don't, so it didn't even cross my mind that this was an assumption I had made. I think this is something really important to keep in mind when designing your own APIs. Any time you diverge from what would be commonly intuitive or standard, even if it makes sense from an infrastructural perspective, poses the risk of this happening. So if you do diverge, you should make it very clear in a visible part of the docs that you did. There is of course the same problem here of what 'standard' even is. Another issue to note here is that these design decisions can be more or less visible. The example I used was wholly visible, so I was able to be made aware of it. But some might be purely internal decisions, and so making the user aware of these will always be a challenge.
- Third, the designer could also just have built the API weirdly for no reason in particular. While quirks of design could be unintuitive but ultimately understandable and predictable, there's not really a way to pre-empt this. But hopefully if you’re using good APIs it should be a rare issue.
The best solutions I can think of right now are; keep these problems in mind, and be aware of when you're making assumptions, because otherwise you won't even be able to make the decision to assume or not assume, it will just happen subconsciously; and be aware once you hit a problem that the cause might be such assumptions, and not syntactic or logical.
2.
The second issue is that even when I can feel that I'm pushing against the system, it's unclear if that means that my approach is impossible, or just not the intended use. It's very clear to me, having dealt with both extremes, how it feels when I'm fighting with an API. Everything takes longer; I have to constantly refer back to docs and discussion boards; and most distinctly, I just fall from one problem right into another.
So has this ever actually worked out? Have I pushed through and hacked together a solution, or do I always divert to a different approach? And if not, why did I do it? From what I can remember, in most of the cases my original plan was impossible, so I was forced to divert. But often the diversion was minor, while still working with the system I was fighting against. So it makes sense that more issues cropped up.
So why didn't I just divert from the underlying system? Well I had analysed my options regarding which system to use beforehand, and determined this to be the best approach. So I didn't want to lose some functionality just because I thought it might take some more time; of course not knowing whether it would just take more time, or if it was actually impossible. I was prioritising quality above all else.
There is of course the option of a balance here between the quality of the result, the time taken to produce that result, and the mental cost in the process. And I want to propose here the idea of slightly deprioritising quality in lieu of time and mental cost. It's entirely possible that not diverting would mean that you follow the original plan until you're completely sure that it's impossible, and then divert to the next option. In this case, you may as well have diverted earlier, as that wouldn't have resulted in a loss of quality. Though of course this is the ideal case. In the general case, there is some probability that diverting prematurely won't have an impact on quality. And this probability is determined by the quality of your predictions: if they're perfect you'll always divert at the right time and maximise time and mental cost without losing quality; and the opposite if they're perfectly inaccurate.
So how do we perform these predictions? Honestly I have no idea, this is something I'll have to think about more.
This is far from a conclusive study of the problem and potential solutions, so I want to check back in after having run into this issue again, and see how I resolved it, and if any improvements have been made.
Grapple Game
More posts
- #6 - Types of CreativityJan 19, 2024
- #5 - Motivation 2Jan 09, 2024
- #3 - Combat problem fixed!Dec 10, 2023
- #2 - Motivation and issue-issue workDec 10, 2023
- #1 - Level Design, Combat, and Art AssetsDec 10, 2023
Leave a comment
Log in with itch.io to leave a comment.