Advance Example
Introduction¶
Bellow we could see a advance example that tries to illustrate how to use an ECS to perform a concurrent task, in this example an animal race.
This separate our entities components and systems, they concern and how they work with each other.
Running the example¶
If you like to run this example, from the root path of this project you could run the gradle task for your platform:
> graddlew runSampleJvm
> graddlew runSampleLinuxDebug
> graddlew runSampleLinuxRelease
> graddlew runSampleMingwDebug
> graddlew runSampleMingwRelease
Source Code¶
You could browse this code on github.
/*
* Copyright (C) 2020 Juan Medina
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import com.juanmedina.kecs.dsl.add
import com.juanmedina.kecs.dsl.world
import com.juanmedina.kecs.system.System
import com.juanmedina.kecs.world.World
import kotlin.math.min
import kotlin.random.Random
/**
* Example of a animal race using a ECS, all animals will race following
* a mechanical rabbit as lure.
*
* This example is has an inspiration of the classical
* horse race example used to teach concurrency and threads.
*
* However since we use a ECS everything runs concurrently in a
* single thread so we could have thousands of animals racing
* without performance impact.
*
* The output of this program when running will be something like :
*
* 100 animals running....
*
* Race complete: 100 % [██████████████████████████████] 9.976s
*
* Race end after 52830 loops
*
* The Winner is Forcibly Vocal Wasp!
*
* Mechanical Rabbit arrived in 5.0s
*
* Final lines:
*
* 1st Forcibly Vocal Wasp in 5.47s
* 2nd Solely Working Guppy in 5.48s
* 3rd Evenly Factual Cougar in 5.531s
* 4st Suitably Elegant Piglet in 5.533s
* ....
* 97st Unlikely Assuring Hagfish in 9.864s
* 98st Extremely Infinite Chipmunk in 9.926s
* 99st Broadly Major Minnow in 9.954s
* 100st Violently Charming Kangaroo in 9.974s9s
*
**/
// Constants
/** How many animals will we have in our race **/
const val NUM_ANIMALS = 100
/** Minimal Speed of each animal, in ft/s **/
const val MIN_ANIMAL_SPEED = 30.0f
/** Max Speed of each animal, ft/s **/
const val MAX_ANIMAL_SPEED = 55.0f
/** Race length in yards **/
const val RACE_LENGTH_IN_YARDS = 100.0f
/** Race length in feet **/
const val RACE_LENGTH = RACE_LENGTH_IN_YARDS * 3.0f
/** The lure speed, it will reach the end in 5s **/
const val LURE_SPEED = RACE_LENGTH / 5.0f
/** Number of blocks for our progress bar **/
const val NUM_BLOCKS = 30
// Components
/** A lure component, it has just a name **/
data class Lure(val name: String)
/** A animal component, it has just a name **/
data class Animal(val name: String)
/** Movement status, running or stopped **/
enum class MovementStatus {
Running,
Stopped
}
/** A movement component, it has an speed, in ft/s, and a status **/
data class Movement(
val speed: Float,
var status: MovementStatus = MovementStatus.Running
)
/** A position component, includes how long has taking to be there **/
data class Position(var at: Float, var time: Float = 0.0f)
/** A winner component, contains its name **/
data class Winner(val name: String)
/** Race status, running or ended **/
enum class RaceStatus {
Running,
Ended
}
// Helpers
/** get a random Float in a range **/
fun ClosedRange<Float>.random() = start + (
(endInclusive - start) *
Random.nextFloat()
)
/** get a random capitalized String from a String List **/
fun List<String>.randomCapitalize(): String {
return this[Random.nextInt(1, this.size)].capitalize()
}
/** get a random animal name like : Unlikely Assuring Hagfish **/
fun randomAnimalName() = "${adverbs.randomCapitalize()} " +
"${adjectives.randomCapitalize()} ${animals.randomCapitalize()}"
/** get a float with 3 decimals positions **/
fun Float.threeDecimals() = (this * 1000).toInt() / 1000.0f
/** get a string with suffix from a Int like: 1st, 2nd, 3rd.. **/
fun Int.withSuffix() = "$this" + when (this) {
1 -> "st"
2 -> "nd"
3 -> "rd"
else -> "st"
}
/** format a int in three digits with spaces on the left **/
fun Int.threeDigits(): String {
val digits = this.toString().length
val remaining = 3 - digits
return " ".repeat(remaining) + "$this"
}
// our race
fun animalRace() {
// we will create our world adding 4 systems, each of them takes care of
// only one concern
// - the movement system it take care or moving things, both animals
// and the lure
// - the winner system will take care or knowing which animal won
// - the race system will take care to know when the race has ended
// - the progress system will draw a progress bar with the overall
// completion, but it could be removed without affecting the logic
val world = world {
+MovementSystem()
+WinnerSystem()
+RaceSystem()
+ProgressSystem()
}
// we create and entity that has the race status set to running
world.add {
+RaceStatus.Running
}
// we create the lure entity, with him name, at the initial position
// and with movement set to the lure speed, we will save the reference
// to use it latter
val lureRef = world.add {
+Lure(name = "Mechanical Rabbit")
+Position(at = 0.0f)
+Movement(speed = LURE_SPEED)
}
// we will create as many entities as animal we need in the race
for (x in 1..NUM_ANIMALS) {
// we add an entity that is an animal, with a random name
// it will start at the initial position and have a
// random speed between the min and max animal speed
world.add {
+Animal(name = randomAnimalName())
+Position(at = 0.0f)
+Movement(speed = (MIN_ANIMAL_SPEED..MAX_ANIMAL_SPEED).random())
}
}
println("$NUM_ANIMALS animals running....\n")
// we will count how many update loops we have done
var loops = 0
// we will ask the world to return a single component from a single
// entity that has a RaceStatus, and end the loop if the race has
// ended
while (world.component<RaceStatus>() != RaceStatus.Ended) {
loops++
// triggers the world update, each time it send the delta time from the
// last update
world.update()
}
println("\n")
// we will print the total loops, this number will be random since we have
// random animal speeds they will take different time to complete the race
println("Race end after $loops loops\n")
// we will get from the world the Winner component from a single entity,
// it will contain the name of the animal that has won
val winner = world.component<Winner>()
println("The Winner is ${winner.name}!\n")
// we will get the name and time component from our lure entity using it
// saved reference, surprisingly it will always take 5s
val (lure, pos) = lureRef.pair<Lure, Position>()
println("${lure.name} arrived in ${pos.time.threeDecimals()}s \n")
println("Final lines:\n")
// we will get all entities that has an Animal and a Position and sorted by
// the time they take to rich that position
world.pairs<Animal, Position>().sortedBy { (_, position) ->
position.time
}.forEachIndexed { place, (animal, animalPos) ->
// get the components of the entity and display it
println(
"${(place + 1).withSuffix()} ${animal.name} in " +
"${animalPos.time.threeDecimals()}s"
)
}
}
/** The system that move things, either animals or the lure **/
class MovementSystem : System() {
override fun update(delta: Float, total: Float, world: World) {
// get entities that has position and movement
world.pairs<Position, Movement> { (position, movement) ->
// if we are running
if (movement.status == MovementStatus.Running) {
// calculate the step base on delta time and speed
val step = (movement.speed * delta)
// calculate new position, without passing the end
position.at = min(position.at + step, RACE_LENGTH)
// add the time running
position.time += delta
// if we are at the end stop
if (position.at == RACE_LENGTH) {
movement.status = MovementStatus.Stopped
}
}
}
}
}
/** THe System that find a winner, only looking at animals, no lure **/
class WinnerSystem : System() {
override fun update(delta: Float, total: Float, world: World) {
// if we dont have a winner
if (!world.hasComponent<Winner>()) {
// get entities that are animal and has position, we
// dont need movement, neither the lure
world.pairs<Position, Animal> { (position, animal) ->
// if we are at the end
if (position.at == RACE_LENGTH) {
// add to the world the winner
world.add { +Winner(animal.name) }
return@update
}
}
}
}
}
/** This System will check when to stop the race **/
class RaceSystem : System() {
override fun update(delta: Float, total: Float, world: World) {
// first we will check if we aren't already ended
if (world.component<RaceStatus>() != RaceStatus.Ended) {
// get from all entities that has movement if they
// are all stopped
val allStopped = world.components<Movement>().all {
it.status == MovementStatus.Stopped
}
// if all are stopped
if (allStopped) {
// set that the race has ended
world.entity<RaceStatus>().set(RaceStatus.Ended)
}
}
}
}
/** This System will draw a progress bar of the race **/
class ProgressSystem : System() {
// how much time we have been racing
var time = 0.0f
// last update, we don't want to update the progress all
// the time, just when the time change (using 3 decimals)
var lastUpdate = Float.MIN_VALUE
/** display a progress bar like:
*
* text 22 % [██████▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒] 1.592s
*
**/
private fun drawBar(completion: Float, time: Float, text: String) {
// get the blocks to fill █
val blocksToFill = (NUM_BLOCKS * completion).toInt()
val filledBlocks = "█".repeat(blocksToFill)
// get the blocks empty |
val blocksEmpty = NUM_BLOCKS - blocksToFill
val emptyBlocks = "▒".repeat(blocksEmpty)
// calculate the percentage
val percent = (completion * 100).toInt()
// compose the bar, we use \r to reset the cursor
print(
"\r$text ${percent.threeDigits()} % " +
"[$filledBlocks$emptyBlocks] " +
"${time.threeDecimals()}s "
)
}
override fun update(delta: Float, total: Float, world: World) {
// get from all entities that has position the position
val positions = world.components<Position>()
// if we average all that we have run so far and divide by the
// length of the race we will have the overall completion (0..1) of
// the race
val completion = positions.map { it.at }.average().toFloat() /
RACE_LENGTH
// We accumulate the race time
time += delta
// we round the time to three decimals
val update = time.threeDecimals()
// if update time has change from the last update
if (update != lastUpdate) {
// draw the bar
drawBar(completion, update, "Race complete:")
// store last update
lastUpdate = update
}
}
}
/** just random animals **/
val animals: List<String> = listOf(
"ox", "ant", "ape", "asp", "bat", "bee", "boa", "bug", "cat", "cod", "cow",
"cub", "doe", "dog", "eel", "eft", "elf", "elk", "emu", "ewe", "fly", "fox",
"gar", "gnu", "hen", "hog", "imp", "jay", "kid", "kit", "koi", "lab", "man",
"owl", "pig", "pug", "pup", "ram", "rat", "ray", "yak", "bass", "bear",
"bird", "boar", "buck", "bull", "calf", "chow", "clam", "colt", "crab",
"crow", "dane", "deer", "dodo", "dory", "dove", "drum", "duck", "fawn",
"fish", "flea", "foal", "fowl", "frog", "gnat", "goat", "grub", "gull",
"hare", "hawk", "ibex", "joey", "kite", "kiwi", "lamb", "lark", "lion",
"loon", "lynx", "mako", "mink", "mite", "mole", "moth", "mule", "mutt",
"newt", "orca", "oryx", "pika", "pony", "puma", "seal", "shad", "slug",
"sole", "stag", "stud", "swan", "tahr", "teal", "tick", "toad", "tuna",
"wasp", "wolf", "worm", "wren", "yeti", "adder", "akita", "alien", "aphid",
"bison", "boxer", "bream", "bunny", "burro", "camel", "chimp", "civet",
"cobra", "coral", "corgi", "crane", "dingo", "drake", "eagle", "egret",
"filly", "finch", "gator", "gecko", "ghost", "ghoul", "goose", "guppy",
"heron", "hippo", "horse", "hound", "husky", "hyena", "koala", "krill",
"leech", "lemur", "liger", "llama", "louse", "macaw", "midge", "molly",
"moose", "moray", "mouse", "panda", "perch", "prawn", "quail", "racer",
"raven", "rhino", "robin", "satyr", "shark", "sheep", "shrew", "skink",
"skunk", "sloth", "snail", "snake", "snipe", "squid", "stork", "swift",
"swine", "tapir", "tetra", "tiger", "troll", "trout", "viper", "wahoo",
"whale", "zebra", "alpaca", "amoeba", "baboon", "badger", "beagle",
"bedbug", "beetle", "bengal", "bobcat", "caiman", "cattle", "cicada",
"collie", "condor", "cougar", "coyote", "dassie", "donkey", "dragon",
"earwig", "falcon", "feline", "ferret", "gannet", "gibbon", "glider",
"goblin", "gopher", "grouse", "guinea", "hermit", "hornet", "iguana",
"impala", "insect", "jackal", "jaguar", "jennet", "kitten", "kodiak",
"lizard", "locust", "maggot", "magpie", "mammal", "mantis", "marlin",
"marmot", "marten", "martin", "mayfly", "minnow", "monkey", "mullet",
"muskox", "ocelot", "oriole", "osprey", "oyster", "parrot", "pigeon",
"piglet", "poodle", "possum", "python", "quagga", "rabbit", "raptor",
"rodent", "roughy", "salmon", "sawfly", "serval", "shiner", "shrimp",
"spider", "sponge", "tarpon", "thrush", "tomcat", "toucan", "turkey",
"turtle", "urchin", "vervet", "walrus", "weasel", "weevil", "wombat",
"anchovy", "anemone", "bluejay", "buffalo", "bulldog", "buzzard", "caribou",
"catfish", "chamois", "cheetah", "chicken", "chigger", "cowbird", "crappie",
"crawdad", "cricket", "dogfish", "dolphin", "firefly", "garfish", "gazelle",
"gelding", "giraffe", "gobbler", "gorilla", "goshawk", "grackle", "griffon",
"grizzly", "grouper", "gryphon", "haddock", "hagfish", "halibut", "hamster",
"herring", "jackass", "javelin", "jawfish", "jaybird", "katydid", "ladybug",
"lamprey", "lemming", "leopard", "lioness", "lobster", "macaque", "mallard",
"mammoth", "manatee", "mastiff", "meerkat", "mollusk", "monarch", "mongrel",
"monitor", "monster", "mudfish", "muskrat", "mustang", "narwhal", "oarfish",
"octopus", "opossum", "ostrich", "panther", "peacock", "pegasus", "pelican",
"penguin", "phoenix", "piranha", "polecat", "primate", "quetzal", "raccoon",
"rattler", "redbird", "redfish", "reptile", "rooster", "sawfish", "sculpin",
"seagull", "skylark", "snapper", "spaniel", "sparrow", "sunbeam", "sunbird",
"sunfish", "tadpole", "termite", "terrier", "unicorn", "vulture", "wallaby",
"walleye", "warthog", "whippet", "wildcat", "aardvark", "airedale",
"albacore", "anteater", "antelope", "arachnid", "barnacle", "basilisk",
"blowfish", "bluebird", "bluegill", "bonefish", "bullfrog", "cardinal",
"chipmunk", "cockatoo", "crawfish", "crayfish", "dinosaur", "doberman",
"duckling", "elephant", "escargot", "flamingo", "flounder", "foxhound",
"glowworm", "goldfish", "grubworm", "hedgehog", "honeybee", "hookworm",
"humpback", "kangaroo", "killdeer", "kingfish", "labrador", "lacewing",
"ladybird", "lionfish", "longhorn", "mackerel", "malamute", "marmoset",
"mastodon", "moccasin", "mongoose", "monkfish", "mosquito", "pangolin",
"parakeet", "pheasant", "pipefish", "platypus", "polliwog", "porpoise",
"reindeer", "ringtail", "sailfish", "scorpion", "seahorse", "seasnail",
"sheepdog", "shepherd", "silkworm", "squirrel", "stallion", "starfish",
"starling", "stingray", "stinkbug", "sturgeon", "terrapin", "titmouse",
"tortoise", "treefrog", "werewolf", "woodcock"
)
/** just random adjectives **/
val adjectives: List<String> = listOf(
"able", "above", "absolute", "accepted", "accurate", "ace", "active",
"actual", "adapted", "adapting", "adequate", "adjusted", "advanced",
"alert", "alive", "allowed", "allowing", "amazed", "amazing", "ample",
"amused", "amusing", "apparent", "apt", "arriving", "artistic", "assured",
"assuring", "awaited", "awake", "aware", "balanced", "becoming", "beloved",
"better", "big", "blessed", "bold", "boss", "brave", "brief", "bright",
"bursting", "busy", "calm", "capable", "capital", "careful", "caring",
"casual", "causal", "central", "certain", "champion", "charmed", "charming",
"cheerful", "chief", "choice", "civil", "classic", "clean", "clear",
"clever", "climbing", "close", "closing", "coherent", "comic", "communal",
"complete", "composed", "concise", "concrete", "content", "cool", "correct",
"cosmic", "crack", "creative", "credible", "crisp", "crucial", "cuddly",
"cunning", "curious", "current", "cute", "daring", "darling", "dashing",
"dear", "decent", "deciding", "deep", "definite", "delicate", "desired",
"destined", "devoted", "direct", "discrete", "distinct", "diverse",
"divine", "dominant", "driven", "driving", "dynamic", "eager", "easy",
"electric", "elegant", "emerging", "eminent", "enabled", "enabling",
"endless", "engaged", "engaging", "enhanced", "enjoyed", "enormous",
"enough", "epic", "equal", "equipped", "eternal", "ethical", "evident",
"evolved", "evolving", "exact", "excited", "exciting", "exotic", "expert",
"factual", "fair", "faithful", "famous", "fancy", "fast", "feasible",
"fine", "finer", "firm", "first", "fit", "fitting", "fleet", "flexible",
"flowing", "fluent", "flying", "fond", "frank", "free", "fresh", "full",
"fun", "funny", "game", "generous", "gentle", "genuine", "giving", "glad",
"glorious", "glowing", "golden", "good", "gorgeous", "grand", "grateful",
"great", "growing", "grown", "guided", "guiding", "handy", "happy", "hardy",
"harmless", "healthy", "helped", "helpful", "helping", "heroic", "hip",
"holy", "honest", "hopeful", "hot", "huge", "humane", "humble", "humorous",
"ideal", "immense", "immortal", "immune", "improved", "in", "included",
"infinite", "informed", "innocent", "inspired", "integral", "intense",
"intent", "internal", "intimate", "inviting", "joint", "just", "keen",
"key", "kind", "knowing", "known", "large", "lasting", "leading",
"learning", "legal", "legible", "lenient", "liberal", "light", "liked",
"literate", "live", "living", "logical", "loved", "loving", "loyal",
"lucky", "magical", "magnetic", "main", "major", "many", "massive",
"master", "mature", "maximum", "measured", "meet", "merry", "mighty",
"mint", "model", "modern", "modest", "moral", "more", "moved", "moving",
"musical", "mutual", "national", "native", "natural", "nearby", "neat",
"needed", "neutral", "new", "next", "nice", "noble", "normal", "notable",
"noted", "novel", "obliging", "on", "one", "open", "optimal", "optimum",
"organic", "oriented", "outgoing", "patient", "peaceful", "perfect", "pet",
"picked", "pleasant", "pleased", "pleasing", "poetic", "polished", "polite",
"popular", "positive", "possible", "powerful", "precious", "precise",
"premium", "prepared", "present", "pretty", "primary", "prime", "pro",
"probable", "profound", "promoted", "prompt", "proper", "proud", "proven",
"pumped", "pure", "quality", "quick", "quiet", "rapid", "rare", "rational",
"ready", "real", "refined", "regular", "related", "relative", "relaxed",
"relaxing", "relevant", "relieved", "renewed", "renewing", "resolved",
"rested", "rich", "right", "robust", "romantic", "ruling", "sacred", "safe",
"saved", "saving", "secure", "select", "selected", "sensible", "set",
"settled", "settling", "sharing", "sharp", "shining", "simple", "sincere",
"singular", "skilled", "smart", "smashing", "smiling", "smooth", "social",
"solid", "sought", "sound", "special", "splendid", "square", "stable",
"star", "steady", "sterling", "still", "stirred", "stirring", "striking",
"strong", "stunning", "subtle", "suitable", "suited", "summary", "sunny",
"super", "superb", "supreme", "sure", "sweeping", "sweet", "talented",
"teaching", "tender", "thankful", "thorough", "tidy", "tight", "together",
"tolerant", "top", "topical", "tops", "touched", "touching", "tough",
"true", "trusted", "trusting", "trusty", "ultimate", "unbiased", "uncommon",
"unified", "unique", "united", "up", "upright", "upward", "usable",
"useful", "valid", "valued", "vast", "verified", "viable", "vital", "vocal",
"wanted", "warm", "wealthy", "welcome", "welcomed", "well", "whole",
"willing", "winning", "wired", "wise", "witty", "wondrous", "workable",
"working", "worthy"
)
/** just random adverbs **/
val adverbs: List<String> = listOf(
"abnormally", "absolutely", "accurately", "actively", "actually",
"adequately", "admittedly", "adversely", "allegedly", "amazingly",
"annually", "apparently", "arguably", "awfully", "badly", "barely",
"basically", "blatantly", "blindly", "briefly", "brightly", "broadly",
"carefully", "centrally", "certainly", "cheaply", "cleanly", "clearly",
"closely", "commonly", "completely", "constantly", "conversely",
"correctly", "curiously", "currently", "daily", "deadly", "deeply",
"definitely", "directly", "distinctly", "duly", "eagerly", "early",
"easily", "eminently", "endlessly", "enormously", "entirely", "equally",
"especially", "evenly", "evidently", "exactly", "explicitly", "externally",
"extremely", "factually", "fairly", "finally", "firmly", "firstly",
"forcibly", "formally", "formerly", "frankly", "freely", "frequently",
"friendly", "fully", "generally", "gently", "genuinely", "ghastly",
"gladly", "globally", "gradually", "gratefully", "greatly", "grossly",
"happily", "hardly", "heartily", "heavily", "hideously", "highly",
"honestly", "hopefully", "hopelessly", "horribly", "hugely", "humbly",
"ideally", "illegally", "immensely", "implicitly", "incredibly",
"indirectly", "infinitely", "informally", "inherently", "initially",
"instantly", "intensely", "internally", "jointly", "jolly", "kindly",
"largely", "lately", "legally", "lightly", "likely", "literally", "lively",
"locally", "logically", "loosely", "loudly", "lovely", "luckily", "mainly",
"manually", "marginally", "mentally", "merely", "mildly", "miserably",
"mistakenly", "moderately", "monthly", "morally", "mostly", "multiply",
"mutually", "namely", "nationally", "naturally", "nearly", "neatly",
"needlessly", "newly", "nicely", "nominally", "normally", "notably",
"noticeably", "obviously", "oddly", "officially", "only", "openly",
"optionally", "overly", "painfully", "partially", "partly", "perfectly",
"personally", "physically", "plainly", "pleasantly", "poorly", "positively",
"possibly", "precisely", "preferably", "presently", "presumably",
"previously", "primarily", "privately", "probably", "promptly", "properly",
"publicly", "purely", "quickly", "quietly", "radically", "randomly",
"rapidly", "rarely", "rationally", "readily", "really", "reasonably",
"recently", "regularly", "reliably", "remarkably", "remotely", "repeatedly",
"rightly", "roughly", "routinely", "sadly", "safely", "scarcely",
"secondly", "secretly", "seemingly", "sensibly", "separately", "seriously",
"severely", "sharply", "shortly", "similarly", "simply", "sincerely",
"singularly", "slightly", "slowly", "smoothly", "socially", "solely",
"specially", "steadily", "strangely", "strictly", "strongly", "subtly",
"suddenly", "suitably", "supposedly", "surely", "terminally", "terribly",
"thankfully", "thoroughly", "tightly", "totally", "trivially", "truly",
"typically", "ultimately", "unduly", "uniformly", "uniquely", "unlikely",
"urgently", "usefully", "usually", "utterly", "vaguely", "vastly",
"verbally", "vertically", "vigorously", "violently", "virtually",
"visually", "weekly", "wholly", "widely", "wildly", "willingly", "wrongly",
"yearly"
)
fun main() {
animalRace()
}