Skip to main content

Data Files

Open this Example

Data Files example

How can I load JSON files and use them in my game?

In this example, we will import world map data (thanks to Natural Earth) and use it to create a game where players race each other around the world! This is an example of how data could be imported from anywhere, not just from JSON files. For example:

  • You might want to use a level editor like Tiled to create your game levels, which you could export as JSON files and then import into your game.
  • Or you might want to use a spreadsheet to create a list of items in your game, and then import them as a CSV file.

See Data Files for a more conceptual overview of how to use data files in Easel.

Step 1: Add your data files to your project​

Start by adding your JSON file directly into your project. In our example, we will add our file called world.geo.json, which contains the world map data, to the mapData folder in our project.

Step 2: Import the data​

To import the data from your JSON file, you can use the import statement. This is how we import the world map data from our world.geo.json file into a variable named CountryData:

pub import CountryData from @world.geo.json as {
features as [{
id = properties.iso_a2 as String,
name = properties.sovereignt as String,
labelPos = @(properties.label_x, properties.label_y),
geometry as {
type as String,
coordinates as [[Vector] | [[Vector]]],
},
}]
}

Notice, we are using the as keyword with a schema to convert the data into a more convenient format for our game.

  • @(properties.label_x, properties.label_y) is used to combine the X and Y coordinates into a single Vector, which is easier to use in our game.
  • coordinates as [[Vector] | [[Vector]]] is used to convert the coordinates into a list of Vectors, which is also easier to use in our game. The reason why we specify two alternate patterns here [Vector] | [[Vector]] is because sometimes GeoJSON adds an extra level of nesting to the coordinates, depending on the type of geometry (Polygon vs MultiPolygon).
info

It is optional, but recommended, to use the as keyword to convert the data into a more convenient format for your game. One key difference to be aware of is the as keyword converts all the String keys in JSON into Symbols in Easel, which allows us to write data.labelPos instead of data["labelPos"] to access the data. This is both faster to type and faster to run, because Symbols are more efficient than Strings. See Schemas to learn more about this topic.

Step 3: Use the data​

Now we have imported our world data into the CountryData variable, we can use it to spawn country entities in our game world:

fn SpawnCountries() {
for data in CountryData.features {
Spawn country {
Country(
pos=data.labelPos*InvertY,
countryId=data.id,
countryName=data.name,
shapes=GeoJsonDataToShapes(
geoJsonFeature=data,
// center the country around its label
// so the body's position is inside the country
center=data.labelPos,
),
)
}
}
}

We have written a function GeoJsonDataToShapes to convert the GeoJSON data shapes into Easel shapes. This function handles the different types of GeoJSON geometries (Polygon vs MultiPolygon) using a match block:

fn GeoJsonDataToShapes(geoJsonFeature, center=@(0, 0)) -> shapes {
let data = geoJsonFeature
let shapes = []
match data.geometry.type {
"Polygon" => {
for regionData in data.geometry.coordinates {
shapes.Push(Polygon(regionData.Map(|p| (p - center) * InvertY)))
}
},
"MultiPolygon" => {
for multiRegionData in data.geometry.coordinates {
for regionData in multiRegionData {
shapes.Push(Polygon(regionData.Map(|p| (p - center) * InvertY)))
}
}
},
}
return shapes
}

See Data Files to learn more about how to import and use data files in Easel.

Full Code Listing​

We have focused only on the parts related to importing and using data files in this example, but there is a lot more to this game! See the full code listing below to learn more.

pub tangible category Category:Hero
pub tangible category Category:Region

pub const BoundaryHalfWidth = 180
pub const BoundaryHalfHeight = 90
pub const InvertY = @(1, -1) // World is Y-up and Easel is Y-down so have to flip

