Collision Configuration

How Reldens creates the physics bodies of room objects and how to configure which objects block the player, wander around or let the player walk through them.

Configuring collisions

Collisions are set in three places: the walls come from the room map, the global physics options come from the config, and each object decides if it blocks the player through its private params. Everything is done from the administration panel.

  1. Log in to the administration panel at /reldens-admin (the path can be changed with the RELDENS_ADMIN_ROUTE_PATH environment variable).
  2. Walls and map boundaries: every layer of the Tiled map whose name contains collisions becomes a solid wall. Open Rooms > Rooms (/reldens-admin/rooms), click the edit icon of the room and upload the map with the collision layers in the Map Filename field. See the layer name conventions for the map side.
  3. Global physics options: open Settings > Config (/reldens-admin/config), type rooms/world in the search box and click Filter. Click the edit icon of a row, change its Value and click Save. A world option that is not in the list (for example bulletsStopOnObject) is added with Create New, scope server and path rooms/world/bulletsStopOnObject.
  4. Per room options: in Rooms > Rooms edit the room and write the options that must be different in that room as JSON in the CustomData field, for example {"bulletsStopOnObject":true}. A room value replaces the global one.
  5. Objects that block the player: open Game Objects > Objects, edit the object and add "collisionType":2 to the JSON of the Private Params field (rocks, chests, fishing spots). Use "collisionType":4 with "hasState":true for the NPCs that walk around, and "collisionResponse":false on the doors so the player walks through them into the change point.
  6. Restart the server: the config, the rooms and the maps are loaded when the server starts.
Admin Panel - Config list filtered by rooms/world with the global physics options Admin Panel - Rooms list with the map file of each room

Configuration reference

Object Private Params (Game Objects > Objects):

  • collisionType - 1 the object is pushed by the player (default, used by the enemies), 2 the object never moves and stops the player, 4 the object moves by itself and stops the player (NPCs with random movement).
  • hasState - true is required by the respawnable objects with collisionType 2 and by every object with random movement.
  • collisionResponse - false lets the player walk through the body while the hit event still runs (doors).
  • randomMovement - makes the object wander around its tile, for example {"maxTiles":5}, see Random Movement below.
  • On a respawn area parent object the private params are copied to every spawned object.

Global world options (Config, scope server, path rooms/world/...), installed rows:

  • bulletsStopOnPlayer (boolean, 1) - a player hit by a bullet stops moving.
  • disableObjectsCollisionsOnChase (boolean, 0) and disableObjectsCollisionsOnReturn (boolean, 1) - the object body collisions are turned off while it chases a player or walks back to its original position.
  • groupWallsHorizontally (boolean, 1) and groupWallsVertically (boolean, 0) - merge the contiguous collision tiles into bigger bodies.
  • movementSpeed (float, 180), timeStep (float, 0.04), onlyWalkable (boolean, 1) and tryClosestPath (boolean, 0) - movement and path finding.

Room CustomData (Rooms > Rooms): the same world option keys, the complete list is in Room customData and the World Options below.

Code integration (advanced)

How the Physics System Works

Reldens uses the p2.js physics engine (server-authoritative). Every object that should physically exist in the world needs a physics body. Bodies fall into three types driven by the p2.js Body.type constant:

  • 1 - DYNAMIC: affected by forces, pushed by other DYNAMIC bodies. Default for all objects.
  • 2 - STATIC: immovable (invMass = 0). Cannot be pushed. Player stops at it.
  • 4 - KINEMATIC: moved only by its own velocity, never pushed (invMass = 0). Used by the interactive NPCs, required for the ones with random movement.

Default Object Body Type

The P2world constructor (lib/world/server/p2world.js):

this.worldObjectBodyType = sc.get(options.worldConfig, 'worldObjectBodyType', Body.DYNAMIC);

All object bodies default to DYNAMIC. The player movement system reapplies velocity every tick via Colyseus state updates. Two DYNAMIC bodies with equal mass push each other, so the player body displaces the NPC body on contact. Collision detection fires correctly (groups and masks include each other), but both bodies move as a result. Setting collisionType:2 (STATIC) on the NPC body gives it invMass=0, directing the full contact impulse to the player and stopping it.

How Objects Are Created With the Right Body Type

P2world.createWorldObject (lib/world/server/p2world.js):

let collisionType = sc.get(roomObject, 'collisionType', this.worldObjectBodyType);

The method reads collisionType directly off the roomObject instance. Because BaseObject.mapPrivateParams runs Object.assign(this, privateParamsObject), any property in the private_params JSON column becomes an instance property - so the value set in the database is picked up here automatically.

Configuring Collision Per Object (Database)

Add "collisionType":2 to the private_params JSON column in the objects table for any NPC that should physically block the player:

UPDATE `objects` SET `private_params` = JSON_SET(`private_params`, '$.collisionType', 2) WHERE `id` = <object_id>;

