Blastback uses a JSON-driven entity-component system. Every game object (player, enemies, weapons, projectiles, UI elements) is defined as a JSON entity file.
Content/Definition/Data/Game/entities/
├── mob/ # Characters (player, enemies)
├── weapons/ # Weapon definitions
├── projectile/ # Projectile definitions
├── drop/ # Pickup/drop definitions
├── props/ # Environmental props
├── ui/ # UI entity definitions
└── raw/ # Raw/utility entities
{
"name": "EntityName",
"visualsPath": "Mobs/sprite.ase",
"colliderSizeX": 12,
"colliderSizeY": 14,
"physicsLayer": 2,
"collisionLayerMask": [6],
"isTrigger": false,
"builtin": false,
"renderLayer": 0,
"components": {
...
},
"model": {
...
}
}| Field | Type | Description |
|---|---|---|
name |
string | Display name of the entity. |
visualsPath |
string | Path to the sprite asset (.ase, .png, or .atlas). |
colliderSizeX |
float | Horizontal collider size in pixels. |
colliderSizeY |
float | Vertical collider size in pixels. |
physicsLayer |
int | Physics layer this entity occupies. Use -1 to disable physics. |
collisionLayerMask |
int[] | Array of physics layers this entity collides with. |
isTrigger |
bool | If true, collisions generate trigger events but no physical response. |
builtin |
bool | If true, the entity uses a built-in C# controller instead of the generic one. |
renderLayer |
int | Rendering order layer. |
| ID | Layer | Description |
|---|---|---|
| 1 | Tilemap | World tiles and terrain |
| 2 | Player | Player entity |
| 3 | Enemy | Enemy mobs |
| 4 | Projectile | Bullets and thrown objects |
| 5 | Weapon | Weapon entities |
| 6 | Props | Environmental objects |
| 7 | Drop | Pickups and drops |
| 8 | Melee Impacts | Melee attack hitboxes |
| 10 | Particle | Particle effects |
Set physicsLayer: -1 and isTrigger: true for entities that don't need physical collision (e.g. floating enemies, guardians).
Components are defined in the components object. Each key is a component ID:
"components": {
"animator": {},
"movement.basic": { "velocity": 60 },
"weapon": { "id": "weapon.macana", "isPersistent": true },
"lua:combat_momentum": { "maxTier": 5 }
}- Standard components use their registered name (e.g.
animator,weapon,playerMovement) - Lua components use the
lua:prefix followed by the script ID - Component-specific config is passed as the JSON object value
Any component can define a commandMap to alias commands:
"melee": {
"hitBoxSize": 40,
"commandMap": {
"deflect": "attack"
}
}This maps the external command deflect to the internal attack command on the melee component.
The model object contains entity-level configuration that isn't component-specific:
"model": {
"maxHP": 1,
"customDeath": true,
"cost": 1,
"drops": [
{ "dropID": "drop.token", "min": 3, "max": 5 }
],
"events": { ... }
}| Field | Type | Description |
|---|---|---|
maxHP |
int | Maximum health points. |
customDeath |
bool | If true, uses custom death logic instead of default. |
cost |
int | Spawn cost for room generation. |
drops |
DropEntry[] | Items dropped on death. |
See models.md for weapon, projectile, and visual model fields.
Events are defined inside model.events and organized by source component:
"model": {
"events": {
"entity": {
"onDeath": {
"sound": "sound/player/hurt",
"particle": { "id": "explosion" },
"commands": [
"controlStateMachine.changeState death",
"world.cam.hitstop 0.5,0.3"
]
},
"onHarmed": {
"commands": ["combat_momentum.onBreak"]
}
},
"weapon": {
"onFire": {
"commands": ["combat_momentum.onHit"]
}
},
"animator": {
"walk.step": {
"sound": "sound/player/steps",
"particle": { "id": "step", "renderLayer": 2 }
}
}
}
}| Source | Events | Description |
|---|---|---|
entity |
onDeath, onHarmed, onFallEnd, onMobHit, onFired, onBounce, onDestroy, onSupersonic |
Core entity lifecycle events |
weapon |
onFire, attachWeapon |
Weapon-related events |
weapon.shooter |
onFire |
Shooter-specific events |
weapon.shooter.rideable |
onFire, onRecover |
Rideable shooter events |
weapon.melee |
onDeflect |
Melee deflection events |
tilemapDetector |
onFall |
Tilemap detection events |
animator |
{trigger}.{frame} |
Animator trigger frame events |
ai |
{nodeID}.onStart, {nodeID}.onEnd |
AI behavior tree node events |
Each event can trigger any combination of:
| Key | Type | Description |
|---|---|---|
sound |
string | Sound effect path to play. |
particle |
object | Particle effect to spawn (id, renderLayer, relX, relY). |
commands |
string[] | Array of command paths to execute. |
Commands in event arrays use space-separated arguments:
"commands": [
"controlStateMachine.changeState death",
"world.cam.hitstop 0.5,0.3",
"world.cam.shake 15",
"weapon.setVisible true",
"world.player.weapon.addAmmo 1",
"controlHandler.registerAction toss lua:action_resolvers.toss"
]{
"name": "Slime",
"visualsPath": "Mobs/slime.ase",
"physicsLayer": 3,
"collisionLayerMask": [2, 8],
"colliderSizeX": 20,
"colliderSizeY": 15,
"components": {
"animator": {}
},
"model": {
"maxHP": 1,
"cost": 1,
"drops": [
{ "dropID": "drop.token", "min": 3, "max": 5 }
]
}
}{
"name": "Player",
"visualsPath": "Mobs/MainCharacter.ase",
"colliderSizeX": 12,
"colliderSizeY": 14,
"physicsLayer": 2,
"collisionLayerMask": [6],
"components": {
"weapon": { "id": "weapon.macana", "isPersistent": true },
"input": { "profile": "input.player", "handlers": { "command": "command" } },
"controlHandler": {
"actionMap": {
"aim": { "resolver": "lua:action_resolvers.aim" },
"flip": { "resolver": "lua:action_resolvers.flip" },
"move": { "resolver": "lua:action_resolvers.move" },
"idle": { "command": "playerMovement.idle" },
"attack": { "command": "weapon.fire" },
"interact": { "command": "interactor.interact" }
}
},
"playerMovement": { "velocity": 120 },
"animator": { "triggers": { "walk": { "step": 4 } } },
"aimAssist": { "radius": 120, "targetClearingTimeout": 0.6 },
"controlStateMachine": { "templateID": "player" },
"tilemapDetector": {},
"stamina": {
"initialContainers": 4,
"maxContainers": 3,
"recoveryMultiplier": 10,
"containerThreshold": 100
},
"lua:combat_momentum": {
"maxTier": 5,
"speedPerTier": 8,
"comboTimeout": 1.5,
"decayInterval": 0.4
}
},
"model": {
"customDeath": true,
"events": {
"entity": {
"onDeath": {
"commands": [
"controlStateMachine.changeState death",
"world.cam.hitstop 0.5,0.3",
"world.cine.play death"
]
},
"onHarmed": {
"sound": "sound/player/hurt",
"commands": [
"world.cam.shake 15",
"world.cam.hitstop 0.07",
"combat_momentum.onBreak"
]
}
},
"weapon": {
"onFire": {
"commands": ["world.cam.shake 4,0.9", "combat_momentum.onHit"]
}
}
}
}
}{
"name": "Macana",
"visualsPath": "Weapons/bat.ase",
"components": {
"melee": {
"canHit": false,
"attackDuration": 0.3,
"hitBoxSize": 40,
"playerFriendly": true,
"commandMap": { "deflect": "attack" },
"deflection": {
"momentum": 200,
"hitstop": 0.4,
"camShake": 1
}
},
"shooter.rideable": {
"defaultProjectileID": "projectile.bat",
"addedCooldown": 0.3,
"commandMap": { "toss": "attack", "aimToss": "aimAttack" },
"projectilesPerShot": 1,
"playerFriendly": true
},
"shooter": {
"defaultProjectileID": "projectile.shell",
"projectilesPerShot": 1,
"playerFriendly": true
}
},
"model": {
"cooldown": 0.2,
"maxAmmo": 4,
"visual": { "aperture": 0.7, "duration": 0.4, "rotation": 1 },
"events": {
"shooter": {
"onFire": {
"sound": "sound/bat/gun",
"particle": { "id": "muzzle", "relX": 8 }
}
},
"melee": {
"onDeflect": { "sound": "sound/bat/return" }
}
}
}
}