The Formula Every Game Developer Needs to Know

Franks laboratory| 00:36:05|May 16, 2026
Chapters10
The chapter explains the core 2D collision concept: use the distance between circle centers and compare it to the sum of their radii to detect collisions, with an optimization of using squared distances to avoid square roots.

A practical, implementable guide to circle-based collision detection and a data-driven asset pipeline for 2D games, demonstrated with a layered health bar and an event-driven architecture.

Summary

Franks laboratory’s walkthrough shows how to build a solid starter kit for 2D games by focusing on circles for collision, data-driven assets, and clean project structure. Franks demonstrates Pyagraas theorem for circle collisions, then optimizes it by comparing squared distances to avoid square roots. The tutorial expands from simple circle collisions to a flexible architecture with a dedicated collision system and a collision manager, plus an event-driven flow that ties in audio and UI updates when damage occurs. The video also covers organizing UI elements (health bar and timer) under a common wrapper, styling with CSS (including a multi-layer health bar using before/after pseudo-elements), and making assets data-driven by loading audio and images from data sources. A new collision system file sits beside a collision manager, while the game orchestrator (game.js) wires events to play sounds and update the health bar. By the end, viewers see how to keep game logic modular, reusable, and easy to extend with additional collision techniques (rectangles, rotated polygons) as needed. Franks also emphasizes checkpointing with a “game starter kit part 17” for quick comparisons and bug fixes, and argues for separating data from logic to simplify future content additions.

Key Takeaways

  • Use Pyagraas (distance between centers) to determine circle collisions and compare against the sum of radii (dx, dy, distance squared) to avoid expensive square roots.
  • Implement a dedicated collision system for math and a collision manager for game logic, enabling clean separation between physics and gameplay responses.
  • Make asset loading data-driven by exporting audioData.js and image entries, then mapping over data to load resources with Promise.all, so new assets auto-load without code changes.
  • Layered health bar: create a two-layer UI using before and after pseudo-elements with CSS variables, achieving fast after-layer updates and a slower, stylized before-layer animation.
  • Bind game events to a decoupled system: when a collision occurs, emit player damaged and enemy died events, trigger sounds via the audio manager, and update UI health without tightly coupling components.
  • Adopt a data-driven approach for enemies: pull image data from enemy data and player data, enabling scalable asset management as new enemies are added.
  • Structure the codebase to support adding new collision types later (rectangle-rectangle, circle-rectangle, or polygon-based using the separating axis theorem) without bloating game.js.

Who Is This For?

Essential viewing for indie developers and JavaScript game devs building a modular 2D starter kit who want reliable circle-based collisions, clean architecture, and data-driven asset loading.

Notable Quotes

""Pyagraas theorem lets us calculate the distance between two points in a 2D space. And we can use it to quickly and cheaply determine if two circles are colliding.""
Intro on circle collision using distance between centers.
""We’ll structure our code base so that we can easily add other techniques later if we want to. Rectangle versus rectangle, circle versus rectangle, or even rotated polygon collisions using the separating axis theorem.""
Emphasis on modularity and future-proofing with additional collision types.
""The job of this file is purely to detect overlaps and report them. It doesn’t apply damage or game logic.""
Collision system vs. collision manager separation.
""We can see how player damage event notifies audio manager to play a sound and UI manager to update the health bar.""
Demonstrates the event-driven flow between subsystems.
""This line automatically maps over all entries. So any new one is included automatically.""
Data-driven asset loading core idea.

Questions This Video Answers

  • How do you implement circle-circle collision detection in a JavaScript game?
  • What is the benefit of comparing squared distances in collision detection?
  • How can I make assets data-driven in a 2D game using JavaScript?
  • What is a collision manager and collision system in game architecture?
  • How can I create a layered health bar with CSS for a game UI?
