Quick answer: Use Construct 3’s Tilemap object for your world, instance variables for grid coordinates and stats, the Tween behavior for smooth tile-to-tile movement, and a JSON object for inventory and quest data. Turn-based combat lives on a separate layout with its own event sheet. The entire RPG framework relies on event sheets for game logic and Local Storage for persistence — no plugins required.
Tile-based RPGs are one of the most rewarding genres to build in Construct 3. The engine’s Tilemap object gives you a fast, GPU-rendered grid. Its event sheet system handles turn logic, stat calculations, and dialogue sequencing without writing a single line of code if you prefer visual scripting. This guide walks through every major system: tilemap world building, grid-snapped movement, turn-based combat, inventory, NPC dialogue, and quest tracking. By the end, you will have a working RPG framework you can expand into a full game.
Setting Up the Tilemap World
Start by creating a new project and adding a Tilemap object. Import a tileset image — a spritesheet where each tile occupies a fixed cell size, typically 16x16 or 32x32 pixels. Set the Tilemap’s tile width and height to match your tileset. Paint your first map directly in the layout editor: grass for walkable ground, walls and water for blocked tiles.
The key decision is how you distinguish walkable tiles from blocked tiles. The simplest approach is to designate specific tile indices as impassable. Store these in a global variable or an Array at the start of the layout.
// Event sheet: System > On start of layout
System: Set BlockedTiles to "2,5,8,12"
// Tile indices 2, 5, 8, 12 are walls/water/obstacles
// Helper function to check walkability:
Function "IsTileWalkable" (GridX, GridY)
Local variable tileIndex = Tilemap.TileAt(GridX, GridY)
If BlockedTiles contains str(tileIndex)
Return 0
Else
Return 1
For larger maps, consider using multiple Tilemap layers. Place ground tiles on one Tilemap, decoration tiles (trees, rocks, chests) on a second Tilemap layered above it, and collision data on a third invisible Tilemap. This keeps your collision logic clean and lets you swap decorations without affecting walkability.
Set the layout size to match your full map dimensions. If your tiles are 32x32 and your map is 30 tiles wide by 20 tiles tall, your layout is 960x640. Add a ScrollTo behavior to the player sprite so the viewport follows the player through maps larger than the screen.
Grid-Based Player Movement
RPG movement snaps to the grid. The player presses an arrow key and the character moves exactly one tile in that direction. This requires three instance variables on the player sprite: GridX (number), GridY (number), and IsMoving (boolean). Add the Tween behavior to the player sprite for smooth interpolation between tiles.
// Event sheet: Player grid movement
// Condition: Arrow key pressed AND not currently moving
Keyboard: On Right arrow pressed
Player: IsMoving = 0
Function: Call "IsTileWalkable" (Player.GridX + 1, Player.GridY) = 1
——
Player: Set GridX to Player.GridX + 1
Player: Set IsMoving to 1
Player: Set animation to "WalkRight"
Player > Tween: Start position tween
X: Player.GridX * TileSize
Y: Player.GridY * TileSize
Duration: 0.2s
Easing: "SmoothStep"
// When tween finishes, allow input again
Player > Tween: On tween finished
Player: Set IsMoving to 0
Player: Set animation to "IdleDown"
// Check for tile events (encounter, item pickup, etc.)
Function: Call "CheckTileEvent" (Player.GridX, Player.GridY)
Repeat the same pattern for left, up, and down, adjusting the GridX/GridY offset and animation name. The IsMoving flag is critical — without it, the player can queue multiple movements during a single tween and teleport across the map.
For diagonal movement (if your RPG allows it), check both the diagonal tile and the two adjacent cardinal tiles to prevent corner-cutting through walls. A diagonal move from (2,2) to (3,3) should only succeed if tiles (3,2) and (2,3) are also walkable.
Turn-Based Combat System
When the player steps on a tile that triggers an encounter (random or scripted), transition to a combat layout. Pass the enemy type and player stats using global variables or the Go to layout action after storing data in a persistent JSON object.
Combat uses a turn-order system. The simplest approach is alternating turns: player acts, then each enemy acts in sequence. Add instance variables to both player and enemy sprites: HP, MaxHP, Attack, Defense, Speed.
// Event sheet: Combat system
// Global variables
Global variable CombatPhase = "PlayerTurn"
Global variable SelectedAction = ""
// Player selects "Attack" from the menu
Button_Attack: On clicked
System: CombatPhase = "PlayerTurn"
——
System: Set SelectedAction to "Attack"
// Calculate damage: Attack - Defense/2, minimum 1
Local variable damage = max(1, Player.Attack - floor(Enemy.Defense / 2))
Enemy: Set HP to max(0, Enemy.HP - damage)
// Show damage number
System: Create DamageText at Enemy.X, Enemy.Y - 20
DamageText: Set text to str(damage)
// Check if enemy is defeated
If Enemy.HP <= 0:
System: Set CombatPhase to "Victory"
Function: Call "AwardExperience" (Enemy.ExpReward)
Else:
System: Set CombatPhase to "EnemyTurn"
System: Wait 0.8 seconds
Function: Call "EnemyAI"
Enemy AI can be as simple or complex as you need. A basic approach: if the enemy’s HP is below 30%, it has a 50% chance to use a heal ability (if it has one), otherwise it attacks. Store enemy ability lists in a JSON object keyed by enemy type. For boss enemies, add phase transitions that change the AI pattern when HP drops below certain thresholds.
After combat ends, return to the world layout and update the player’s stats. If the encounter was a scripted event (a boss guarding a bridge), set a global flag so it does not repeat. For random encounters, use a step counter that rolls a random chance every N steps.
Inventory and Item System
A JSON object is the most flexible way to store inventory in Construct 3. Create a global JSON variable called Inventory and initialize it as an empty array. Each item is an object with properties: id, name, type, quantity, and any stat modifiers.
// Scripting API: Inventory management
// Initialize inventory on game start
const inventory = runtime.globalVars.Inventory;
inventory.setAsJSON("[]");
// Add item function
function addItem(id, name, type, quantity) {
const items = JSON.parse(inventory.getAsJSON());
const existing = items.find(i => i.id === id);
if (existing) {
existing.quantity += quantity;
} else {
items.push({ id, name, type, quantity, stats: {} });
}
inventory.setAsJSON(JSON.stringify(items));
}
// Use item in combat
function useItem(id) {
const items = JSON.parse(inventory.getAsJSON());
const item = items.find(i => i.id === id);
if (!item || item.quantity <= 0) return false;
item.quantity -= 1;
if (item.quantity <= 0) {
const idx = items.indexOf(item);
items.splice(idx, 1);
}
inventory.setAsJSON(JSON.stringify(items));
return true;
}
For the inventory UI, create a scrollable list using a container with clipping. Each visible slot is a Sprite with a Text object for the item name and quantity. When the player opens the inventory, loop through the JSON array and populate the visible slots. Use the Pick by comparison condition to filter items by type (weapons, armor, consumables) for tabbed views.
Equipment works by adding instance variables to the player: WeaponID, ArmorID, AccessoryID. When the player equips an item, store its ID, look up its stats from a master item database (another JSON), and apply the stat bonuses to the player’s combat values.
NPC Dialogue and Quest Tracking
Dialogue uses an Array or JSON structure where each NPC has a conversation tree. The simplest approach is a linear sequence of text lines displayed one at a time with a typewriter effect. For branching dialogue, store each node with an ID, the text, and an array of choices that each point to another node ID.
// Event sheet: Dialogue system
// Dialogue data stored as JSON (set per NPC)
// Structure: { "nodes": [ { "id": 0, "text": "...", "choices": [...] } ] }
// Player presses action key near NPC
Keyboard: On Space pressed
Player: IsMoving = 0
NPC: Is overlapping offset (facing direction check)
——
System: Set DialogActive to 1
System: Set CurrentNode to 0
DialogBox: Set visible to true
DialogText: Set text to ""
// Start typewriter effect
System: Set TypewriterIndex to 0
System: Set FullText to NPC.DialogJSON.Get("nodes." & CurrentNode & ".text")
// Every 0.03 seconds, add next character
System: Every 0.03 seconds
System: DialogActive = 1
System: TypewriterIndex < len(FullText)
——
System: Add 1 to TypewriterIndex
DialogText: Set text to mid(FullText, 0, TypewriterIndex)
Quest tracking builds on top of dialogue. When an NPC gives a quest, set a global variable or add an entry to a Quests JSON object. Each quest has an ID, a description, an objective (like “defeat 3 slimes” or “deliver item to NPC”), a progress counter, and a completion flag.
When the player defeats an enemy of the required type, increment the quest counter. When they talk to the target NPC, check quest progress. If complete, award rewards and update the quest status. Persist the quest log alongside inventory and player stats using Local Storage so progress survives between sessions.
// Save game to Local Storage
Local Storage: Set item "save_player"
Value: JSON.stringify({
gridX: Player.GridX,
gridY: Player.GridY,
hp: Player.HP,
maxHp: Player.MaxHP,
level: Player.Level,
exp: Player.Exp,
layoutName: LayoutName
})
Local Storage: Set item "save_inventory"
Value: Inventory.AsJSON
Local Storage: Set item "save_quests"
Value: QuestLog.AsJSON
Related Issues
If your tilemap collisions are not registering properly, see Fix: Construct 3 Tilemap Collision Not Working. For issues with objects disappearing after switching layouts, check Fix: Construct 3 Object Not Found After Layout Change. If your save/load system is not restoring state between sessions, see Fix: Construct 3 Save/Load System Not Restoring State. And if animations are not playing when you expect them to, see Fix: Construct 3 Animation Not Playing or Stuck.
Start with movement and one enemy. Get combat feeling right before adding inventory and quests.