collisionType Values

  • 2 (STATIC) - the body cannot be pushed or moved, p2 never integrates its velocity. Player stops when walking into it. Use for rocks, chests, fishing spots and any interactable that never moves.
  • 1 (DYNAMIC) - default. NPC body is pushed by the player. Use for enemies that chase (they must move) and any object that should not block.
  • 4 (KINEMATIC) - the body moves by its own velocity and cannot be pushed, so the player stops when walking into it. Use for the interactive NPCs with randomMovement, a STATIC body never moves.

hasState Requirement for Respawnable STATIC Objects

When collisionType:2 is used on a respawnable object (e.g. the mining rock) that also has animation state synced via Colyseus, hasState:true must also be set in private_params. This ensures the body is created as a PhysicalBody with a bodyState attached, which is required for timing/animation state updates.

{"collisionType":2,"hasState":true}

Without hasState, the body is a plain p2.Body with no bodyState, and the Respawn plugin skips adding it to the room state entirely (RespawnPlugin.createRespawnObjectsInstancesInState, lib/respawn/server/plugin.js):

if(!objInstance.hasState){
    continue;
}
room.state.addBodyToState(objInstance.state, objInstance.client_key);

Which Objects Should Block the Player

Moving NPCs: collisionType:4

The interactive NPCs use KINEMATIC bodies with hasState:true, and the ones that wander around their tile also have randomMovement (ids from migrations/production/reldens-sample-data-v4.0.0.sql):

  • npc_1 (Alfred, id=5) - town NPC
  • npc_2 (Mamon/healer, id=8) - town NPC, KINEMATIC without randomMovement, so it never moves
  • npc_3 (Gimly/merchant, id=10) - town NPC
  • npc_4 (Barrik/weapons master, id=12) - town NPC
  • npc_5 (Miles/quest NPC, id=13) - forest level 1 NPC

Static interactables: collisionType:2

  • rock_forest_1_area (id=16) - mining rock respawn parent, also needs hasState:true
  • fish_spawn_forest_1 (id=17) - fishing spot in the river
  • chest_forest_1 (id=18) - treasure chest

Enemies: DYNAMIC

Enemy objects (EnemyObject, class type 4, created in the sample data by the respawn parents with class_type=7 and childObjectType=4) use DYNAMIC bodies - they need to move and chase the player. Movement stopping on enemy contact comes from game logic: CollisionsManager.playerHitObjectBegin (lib/world/server/collisions-manager.js) calls roomObject.onHit() on the enemy, which starts the battle, and CollisionsManager.playerHitObjectEnd stops the player body (playerBody.stopFull) when the contact ends.

Doors and transition triggers: no body blocking

Doors (class_type=2, runOnHit:true) fire the hit event when the player touches their body, which runs the door animation only. The door body is a full tile (shifted by yFix) on the same tile as the change point, while the change point body is half a tile (P2world.createChangePoint), so the door needs "collisionResponse":false in private_params: the player then walks through the door body (the hit event still fires) into the change point. With the default collisionResponse (true) the solid door body stops the player before the change point and the door never leads anywhere.

The sample town doors (objects 19, 22 to 27) use:

{"runOnHit":true,"roomVisible":true,"yFix":6,"collisionResponse":false}

The room change is NOT done by the door: it comes from the change point body on that tile, created either from a map layer whose name contains change-points or from the rooms_change_points records by StorageChangePointsCreator (lib/world/server/storage-change-points-creator.js). A door without a change point on its tile opens and does nothing else.

Fish spawn: tile layer boundary

The fish spawn point (id=17, fish_spawn_forest_1) sits in the river. The river's physical boundary comes from the map tile collision layer. The object itself carries collisionType:2, so its body is STATIC and also acts as an interaction target the player cannot push.

Random Movement

An object wanders around its original tile when its private_params contain randomMovement:

{"collisionType":4,"hasState":true,"randomMovement":{"maxTiles":5}}
  • maxTiles (default 3) - the farthest column and row offset from the original tile.
  • minDelay and maxDelay (defaults 3000 and 8000) - the random wait in milliseconds between two moves.
  • targetAttempts (default 10) - the random tiles tried per move before skipping it.

The flow:

  • ObjectsPlugin listens reldens.createdWorldObject and calls ObjectsManager.startObjectRandomMovement (lib/objects/server/manager.js), which sets the body original tile (originalCol, originalRow) to the tile the body was created on and creates the ObjectRandomMovement (lib/objects/server/object/object-random-movement.js) on roomObject.randomMovementBehavior. The respawn restore sets the original tile again on every new respawn tile.
  • The body must be a PhysicalBody (hasState:true, enemies set it in their constructor), otherwise a warning is logged and the object stays still.
  • Each move picks a walkable tile of the body path finder grid inside maxTiles whose whole path stays inside maxTiles from the original tile (a tile behind a wall reached only by walking out of the area is not used) and sets that path on autoMoving. A body outside the area walks back to its original tile. The move is skipped while the body is auto moving (chase or return), while its state is not active (dead) or while it is in battle with a player.
  • A path that kept the body on the same tile between two moves (blocked by a wall corner or by another body standing on it) is stopped (stopAutoMoving) and the body gets a new random target in the same move. The path of an object in battle is never stopped this way. A body pushed out of its area by another body walks back inside on its next move, because every target is inside maxTiles from the original tile.
  • ObjectsPlugin listens reldens.sceneRoomOnCreate and calls ObjectsManager.addStateBodiesToRoomState, so the bodies with state of the room objects are synced to the clients by the client_key (not only the respawn and bullet bodies).
  • NpcObject.executeMessageActions (also used by the traders) moves the interaction area of an NPC with random movement to its current body state position (this.state.x, this.state.y) before validating the interaction. The objects that never move keep the area of their creation or respawn position.
  • NpcObject.executeMessageActions (also used by the traders) calls ObjectRandomMovement.pauseForInteraction(client.sessionId) after a valid interaction: the current path is stopped and no new move starts while any player keeps the dialog open. The dialog close button sends {act: 'closeUi', id: objectId}, NpcObject calls resumeAfterInteraction(client.sessionId) on it and on the out of reach close, and ObjectsPlugin listens reldens.removePlayerBefore to call ObjectsManager.resumeObjectsMovementAfterInteraction for a player that left the room with a dialog open. When the last dialog is closed the next move is scheduled again with a new random delay.
  • RoomScene.handleObjectsManagerOnRoomDispose stops the timers.

