I Made Enemy AI That Can Do Anything (Vanilla JavaScript)
Chapters10
Introduces using the strategy pattern to implement multiple enemy behaviours as modular, interchangeable pieces; shows examples like stalkers, chargers, egg layers, and emphasizes a clean, extensible, data-driven system.
Franks Laboratory shows how to build flexible, data-driven enemy AI in vanilla JavaScript using the strategy pattern, composition, and modular behaviors.
Summary
Franks Laboratory delivers a hands-on guide to making enemy AI in vanilla JavaScript that’s clean, extensible, and easy to scale. The key idea is composition over inheritance: each behavior lives in its own file and can be swapped at runtime via a simple behavior factory. You’ll see a basic seek behavior that chases the player, then a drift behavior that wanders, and how to combine them for different enemy types. Frank demonstrates a data-driven approach, where enemy data defines type, size, speed, color, and which behavior to instantiate. The video then expands the system to multiple pools (one per enemy type) pulled by an object pooler, with a dedicated enemy spawner to handle waves and spawn edges. A crucial step is drawing sprites instead of rectangles and adding image support, then making sprites flip to face movement direction. All of this is tied together with a clear emphasis on modularity, dependency injection, and a scalable architecture that can support hundreds of enemy types and future features like waves, per-enemy resets, and reset hooks for behaviors that need state. By the end, Franks Laboratory shows a robust starter kit structure (part 14) you can download and compare against, or use as a checkpoint for your own projects.
Key Takeaways
- Use the strategic design pattern (a form of composition) to swap enemy behaviors at runtime without rewriting the base enemy class.
- Seek behavior updates the enemy toward the player every frame, while drift behavior randomizes direction every 2 seconds for wandering enemies.
- Behavior factory.create uses a static method and a switch statement to instantiate the correct behavior class based on enemy data, enabling easy expansion.
- A separate object pool per enemy type keeps memory usage predictable and scales as you add more enemy types; the spawner pulls from these pools to spawn enemies.
- Spawning from all four screen edges with randomized positions creates varied entry points, and a data-driven enemy file lets you add new types without changing core logic.
- Sprites can be flipped to face movement direction by rendering with canvas transforms (translate and scale), improving visual polish without extra animation work.
Who Is This For?
Essential viewing for game developers who want a scalable, data-driven approach to enemy AI in vanilla JavaScript, especially those looking to move from hard-coded behaviors to modular, interchangeable systems.
Notable Quotes
"Every behavior lives in its own standalone file. Modular independent pieces that plug seamlessly into your systems."
—Frank introduces the modular design where each behavior is a separate, pluggable unit.
"This is how real games are structured, and after this video, you'll be able to do it yourself."
—Emphasis on real-world applicability and the educational goal of the tutorial.
"We can do pretty much anything here because the system we are about to implement is robust and flexible enough to handle all of that."
—Describes the power and flexibility of the composition-based system.
"Composition makes our code more modular, reusable, and easier to scale as the game grows."
—Defines the core benefit of the design approach.
"The static create method is a class level utility that lets us call it without instantiating a factory object."
—Explains how the behavior factory is used.
Questions This Video Answers
- How does the strategy pattern help manage multiple enemy behaviors in a vanilla JS game?
- What's the difference between a behavior's update and reset methods, and why do some have reset?
- How can I implement per-type object pools for enemies in my game?
- How do I add new enemy types and behaviors without touching the core enemy manager?
- How do I flip sprites in HTML canvas to face movement direction without breaking alignment?
Vanilla JavaScriptGame AIStrategy patternComposition over inheritanceBehavior patternDrift behaviorSeek behaviorBehavior factoryObject poolingEnemy spawner','Spawn systems','Sprite rendering','Canvas transformations
Full Transcript
Enemy variety is one of the simplest ways to make your game more fun to play. We start with a simple enemy that chases the player or one that ignores them entirely and just wanders around. We can make an enemy flee after being hit or turn aggressive and attack. How do we achieve all of this in plain vanilla JavaScript? By using something called the strategic design pattern. A form of composition that keeps your code clean, organized, and endlessly extendable. With the system we're building today, we can go much further than the basics. Take a stalker enemy.
It only approaches from behind and turns invisible and unargetable the moment the player looks at it. For a charger, which wanders aimlessly until you stray too close, then launches straight at you. You can see here how we can use the render system to draw telegraphs, making the danger zone clear to the player. We can get as creative as we want. Look at the egg layer. It flies overhead, dropping eggs that you can shoot down, but if they hit the ground, they hatch into swarm links that will overwhelm you if left unchecked. Each one of these behaviors is simple to write on its own.
The challenge is structuring your codebase so that as you add more, it doesn't turn into a mess. That's exactly what the strategy pattern solves. Every behavior lives in its own standalone file. Modular independent pieces that plug seamlessly into your systems. We're building this like Lego. You can add new pieces, remove old ones, and nothing breaks. This is how real games are structured, and after this video, you'll be able to do it yourself. The same concepts apply far beyond enemies, too. Weapon systems, particle effects, special abilities. Once you have this pattern, you'll find yourself reaching for it everywhere.
Source code for the project at this stage is available to download in the resources section below. I called it game starter kit part 13. 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. Right now, we are hard coding this behavior into the base enemy class.
This code causes the enemy to chase the player. But what if I want to have a system where we have multiple behaviors for enemies and I can swap between them. Enemies can have different behavior based on the enemy type or maybe an enemy switches behavior depending on what is happening in the game. With the system we are about to implement, we could also combine behaviors. have an enemy chasing the player as one behavior while shooting projectiles at the player as a separate behavior. Or maybe we have an enemy that is running away from the player and burrows underground when the player is nearby.
We can do pretty much anything here because the system we are about to implement is robust and flexible enough to handle all of that. Inside the entities folder, I create a subfolder I call behaviors. Inside, I create a file I call seek behavior.js. JS. This is the one where the enemy seeks the player. It's hunting and chasing the player, always trying to move towards the player at all times. Inside, I export seek behavior as a class. If you like the art style, I've linked the artist below. They have a store with all these assets and a lot more.
If you want me to continue this series, don't forget to like the video to let me know and drop a comment. Can you think of any other enemy behaviors that would be fun to implement? I want to give it an update method like we have here on the base enemy class. We know that for an enemy to chase the player, we need the position of the enemy and the position of the player. And we always need delta time for everything to make sure things in our game happen based on real time, not based on frame rate.
from enemy.js. I will cut this entire code block and paste it here inside the update method on the seek behavior class instead. We are no longer inside the enemy class. So I have to replace this in these six places with an enemy reference that will be passed as a parameter. Here we are still comparing the difference between the position of the player and the enemy. We get a normalized vector to calculate the direction. And we apply that vector multiplied by enemy speed accounting for delta time to make the enemy with this behavior constantly try to move towards the player.
We just move that logic into a separate seek behavior class. And now we are able to apply this behavior to any enemy we want. I will create another file I call behavior factoryjs. This file will be just a simple middleman. Basically a centralized place that decides which behavior object to create and return based on some input. Instead of the enemy class knowing how to construct different behaviors directly, I will simply ask the behavior factory for the one it needs. Inside I export a class called behavior factory. It will have a static method I call create which expects behavior type as a parameter.
The static keyword here means this method belongs to the class itself not to individual instance of the class. So we don't need to create a new behavior factory object to use it. Instead of writing new behavior factory.create, we can simply call behavior factory create directly like this. By giving this function the static keyword, we are making it a class level method instead of an instance method. This method doesn't depend on any specific object. It's just a utility function attached to the class. This makes it easier to use and access because we can call behavior factory create directly without creating an instance first.
This method will be a simple switch statement. If the behavior type passed to it is seek, we return a new instance of seek behavior. We will also add a default case which will run if none of the cases match. Basically, as a fallback, I will import the seek behavior class up here at the top of the file. I'll also add a console log inside the default case just so we know if it ever gets triggered. For now, we only have seek behavior, but we can easily add many more later. As you can see here, the switch statement evaluates an expression.
In this case, behavior type. The value of that expression is then compared against the value of each case. If there is a match, the associated block of code runs. The way this will work, we will associate each enemy type with a specific behavior here in the data. Later, when we want an enemy to have multiple behaviors, we can turn this into an array. But let's take it step by step. For now, I have a drifter enemy type and its behavior type is set to seek. The enemy class constructor will expect data and behavior as parameters and we save them as class properties.
Now, I want to call the update method and run this code. So inside the enemies update method I say this.behavior.update we know this update method expects enemy delta time and player in this order. So I pass this as the enemy because here we are inside the enemy class. Then I pass along the delta time and player references. The trick here is that if we point this behavior on the enemy to a different behavior class, the enemy behavior will change. We just need to make sure all of our behavior classes have an update method. Enemy manager will handle assigning these behaviors to enemies through the behavior factory we wrote.
I imported up here from the entities behaviors folder. If you remember object pooler expects two parameters a factory function and the pool size. The factory function is the code here and this code is responsible for creating enemies to be stored in the enemy object pool. I will rewrite this factory function. Let's make it completely empty at first like this. Now I expand the body of the function here and I create a helper variable called data which will be equal to enemy data.drifter because we only have that one enemy type for now. The second temporary variable will be called behavior and it will be equal to behavior factory.create.
This static method we have here which expects behavior type and runs the switch statement to decide which behavior to instantiate. For now we only have the seek behavior. But I don't want to hardcode it. I want that behavior type to come from enemy data. So I pass data dobehavior type to make this datadriven. Now I can finally return the new enemy we are creating inside this factory function. I know the enemy class constructor now expects data and behavior as parameters because we set it up like that earlier from that behavior which we are now sourcing from enemy data.
We are calling the update method inside the enemy's update method which now points to the update method on the seek behavior class making this enemy chase the player. When we create the object pool of inactive enemies at the very beginning as soon as the game is loaded into the browser and the HTML canvas is initialized we pass this factory function into object pooler. That function defines the data and behavior associated with each pulled enemy. Notice that if I let the enemies catch up with the player, they all overlap and align behind the player because we haven't implemented any collision rules yet.
You can also see they don't align exactly with the player because the enemies and the player are different sizes. And here in the seek behavior, we are comparing the top left corners of the enemy and the player. Rectangles and images on HTML canvas are drawn from the top left corner. But we actually want to work with the vertical and horizontal center of each object for perfect alignment. So here, instead of comparing their top left corners, I will offset the player and enemies horizontal positions by half of their width like this. and player and enemies vertical positions by half of their height like this.
And now we are calculating their relative positions from their center points. If I play and let them align behind the player, you can see we get perfectly centered alignment. What we're doing here with separate behavior classes and behavior factory is called composition. Composition means we build complex objects by combining smaller independent pieces of functionality instead of hard- coding everything directly into one big class. It allows us to swap behaviors in and out at runtime, mix different behaviors together, and extend functionality without modifying the core enemy class. This is a common way to design flexible systems in game development.
Instead of using deep inheritance hierarchies, we compose objects from smaller parts that each handle one responsibility. Composition makes our code more modular, reusable, and easier to scale as the game grows. Let's add another behavior class just so we can really see how it all fits together. Inside the entities behavior folder, I create a new file called drift behavior.js. Inside, I export drift behavior class. Drift in motion will make the enemy choose a new random position at specified intervals and move towards it. This enemy is not chasing the player. It's minding its own business, drifting and wandering around.
In here, we can use any kind of custom logic we want to achieve the behavior we need. I will have this dot angle property. It will be a random value between zero and math.py * 2 which is the full circle. This enemy will be able to move in any direction. We will have two helper properties timer and interval. Timer will be counting from zero to the interval value over and over. Every time timer reaches 2 seconds, it will make the enemy change direction and the timer will reset and count again. We will handle that inside an update method.
I have to make sure all the update methods on all my enemy behaviors have uniform signature which means they all expect the same parameters in the same order. So this new update method will also take enemy delta time and player even if this specific behavior doesn't need the player reference. As the update method runs most likely 60 times per second. Change timer from line four will be accumulating delta time. Delta time is real time that is passing by between frames. So change timer here is basically just counting real time. If change timer accumulated enough that it's more than change interval which we set to 2 seconds.
Here we will randomize the angle again to make the enemy move in a different direction. And we reset timer back to zero so that it can count again. So at this point we have an angle value that gets randomized every 2 seconds. Now let's actually make the enemy move in that direction. We calculate dx and dy which represents the horizontal and vertical components of movement. dx is cosine of this angle and dy is s of the same angle. Passing this angle to math cosine gives the horizontal component of the unit vector in that direction and sign of the angle gives the vertical component.
Together dx and dy form a normalized vector pointing in the direction we want the enemy to move. Then we multiply these by the enemy's speed and delta time to update the enemy's position each frame. enemy x plus equals dx * enemy speed time delta time. The speed will be coming from enemy data. And each enemy type can have a different speed. I do the same for the vertical Y position as well. Enemy Y plus equals dy * enemy speed time delta time. This makes the enemy drift smoothly in the chosen direction. And every 2 seconds, the angle changes, causing the enemy to pick a new direction.
and drift naturally around the map. If you are a beginner, you don't have to fully understand the math behind this. It's a standard formula. You can always copy from somewhere. It will make more sense the more you use it. This drift behavior class has state. It has angle and change timer values that change during the lifetime of the enemy. Because of this, we might want to create a reset method to make sure these values are always set to their defaults whenever the enemy spawns or respawns. With this particular behavior, it wouldn't be a big deal if angle persisted from the previous life cycle since the angles are random anyway, but some behaviors will require a proper reset method to function correctly.
So, we might as well hook reset into the rest of our enemy logic at this point. I will also reset change timer back to zero to make sure the drifting timer starts fresh each time the new enemy is reused. Let's compare this drift behavior with seek behavior we wrote earlier. You can see that seek behavior is just an update method. It's stateless and the update method is all it needs. Drift behavior on the other hand has helper properties in the constructor which creates a need for it to have a reset method. We have to keep this in mind to preserve the plug-andplay nature of behaviors.
All behaviors will have an update method but some will also have a reset method, not all of them. To see this in action, we need more than one enemy type so we can observe the two behaviors side by side. Inside enemy data, I create a new entry for an enemy type I call seeker. I give it different dimensions, higher speed and so on. But mainly notice here that I'm giving it behavior type seek and the drifter enemy will have behavior type drift. Now I import the drift behavior class into behavior factory and I create a case for drift which will return new drift behavior like this.
So when we create our enemy object pool passing object pooler this custom factory function behavior factory create will assign either a seek or drift behavior depending on which behavior type we associated with that enemy inside enemy data. In the enemy reset method, we check if the behavior assigned to the enemy actually has a reset method. Some behaviors like seek behavior do not have a reset method. But drift behavior does. So we only call reset if that method exists. This setup keeps everything modular, flexible, and plugand play. Inside enemy manager, we currently have just a single enemy pool that we're filling with the drifter enemy types.
We want to have a separate pool for each enemy type. So I will create a property I call this.pools and set it to an empty object. We will create a pool for each enemy type here. I'll comment out the old single pool code for reference and rewrite it slightly. To do this, we'll look into our enemy data file. And for each enemy type defined here, we will create one object pooler. If I write for constant type in enemy data, this loop will iterate over all keys in the enemy data object. Each key represents an enemy type like drifter or seeker.
Here I'm basically giving each key a temporary variable name called type. inside the body of the loop. Type stands for drifter, seeker or any other enemy type as the forin loop iterates over enemy data. Inside the loop, we can create a new object pooler for that type and store it in this.pools under type. That way, each enemy type has its own reusable pool, keeping everything organized and efficient. What we're essentially creating here is a data structure that looks something like this. This approach also makes it easy to scale the game. Whenever we add a new enemy type to enemy data, the manager will automatically create a pool for it without any extra manual setup.
We already know that object pooler expects a factory function that creates one new object of the type we want to pull and enemy pool size as the second argument. Again I will just expand the body of the factory function and it will be almost the same as before with one little difference. The data we will be pulling from will be enemy data at the index of type. We know that type means drifter or a seeker and so on. So as the forin loop runs and cycles over the enemy types in enemy data, first it will create a pooler with a drifter type and then another pooler for seeker type.
The next line will be the same as before. Behavior is equal to behavior factory create and we pass it the behavior type from the data. So again first it will be drift then it will be seek for the second object pool. Doing it this way will also automatically create more object pools when we add more enemy types here in the data. Then this line will be the same as well. We simply create a new instance of enemy passing it the data and behavior. This foreign loop is currently cycling over. And now instead of having just one object pool, this code block will automatically create a separate object pool for each enemy type we have in the data.
Then we want to spawn an enemy. We will have this helper pool variable which will check this.pools from line 8 at the index of the enemy type we are looking to spawn. If there is no pool for this particular enemy type we are passing to it, we will create a console warn message that will say unknown enemy type and we will return a null. Then we change this line to call get on the pool from line 21 and we spawn that enemy and return that Spawn method will expect three parameters. what type of enemy we want to spawn and x and y coordinates where we want to spawn it.
Then we find the pool of that particular enemy type inside this dotpools from line 8. Now that we narrowed down the pool we need we get one enemy from that pool and we spawn it to get active enemies. We now need to return active enemies from all available object pools. So I create a helper variable called enemies and I set it to an empty array at first. We'll perform some operations on it here and then we return that array at the end. Again we create a forin loop that cycles over all entries inside this.pools from line 8.
Inside this pools we created one object pooler for each enemy type. So now it looks something like this. Each property holds a separate pool instance and each of those pool instances has its own active array. Inside the loop, I write enemies push and I pass it this.pools at index of type doactive. This line uses the spread operator to take all elements from that active array and push them individually into the enemies array. So instead of pushing the entire active array as a single nested array, we spread its elements out and merge them into one flat array.
By the time the loop finishes, enemies will contain all active enemies from every pool combined into one array. And then we simply return enemies. This way, the rest of the game doesn't need to care how many pools we have internally. It just gets one clean list of all currently active enemies. The spread operator is a JavaScript operator that expands an iterable like an array into individual elements. It's commonly used for copying arrays, merging arrays, and passing multiple values into functions. In our case, the spread operator takes all elements from the active array and spreads them out as separate values.
So, push adds them one by one into the enemies array instead of pushing the entire array as a single nested item. flat array looks like this. This is what we want. Without the spread operator, we would get a nested array. This would make iteration more complicated because we need two loops, one for the outer array and one for each inner array. By using the spread operator to flatten the arrays, we avoid that problem. Now, every time get active enemies is called, it returns one flat array containing all active enemies from all object pools. Inside the update method, we again use a forin loop to cycle over each type inside this.pools.
For every type, we call update all on the corresponding object pooler instance. This means each enemy pool updates its own active objects separately, but all within the same frame. The update all method will loop over that pool's active array and call update on each enemy. So even though we now have multiple pools internally, from the game's perspective, everything still updates cleanly and consistently every frame. And we do basically the same thing inside the reset method. For every type inside this.pools, we call release all on the corresponding object pooler instance to deactivate all enemies and move them back into that pool's inactive array.
When the game restarts, every enemy is properly reset and returned to its reusable pool, leaving all active arrays empty and ready for the next round. Enemy manager is now able to work with multiple object pools, and everything is automatic and datadriven. We can simply add new enemy type into enemy data.js, and the system will pooler for each one. assign the correct behavior through behavior factory create and manage their spawning, updating and resetting without changing anything inside enemy manager itself. This makes the whole system scalable and easy to extend. Adding a new enemy type is now mostly a matter of adding data, not rewriting logic.
And this is how you create code bases that scale. Before we add images, we will have each enemy type represented by a rectangle of a different color. That color will also come from enemy data. You can use any colors you want there. We are handling all drawing inside render system.js. So down here inside render enemies instead of this hard-coded color, we will make it dynamic by accessing the enemy we are drawing its data property and the color will sit on it there. Now let's spawn different enemy types. We are spawning here inside start game method but we changed the spawn method to expect type as the first parameter and then X and Y coordinates.
So I pass it drifter type here. The first three will be drifters that randomly walk around and the next three will be seekers that chase the player. If I save and play, you can see we successfully implemented two enemy types. Some of them are trying to catch the player. Some of them are completely ignoring the player and minding their own business. We have a solid, robust, datadriven system that will support hundreds of different enemy types with different stats and different behaviors. Before we have fun adding more enemy types, we should probably implement a proper spawning system rather than hard-coding six enemies here in the start game method.
And we will also add image support so that we can draw creatures and monsters instead of these simple rectangles. Source code for the project at this stage is available to download in the resources section below. I called it game starter kit part 14. Use it if you hit a bug and want to we'll all have a well ststructured Inside the managers folder, I create a new JavaScript file and I call it enemy spawner.js. The job of this file will be to control when and where enemies appear in the game. Instead of spawning enemies directly inside the main game loop or inside the enemy manager, we separate this responsibility into its own class.
This keeps our code modular and easier to scale later. The enemy spawner will handle spawn timing, choose which enemy type to create, and decide from which edge of the screen the enemy enters. When it's time to spawn, it simply tells the enemy manager to actually create the enemy instance. We separate this into its own class because spawning enemies is a completely different responsibility from managing them. The enemy manager should focus on things like updating enemies, rendering them, and removing them when they die. It shouldn't care when enemies appear or where they come from. That's a different concern.
By introducing an enemy spawner, we follow the single responsibility principle. Each system does one job and does it well. It gives us several advantages. We can easily tweak spawn timing without touching enemy logic. We can later introduce waves, difficulty scaling, or special spawn patterns. We can even have multiple spawners running at the same time. The enemy manager stays clean and focused. As usual, I export a class called enemy spawner and give it a constructor. It will also need an update method which will track time and decide when it's time to spawn a new wave. Then we create a spawn wave method which will choose a random enemy type, determine a spawn position, and tell the enemy manager to create that enemy.
Finally, we add a reset method which simply resets the internal spawn timer back to zero. This allows us to cleanly restart spawning. When the game restarts, a new run begins or the state changes without carrying over leftover timing from the previous session. I will need some helper constants. So, inside constants.js, I define enemy spawn margin and I set it to 100 pixels for now. We will also need enemy spawn interval. Let's set it to 2 seconds. Inside enemy spawner, I will import the constants I need. Game width, game height, enemy spawn interval, and enemy spawn margin.
The constructor will need a reference to enemy manager. So, it expects it as a parameter. Inside the constructor, we assign it to a class property called thisanime manager. passing a dependency into a class like this is called dependency injection. Instead of the spawner creating or searching for the enemy manager itself, we provide it from the outside. This keeps our systems loosely coupled and easier to test, replace or extend later. We will have a helper property I call spawn timer which will count from zero each time we want to spawn a new enemy or a new wave.
spawn interval will come from the imported constant. Now I want to cache the enemy types from the data because we don't want to loop through enemy data every time we spawn something. At the moment we have two enemy types, drifter and seeker. Enemy data is an object with key value pairs where the enemy type is the key and its associated configuration is the value. Inside the constructor, I initialize this enemy types as an empty array. Then I run a forin loop directly in the constructor. Which means this logic executes immediately when we create a new instance of enemy spawner.
The loop iterates over all keys in enemy data and pushes each key into this enemy types. So after this runs, we end up with something like this. This makes the system dynamic. If we add more enemy types to the data file later, they will automatically be picked up and included by the spawner without changing this code. As the update method runs, we increase this spawn timer by delta time. The real time that has passed since the previous frame. That delta time is passed into the method as a parameter. Whenever spawn timer becomes greater than or equal to spawn interval, we call the spawn wave method.
After that, we reset spawn timer back to zero so we can start counting again toward the next wave. Currently, spawn interval is set to 2 seconds. So, the spawn wave method will be triggered every 2 seconds. Inside spawn wave, we first decide which type of enemy to spawn. We choose a random type from enemy types array. If the index is zero, we get drifter. If it's one, we get seeker. To randomize this, we use math.random multiplied by the length of the enemy types array. Since array indexes cannot be decimal numbers, we wrap the entire expression in math.f floor to round it down to the nearest integer.
This line will now return either drifter or seeker or any additional types we add later. Now, let's say I want the enemies to spawn from all four sides of the game area. We can change this later if we want them to come from somewhere else. I'll create a helper variable called edge. There are four edges, top, right, bottom, left. And I want to choose one randomly. So, I use math.random* 4 wrapped in math. This will give me 0, 1, 2, or three. Then I create helper variables X and Y and a switch statement that uses the randomized H value.
If H is zero, we spawn the enemy at the top edge. Its horizontal position will be anywhere between zero and game width. So anywhere from left to right of the game area. Vertically, it will be hidden just above the top edge so it can float in smoothly. Currently enemy spawn margin is 100 pixels. So the enemy will start 100 pixels above the top edge and then move toward the player. Notice I use break here to stop the switch statement from continuing to the next case. If the randomized edge is one, we want to spawn the enemy 100 pixels beyond the right edge of the visible game area at a random vertical position between zero and game height.
Then we cover the bottom edge, a randomized horizontal position and the vertical position is the bottom of the visible area plus spawn margin. Finally, the last case is the enemy coming from the left like this. We basically generate a random number and based on that we set the enemy's X and Y position so that it can spawn at the top, right, bottom or left edge. Now we know what type of enemy we want to spawn and where to spawn it. So I can use enemy manager and its spawn method. We know that method expects type and X and Y position.
It will then try to get that enemy type from a pool and if it can find it, it will spawn it. So here I pass it type and X and Y position values. Reset method will just reset spawn timer from line six back to zero. We have a simple enemy spawner. Let's use it. As always, we will connect all the logic together here inside game.js. I import enemy spawner here. I instantiate it here with all the other managers. I know the constructor expects enemy manager. So I pass it here. The constructor expects it here. We are converting it to a class property here on line five.
And we do that because we want to call its spawn method here, which will spawn an enemy of the specified type at the specified X and Y coordinates. As the update method runs over and over, we also want to call enemy spawner update passing it delta time so that it can count time towards the next spawn by increasing spawn timer by the delta time value. Every time spawn timer reaches spawn interval, we run spawn wave function from line 21, which will pick a random enemy type and a random position. We can change this logic later if we want enemy waves to work differently.
We are well organized to do anything we want here inside start game method. Instead of hard coding these six enemies that spawn immediately as the game starts, the spawning is now handled periodically inside the update method. So here all I have to do is call enemy spawner reset to make sure we start from a clean starting point each time we start a new game. For now, all it does is reset the spawn timer to zero, but we might add more things here if we want more complex wave spawning system later. So, enemy spawner has an update method that runs over and over colon spawn wave method whenever the timer reaches the interval value.
That method chooses a random type and spawn position for the new enemy and spawns it in the game. If I save and run the code, I get a console error that says enemy data is not defined. I can see the error comes from enemy spawner on line 10. So I check it. We are looping over enemy data here. So we have to make sure we import it up here. Now if I save and play, we are spawning enemies periodically. We are spawning both types. We create it randomly. Seekers chase the player. Drifters just walk around randomly, completely ignoring the player.
Enemies come from four edges of the visible game area randomly. If we want to, we can easily change where the enemies spawn from and what types of enemies spawn. We have a simple working enemy spawner. Awesome. We are importing it to game.js JS up here instantiating it with the other manager scripts and we are passing it enemy manager as dependency injection. We are updating it passing it delta time so that it can count real time towards the next spawn. The update method calls spawn wave method periodically and this method decides which enemy type we want to spawn and where.
It doesn't spawn the enemy itself. It passes the type it decided and the position it randomly chose to enemy manager and its spawn method. Enemy manager will take those values and based on that it will know what enemy type to request from the object pool. It will then get that enemy from the pool and call spawn method on that enemy passing it the X and Y position where we want the enemy to spawn. The spawn method on the enemy will position that enemy at those We are drawing enemies as rectangles. I want to draw them as sprites.
Inside enemy data, I give drifter enemy type a new property. I call image and set it to enemy drifter. On a seeker enemy, I will add image and set it to enemy seeker. This is how I will name those assets. For now, they will be simple single frame images. I set width and height to match the image I'm using. Inside image manager js, I create a separate load call for each new art asset here inside load all method. I pass it the name of the image and the path where the art asset sits in our project folders.
You can use your own art assets or download the ones I'm using in the resources down below. If I save, I will get failed to load messages in the console. So I add the sprites into my images folder. And now I get confirmation messages that the image was loaded. Now that we are successfully loading the images, we also now have to draw them using render system. I will close all other files to keep myself organized. Inside render system, we have this render enemies method that currently just cycles over the array of active enemies it's receiving and draws a colored rectangle representing each enemy.
I create a helper variable I call enemy image. It will be equal to image manager get image manager is coming from up here on line 8 and we know its get method needs the name of the image we want. So I pass it that name from enemy data stored under enemy data image like this. This way it will draw the correct image for each enemy type. Same as we did for the player image. If the enemy image was found, we use the built-in draw image method. We pass it the image to draw X Y width and height of the anime image.
If no enemy image is found, this else block will run where we draw that fallback colored rectangle. If I play, you can see these little seeker bots. And my drifter enemy type is a little sluck with a big head. So far, the enemies are just a single frame, which might even be enough for some games. For example, games like Vampire Survivors only use minimalistic animations or no animations at all, which is better performance-wise if you want a game with hundreds of enemies on the screen at the same time. Our codebase will support simple sprites like this, but we can also easily add a sprite animation system if we want.
It will be nice to have the enemies flip left and right depending on their movement direction to make sure they always face the direction they are moving. Before we implement this, I open UI manager. Inside setup event listeners, I add question mark dot after each button reference. This is the optional chaining operator which safely checks if element exists before trying to use it. If the element is null, for example, if that button isn't present in the current HTML, JavaScript will simply skip the add event listener call instead of throwing an error. This makes our UI manager more robust because the same code can work across multiple screens without breaking if some UI elements aren't present.
Now, let's make enemy sprites flip left and right so they always face the direction they are moving. On the enemy class, I create a new property called this dotf facing left and initially set it to false. This will tell the render system which direction the sprite should face. Inside the update method, I store alt X the enemy's horizontal position before the movement happens. Then I run the behavior logic which may change the enemy's position. After that, I compare the new this.x value with old X. The enemy's current position compared to its old position. I want to figure out which direction is the enemy moving right now.
If this dox enemy's current position is smaller than old x enemy's previous position, it means the enemy moved to the left. So I set facing left to true. Otherwise, the enemy moved to the right and facing left is set to false. This way, the render system can simply check facing left and flip the sprite when needed, so the enemy always faces the direction it's moving. Keep in mind that this logic works for sprites that are facing to the right by default like this. If your sprite images are facing left, then you would need to invert this logic or edit the actual sprite sheets so the characters face right by default.
Now to actually draw the horizontally flipped sprites inside render system.js here in render enemies method. If we have an enemy image to draw, I wrap this entire code block in save and restore. This saves the current canvas state before we apply any transformation like translate or scale and then restores it afterward. That way the transformations we use for one enemy don't affect anything else we draw on the canvas. We are restricting all changes to this block between safe and restore only. If enemy facing left is true, we flip the canvas horizontally by first calling translate and passing it enemy x plus enemy width as the x position and enemy y as the y position.
The translate method moves the origin of the canvas coordinate system to this new position. Then I call ctx scale and pass it minus one and one like this. The scale method is used to scale the canvas coordinate system. A value of minus1 on the x-axis flips everything horizontally and one on the y-axis keeps the vertical scale unchanged. In our case, this effectively mirrors the sprite horizontally. So when we draw the image, the enemy appears flipped and faces the opposite direction. Scale flips the canvas horizontally from the origin point we set using translate. We are setting the origin point to enemy X plus enemy width.
Because when we flip the canvas with scale, drawing starts in the opposite direction from that origin. By shifting the origin to enemy's right edge, the sprite stays in the correct position on screen instead of appearing mirrored somewhere else. Then we call the same draw image method as before, but this time we pass it 0 0 as the X and Y values because after calling translate, we moved the canvas origin to the enemy's position. So 0 0 now represents that new origin point and the sprite is drawn relative to it. Else meaning that enemy facing left is false, the enemy is facing right.
We will draw the sprite as normal like this. Canvas transformations and how the origin point works might be a little confusing for beginners. It's not as important to fully understand at this point. Just keep in mind translate moves the canvas origin and scale can flip or stretch the coordinate system. For more information, you can look up HTML canvas transformations or check out some of my HTML canvas classes where we draw fractals because there we dive much deeper into how all of this works. inside enemy.js here on line 45 instead of this old x this will be just a helper constant variable like this because it's not needed anywhere else in this class okay so facing left is a class property here old x is just a variable like this limited to this code block if I play I will wait for enemies to come and when the player moves left of the enemy the enemy flips to face the player and how about the enemies that don't follow the player.
If you look at the enemy slug, it also flips to always face the direction it's moving. This is a pretty good simple way to handle your enemy sprites if your game uses only single frames. If you want to use animated sprite sheets like I use in some of my examples, we can cover that as well. Our codebase is fully ready to handle all of that and more.
More from Franks laboratory
Get daily recaps from
Franks laboratory
AI-powered summaries delivered to your inbox. Save hours every week while staying fully informed.