const FireworkColors = [#f00, #0f0, #f80, #00f, #b0f]

pub prop World.MinimapHidden = false
pub const Camera:Minimap = Spawn camera {
with MinimapHidden {
Viewport(
scale=(MinimapHidden ? 0 : 0.3),
anchor=@(-1,1),
aspectRatio=BoundaryHalfWidth/BoundaryHalfHeight
)
}
SolidBackground(color=#334e)
Camera(body=@(0, 0), radius=@(BoundaryHalfWidth, BoundaryHalfHeight))
}

pub import CountryData from @world.geo.json as {
features as [{
id = properties.iso_a2 as String,
name = properties.sovereignt as String,
labelPos = @(properties.label_x, properties.label_y),
geometry as {
type as String,
coordinates as [[Vector] | [[Vector]]],
},
}]
}

pub import ContinentData from @ne_110m_land.geo.json as {
features as [{
geometry as {
type as String,
coordinates as [[Vector] | [[Vector]]],
},
}]
}

fn GeoJsonDataToShapes(geoJsonFeature, center=@(0, 0)) -> shapes {
let data = geoJsonFeature
let shapes = []
match data.geometry.type {
"Polygon" => {
for regionData in data.geometry.coordinates {
shapes.Push(Polygon(regionData.Map(|p| (p - center) * InvertY)))
}
},
"MultiPolygon" => {
for multiRegionData in data.geometry.coordinates {
for regionData in multiRegionData {
shapes.Push(Polygon(regionData.Map(|p| (p - center) * InvertY)))
}
}
},
}
return shapes
}

pub prop World.TargetCountry
pub prop World.HeroFoundTarget
pub game fn World.Main(maxHumanPlayers=5) {
SolidBackground(#332255)
ImageSprite(@grain.png, body=@(0, 0), radius=4, color=#223355, repeatX=1000, repeatY=1000, layer=-100)
Camera(body=@(0, 0), radius=@(BoundaryHalfWidth, BoundaryHalfHeight))
Vignette(opacity=0.3)

SpawnContinents
SpawnCountries

SpawnEachPlayer owner {
await Tick(2s)
Subspawn unit {
Hero
}
}

TopContent<mission> {
H1 { "Welcome traveller!" }
}
await Tick(2s)
loop {
TopContent<mission> {
P(fontSize=2, bold=true) {
for i in Range(0, 3) {
%(3-i)
await Tick(1s)
}
}
}

let country = PickRandom(AllCountries)
TopContent<mission> {
P(fontSize=2, animate=Animate:FlyFromBottom) {
"Fly to "
Span(bold=true, color=#0f0) { %(country.CountryName) }
"!"
}
}
HeroFoundTarget = null
TargetCountry = country

MinimapHidden = true
while !HeroFoundTarget { await HeroFoundTarget }
let unit = HeroFoundTarget
MinimapHidden = false

unit.Strobe(shine=0.5, growth=0.5, dissipate=2s)
repeat 20 {
Spark(
body=unit, color=PickRandom(FireworkColors),
radius=0.25, speed=10, dissipate=1s, layer=-1,
glare=1, bloom=3, bloomAlpha=1, luminous=1, opacity=0.2,
)
}

TopContent<mission> {
P(fontSize=2, animate=Animate:FlyFromBottom) {
PlayerNameDisplay(unit.Owner)
" found "
Span(bold=true, color=#0f0) { %(country.CountryName) }
"!"
}
}

await Tick(2s)
}
}

fn SpawnContinents() {
for geoJsonFeature in ContinentData.features {
let shapes = GeoJsonDataToShapes(geoJsonFeature)
for use shape in shapes {
Spawn {
PolygonSprite(body=@(0, 0), color=#fff4, camera=Camera:Minimap)
}
}
}
}

fn SpawnCountries() {
for data in CountryData.features {
Spawn country {
Country(
pos=data.labelPos*InvertY,
countryId=data.id,
countryName=data.name,
shapes=GeoJsonDataToShapes(
geoJsonFeature=data,
// center the country around its label
// so the body's position is inside the country
center=data.labelPos,
),
)
}
}
}

pub collect World.AllCountries
pub field country.CountryId
pub field country.CountryName
pub field region.RegionCountry
pub fn country.Country(pos, countryId, countryName, shapes) {
use body=this
let color=HSL(25deg + 90deg*Random, 0.35 + 0.1*SignedRandom, 0.5)

CountryId = countryId
CountryName = countryName

Body(pos=, immovable=true)

for use shape in shapes {
Subspawn region {
RegionCountry = country
PolygonSprite(color=, layer=-50, shading=0.2)
PolygonCollider(
category=Category:Region,
collideWith=Category:None, sense=Category:Hero,
)
}
}

give AllCountries(country)
}

pub fn unit.Hero([owner]) {
use body=unit, radius=0.25, color=#fff
Body(pos=@(SignedRandom*100, SignedRandom*15), heading=1rev*Random)
ImageSprite(@plane.png, radius=4*radius, ownerColor=1, angleOffset=0.25rev)
PolygonSprite(Circle(radius=0), bloom=5, bloomAlpha=1, ownerColor=true, luminous=1, opacity=0.2)
ImageSprite(@plane-shadow.png, radius=4*radius, feather=0.5, screenOffset=@(0, 1.5 * radius), angleOffset=0.25rev, opacity=0.5, layer=-1)
PolygonCollider(shape=Circle, category=Category:Hero, sense=Category:Region)
TextSprite(PlayerName, fontSize=1, screenOffset=@(0, -1), vAlign=VAlign:Bottom, color=#fff, upright=true, strobe=false)
Camera(radius=10, noRotation=false, angleOffset=0.25rev, acquireRate=0.1, panningRate=1)

PolygonSprite(shape=Equilateral(radius=3), ownerColor=true, camera=Camera:Minimap, luminous=1)
on Paint {
Streak<trail>(radius=1, ownerColor=true, camera=Camera:Minimap, bloom=1, bloomAlpha=1, luminous=1, layer=-1, dissipate=5s)
}

on BeforePhysics {
if Joystick.X != 0 {
TurnRate *= 0.99
TurnRate += 0.005rev * Joystick.X
} else {
TurnRate *= 0.95
}

if Joystick.Y < 0 {
let direction = Heading.Direction
Velocity -= 0.01 * direction * Velocity.Dot(direction)
Velocity -= 0.1 * direction.RotateRight * Velocity.Dot(direction.RotateRight)

Velocity += -0.2 * Joystick.Y * direction
Spark(color=#fff, bodyOffset=@(0.5*radius, -radius), splatter=0.5, feather=1, glare=1, opacity=0.05, layer=-1, dissipate=1s)
Spark(color=#fff, bodyOffset=@(0.5*radius, radius), splatter=0.5, feather=1, glare=1, opacity=0.05, layer=-1, dissipate=1s)
} else {
Velocity *= 0.95
}

// Wrap around world
if Pos.X.Abs > BoundaryHalfWidth && Velocity.X.Sign == Pos.X.Sign {
delete Streak<trail>
Pos *= @(-1, 1)
}
if Pos.Y.Abs > BoundaryHalfHeight && Velocity.Y.Sign == Pos.Y.Sign {
Velocity *= @(1, 0)
}
}

prop unit.HeroRegion
on BeforeCollide that {
if !that.RegionCountry { continue }
HeroRegion = that
}
on AfterCollide that {
if !that.RegionCountry { continue }
if HeroRegion == that {
HeroRegion = null
}
}

BottomLeftContent {
TouchscreenOnly {
VirtualJoystick(radius=5, padding=2, knobColor=#fff8)
}
}

BottomRightContent {
HStack(padding=1) {
P(fontSize=1.5) {
with HeroRegion {
if HeroRegion {
"Current Location: "
Span(bold=true) { %(HeroRegion.RegionCountry.CountryName) }
}
}
}
}
}

with TargetCountry, HeroRegion {
if TargetCountry && HeroRegion.RegionCountry == TargetCountry {
HeroFoundTarget = unit
}
}

Subspawn {
with HeroRegion region {
let country = region.RegionCountry
TextSprite(country.CountryName, body=country, color=#fff, fontSize=1.5, stroke=0.3, upright=true, layer=-1)
Strobe(fade=1)
}
}
}
Open in Editor