Hello! Welcome back to our JRPG engine-building journey.
In our last lesson, you implemented a smooth, professional-feeling camera that follows the player around the map. Our world is now explorable, but it's also a bit lonely. Classic JRPGs are filled with life—towns bustling with people, dungeons with wandering monsters. It's time to start populating our world.
This lesson addresses the final learning outcome of Module 2: Design a data structure for NPC data and implement simple random movement patterns. We'll breathe life into our game world by adding characters who move around on their own.
By the end of this lesson, you will have:
- Designed an efficient, data-driven structure for defining NPCs.
- Refactored your code to accommodate both Players and NPCs.
- Implemented a simple "wandering" AI to make your NPCs move randomly around the map.
This step is crucial for making your game world feel dynamic and alive, a core element of the SNES-era JRPGs that inspire you.
1. Designing the NPC Data Structure
First, let's think about what information an NPC needs. From an architectural standpoint, we want a data-driven approach. Instead of hard-coding each NPC as a unique class, we should be able to define them in a configuration file, much like you'd fetch and display data from a JSON API in your front-end work.
An NPC needs:
- A unique ID.
- A starting position on the map (
x,y). - Appearance data (spritesheet, animation frames).
- Behavioral data (how it moves, how often, etc.).
- Interaction data (e.g., what dialogue it speaks).
While a simple JRPG with a few NPCs won't strain a modern computer, understanding how to structure this data for performance is a key architectural skill. In game development, how you lay out data in memory can have a massive impact, far more than in typical web applications.
This principle is at the heart of Data-Oriented Design (DOD). Your background in radiophysics gives you a head start in appreciating how hardware works, and this concept dives right into the interplay between software and the CPU/memory architecture.
Intro to Data Oriented Design for Games
To understand the professional mindset behind performant game data structures, let's watch a few key segments from the video 'Intro to Data Oriented Design for Games' by Nic Barker. It gives excellent, concrete examples of how small changes in data layout can lead to significant performance gains.
Please watch the data optimizations from 'Optimizing Data Structures: Structs vs. Classes' through to the end of 'Replacing Booleans with Enums and Avoiding Strings'. Focus on the 'why' behind each optimization: Why are arrays of structs faster than arrays of classes? How does member ordering affect memory usage? And why are string IDs a performance killer?
The key takeaways from that video for our purposes are:
- Memory Layout is King: Arrays of simple data objects (like C#
structsor JavaScript plain objects) are stored contiguously in memory. This is great for the CPU cache. Arrays of class instances are often arrays of pointers, leading to scattered memory access ("pointer chasing"), which is slow. - Size and Order Matter: By ordering object fields from largest to smallest (e.g., a number before a boolean) and using the smallest necessary data types (e.g., a 1-byte enum instead of a 4-byte default), you can pack more data into a single cache line, getting more value from each expensive trip to main memory.
- Avoid Strings for IDs: As the GTA Online example showed, using strings for internal identifiers is a performance trap. They are slow to process and compare. Unsigned integers are vastly superior.
With these principles in mind, here is a robust, data-driven structure for an NPC. We can imagine this living in a npcs.json file.
// A potential entry in our future npcs.json file
{
"id": 1, // Using a number, not a string!
"x": 8,
"y": 5,
"spriteSheetSrc": "/images/characters/npc1.png",
// We'll add animation data later
"behavior": {
"type": "WANDER", // The type of movement AI
"moveInterval": 4000, // Time in ms between move attempts
"moveSpeed": 1, // Tiles per move
"moveFrequency": 0.8 // 80% chance to move after interval
},
"dialogue": [
"The weather is nice today.",
"Have you seen the king?"
]
}
This structure is clean, performant, and easy to manage, separating the "what" (data) from the "how" (code). The behavior object is directly inspired by the properties seen in tools like RPG Maker and RPG in a Box.
2. Implementing NPC Movement Behavior
Now that we've designed the data, let's implement the behavior. How do we make an NPC wander around?
The core logic is a simple state machine: an NPC is either waiting or moving. It waits for a set interval, then attempts to move in a random direction.
This requires a small but important refactoring. Until now, our movement logic was likely tied directly to the Player class. To support NPCs, we need to generalize it.
Game Dev: NPC Movement - Rob Williams
The article 'Game Dev: NPC Movement' by Rob Williams perfectly describes this exact process. He details refactoring a player-only system to be generic and then building a simple 'Strolling' AI on top.
Please read the opening sections of the article, from the beginning down to the Strolling image. Notice his realization that the movement system should have been generic from the start, and pay close attention to how he models the NPC's state machine: a 'Wait state' and a 'Movement state'.
Let's apply these ideas to our engine.
2.1 Refactoring for Generic Characters
First, create a Character class that Player and NPC can both extend. This class will contain all the shared logic you've already built, like position, animations, and the visual interpolation for smooth movement.
// Character.js
class Character extends GameObject {
constructor(config) {
super(config);
// ... all the properties like isMoving, pixelX, pixelY, etc.
}
// ... move, updatePosition, updateAnimation methods
}
// Player.js
class Player extends Character {
constructor(config) {
super(config);
// ... player-specific logic
}
handleInput(input) {
// ... logic for player movement based on keys
}
}
// NPC.js
class NPC extends Character {
constructor(config) {
super(config);
// ... npc-specific logic
}
// We will implement the AI here
}
This is a much cleaner architecture that avoids code duplication and makes it easier to add new types of characters in the future.
2.2 Implementing the Wander AI
Now we can implement the "wander" AI within the NPC class. We'll use a timer to manage the wait state.
- Each
NPCwill have a timer that counts down. - When the timer reaches zero, the NPC decides on its next action.
- It picks a random direction.
- It calls the generic
movemethod (which you'll move to theCharacterclass). This method already handles collision checks. - The timer is then reset.
Here’s what the NPC class might look like:
// NPC.js
class NPC extends Character {
constructor(config) {
super(config);
this.behavior = config.behavior;
// Timer for the WAIT state. Initialize with a random offset.
this.movementInterval = this.behavior.moveInterval;
this.movementTimer = this.movementInterval * Math.random();
}
update(deltaTime, { map }) {
// Call the parent update to handle ongoing movement interpolation
super.update(deltaTime);
// If we are already moving, don't decide a new move
if (this.isMoving) {
return;
}
// Count down the timer
this.movementTimer -= deltaTime;
if (this.movementTimer <= 0) {
this.decideNextMove(map);
}
}
decideNextMove(map) {
// Reset the timer for the next decision
this.movementTimer = this.movementInterval + (Math.random() - 0.5) * 500;
// Only move some of the time, based on frequency
if (Math.random() > this.behavior.moveFrequency) {
return;
}
// Pick a random direction
const directions = ["up", "down", "left", "right"];
const chosenDirection = directions[Math.floor(Math.random() * directions.length)];
// Attempt to move (the move method is now in the Character class)
this.move({ direction: chosenDirection, map });
}
}
Finally, in your main game file, you would create instances of these NPCs and call their update and draw methods in the game loop, just like you do for the player.
// In your main game initialization
const npcDataList = [ /* ... load from your JSON data ... */ ];
const npcs = npcDataList.map(data => new NPC(data));
// In your game loop's update phase
npcs.forEach(npc => npc.update(deltaTime, { map }));
// In your game loop's draw phase (inside the camera translation)
npcs.forEach(npc => npc.draw(ctx));
Test your understanding!
In our NPC's behavior data, we have moveInterval: 4000 and moveFrequency: 0.8. Describe what you would observe if you changed these values to moveInterval: 1000 and moveFrequency: 0.1.
Show answer
With moveInterval: 1000 and moveFrequency: 0.1, the NPC would think about moving much more often (every second), but would actually choose to move only 10% of those times. The overall effect would be an NPC that moves very infrequently and seems hesitant. It might twitch by changing direction often without actually taking a step. This is in contrast to the original settings, where it decides to move less often (every 4 seconds) but is much more likely (80% chance) to follow through with a move when it does.
Conclusion
You've now added one of the most important ingredients for a living JRPG world: autonomous characters. Your town maps will no longer feel like static images but like places with inhabitants going about their day.
Today's key takeaways are:
- Data-Driven Design: We designed a clean, extensible data structure for NPCs using JSON, informed by professional performance considerations like data locality and using integer IDs.
- Code Generalization: We refactored our character logic, creating a base
Characterclass to share code between thePlayerandNPCs, which is a robust and scalable architectural pattern. - Simple State Machines: We implemented a simple but effective "wander" AI using a timer-based state machine to switch between
WAITINGandMOVINGstates.
With this, you have completed Module 2! You now have a solid foundation for world navigation and interaction.
Preview of the next lesson:
Our character can now explore a world populated with other characters. But how do we transition from the world map to a menu, or a battle? In the first lesson of Module 3, we will tackle a cornerstone of game architecture: designing and implementing a scene manager to handle transitions between different game states like the map, a main menu, and back again.
Can't find a good explanation? Sign up and we'll make it for you
Sign up