The respawn parents pass randomMovement to their children. The sample data uses 3 for the aggressive enemies, 8 for the passive enemies and 5 for the NPCs.

Room customData and the World Options

A room customData key does NOT reach the physics world by itself. Two separate paths exist and both are explicit:

  • WorldConfig.mapWorldConfigValues(room, config) (lib/rooms/server/world-config.js) reads a fixed list of keys from room.customData into room.worldConfig: applyGravity, gravity, globalStiffness, globalRelaxation, useFixedWorldStep, timeStep, maxSubSteps, movementSpeed, allowPassWallsFromBelow, jumpSpeed, jumpTimeMs, tryClosestPath, onlyWalkable, wallsMassValue, playerMassValue, bulletsStopOnPlayer, bulletsStopOnObject, disableObjectsCollisionsOnChase, disableObjectsCollisionsOnReturn, collisionsGroupsByType, groupWallsVertically and groupWallsHorizontally. It runs before the world is created and worldConfig is what P2world reads for those values.
  • Anything P2world reads from the options root (allowSimultaneous, allowChangePoints, allowRoomObjectsCreation, usePathFinder, allowBodiesWithState, type) has to be passed in the object built by RoomScene.createWorld (lib/rooms/server/scene.js). usePathFinder is passed there from customData and allowSimultaneous from the client/general/controls/allowSimultaneousKeys config. allowChangePoints, allowRoomObjectsCreation, allowBodiesWithState and type are not passed by anything, so the first three are always true and type is always the default (nothing reads world.type).

Adding a new per room physics key means adding it to one of those two places, otherwise it is silently ignored.

Collision Groups and Masks

The group bits are defined in WorldConst.COLLISIONS (lib/world/constants.js) and the masks are built in WorldConfig.mapWorldConfigValues (lib/rooms/server/world-config.js) as collisionsGroupsByType, which P2world.createCollisionShape reads. They control WHICH bodies detect collision with each other:

  • PLAYER (group=1, mask=127) - detects collision with everything
  • OBJECT (group=2, mask=15) - detects players, objects, walls and player bullets
  • WALL (group=4, mask=127) - detects everything; used for map tile boundaries
  • BULLET_PLAYER (group=8, mask=63) - everything except drops
  • BULLET_OBJECT (group=16, mask=13) - players, walls and player bullets
  • BULLET_OTHER (group=32, mask=15) - players, objects, walls and player bullets
  • DROP (group=64, mask=5) - players and walls only

The whole collisionsGroupsByType map can be overridden per room through customData or globally through the server/rooms/world config. All game objects use group=2 (OBJECT) by default. The collisionGroup property on the object instance can override this. OBJECT (2) is the correct group for NPCs, it includes players and other objects in its collision mask.

collisionType Propagation for Respawnable Objects

For respawn parent objects (class_type=7), the private_params JSON is inherited by child instances in RoomRespawn.createNewObjectInstance (lib/respawn/server/room-respawn.js):

let clonedObjProps = Object.assign({}, multipleObj.objProps);

So setting "collisionType":2 on the respawn parent row is sufficient, all spawned children will also have collisionType=2 on their instances.

Constructor and DB Load Order

The BaseObject constructor calls mapPrivateParams(props) which does Object.assign(this, privateParamsObject), promoting all private_params JSON fields to instance properties. AnimationObject, NpcObject and EnemyObject call this.mapClientParams(props) and this.mapPrivateParams(props) again at the end of their constructors, so the DB private_params values override the assignments those constructors make for the same property (for example the interactionArea that NpcObject reads from the config is replaced by the "interactionArea":48 of the mining rock).

Only the assignments that run after the last mapPrivateParams call take precedence over the DB values: the fields set by subclass constructors that do not map the params again (like the server TimingObject isActive, timingTimer and timingCheckInterval) and the class fields like RockObject.respawnStateTime, which are set when the parent constructors already returned. collisionType and hasState are left to private_params so the DB drives them.

Related Documentation

Go Up