Recently, I built a video game. I couldn't help the urge. It's an online multiplayer game inspired by classic Pong from the 70s.
You can play it here: https://www.play-pong-online.site
I built it with TypeScript, React, p5.js, GraphQL subscription, AWS serverless application model, and pnpm monorepo applying domain-driven architecture, but I don't go into detail here. Read the source code if you're curious. Instead, I wanna share what I learnt during this development.
I started building it on May 2nd. I built it within a week. A week of intense development. I enjoyed it and learned a lot. A personal project of this kind is the best place to learn, and I want to inspire you. I'll be happy if you get inspired to build your own stuff maybe next holiday :)
The Requirements
Pong is like the Godfather of classic video games like Pacman and Space Invaders. I read about it and felt like building it myself.
The rules (requirements) are as follows:
There are two players
A player can move the bar
The ball should move
The ball can be reflected against the bar and side edges
It adds 1 point when a player misses the ball
The winner is determined when either of the players gets specific points
How I built it
I built it as described below:
Made the overall design - 1 day
Implemented the game that works offline - 1 day
Implementation of backend - 1 day
Put things together and made them work through network connection - 3 days
DevOps and deploy - 1 day
Here are the time logs I took:
It took me 63 hours and 51 minutes. Whoa. For that price, I earned 10 lessons below.
1 Design comes first
Design comes first. Always. However the project you're working on is like, design is important. You should always make a design before you write a single line of code. I took half a day to design the technical specs to define features, architecture, structure, process, and deployment. As a result, I could smoothly work on the actual implementation without freezing much in front of the editor.
I usually think of design based on 4+1 views. I draw several diagrams and tables before writing code. This helps you have different points of view and significantly helps you know what you are doing.
Do you clearly see the big picture of what you are implementing? If not, get out of your editor. Your code would mumble if you didn't know what you were doing.
2 Your design doesn't have to be perfect
Design is important, but I didn't say it should always be perfect. You can be wrong about it. What matters is that you have a reasonable hypothesis to test.
At first, I planned to implement an online multiplayer feature by having a buffer for the state at each frame. But as I implemented the offline version, which includes pure immediate state calculation, I realised that I could recursively estimate the current state if I had the delay of the opponent's state. I changed the design. It worked well and became concise and straightforward as a result.
Even if you spend time on technical design, you can never know everything before coding. Some things you realise after beginning implementation. So it's okay to have not-sure-until-I-try parts in your initial design. You might get a better idea later. But don't start without one. You should at least have an assumption to examine during development.
3 It's okay to have ugly code
You may not know what you're doing well in the initial development phase, but you will gradually have better ideas later. This means you might produce some ugly pieces of code at first, but that's okay.
My first implementation of solving state was not that pretty. It was like, "I'm gonna do this, then this, and this one too, finally we can handle this, and then...". Even though I had an initial design (so it was at least testable), my code was not the best. But that's okay because you can prettify it later if you have a test for it.
If it works, it works. Your client or the app's users don't care how pretty your code is. Get the thing done first. If I had been obsessed with the prettiness of each line of code, I would still be working on it—for good.
4 Define "Done"
Before working on a feature, you should define what condition makes the task "done." This will prevent unnecessary development hours. When do you move on to the next ticket? When the test passes? Or when things look fine? How do you know that? If that condition is subjective or unclear, it might produce unexpected costs.
If you're a passionate developer who loves coding, it is always tempting to refactor your code postmortem. You find some stuff ugly, and you're now distracted by it and can't help spending extra minutes on it. And sometimes, you find yourself spending hours. It is like having potato chips. You refactor one line of code, and then another, then another.
I confess I also did that in the development of Pong, and it was always when I didn't clearly define "done". Yeah, there are many good practices you wanna do. But they're not necessarily the best. Life is short. Your budget is limited. Make sure you can change it later (by having tests) and move on to the next.
5 Make the change easy, then make the easy change
Writing code is mostly about changing the existing one. After the initial implementation, writing things from scratch is relatively rare. And it is always your code that gets in your way. If that's the case, it's time to practice preparatory refactoring.
As I said, not all the code is perfect, and that's okay. It should always be determined in the context of how likely it will change. If the code is not likely to change, you can leave it as is. But if that piece is the one you wanna change now, refactor it now. You wanna add a feature? Make the change easier to make an easy change. Don't hustle.
6 You don't always have to follow TDD
Have you noticed that I mentioned "tests" several times up here? It might sound like I always had the tests before moving to the next. No, I didn't. Not always.
For example, the game's event logic was constantly changing, so I didn't for some part. More precisely, I was not sure about the design of the code. I wrote the code anyway, played the game, and saw how it felt as a player.
In other words, if coding also means design, which is sometimes the case, you shouldn't write tests. Instead, try as many things as possible and write tests once you are determined by the code.
7 Do one thing at a time
I manage tasks by having tickets on my notion board.
Whatever you use, your tickets or to-do items should be as granular as possible. They should always mean "do this one thing" because your working process can break if they mean more than one thing.
I had a ticket called "Polish" on my ticket board. I don't know what I intended when I made that ticket. Maybe I intended to refine the state lifecycle or something. What I know is that I ended up spending 2 hours on what seemed like a half-an-hour task. I found myself stuck in front of the monitor, thinking about how to change the structure, which I would regard as a bad practice. That obviously means the lack of a design process, but I skipped it somehow. Why? Because the resolution of the task was not clear in the first place. To have a clear technical design, you must also have a clear design of your development process.
Do one thing. Know what you're doing. This will encourage you to follow the persistent process of consuming a task, and you will thank yourself.
8 You can do it later(or maybe never)
There is another reason to have granular tickets on your board. It makes it possible to give up some of the tickets that are not mandatory. You can get it out of scope. Remember you can do some things later if it's not urgent.
In fact, I actually gave up on some of the features I was planning. I initially planned a "retry" feature to have a match again, but now, the game happens only once. I gave up on that feature. I decided it was not mandatory for the initial release. Sure, it would be better to have it, but done is better than perfect.
A project can get "done" only when you define what condition is "done". Otherwise, you will end up implementing good-to-haves for good. Unfortunately, I don't have a good level of self-control. I can't stop myself while I'm into coding, so I defined the scope of the release, even though the game is totally a personal project and has no business due; not to say that you should be more aware of the scope when you're working on real business projects.
9 It can never be perfect, but that's okay
Software can never be perfect. Requirements can grow. The business scale can grow. The budget can change. Everything's changing in a software project. How can it be perfect in a situation like that?
My current version of the Pong Game is imperfect. It works, though. You can play the game with your friend through an internet connection. So, it at least satisfies the requirements. However, the application does not expect thousands of users at the same time. The server won't handle it. AppSync itself is not made for this kind of intense real-time data in the first place. But I needed to build a POC (proof of concept) version and see how it would feel. I learned some things, and that's all I need for the first version. So, maybe I'll implement a better server for the game in the future, but only if I find it worth it. Or not at all. I'll just leave it as is and improve the UX once I get more users (and will put some Google ads and earn 8 bucks a month😂).
What's more important than shipping a perfect version is to expect changes. Keep further changes possible when necessary. Software can never be perfect, but it should always be "soft" enough to be changed.
10 Challenges are the best reward
Seriously, the best reward for developers is challenges.
To be honest, implementing the online multiplayer feature was hard. I had no knowledge of syncing states in multiple clients in real-time, which was one of the trickiest things I have implemented. But you know what? I enjoyed it. When the thing worked for the first time, I was so excited. I love this kind of sense I earn as I solve difficult problems. It doesn't come for free; it requires hard work, but therefore, it is worthwhile.
Go build your own stuff
So that's all from me. As I said first, you won't learn until you try it yourself. You may find my arguments wrong once you try, but that's also cool. That's bottom-up, actual knowledge with blood and bone, which is precious to have as a developer.
This kind of small, personal project is not only fun but also the best place to learn because you can try as many ideas as possible and often fail, which is not ideal in your work where you'd rather stay conservative.
What are you waiting for? Go build your own stuff and have fun. Happy coding:)
Pong game: https://www.play-pong-online.site
Source Code: https://github.com/yozibak/pong-online