How to Design Levels in Stella Marina

Everything is Data Driven

In Stella Marina, each level is described by a JSON file that is exported from the Tiled Map Editor.

An example of a Level in Tiled

The system I created reads the JSON files and tells the game when and where an enemy should spawn, along with its custom properties that determine its type.

It does this by associating each Tiled object with its position on the y-axis, which directly represents when the enemy should spawn. The system then sorts the objects and inserts them into a std::queue, with the next enemy to spawn in the front. Thanks to this, finding the next enemy to spawn is an O(1) operation. It also allowed me to easily spawn the player anywhere on the level (to allow Level Designers to quickly test a specific part of the level), by simply filtering out enemies with a y-value under a certain threshold.

Enemy Properties

Another of my tasks was to make a system that allows the Level Designers to define custom properties for the enemies. The properties could set things like the movement speed or the bullet pattern of the enemies.

A list of properties for enemies

Modular Weapons

The most interesting property to explain is the weapon field. It is a modular bullet script written in JSON. Here is an example:

{
  "sprite": "bullet",
  "shots_per_second": 1.5,
  "bullet_speed": 30.0,
  "spin_speed": 30,
  "bullet_size": 4,
  "bullet_collider_size": 4,
  "bullets_per_shot": 5,
  "angle_between_bullets": 72,
  "angle_offset": 0,
  "use_rotation": true,
  "shoot_towards_player": false
}

How it will look in-game:

Thanks to the modularity of the bullet scripts, they can also be used outside the Tiled editor.

JSON::Weapon weapon = JSON::ReadWeapon("Weapons/boss_weapon_left_hand.json");
WeaponConfig weaponConfig(weapon, false);
myWeapon.Init(myPlayer, myBulletManager, weaponConfig, mySprite);

Because all it takes is a JSON file, the final boss was completely data-driven simply by reusing the existing bullet scripting system.