/* * Artificial Intelligence for Humans * Volume 2: Nature Inspired Algorithms * Java Version * http://www.aifh.org * http://www.jeffheaton.com * * Code repository: * https://github.com/jeffheaton/aifh * * Copyright 2014 by Jeff Heaton * * 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. * * For more information on Heaton Research copyrights, licenses * and trademarks visit: * http://www.heatonresearch.com/copyright */ package com.heatonresearch.aifh.examples.swarm.flock; import com.heatonresearch.aifh.distance.CalculateDistance; import com.heatonresearch.aifh.distance.EuclideanDistance; import javax.swing.*; import java.awt.*; import java.awt.event.ComponentEvent; import java.awt.event.ComponentListener; import java.awt.event.WindowEvent; import java.awt.event.WindowListener; import java.awt.image.BufferedImage; import java.util.ArrayList; import java.util.Collection; import java.util.List; /** * This example plots a flock of particles that exhibit flocking behavior. This is governed by the following three * simple rules: * <p/> * 1. Separation - avoid crowding neighbors (short range repulsion) * 2. Alignment - steer towards average heading of neighbors * 3. Cohesion - steer towards average position of neighbors (long range attraction) * <p/> * References: * <p/> * http://en.wikipedia.org/wiki/Flocking_(behavior) */ public class Flock2dWindow extends JFrame implements Runnable, ComponentListener, WindowListener { /** * The number of particles. */ private final int PARTICLE_COUNT = 100; /** * The size of each particle. */ private final double PARTICLE_SIZE = 10; /** * The particles. */ private final List<Particle> particles; /** * An off-screen image to render to. */ private Graphics offscreenGraphics; /** * The off screen image. */ private BufferedImage offscreenImage; /** * Distance calculation. */ private CalculateDistance distanceCalc = new EuclideanDistance(); /** * The constant for cohesion. */ private double constCohesion = 0.01; /** * The constant for alignment. */ private double constAlignment = 0.5; /** * The constant for separation. */ private double constSeparation = 0.25; /** * The constructor. */ public Flock2dWindow() { setTitle("Flocking in 2D"); setSize(1024, 768); this.particles = new ArrayList<Particle>(); for (int i = 0; i < PARTICLE_COUNT; i++) { Particle p = new Particle(2); p.getLocation()[0] = Math.random() * this.getWidth(); p.getLocation()[1] = Math.random() * this.getHeight(); p.getVelocity()[0] = 3; p.getVelocity()[1] = Math.random() * 2.0 * Math.PI; this.particles.add(p); } // register for events this.addWindowListener(this); this.addComponentListener(this); } /** * Main entry point. * * @param args Not used. */ public static void main(String[] args) { Flock2dWindow app = new Flock2dWindow(); app.setVisible(true); } /** * Find the index that has the max value in a vector. * * @param data The vector. * @return The index. */ public static int maxIndex(double[] data) { int result = -1; for (int i = 0; i < data.length; i++) { if (result == -1 || data[i] > data[result]) { result = i; } } return result; } /** * Get the mean particle location for the specified dimension. * * @param particles The particles. * @param dimension The dimension. * @return The mean. */ public static double particleLocationMean(Collection<Particle> particles, int dimension) { double sum = 0; int count = 0; for (Particle p : particles) { sum += p.getLocation()[dimension]; count++; } return sum / count; } /** * Get the particle velocity mean for the specified dimension. * * @param particles The particles. * @param dimension The dimension. * @return The velocity mean. */ public static double particleVelocityMean(Collection<Particle> particles, int dimension) { double sum = 0; int count = 0; for (Particle p : particles) { sum += p.getVelocity()[dimension]; count++; } return sum / count; } /** * Find the nearest neighbor particle. * * @param target The particle to look for neighbors to. * @param particles All particles. * @param k The number of particles to find. * @param maxDist The max distance to check. * @return The nearest neighbors. */ private Collection<Particle> findNearest(Particle target, Collection<Particle> particles, int k, double maxDist) { List<Particle> result = new ArrayList<Particle>(); double[] tempDist = new double[k]; int worstIndex = -1; for (Particle particle : particles) { if (particle == target) { continue; } double d = this.distanceCalc.calculate(particle.getLocation(), target.getLocation()); if (d > maxDist) { continue; } if (result.size() < k) { tempDist[result.size()] = d; result.add(particle); worstIndex = maxIndex(tempDist); } else if (d < tempDist[worstIndex]) { tempDist[worstIndex] = d; result.set(worstIndex, particle); worstIndex = maxIndex(tempDist); } } return result; } /** * Perform the flocking. */ private void flock() { for (Particle particle : this.particles) { /////////////////////////////////////////////////////////////// // Begin implementation of three very basic laws of flocking. /////////////////////////////////////////////////////////////// Collection<Particle> neighbors = findNearest(particle, this.particles, 5, Double.POSITIVE_INFINITY); Collection<Particle> nearest = findNearest(particle, this.particles, 5, 10); // 1. Separation - avoid crowding neighbors (short range repulsion) double separation = 0; if (nearest.size() > 0) { double meanX = particleLocationMean(nearest, 0); double meanY = particleLocationMean(nearest, 1); double dx = meanX - particle.getLocation()[0]; double dy = meanY - particle.getLocation()[1]; separation = Math.atan2(dx, dy) - particle.getVelocity()[1]; separation += Math.PI; } // 2. Alignment - steer towards average heading of neighbors double alignment = 0; if (neighbors.size() > 0) { alignment = particleVelocityMean(neighbors, 1) - particle.getVelocity()[1]; } // 3. Cohesion - steer towards average position of neighbors (long range attraction) double cohesion = 0; if (neighbors.size() > 0) { double meanX = particleLocationMean(this.particles, 0); double meanY = particleLocationMean(this.particles, 1); double dx = meanX - particle.getLocation()[0]; double dy = meanY - particle.getLocation()[1]; cohesion = Math.atan2(dx, dy) - particle.getVelocity()[1]; } // perform the turn // The degree to which each of the three laws is applied is configurable. // The three default ratios that I provide work well. double turnAmount = (cohesion * this.constCohesion) + (alignment * this.constAlignment) + (separation * this.constSeparation); particle.getVelocity()[1] += turnAmount; /////////////////////////////////////////////////////////////// // End implementation of three very basic laws of flocking. /////////////////////////////////////////////////////////////// } } /** * {@inheritDoc} */ @Override public void run() { // create offscreen drawing buffer this.offscreenImage = new BufferedImage(getWidth(), getHeight(), BufferedImage.TYPE_INT_ARGB); this.offscreenGraphics = this.offscreenImage.createGraphics(); for (; ; ) { // clear the off screen area this.offscreenGraphics.setColor(Color.black); this.offscreenGraphics.fillRect(0, 0, getWidth(), getHeight()); // render the particles int[] x = new int[3]; int[] y = new int[3]; this.offscreenGraphics.setColor(Color.white); for (Particle p : this.particles) { x[0] = (int) p.getLocation()[0]; y[0] = (int) p.getLocation()[1]; double r = p.getVelocity()[1] + (Math.PI * 5.0) / 12.0; x[1] = x[0] - (int) (Math.cos(r) * PARTICLE_SIZE); y[1] = y[0] - (int) (Math.sin(r) * PARTICLE_SIZE); double r2 = p.getVelocity()[1] + (Math.PI * 7.0) / 12.0; x[2] = x[0] - (int) (Math.cos(r2) * PARTICLE_SIZE); y[2] = y[0] - (int) (Math.sin(r2) * PARTICLE_SIZE); this.offscreenGraphics.drawPolygon(x, y, 3); //this.offscreenGraphics.fillOval((int)p.getLocation()[0],(int)p.getLocation()[1],10,10); // move the particle double dx = Math.cos(r); double dy = Math.sin(r); p.getLocation()[0] += (dx * p.getVelocity()[0]); p.getLocation()[1] += (dy * p.getVelocity()[0]); // handle wraps if (p.getLocation()[0] < 0) { p.getLocation()[0] = getWidth(); } if (p.getLocation()[1] < 0) { p.getLocation()[1] = getHeight(); } if (p.getLocation()[0] > getWidth()) { p.getLocation()[0] = 0; } if (p.getLocation()[1] > getHeight()) { p.getLocation()[1] = 0; } } flock(); try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); //To change body of catch statement use File | Settings | File Templates. } // update the screen Graphics g = this.getGraphics(); g.drawImage(this.offscreenImage, 0, 0, this); } } /** * {@inheritDoc} */ @Override public void componentResized(final ComponentEvent e) { // create off-screen drawing area this.offscreenImage = new BufferedImage(getWidth(), getHeight(), BufferedImage.TYPE_INT_ARGB); this.offscreenGraphics = this.offscreenImage.getGraphics(); } /** * {@inheritDoc} */ @Override public void componentMoved(final ComponentEvent e) { } /** * {@inheritDoc} */ @Override public void componentShown(final ComponentEvent e) { } /** * {@inheritDoc} */ @Override public void componentHidden(final ComponentEvent e) { } /** * {@inheritDoc} */ @Override public void windowOpened(final WindowEvent e) { // start the thread Thread t = new Thread(this); t.start(); } /** * {@inheritDoc} */ @Override public void windowClosing(final WindowEvent e) { } /** * {@inheritDoc} */ @Override public void windowClosed(final WindowEvent e) { } /** * {@inheritDoc} */ @Override public void windowIconified(final WindowEvent e) { } /** * {@inheritDoc} */ @Override public void windowDeiconified(final WindowEvent e) { } /** * {@inheritDoc} */ @Override public void windowActivated(final WindowEvent e) { } /** * {@inheritDoc} */ @Override public void windowDeactivated(final WindowEvent e) { } }