2D game developmentcircle collision detectionPythagorean theoremdata-driven assetsPromise.allcollision systemcollision managerevent-driven architecturehealth bar UICSS before/after pseudo-elements
Full Transcript
If someone asked me what the most crucial and most useful formula in 2D game development is, the answer would be straightforward. Pyagraas theorem lets us calculate the distance between two points in a 2D space. And we can use it to quickly and cheaply determine if two circles are colliding. In our game, so many things can be represented as circles. The player, enemies, world objects, projectiles, particles. If we want these objects to interact, we need to be able to determine whether they touch or overlap. To check if two circles collide, we need to know where they are. We treat their center points as two points in a 2D space and calculate the distance between them. We then compare that distance to the sum of the two radi. If the distance is greater than the sum of the radi, the circles don't touch. If it's less, they're overlapping. That's our collision. In practice, we can also skip the square root that Pythagoras normally requires and compare squared distances instead, which makes the check even cheaper. We'll structure our code base so that we can easily add other techniques later if we want to. Rectangle versus rectangle, circle versus rectangle, or even rotated polygon collisions using the separating axis theorem. In this class, we'll put circle collision to work. will detect collisions between the player and enemies, create a player health bar, and make it animate whenever the player takes a hit. Source code for the project at this stage is available to download in the resources section below. I called it game starter kit part 16. Use it if you hit a bug and want to compare your code to mine or just save it as a checkpoint you can always come back to. I'll be providing source code checkpoints throughout this project to make sure everyone can follow along and nobody gets left behind. By the end, we'll all have a well ststructured optimized starter kit we can use to build many different games. Some elements like game timer and health bar will be hidden in main menu and shown during game play. So let's put them inside a hot wrapper like this. So we can control them at the same time. We will have health bar container and health bar fill. This simple markup is all we need. Notice they are inside the hot div. Let's give it some CSS styles. I want it to cover the whole page. So, position absolute and inset zero will stretch it to fill the entire viewport and cran it to all four edges of the screen. I set pointer events to none to make sure it doesn't block mouse clicks and the Z index 1,00 to put it on top in front of all other elements. And because the timer is nested under hot, we can remove all the positioning code from the timer. Now, since its parent container is handling it, I also move the timer a bit down. We will control visibility of all elements nested under HUT by setting display to none. Here the timer and health bar will be hidden and shown that way. The health bar is made of a container and a fill. So let's style them. I set the container to position absolute. Give it a semi-transparent black background. width of 50%, height 25 pixels. And now we can see it here. I add a white border, overflow hidden, position it at top 25 pixels and left 25% to center it horizontally. This is just CSS styling. Feel free to design your health bar however you like. For example, I can give it a comic book style by applying transform skew 20°. And then add a box shadow like this. If you want more episodes in this series, let me know by hitting the like button. I'm planning to wrap up the main game loop before implementing any genre specific systems. I want to build a generic, flexible, and reusable boiler plate first. But what are we missing? I'm looking for things that every game needs. Particle systems, sprite animation. What else should we add to this base? We also have health bar fill, which sits inside the container. I give it width and height of 100% of its parent, a red background, and position relative. We could leave it like this and simply update its width with JavaScript based on the player's current health. And that would be the simplest approach. But if we want something a bit more interesting, we can also create a multi-layered animated health bar. I'll add a before pseudo element to the fill. This requires a mandatory content property set to an empty string like this. Then I set position absolute and insert zero so it covers the full size of the parent element. Now I remove the background from the parent and apply the background color to the pseudo element instead. Now the visible fill of the health bar depends on the size of this pseudo element. If I set transform scale X to 0.5, you can see it shrinks from the middle. I set transform origin to left to make it behave like a regular health bar. We will control the fill amount by setting a CSS variable, for example, health percentage spelled like this, which will update from JavaScript in a moment. This line simply means scale the fill horizontally based on the value of this variable or fall back to one meaning full width if the variable isn't defined. But as we said we want multiple layers in CSS. We can create an after pseudo element. The difference between before and after is simply their placement in the elements content. They behave the same way but before is rendered before the element's content and after is rendered after it. Both of them will share most of this base code and then I'll define styles specific to each one. For the before element, I remove the solid background and instead give it a linear gradient. I rotate it 90° so it goes left to right using a light red and white. I want a sharp break between the colors. So I set both stops to 85% like this. So this basically means the first color fills up to 85% and then immediately switches to white at the exact same point creating a hard edge instead of a smooth blend. Then I set transition. This means the transform animation will take 0.6 seconds use an ease out curve and start after a 0.5 second delay. I also give it a set index of one. Now for the after element, I give it its own gradient. For example, this color at 50% and this color at 50% as well. Notice again that both color stops are at 50%, which creates a sharp split instead of a smooth gradient. I set its Z index to two, so it appears in front of the before element. The key idea here is that the after element reacts to changes in the health percentage variable almost instantly while the before element animates with a delay. I can give the after element transition transform 0.2 seconds is out which makes it respond quickly. Now when the health bar value changes both layers scale at different speeds. after updates fast and before lags behind slightly and that creates this nice multi-layered animated health bar effect. Hut is the parent of the timer and health bar so it controls their visibility. I can hide them by setting display to none here. I want hat to be hidden in the main menu when the game first loads. I will control it from UI manager.js to follow our existing pattern. I define a property for hatelement and I point it to the HTML element using get element by ID. I separate these blocks and we also need access to the health bar. Notice we want the fill element, not the container. We no longer want to show and hide the timer but the entire hut diff which contains the timer and health bar. So I renamed this method to show hut and hide hut. And in all these four places I want to target this hot element variable. We renamed these methods. So now I have to go to game.js and inside start game instead of show timer we call show hut. And inside return to menu, we call hide hot. If your codebase has other references to show timer and hide timer, they have to be replaced with show hut and hide hot. I start in the main menu. There is no timer, no health bar. I press play and the health bar and timer appear. I can play the game and when I quit to menu the timer and health bar hide. We are trying to build a datadriven codebase but our audio and image assets are not datadriven. The load all method has all assets hardcoded as individual load calls. We want to follow a principle where we keep our data separated from our logic. So adding more content doesn't mean editing code. Inside the data folder, I create a new file I call audio data.js. I also add two more MP3 files to my audio folder. Player hurt MP3 and game over MP3. We will need these soon. You can use your own audio files or you can download these in the resources section below if you're following along. Inside audio data js, I export a constant called audio data. It will be an array containing one object for each audio file with a name and path like this. Notice that the load method expects name and path as parameters in this order as well. For now, we have audio file for pause, unpause, button hover, button click, player hurt, and game over. All of them are MP3 files sitting inside the audio folder. So here we have just data, no logic. I open audio manager JS. Up here I import audio data we just created. Down here we are hard coding each sound as individual this.load call inside promise all. I delete all of this and instead I await promise all. And inside we map over the audio data array. In JavaScript the map method takes an array and produces a new version of it where every item has been transformed by a function you provide. So here we take each name path object and turn it into a call to this.load passing it name and path which returns a promise for each call. The job of this line is to load every sound file at the same time. It takes the audio data array each entry being name path object that looks like this and it maps over it to turn each one of them into a call to this doload pass in the name and path as arguments. Load method returns promise each time it's called and promise all then takes all those promises and waits for every single one of them to finish before moving on. So now instead of one hard-coded line per sound file, we have a single clean line that handles all of them. And adding a new sound file only means adding a new entry to audio data.js. This line automatically maps over all entries. So any new one is included automatically. I open image managerjs and you can see we are doing the same thing here hard coding each image asset as an individual load call inside load all. How do I make this data driven? The player image will be defined inside player data js and enemy images are already referenced in enemy data js. So I import both of those files up here. In enemy datajs, you can see each enemy type has an image property right here. Images we want to load will be coming from different data sources. So inside image manager and its load all method, I create a variable I call image entries. It will be an array, a flat list of all images in our codebase. So we can call load on each of them. To get all anime images in, I use a spread operator on object values, passing it enemy data. Let me show you what this means step by step. Enemy data is an object with named keys, drifter, seeker, and so on. It looks like this. Object values scripts those keys and gives us plain array of just the values which looks something like this. Object values is a built-in JavaScript method that extracts all the values stored inside an object and returns them as an array. Objects store data as key value pairs. Object values ignores the keys and just gives us the values. In our case, that means we drop the enemy names drifter seeker and we keep just the objects containing their stats. Now I can chain map onto that. map loops over every item in the array and transforms it into something new. For each enemy object, we only care about one thing, its image property. So, we use it to build a new simpler object with just name and a path. It looks like this. This is exactly the shape our load method needs. Now, I need to get those entries into image entries. If I just put the map result directly without the spread, it would land as a nested array. a single item containing both enemies rather than two separate entries. We don't really want that. We are using the spread operator, the three dots to unpack that array. So each enemy lines as its own flat item inside image entries like this. And inside player data, I create an image property and name it the same as the image file I'm using for the player. In this case, player like this. Back in image entries, I include it as a simple object where both name and the path are taken from player data do image which basically resolves to this. Finally, just like in audio manager, I pass image entries into promise all with a map to this.load. The load method takes a name and a path for each image and loads it. Now any new entry added to enemy data with an image property is automatically loaded. Adding new enemies just means adding them to enemy data. No changes to image manager needed. I press play. Images and audio are still working. But now our assets are datadriven. Source code for the project at this stage is available to download in the resources section below. I called it game starter kit part 17. Use it if you hit a bug and want to compare your code to mine or just save it as a checkpoint. You can always come back to I'll be providing source code checkpoints throughout this project to make sure everyone can follow along and nobody gets left behind. By the end, we'll all have a well structured, optimized starter kit we can use to build many different games. Inside the systems folder, I create a new file I call collision system.js. Inside, I export class collision system. The job of this file is purely to detect overlaps and report them. It doesn't apply damage or game logic. We will start with collision detection between circles, but if we need, we can also write a function for collision detection between rectangles. Or we can use the separating axis theorem here to detect collision between polygons or whatever other collision detection method we might need. For now, simple circle versus circle collision detection is all we need. Collision system basically stores our generic collision detection methods which we can use whenever we need them. I will call this method check circle circle and it will expect two parameters A and B which will stand for both circles that we want to compare to see if they collide. For example, when we call it, we can pass it the player object as A and the enemy object as B. And this method will return true or false depending on whether the circular hitboxes overlap or not. To detect collision between circle A and circle B, we need to know where they are. So I will need the X and Y position of the center point of each circle and their radius. The horizontal coordinate of the center point of circle A is the horizontal position of the object passed S A, for example, the player plus the width of that object divided by two to get the middle. The vertical position of the center point of circle A will be the vertical position of object A plus half of the object's height. We will do the same for object B, which will be, for example, the enemy. The horizontal center point of circle B is the position of object B plus half of its width. And the vertical position of the center point is the vertical position of object B plus half of its height. Now that we have the precise anchor point for both objects, we can calculate the distance between them. To calculate if two circles collide, we need to know the distance between their center points and compare it to the sum of their radi. If we know where the circles are and how large they are, we can determine if they collide or not. So, I will need dx, the difference between the center point of circle a and the center point of circle b on the horizontal x-axis. and I will need dy where we compare the vertical center points of circle a and circle b. Then I calculate distance squared which is the squared distance between the two center points. This line multiplies dx * dx and dy * dy and adds them together. We do this because it avoids using a square root which would be more expensive and we can still compare distances using squared values. This formula is based on the Pythagorean theorem. We already spoke about it. Then I calculate rad sum which is the sum of both collision radi. So a collision radius plus b collision radius. Finally I return the result of distance squared less or equal than rad sum time rod sum. This means if the distance between the center points is smaller than the sum of the radi the circles overlap. So we return true. Otherwise if they are not touching we return false. This line might be a bit strange. Why am I multiplying the sum of radi by itself? It's because computing the actual distance would require a square root using this common formula for the distance between two points in a 2D space. Using a square root is relatively expensive for the CPU to calculate, especially if this collision check runs hundreds of times per frame. Instead, we can compare the squared distance against the squared sum of radi. This expression is mathematically equivalent to this expression but avoids the square root entirely. Since both sides of the comparison are squared, the relationship between them is preserved. If one is larger than the other, squaring both sides doesn't change which one is bigger. So writing it this way, we get the same result, but we avoid square root, which means faster code. We have our utility method check circle circle for circle on circle collision. We just need to make sure that both objects we pass in as A and B have width, height and collision radius properties. This method runs our collision detection logic and directly returns true or false. We know enemies will be involved in collision detection. And here in enemy data, we can see we already have width, height, and collision radius on each enemy. The value I use for collision radius could simply be half of the size of the enemy. For example, the enemy is 48 * 48 pixels. So the radius of the collision circle could be 24 pixels. We can change that later. We can also talk about visualizing these hitboxes or positioning hitboxes over irregular sprites later if we need that. For now, I will keep this simple. The player will also be involved in collision detection. So, it has to have width, height, and collision radius. Here, I set collision radius to 28 pixels, for example. It doesn't really matter. Now, to visualize this, the player sprite sheet is 64 * 64 pixels. The collision circle will originate from the middle of the sprite and its radius will be 28 pixels. You can use any values you want here depending on the art you are using. Width, height and collision radius are in the data but I also have to make sure we are turning them into properties on the entity itself. You can see we are doing that and the enemy has this dot width, this dot height and this dotc collision radius properties. On the player, we have width and height. And I need to add collision radius as a property here like this. The player has a reset method and update method. And down here, I add one more method. I call take damage. This method will simply take the amount of damage received and it will reduce the health of the player by that amount. In player data, I define max health. I set it to 12 for now. We are already importing player data here. So inside the player class, I create this domax health property and I set it to max health we just defined in player data. We will also need this.alth the current health which can be less than max health if the player took some damage. Initially when the player object is created, it will also be set to max health. Okay, so the player has health and when it takes damage, the health is reduced by the incoming damage amount. If I want to make sure health never drops below zero, I can set health to math max between zero and health reduced by the amount like this. If the damage amount would take health below zero, math but max will instead set it to zero because in that case zero would be the higher value. Just a little safeguard. We will also return true. We will need it when we implement invincibility in a moment. We have a health bar. We have a player that has health and can take damage. And we have a collision system with logic that can tell us if two objects overlap or not. Now, I need to write the actual collision checks. I could simply put that code here inside the update method on the game class, but we really want to prevent game.js from becoming huge and dealing with everything directly. So to stay organized, let's handle collision checks in a separate dedicated file. Inside the managers folder, I create a new file called collision manager.js. Inside, I export class collision manager. As usual, the job of this file will be to handle collision checks and collision response. This way gamejs is just a pure coordinator. Collision manager owns all game logic and responses and collision system contains only math. It just stores our collision formulas. The constructor here will expect collision system because we will need that circle on circle collision formula and it will also need event emitter so that we can send out events when things collide. It will have an update method that will run over and over and it will have a special method for each collision check we need. For now, we will check collision between the player and all active enemies. The update method will expect a player object and an array of active enemies as parameters. It will call this dot check player versus enemies and it will pass that player and enemies along as arguments and that method will expect them. Keep in mind that the enemies are reusable pulled objects with an active flag. So just as a safeguard, I say if the enemy we are checking is not active, continue. The continue keyword here means stop checking this enemy and move on to the next one. If this enemy is active, we run this line of code which will take collision system and its check circle circle method and it will pass it the player as circle A and the enemy as circle B. This method will take the player and enemy run the collision detection formula and directly return true if the circles collide and false if they don't. So if this expression is true, if the player and this enemy we are currently iterating over collide, we take that enemy and set its active flag to false. For now, the enemies don't have health. They will be instantly destroyed when they touch the player. Then a constant I call damage applied. It will be equal to player take damage. And as the amount we pass it enemy dot damage, the damage this particular enemy type is dealing. We have to make sure that our enemies have damage defined in the data and that we are converting that data value into a property on the enemy class. We wrote take damage so that it returns true. It's needed for when the player gets immunity later. For now, take damage always applies damage. So if take damage returns true, if damage applied is true, we want to emit an event. Let's go to constants.js JS and prepare these events. Down here inside the events object, I will add player related events. We will need player damaged event like this just following the established pattern and also player died which we will need when we implement game over soon. We will also have some enemy related events enemy damaged and enemy died like this. Back here, if the player took damage, I take this.vents from the constructor, which is the event emitter, and we call its emit method. We want to emit events player damaged. And as additional data, we pass it the player's current health and player's max health. For now, since the enemy will be destroyed after one hit, we will also emit events enemy died. And as additional data, we give it the enemy that was just destroyed. GameJS is our main orchestrator. As always, it just connects all of this together. Up here, I import collision system, which holds our circle on circle formula, and collision manager, which has the logic to check collisions between the player and enemies. And it has this update method that I need to call over and over. So, these checks actually run. I instantiate them here inside the constructor as usual. Collision system first because I need to pass it to collision manager as a dependency injection. I also pass it this eventevents the event emitter as the second argument. Collision manager expects both of these here. From inside the update method, we want to run collision manager update to actually do the collision checks. It will need the player and the list of all currently active enemies. Back here, render system also takes that list of active enemies and now we need them in two places. So instead of calling get active enemies twice, I will cach it in a helper variable. I call active enemies. It will be equal to enemy manager get active enemies. Enemies are reusable pulled objects. We only want the ones that are currently active in the game. Now I pass that cacheed reference to render system and as the second argument to update. I update its signature to make sure active enemies are expected here. As the update runs over and over, we want to be calling collision manager update passing it the player and active enemies. And this method will call check player versus enemies. This method will use our custom check circle circle for the player and enemies to see if the player is colliding with any of the enemies. We are comparing player against the array of all active enemies. So I actually have to put it into a for loop that will cycle over all of them. Now it's correct. Every time the update method runs, it will call check player versus enemies. But later it can also call check projectiles versus enemies or check player versus world objects and so on. We will handle all our collision checks here. Check player versus enemies method will cycle over all active enemies. If the player collides with an enemy, the player takes damage and we emit player damaged event and enemy died event. For now enemies get destroyed on contact with the player. We are using circle versus circle collision, but we can also swap this for a different method later if we want. We can add rectangle and rectangle collision logic here or even some more complex ones. We are well organized for all of that. As the update method runs, we are running the collision checks over and over to make sure the code reacts when a collision happens. We are emitting player damaged and enemy died events here, but we haven't registered listeners for them yet. they don't exist in our event emitter. So here in gamejs, I first move these lines here because all these are game state events and we will register player related event listeners. Here on events player damaged, we want to register a callback function that takes the player's current health and max health. Here in the audio folder, we have player hurt MP3 file. We included it inside audio data under this name. So when the player gets damaged, we will emit a sound event. We can see that event expects the name of the sound we want to play. So I give it player hurt. And the event will tell audio manager to play a sound with that name. Here in collision manager, we are emitting an event. So I need to make sure I import events from constants.js. I also fix my typo here. This should be player damaged spelled like this. When the player takes damage, the event is emitted. We registered a listener for it here which will react by doing a few things. But first, it will emit a sound event to play the player heard sound. If I save and play, if the player collides with an enemy, the player heard sound plays and the enemy is destroyed. Inside UI manager, we have health bar fill element. So I create a new method. I call update health bar. It will expect the player's current health and max health. If there is no health bar fill element, we return. Otherwise, we calculate the percentage which will be the ratio between current health and max health. I wrap it in math max and use zero here. This way, the percentage value can never drop below zero. Now I take that HTML element we created earlier. I use its style attribute and I use set property to set the value of the health percentage variable to the percentage we just calculated. This value will be anything between one full health bar and zero empty health bar. So 0.5 for example means the player is at 50% health. This health percentage variable will be used here in styles CSS inside scale X like this. We use that variable to determine how wide this element is or use one as a fallback. One means full scale original size. We are applying this to both before and after elements. And if you remember, we gave each one a different color and different transition speed and delay to create a layered health bar animation effect. Back in gamejs on events player damaged, I register a listener that will make UI manager call this new update health bar method passing it health and max health. This method will calculate the ratio between the current health and max health and set that variable to that value to transform and horizontally resize the health bar fill element itself. Now, if I play, whenever the player collides with an enemy, the health bar animates to show the player's current health. Notice that if I quit and restart the game, the health bar needs to reset. in player.js in its reset method, I set health back to max health. This will still do nothing because we are currently resizing the health bar only when take damage fires. But when the game starts, we always want the health bar to be full. So I can go to the start game method and call UI manager update health bar directly here. passing it the player's current health and the player's max health like this. Now, if I play, hitting an enemy plays the player heard sound. The enemy is destroyed and the health bar animates with the nice two-layered animation we set up. This is how using events helps to make our codebase more modular and decoupled. You can see how player damage event notifies audio manager to play a sound and UI manager to update the health bar. If we had a particle system, for example, we could just add that here to make it spawn a burst of particles or we can do anything your game needs. This is one of the cleanest ways you can handle a project like this.

Get daily recaps from
Franks laboratory

AI-powered summaries delivered to your inbox. Save hours every week while staying fully informed.