Feel free to join the next Helmholtz Hacky Hour #26 on Wednesday, April 21, 2021 from 2PM to 3PM!

Commit c505787f authored by Martin Lange's avatar Martin Lange

restructured according ODD, text for general sections

parent 2286af89
Pipeline #12093 passed with stage
in 13 seconds
# ECS Tutorial with Java and Artemis-odb
This projects demonstrates the use of an Entity-Component-System for the implementation of an individual-based model (IBM). We use Java and the ECS library [Artemis-odb](https://github.com/junkdog/artemis-odb) to build a simple model.
This projects demonstrates the use of an Entity-Component-System for the implementation of an individual-based model (IBM). We use Java and the ECS library [Artemis-odb](https://github.com/junkdog/artemis-odb) to build a grassing model.
## Components
The model description is structured following the ODD protocol (Grimm et al. 2006, 2010) to demonstrate the good fit between ECS and ODD.
<img title="Screenshot of the model" src="https://git.ufz.de/oesa/ecs-tutorial/uploads/aca134ea4e37bada52effa167172d51a/image.png" alt="Screenshot of the model" width="400">
*Screenshot of the model*
## What is an ECS?
## Purpose
The purpose of this model is to demonstrate the use of Entity-Component-Systems for individual-based models (IBMs).
## Entities, state variables and scales
In an ECS, all entities are generic, but characterized by the components they possess. Comonents contain an entity's state variables.
In this grassing model, the only entities are grassers. All grassers possess the components `Position`, `Heading` and `Energy`. Behaviour of grassers is governed by the components `IsGrassing` or `IsSearching`, which each grasser possess one of.
Component `Position` contains continuous coordinates in a two-dimensional world.
```java
/// file:src/main/java/grassing/comp/Position.java
......@@ -11,7 +29,7 @@ package grassing.comp;
public class Position extends com.artemis.Component {
public float x;
public float y;
public Position() {}
public Position(float x, float y) {
this.x = x;
......@@ -20,13 +38,15 @@ public class Position extends com.artemis.Component {
}
```
Component `Heading` contains the angle the entity is heading towards.
```java
/// file:src/main/java/grassing/comp/Heading.java
package grassing.comp;
public class Heading extends com.artemis.Component {
public float angle;
public Heading() {}
public Heading(float angle) {
this.angle = angle;
......@@ -34,13 +54,15 @@ public class Heading extends com.artemis.Component {
}
```
Component `Energy` contains the entity's current energy budget.
```java
/// file:src/main/java/grassing/comp/Energy.java
package grassing.comp;
public class Energy extends com.artemis.Component {
public float value;
public Energy() {}
public Energy(float value) {
this.value = value;
......@@ -48,6 +70,8 @@ public class Energy extends com.artemis.Component {
}
```
Component `IsGrassing` labels an entity as currently following the grassing behaviour.
```java
/// file:src/main/java/grassing/comp/IsGrassing.java
package grassing.comp;
......@@ -55,6 +79,8 @@ package grassing.comp;
public class IsGrassing extends com.artemis.Component {}
```
Component `IsSearching` labels an entity as currently following the searching behaviour.
```java
/// file:src/main/java/grassing/comp/IsSearching.java
package grassing.comp;
......@@ -62,7 +88,28 @@ package grassing.comp;
public class IsSearching extends com.artemis.Component {}
```
## Resources
Entities live on a two-dimensional landscape. The landscape is represented by a grid of grass the grassers consume.
```java
/// file:src/main/java/grassing/res/Grass.java
package grassing.res;
import grassing.util.Grid;
public class Grass {
final public Grid.Float grass;
public Grass(int width, int height, float initial) {
this.grass = new Grid.Float(width, height);
for (int i = 0; i < grass.length; i++) {
grass.set(i, initial);
}
}
}
```
A single global pseudo random number generator is used for all processes thet include randomness.
```java
/// file:src/main/java/grassing/res/Randomness.java
......@@ -71,35 +118,97 @@ package grassing.res;
import java.util.Random;
public class Randomness {
public final Random rng;
public Randomness(long seed) {
this.rng = new Random(seed);
}
}
```
## Process overview and scheduling
Processes in the model are grass growth, grasser metabolism, grasser reproduction and the two grasser behaviours grassing and searching. Processes are executed in the given order.
```java
/// file:src/main/java/grassing/res/Grass.java
package grassing.res;
/// file:src/main/java/grassing/Main.java
package grassing;
import grassing.util.Grid;
import com.artemis.World;
import com.artemis.WorldConfigurationBuilder;
import grassing.comp.*;
import grassing.res.*;
import grassing.sys.*;
import grassing.util.MathUtil;
public class Grass {
final public Grid.Float grass;
public Grass(int width, int height, float initial) {
this.grass = new Grid.Float(width, height);
for (int i = 0; i < grass.length; i++) {
grass.set(i, initial);
import java.util.Random;
public class Main {
// ==> Parameters.
public static void main(String[] args) {
var setup = new WorldConfigurationBuilder()
.with(new LogisticGrassGrowth(GRASS_GROWTH_RATE))
.with(new Metabolism(ENERGY_CONSUMPTION))
.with(new Reproduction(REPRODUCTION_PROB))
.with(new GrassingBehaviour(GRASS_START_SEARCHING, GRASS_CONSUMPTION))
.with(new SearchBehaviour(GRASS_START_GRASSING, RANDOM_WALK_ANGLE, RANDOM_WALK_SPEED))
.with(new Graphics(6))
.build()
.register(new Randomness(System.currentTimeMillis()))
.register(new Grass(WORLD_WIDTH, WORLD_HEIGHT, 0.8f));
var world = new World(setup);
createEntities(world, world.getRegistered(Randomness.class).rng);
for (int i=0; i < 1000000; i++) {
world.process();
try {
Thread.sleep(10);
} catch(InterruptedException ignored) {}
}
}
// ==> Create entities.
}
```
## Systems
## Design details
Grassers grass on the grass, and grass regrows. Grassers have a metabolism consuming their energy. When energy drops to zero, a grasser dies. Grassers reproduce stochastically.
Grassers have two different behaviours: grassing and searching. Behaviour changes based grass availability.
## Initialization
The model is initialized with `NUM_GRASSERS` grassers. All grassers start with the grassing behaviour.
```java
/// Create entities
private static void createEntities(World world, Random rng) {
for (int i = 0; i < NUM_GRASSERS; i++) {
int entity = world.create();
world.edit(entity)
.add(new Position(rng.nextFloat() * WORLD_WIDTH, rng.nextFloat() * WORLD_HEIGHT))
.add(new Heading(rng.nextFloat() * 2 * (float) Math.PI))
.add(new Energy(1f))
.add(new IsGrassing());
}
}
```
## Input data
The model uses no input data.
## Submodels
Submodels are described in their order of execution.
### Grass growth
```java
/// file:src/main/java/grassing/sys/LogisticGrassGrowth.java
......@@ -110,18 +219,18 @@ import com.artemis.annotations.Wire;
import grassing.res.Grass;
public class LogisticGrassGrowth extends BaseSystem {
@Wire
Grass grass;
private final float growthRate;
private final float capacity;
public LogisticGrassGrowth(float growthRate) {
this.growthRate = growthRate;
this.capacity = 1f;
}
@Override
protected void processSystem() {
for(int i=0; i<grass.grass.length; i++) {
......@@ -132,6 +241,8 @@ public class LogisticGrassGrowth extends BaseSystem {
}
```
### Grasser metabolism
```java
/// file:src/main/java/grassing/sys/Metabolism.java
package grassing.sys;
......@@ -143,15 +254,15 @@ import grassing.comp.Energy;
@All(Energy.class)
public class Metabolism extends IteratingSystem {
ComponentMapper<Energy> mEnergy;
private final float consumption;
public Metabolism(float consumption) {
this.consumption = consumption;
}
@Override
protected void process(int id) {
Energy e = mEnergy.get(id);
......@@ -163,6 +274,8 @@ public class Metabolism extends IteratingSystem {
}
```
### Grasser reproduction
```java
/// file:src/main/java/grassing/sys/Reproduction.java
package grassing.sys;
......@@ -176,17 +289,17 @@ import grassing.res.Randomness;
@All({Position.class, Energy.class})
public class Reproduction extends IteratingSystem {
ComponentMapper<Position> mPosition;
ComponentMapper<Energy> mEnergy;
@Wire Randomness random;
private final float reproductionProb;
public Reproduction(float reproductionProb) {
this.reproductionProb = reproductionProb;
}
@Override
protected void process(int id) {
if(random.rng.nextFloat() < reproductionProb) {
......@@ -204,6 +317,8 @@ public class Reproduction extends IteratingSystem {
}
```
### Grassing
```java
/// file:src/main/java/grassing/sys/GrassingBehaviour.java
package grassing.sys;
......@@ -217,21 +332,21 @@ import grassing.res.Grass;
@All({Position.class, Energy.class, IsGrassing.class})
public class GrassingBehaviour extends IteratingSystem {
ComponentMapper<Position> mPosition;
ComponentMapper<Energy> mEnergy;
ComponentMapper<IsGrassing> mGrassing;
ComponentMapper<IsSearching> mSearching;
@Wire Grass grass;
private final float minGrass;
private final float consumption;
public GrassingBehaviour(float minGrass, float consumption) {
this.minGrass = minGrass;
this.consumption = consumption;
}
@Override
protected void process(int id) {
Energy e = mEnergy.get(id);
......@@ -251,6 +366,8 @@ public class GrassingBehaviour extends IteratingSystem {
}
```
### Searching
```java
/// file:src/main/java/grassing/sys/SearchBehaviour.java
package grassing.sys;
......@@ -266,7 +383,7 @@ import grassing.util.MathUtil;
@All({Position.class, IsSearching.class})
public class SearchBehaviour extends IteratingSystem {
ComponentMapper<Position> mPosition;
ComponentMapper<Heading> mHeading;
ComponentMapper<IsGrassing> mGrassing;
......@@ -274,17 +391,17 @@ public class SearchBehaviour extends IteratingSystem {
@Wire
Grass grass;
@Wire Randomness random;
private final float minGrass;
private final float maxAngle;
private final float speed;
public SearchBehaviour(float minGrass, float maxAngle, float speed) {
this.minGrass = minGrass;
this.maxAngle = maxAngle;
this.speed = speed;
}
@Override
protected void process(int id) {
Position pos = mPosition.get(id);
......@@ -296,12 +413,12 @@ public class SearchBehaviour extends IteratingSystem {
randomWalk(id);
}
}
private void randomWalk(int id) {
var pos = mPosition.get(id);
var heading = mHeading.get(id);
heading.angle += (float) random.rng.nextGaussian() * maxAngle;
var head = MathUtil.heading(heading.angle);
float xNew = pos.x + head.x * speed;
float yNew = pos.y + head.y * speed;
......@@ -315,74 +432,25 @@ public class SearchBehaviour extends IteratingSystem {
}
```
## Putting it together
## Parameters
```java
/// file:src/main/java/grassing/Main.java
package grassing;
/// Parameters
final static int WORLD_WIDTH = 100;
final static int WORLD_HEIGHT = 100;
import com.artemis.World;
import com.artemis.WorldConfigurationBuilder;
import grassing.comp.*;
import grassing.res.*;
import grassing.sys.*;
import grassing.util.MathUtil;
final static int NUM_GRASSERS = 1000;
import java.util.Random;
final static float GRASS_GROWTH_RATE = 0.01f;
final static float GRASS_START_SEARCHING = 0.1f;
final static float GRASS_START_GRASSING = 0.2f;
final static float GRASS_CONSUMPTION = 0.05f;
public class Main {
final static int WORLD_WIDTH = 100;
final static int WORLD_HEIGHT = 100;
final static int NUM_GRASSERS = 1000;
final static float GRASS_GROWTH_RATE = 0.01f;
final static float GRASS_START_SEARCHING = 0.1f;
final static float GRASS_START_GRASSING = 0.2f;
final static float GRASS_CONSUMPTION = 0.05f;
final static float ENERGY_CONSUMPTION = 0.02f;
final static float REPRODUCTION_PROB = 0.05f;
final static float RANDOM_WALK_SPEED = 0.25f;
final static float RANDOM_WALK_ANGLE = MathUtil.deg2rad(30);
public static void main(String[] args) {
var setup = new WorldConfigurationBuilder()
.with(new LogisticGrassGrowth(GRASS_GROWTH_RATE))
.with(new Metabolism(ENERGY_CONSUMPTION))
.with(new Reproduction(REPRODUCTION_PROB))
.with(new GrassingBehaviour(GRASS_START_SEARCHING, GRASS_CONSUMPTION))
.with(new SearchBehaviour(GRASS_START_GRASSING, RANDOM_WALK_ANGLE, RANDOM_WALK_SPEED))
.with(new Graphics(6))
.build()
.register(new Randomness(System.currentTimeMillis()))
.register(new Grass(WORLD_WIDTH, WORLD_HEIGHT, 0.8f));
var world = new World(setup);
createEntities(world, world.getRegistered(Randomness.class).rng);
for (int i=0; i < 1000000; i++) {
world.process();
try {
Thread.sleep(10);
} catch(InterruptedException ignored) {}
}
}
private static void createEntities(World world, Random rng) {
for (int i = 0; i < NUM_GRASSERS; i++) {
int entity = world.create();
world.edit(entity)
.add(new Position(rng.nextFloat() * WORLD_WIDTH, rng.nextFloat() * WORLD_HEIGHT))
.add(new Heading(rng.nextFloat() * 2 * (float) Math.PI))
.add(new Energy(1f))
.add(new IsGrassing());
}
}
}
final static float ENERGY_CONSUMPTION = 0.02f;
final static float REPRODUCTION_PROB = 0.05f;
final static float RANDOM_WALK_SPEED = 0.25f;
final static float RANDOM_WALK_ANGLE = MathUtil.deg2rad(30);
```
## Appendix
......@@ -394,44 +462,44 @@ public class Main {
package grassing.util;
public abstract class Grid {
final public int width;
final public int height;
final public int layers;
final public int length;
protected final Object data;
protected Grid(Object data, int width, int height, int layers) {
this.data = data;
this.width = width;
this.height = height;
this.layers = layers;
this.length = width * height * layers;
}
protected Grid(Object data, int width, int height) {
this(data, width, height, 1);
}
public final int getIndex(int x, int y) {
return (y * width + x) * layers;
}
public final int getIndex(int x, int y, int l) {
return (y * width + x) * layers + l;
}
public final boolean contains(int x, int y) {
return x >= 0 && y >= 0 && x < width && y < height;
}
public final boolean contains(int x, int y, int l) {
return x >= 0 && y >= 0 && l >= 0 && x < width && y < height && l < layers;
}
public static final class Float extends Grid {
public Float(int width, int height, int layers) {
super((Object) (new float[width * height * layers]), width, height, layers);
......@@ -469,7 +537,7 @@ public abstract class Grid {
package grassing.util;
public abstract class MathUtil {
public static Point heading(float angle) {
return new Point((float) Math.cos(angle), (float) Math.sin(angle));
}
......@@ -486,7 +554,7 @@ package grassing.util;
public class Point {
public float x;
public float y;
public Point(float x, float y) {
this.x = x;
this.y = y;
......@@ -515,64 +583,64 @@ import javax.swing.*;
import java.awt.*;
public class Graphics extends BaseSystem {
protected ComponentMapper<Position> mPosition;
protected ComponentMapper<Heading> mHeading;
protected ComponentMapper<IsGrassing> mGrassing;
protected ComponentMapper<IsSearching> mSearching;
private EntitySubscription grasserSubs;
@Wire Grass grass;
final private int cellSize;
private Canvas canvas;
public Graphics(int cellSize) {
super();
this.cellSize = cellSize;
}
@Override
protected void initialize() {
grasserSubs = world.getAspectSubscriptionManager()
.get(Aspect.all(Position.class).one(IsGrassing.class, IsSearching.class));
var frame = new JFrame();
frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
canvas = new Canvas();
var dim = new Dimension(grass.grass.width * cellSize, grass.grass.height * cellSize);
canvas.setPreferredSize( dim );
frame.add(canvas);
frame.pack();
frame.setVisible(true);
}
@Override
protected void processSystem() {
canvas.paintImmediately(0, 0, grass.grass.width * cellSize, grass.grass.height * cellSize);
}
class Canvas extends JPanel {
@Override
public void paint(java.awt.Graphics g) {
super.paintComponent(g);
Graphics2D g2d = (Graphics2D) g;
Grid.Float grass = Graphics.this.grass.grass;
int s = Graphics.this.cellSize;
setBackground(Color.BLACK);
for(int y=0; y<grass.height; y++) {
for(int x=0; x<grass.width; x++) {
g2d.setPaint(new Color(0f, 0.7f * grass.get(x, y), 0f));
g2d.fillRect(x*s, y*s, s, s);
}
}
int[] x = new int[3];
int[] y = new int[3];
IntBag grassers = Graphics.this.grasserSubs.getEntities();
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment