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

Commit 2286af89 authored by Martin Lange's avatar Martin Lange

simple grassing system with reproduction and grass/search behaviour

parent 5c9f0fa6
Pipeline #12091 passed with stage
in 37 seconds
......@@ -5,30 +5,96 @@ This projects demonstrates the use of an Entity-Component-System for the impleme
## Components
```java
/// file:src/main/java/ecs_tutorial/Position.java
package ecs_tutorial;
/// file:src/main/java/grassing/comp/Position.java
package grassing.comp;
import com.artemis.*;
public class Position extends com.artemis.Component {
public float x;
public float y;
public Position() {}
public Position(float x, float y) {
this.x = x;
this.y = y;
}
}
```
```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;
}
}
```
```java
/// file:src/main/java/grassing/comp/Energy.java
package grassing.comp;
public class Position extends Component {
int x;
int y;
public class Energy extends com.artemis.Component {
public float value;
public Energy() {}
public Energy(float value) {
this.value = value;
}
}
```
```java
/// file:src/main/java/grassing/comp/IsGrassing.java
package grassing.comp;
public class IsGrassing extends com.artemis.Component {}
```
```java
/// file:src/main/java/grassing/comp/IsSearching.java
package grassing.comp;
public class IsSearching extends com.artemis.Component {}
```
## Resources
```java
/// file:src/main/java/ecs_tutorial/Randomness.java
package ecs_tutorial;
/// file:src/main/java/grassing/res/Randomness.java
package grassing.res;
import java.util.Random;
public class Randomness {
final Random rng;
public final Random rng;
public Randomness(long seed) {
rng = new Random(seed);
this.rng = new Random(seed);
}
}
```
```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);
}
}
}
```
......@@ -36,28 +102,215 @@ public class Randomness {
## Systems
```java
/// file:src/main/java/ecs_tutorial/RandomWalkSystem.java
package ecs_tutorial;
/// file:src/main/java/grassing/sys/LogisticGrassGrowth.java
package grassing.sys;
import com.artemis.BaseSystem;
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++) {
float v = grass.grass.get(i);
grass.grass.set(i, v + growthRate * v * (capacity - v) / capacity);
}
}
}
```
```java
/// file:src/main/java/grassing/sys/Metabolism.java
package grassing.sys;
import com.artemis.ComponentMapper;
import com.artemis.annotations.All;
import com.artemis.systems.IteratingSystem;
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);
e.value -= consumption;
if(e.value <= 0) {
world.delete(id);
}
}
}
```
```java
/// file:src/main/java/grassing/sys/Reproduction.java
package grassing.sys;
import com.artemis.ComponentMapper;
import com.artemis.annotations.All;
import com.artemis.annotations.Wire;
import com.artemis.systems.IteratingSystem;
import grassing.comp.*;
import grassing.res.Randomness;
@All(Position.class)
public class RandomWalkSystem extends IteratingSystem {
@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) {
var pos = mPosition.get(id);
var e = mEnergy.get(id);
int entity = world.create();
world.edit(entity)
.add(new Position(pos.x, pos.y))
.add(new Heading(random.rng.nextFloat() * 2 * (float) Math.PI))
.add(new Energy(e.value / 2))
.add(new IsGrassing());
e.value /= 2;
}
}
}
```
protected ComponentMapper<Position> mPosition;
```java
/// file:src/main/java/grassing/sys/GrassingBehaviour.java
package grassing.sys;
import com.artemis.ComponentMapper;
import com.artemis.annotations.All;
import com.artemis.annotations.Wire;
import com.artemis.systems.IteratingSystem;
import grassing.comp.*;
import grassing.res.Grass;
@All({Position.class, Energy.class, IsGrassing.class})
public class GrassingBehaviour extends IteratingSystem {
@Wire
Randomness randomness;
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);
if(e.value >= 1) {
return;
}
Position pos = mPosition.get(id);
float g = grass.grass.get((int) pos.x, (int) pos.y);
if( g - consumption > minGrass ) {
e.value += consumption;
grass.grass.set((int) pos.x, (int) pos.y, g - consumption);
} else {
mGrassing.remove(id);
mSearching.create(id);
}
}
}
```
```java
/// file:src/main/java/grassing/sys/SearchBehaviour.java
package grassing.sys;
import com.artemis.ComponentMapper;
import com.artemis.annotations.All;
import com.artemis.annotations.Wire;
import com.artemis.systems.IteratingSystem;
import grassing.comp.*;
import grassing.res.Grass;
import grassing.res.Randomness;
import grassing.util.MathUtil;
@All({Position.class, IsSearching.class})
public class SearchBehaviour extends IteratingSystem {
ComponentMapper<Position> mPosition;
ComponentMapper<Heading> mHeading;
ComponentMapper<IsGrassing> mGrassing;
ComponentMapper<IsSearching> mSearching;
@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);
float g = grass.grass.get((int) pos.x, (int) pos.y);
if( g >= minGrass ) {
mSearching.remove(id);
mGrassing.create(id);
} else {
randomWalk(id);
}
}
private void randomWalk(int id) {
var pos = mPosition.get(id);
pos.x += randomness.rng.nextInt(3) - 1;
pos.y += randomness.rng.nextInt(3) - 1;
System.out.println(pos.x+" "+pos.y);
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;
if(grass.grass.contains((int) xNew, (int) yNew)) {
pos.x = xNew;
pos.y = yNew;
} else {
heading.angle = (heading.angle + (float) Math.PI) % (2 * (float) Math.PI);
}
}
}
```
......@@ -65,25 +318,68 @@ public class RandomWalkSystem extends IteratingSystem {
## Putting it together
```java
/// file:src/main/java/ecs_tutorial/Main.java
package ecs_tutorial;
/// file:src/main/java/grassing/Main.java
package grassing;
import com.artemis.World;
import com.artemis.WorldConfigurationBuilder;
import grassing.comp.*;
import grassing.res.*;
import grassing.sys.*;
import grassing.util.MathUtil;
import com.artemis.*;
import java.util.Random;
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 RandomWalkSystem())
.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(0));
.register(new Randomness(System.currentTimeMillis()))
.register(new Grass(WORLD_WIDTH, WORLD_HEIGHT, 0.8f));
var world = new World(setup);
int entityId = world.create();
world.edit(entityId).create(Position.class);
for (int i=0; i < 10; i++) {
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());
}
}
}
......@@ -91,6 +387,222 @@ public class Main {
## Appendix
### Utilities
```java
/// file:src/main/java/grassing/util/Grid.java
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);
}
public Float(int width, int height) {
this(width, height, 1);
}
private float[] getData() {
return (float[]) data;
}
public final float get(int idx) {
return getData()[idx];
}
public final float get(int x, int y) {
return getData()[getIndex(x, y)];
}
public final float get(int x, int y, int l) {
return getData()[getIndex(x, y, l)];
}
public final void set(int idx, float value) {
getData()[idx] = value;
}
public final void set(int x, int y, float value) {
getData()[getIndex(x, y)] = value;
}
public final void set(int x, int y, int l, float value) {
getData()[getIndex(x, y, l)] = value;
}
}
}
```
```java
/// file:src/main/java/grassing/util/MathUtil.java
package grassing.util;
public abstract class MathUtil {
public static Point heading(float angle) {
return new Point((float) Math.cos(angle), (float) Math.sin(angle));
}
public static float deg2rad(float deg) {
return (float) (deg * Math.PI / 180f);
}
}
```
```java
/// file:src/main/java/grassing/util/Point.java
package grassing.util;
public class Point {
public float x;
public float y;
public Point(float x, float y) {
this.x = x;
this.y = y;
}
}
```
### Graphics
```java
/// file:src/main/java/grassing/sys/Graphics.java
package grassing.sys;
import com.artemis.Aspect;
import com.artemis.BaseSystem;
import com.artemis.ComponentMapper;
import com.artemis.EntitySubscription;
import com.artemis.annotations.Wire;
import com.artemis.utils.IntBag;
import grassing.comp.*;
import grassing.res.Grass;
import grassing.util.Grid;
import grassing.util.MathUtil;
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();
for (int i = 0; i < grassers.size(); i++) {
int entity = grassers.get(i);
var pos = mPosition.get(entity);
var head = mHeading.get(entity);
if(mGrassing.has(entity)) {
g2d.setPaint(Color.WHITE);
} else if(mSearching.has(entity)) {
g2d.setPaint(Color.ORANGE);
} else {
g2d.setPaint(Color.MAGENTA);
}
//g2d.fillOval((int) (pos.x*s)-1, (int) (pos.y*s)-1, 2, 2);
var f = MathUtil.heading(head.angle);
var r = MathUtil.heading(head.angle + 0.5f * (float) Math.PI);
x[0] = (int) ((pos.x + f.x) * s);
y[0] = (int) ((pos.y + f.y) * s);
x[1] = (int) ((pos.x + 0.4f * r.x) * s);
y[1] = (int) ((pos.y + 0.4f * r.y) * s);
x[2] = (int) ((pos.x - 0.4f * r.x) * s);
y[2] = (int) ((pos.y - 0.4f * r.y) * s);
g2d.fillPolygon(x, y, 3);