On a solitary Friday night in search of inspiration, you decide to recall your past victories on the programmer front. The archive from the old hard drive slowly opens and now the code of the glorious distant times unfolds before you ...
Oh no. This is not what you expected to see. Is it true that everything was so bad? Why didn't anyone tell you? How was it possible to go so far? Such a number of go-to operators in a single function - is this generally legal? You hastily close the project. For a second, you are tempted to delete the file, and all the contents of the hard drive at the same time.
')
Below you will find a collection of lessons, instructive examples and warnings that I have taken from my own journey into the past. All names are given without change to expose the perpetrators.
2004
I was thirteen years old. The project called “
Red Moon ” was a very ambitious game about third-person air battles. Those few code fragments that I did not copy a symbol into a symbol from the “
Developing Games in Java ” manual were a disgrace. Let's see concrete examples.
I wanted the players to have several weapon options and the ability to choose. In the plans, it all looked like this: the weapon model moves down and inside the player model, replaced with the next one, and so on. Here is the animation code. Better not much to understand.
public void updateAnimation(long eTime) { if(group.getGroup("gun") == null) { group.addGroup((PolygonGroup)gun.clone()); } changeTime -= eTime; if(changing && changeTime <= 0) { group.removeGroup("gun"); group.addGroup((PolygonGroup)gun.clone()); weaponGroup = group.getGroup("gun"); weaponGroup.xform.velocityAngleX.set(.003f, 250); changing = false; } }
I would like to draw your attention to a couple of interesting facts. First, estimate how many variables are here:
- changeTime
- changing
- weaponGroup
- weaponGroup.xform.velocityAngleX
But with all this, something seemed to be lacking ... oh yeah, we also need a variable to keep track of which weapon is in use at the moment. And, of course, this requires a separate file.
And one more thing: in the end, I was not going to make more than one model of weapon. That is, the same model was used for each of these types. From all this code there was absolutely no proc.
How to fix it
Remove unnecessary variables. In this case, the state of the weapon could be described with only two: weaponSwitchTimer and weaponCurrent. All other information could be extracted from them.
Explicitly initialize everything you can. This function checks whether the weapon is null and, if necessary, starts the initialization process. After thinking for at least thirty seconds, I could figure out that in this game the user always has some kind of weapon, and if not, then it is simply impossible to play and it is possible with a clear conscience to crash the entire program.
Obviously, at some point I ran into a NullPointerException in this function, but instead of wondering where it came from, I quickly checked for a null value, and this ended the matter.
Take the initiative and make decisions yourself! Do not let your computer figure it out.
Titles
boolean noenemies = true;
Call variables of type Boolean without using negation. If you are writing something like this in the code, then it’s time to rethink your approach to life:
if (!noenemies) {
Error processing
Similar things come across in the code all the time:
static { try { gun = Resources.parseModel("images/gun.txt"); } catch (FileNotFoundException e) {}
You probably are thinking now: “We need to handle this error in a more elegant way! Well, at least a message to the user to withdraw or something like that. ” But I, frankly, hold the opposite point of view.
Eliminating errors is really never superfluous, but with the processing you can easily overdo it. In this case, it is still impossible to play without a weapon model, so you can just as well crash the game. Do not try with dignity to get out of the situation, if it is obviously hopeless.
In addition, returning to the above, here you need to independently decide which errors to consider as correctable and which ones should not be. Unfortunately, Sun believes that almost all errors in Java simply have to be fixed, and as a result we have cases of lazy processing - one of them is given above.
2005-2006
By this time I had already mastered C ++ and DirectX. I decided to create a reusable engine so that humanity could safely fall into a storehouse of wisdom and experience that I had accumulated during my long fourteen-year life.
Do you think it was painful to watch the first trailer?
You have not seen anything yet .
At that time, I already knew that object-oriented programming is awesome. This knowledge led to horrors of this kind:
class Mesh { public: static std::list<Mesh*> meshes;
In addition, I learned that comments are awesome, and this made me leave words like:
D3DXVECTOR3 GetCenter();
However, there are more serious problems with this class. The Mesh concept is a kind of hazy abstraction that cannot be matched with the equivalent of reality. Even when I wrote it, I did not understand anything. What is it - a container that contains vertices, indices and other data? A resource manager that loads and uploads data to disk? What are the rendering tools that the data sends to the GPU? All at once.
How to fix it
The Mesh class must be a simple data structure. It should not be smart types. And this means that you can throw out all the getters and setters with peace of mind and make all the fields public.
Next, you can separate the resource management and rendering into separate systems that work with inert data. Yes, it is systems, not objects. Do not try to tailor each problem to an object-oriented abstraction if another type of abstraction may be better suited.
The surest way to correct comments is to delete them, as a rule. Comments quickly become irrelevant white noise, which only confuses - the compiler does not look at them anyway. I insist that comments should be disposed of unless they belong to one of the following groups:
- Comments that answer the question “why?” And not “what?”. From them the most confusing.
- Comments that describe in a few words what makes a huge piece of code that follows. They facilitate the process of reading and navigation;
- Comments in the data structure declaration explaining what each field contains. Often they are superfluous, but in some cases it is not intuitive to understand how the structure is located in memory, and then comments with a similar description are necessary.
2007-2008
This period of my life I call the “dark ages of PHP”.
2009-2010
I'm already in college. I am working on a third-person Python multiplayer shooter called Acquire, Attack, Asplode, Pwn. I can not say anything in my defense. Still more shameful, only now with a spicy aftertaste of copyright infringement.
When I wrote this game, I was just enlightened that global variables are evil. They turn code into a jumble. They allow function A, changing the global state, to break / violate function B, which has nothing to do with it. They do not work with threads.
However, almost the entire gameplay code requires access to the state of the world matrix entirely. I “solved” this problem by saving everything in this object and transferring it to each of the functions. And no global variables for you! It seemed to me that I had a great idea, because it turns out that in theory you can run several autonomous world matrices at the same time.
In fact, the world, in fact, was a container of a global state. The concept with several world was, of course, completely meaningless, no one ever tested it, and I strongly suspect that it could work only if it was seriously refactored.
Those who enter into a sect of opponents of global variables are discovering a whole world of creative methods of self-deception. The worst of them is singleton.
class Thing { static Thing i = null; public static Thing Instance() { if (i == null) i = new Thing(); return i; } } Thing thing = Thing.Instance();
Krible krable booms! Not a single global variable in sight! But there is one “but”: singleton is much worse for the following reasons:
- All potential flaws of global variables persist here. If you think that singleton is not global, then stop lying to yourself;
- At best, singleton adds a resource-intensive branching operator to your program. At worst, this is a full function call;
- Until you start the program, it is not known when singleton is initialized. This is another example of how lazy programmers let the system decide what they should have planned at the design stage.
How to fix it
If something should be global, let it be. When making this decision, consider how it will affect the project as a whole. With experience, it becomes easier.
The problem, in fact, lies in the interdependencies in the code. Global variables can lead to invisible dependencies between fragmented code fragments. Group related pieces of code into a complete system to minimize these invisible dependencies. A good way to achieve this is to drop everything that relates to one system into a single thread and force the rest of the code to interact with it through messages.
Boolean Type Parameters
class ObjectEntity: def delete(self, killed, local):
Perhaps you had to write code like this:
class ObjectEntity: def delete(self, killed, local):
Here we have four separate delete operations that are very similar to each other - all minor differences are tied to two Boolean type parameters. It seems to be all logical. And now let's look at the client code that calls this function:
obj.delete(True, False)
It doesn't look particularly readable, does it?
How to fix it
Here you need to look at a specific case. But there is one
piece of advice from
Casey Muratori , which is always relevant: start with client code. I am convinced that no person in his mind will write such a client code as mentioned above. Rather, it will write the following:
obj.killLocal()
And then he will register the implementation of the killLocal () function.
Titles
It may seem strange that I am pushing on the names so much, but, as the old joke says, this is one of two unresolved programming problems (the second is invalidation of the cache and an error by one).
Take a close look at these features:
class TeamEntityController(Controller): def buildSpawnPacket(self):
It is clear that the first two functions are interconnected, just like the last two. But their names do not indicate this fact. If I start typing self in the IDE, these functions will not appear one after another in the autocomplete menu.
Accordingly, it is better to start the name with a general, and finish privately. Like this:
class TeamEntityController(Controller): def packetSpawnBuild(self):
With this code, the autofill menu will look much more logical.
2010-2015
Some 12 years of work -
and I finished the game . Suppose I learned a lot in the process, in the final version there were still serious punctures.
Data binding
At that time, the fever of reactive UI frameworks such as Microsoft’s MVVM and Google’s Angular UI was just beginning. Today, this programming style is preserved mainly in React.
All these frameworks work according to the same scheme. You see a text field for HTML, an empty element and a single line of code that inextricably links them. Enter text in the field - and voila! magically updated.
In the context of the game, it will look something like this:
public class Player { public Property<string> Name = new Property<string> { Value = "Ryu" }; } public class TextElement : UIComponent { public Property<string> Text = new Property<string> { Value = "" }; } label.add(new Binding<string>(label.Text, player.Name));
Wow, the interface is now automatically updated when you enter the player's name! The interface and code of the game can be completely independent of each other. This is tempting: we get rid of the state of the interface, taking it out of the state of the game.
However, there were some alarm bells. I had to turn each field in the game into a Propert-object, which included a number of dependent links.
public class Property<Type> : IProperty { protected Type _value; protected List<IPropertyBinding> bindings; public Type Value { get { return this._value; } set { this._value = value; for (int i = this.bindings.Count - 1; i >= 0; i = Math.Min(this.bindings.Count - 1, i - 1)) this.bindings[i].OnChanged(this); } } }
Absolutely behind each field, down to the last Boolean, a bulky dynamic array was assigned.
Take a look at the cycle that notifies the associated data of a property change, and you will immediately understand what problems I ran into because of this paradigm. It has to iterate the list of related data in the reverse order, since the binding can add or remove interface elements, which leads to changes in the list.
However, I was so imbued with data binding that I built the whole game on it. I have broken objects into components and related their properties. The situation began to spin out of control.
jump.Add(new Binding<bool>(jump.Crouched, player.Character.Crouched)); jump.Add(new TwoWayBinding<bool>(player.Character.IsSupported, jump.IsSupported)); jump.Add(new TwoWayBinding<bool>(player.Character.HasTraction, jump.HasTraction)); jump.Add(new TwoWayBinding<Vector3>(player.Character.LinearVelocity, jump.LinearVelocity)); jump.Add(new TwoWayBinding<BEPUphysics.Entities.Entity>(jump.SupportEntity, player.Character.SupportEntity)); jump.Add(new TwoWayBinding<Vector3>(jump.SupportVelocity, player.Character.SupportVelocity)); jump.Add(new Binding<Vector2>(jump.AbsoluteMovementDirection, player.Character.MovementDirection)); jump.Add(new Binding<WallRun.State>(jump.WallRunState, wallRun.CurrentState)); jump.Add(new Binding<float>(jump.Rotation, rotation.Rotation)); jump.Add(new Binding<Vector3>(jump.Position, transform.Position)); jump.Add(new Binding<Vector3>(jump.FloorPosition, floor)); jump.Add(new Binding<float>(jump.MaxSpeed, player.Character.MaxSpeed)); jump.Add(new Binding<float>(jump.JumpSpeed, player.Character.JumpSpeed)); jump.Add(new Binding<float>(jump.Mass, player.Character.Mass)); jump.Add(new Binding<float>(jump.LastRollKickEnded, rollKickSlide.LastRollKickEnded)); jump.Add(new Binding<Voxel>(jump.WallRunMap, wallRun.WallRunVoxel)); jump.Add(new Binding<Direction>(jump.WallDirection, wallRun.WallDirection)); jump.Add(new CommandBinding<Voxel, Voxel.Coord, Direction>(jump.WalkedOn, footsteps.WalkedOn)); jump.Add(new CommandBinding(jump.DeactivateWallRun, (Action)wallRun.Deactivate)); jump.FallDamage = fallDamage; jump.Predictor = predictor; jump.Bind(model); jump.Add(new TwoWayBinding<Voxel>(wallRun.LastWallRunMap, jump.LastWallRunMap)); jump.Add(new TwoWayBinding<Direction>(wallRun.LastWallDirection, jump.LastWallDirection)); jump.Add(new TwoWayBinding<bool>(rollKickSlide.CanKick, jump.CanKick)); jump.Add(new TwoWayBinding<float>(player.Character.LastSupportedSpeed, jump.LastSupportedSpeed)); wallRun.Add(new Binding<bool>(wallRun.IsSwimming, player.Character.IsSwimming)); wallRun.Add(new TwoWayBinding<Vector3>(player.Character.LinearVelocity, wallRun.LinearVelocity)); wallRun.Add(new TwoWayBinding<Vector3>(transform.Position, wallRun.Position)); wallRun.Add(new TwoWayBinding<bool>(player.Character.IsSupported, wallRun.IsSupported)); wallRun.Add(new CommandBinding(wallRun.LockRotation, (Action)rotation.Lock)); wallRun.Add(new CommandBinding<float>(wallRun.UpdateLockedRotation, rotation.UpdateLockedRotation)); vault.Add(new CommandBinding(wallRun.Vault, delegate() { vault.Go(true); })); wallRun.Predictor = predictor; wallRun.Add(new Binding<float>(wallRun.Height, player.Character.Height)); wallRun.Add(new Binding<float>(wallRun.JumpSpeed, player.Character.JumpSpeed)); wallRun.Add(new Binding<float>(wallRun.MaxSpeed, player.Character.MaxSpeed)); wallRun.Add(new TwoWayBinding<float>(rotation.Rotation, wallRun.Rotation)); wallRun.Add(new TwoWayBinding<bool>(player.Character.AllowUncrouch, wallRun.AllowUncrouch)); wallRun.Add(new TwoWayBinding<bool>(player.Character.HasTraction, wallRun.HasTraction)); wallRun.Add(new Binding<float>(wallRun.LastWallJump, jump.LastWallJump)); wallRun.Add(new Binding<float>(player.Character.LastSupportedSpeed, wallRun.LastSupportedSpeed)); player.Add(new Binding<WallRun.State>(player.Character.WallRunState, wallRun.CurrentState)); input.Bind(rollKickSlide.RollKickButton, settings.RollKick); rollKickSlide.Add(new Binding<bool>(rollKickSlide.EnableCrouch, player.EnableCrouch)); rollKickSlide.Add(new Binding<float>(rollKickSlide.Rotation, rotation.Rotation)); rollKickSlide.Add(new Binding<bool>(rollKickSlide.IsSwimming, player.Character.IsSwimming)); rollKickSlide.Add(new Binding<bool>(rollKickSlide.IsSupported, player.Character.IsSupported)); rollKickSlide.Add(new Binding<Vector3>(rollKickSlide.FloorPosition, floor)); rollKickSlide.Add(new Binding<float>(rollKickSlide.Height, player.Character.Height)); rollKickSlide.Add(new Binding<float>(rollKickSlide.MaxSpeed, player.Character.MaxSpeed)); rollKickSlide.Add(new Binding<float>(rollKickSlide.JumpSpeed, player.Character.JumpSpeed)); rollKickSlide.Add(new Binding<Vector3>(rollKickSlide.SupportVelocity, player.Character.SupportVelocity)); rollKickSlide.Add(new TwoWayBinding<bool>(wallRun.EnableEnhancedWallRun, rollKickSlide.EnableEnhancedRollSlide)); rollKickSlide.Add(new TwoWayBinding<bool>(player.Character.AllowUncrouch, rollKickSlide.AllowUncrouch)); rollKickSlide.Add(new TwoWayBinding<bool>(player.Character.Crouched, rollKickSlide.Crouched)); rollKickSlide.Add(new TwoWayBinding<bool>(player.Character.EnableWalking, rollKickSlide.EnableWalking)); rollKickSlide.Add(new TwoWayBinding<Vector3>(player.Character.LinearVelocity, rollKickSlide.LinearVelocity)); rollKickSlide.Add(new TwoWayBinding<Vector3>(transform.Position, rollKickSlide.Position)); rollKickSlide.Predictor = predictor; rollKickSlide.Bind(model); rollKickSlide.VoxelTools = voxelTools; rollKickSlide.Add(new CommandBinding(rollKickSlide.DeactivateWallRun, (Action)wallRun.Deactivate)); rollKickSlide.Add(new CommandBinding(rollKickSlide.Footstep, footsteps.Footstep));
This caused a lot of problems. I created loops of connections that loop back. It turned out that the order of initialization is often of great importance, and when data is bound, initialization becomes a real hell, since as links are added, some properties are initialized several times.
When it came time to add animation, it turned out that with data binding, to animate the transition between two states is a complex and non-intuitive process. And I'm not the only one who thinks so. You can watch this video with a Netflix programmer, who crumbles in praises of React, and then tells you how to turn it off whenever they start the animation.
I also guessed to use the full power of disabling connections. And added another field:
class Binding<T> { public bool Enabled; }
Unfortunately, this lost all meaning of binding. I wanted to get rid of the states, but with such a code there are only more of them. Here's how to remove this state?
I know! With the help of binding!
class Binding<T> { public Property<bool> Enabled = new Property<bool> { Value = true }; }
Yes, there was a moment when I seriously tried to implement something like this. I connected everything that is connected. But I quickly came to my senses and realized that this was crazy.
How can I fix the binding situation? Try to make your interface function normally without states. A good example is
dear imgui . Separate behavior and state as much as possible. Avoid the technician with whom it is easy to create states. Creating a state should be a painful step for you.
Conclusion
One could go on, this is not all my dumb mistakes. I wrote another “original” method to avoid global variables. There was a period when I was obsessed with closures. I created the so-called entity-object system, and it was anything but an entity-object system. I tried to implement multithreading in a voxel engine, generously setting locks.
Here are the conclusions I came to:
- Make decisions yourself, do not delegate them to the computer;
- Separate behavior and state;
- Write clean functions;
- Start with client code;
- Write a boring code.
Here is my story. Do you have a dark past that you are willing to share?