2 * Copyright (c) 2002 Erik Rasmussen - All Rights Reserved
6 import appletbots
.geometry
.Point
;
7 import appletbots
.geometry
.Vector
;
11 import java
.util
.ArrayList
;
12 import java
.util
.Collections
;
13 import java
.util
.Comparator
;
14 import java
.util
.HashMap
;
15 import java
.util
.Iterator
;
16 import java
.util
.List
;
20 * This class represents a world in which multiple agents can exist and
21 * interact with each other or with objects in the world. The positions and
22 * velocities of objects and agents are kept internally. The positions cannot
23 * be accessed by the agents or the objects themselves. This keeps all agent
24 * algorithms completely vector-based and, therefore, scalable.
26 * @author Erik Rasmussen
28 public class World
extends JPanel
31 * The maximum number of attempts (1000) that should be made trying to add
32 * an object randomly to the world. This is to prevent infinite loops if
33 * there is no space for the object.
35 protected static final int MAX_ADD_OBJECT_ATTEMPTS
= 1000;
37 * A hashtable mapping each object to its location and velocity datum
39 protected Map objectsTable
= new HashMap();
41 * The number of milliseconds to sleep in between time cycles
43 protected int delay
= 500;
45 * The thread driving the world
47 protected WorldThread thread
;
49 * The number of time cycles that have passed
51 protected long time
= 0;
53 * Registered listeners
55 protected List listeners
= new ArrayList();
57 * The object that is currently selected
59 protected WorldObject selectedObject
;
61 * The color to paint the object that is selected
63 protected Color selectedObjectColor
= Color
.yellow
;
65 * The color in which to paint the "sight circle" of the selected object
67 protected Color selectedSight
;
70 * Constructs a new world with the given dimensions
72 * @param width The width of the world in pixels
73 * @param height The height of the world in pixels
75 public World(final int width
, final int height
)
77 setMinimumSize(new Dimension(width
, height
));
78 setMaximumSize(new Dimension(width
, height
));
79 setBackground(Color
.black
);
83 * Paints the world (invoked by Swing)
85 * @param g The graphics object on which to paint the world
87 public void paint(final Graphics g
)
94 * Paints the objects in the world
96 * @param g The graphics object on which to paint the objects
98 protected void paintObjects(final Graphics g
)
100 if (selectedSight
!= null && selectedObject
instanceof Agent
)
102 g
.setColor(selectedSight
);
103 final int sight
= ((Agent
) selectedObject
).getSight();
104 final WorldObjectData selectedObjectData
= getData(selectedObject
);
105 g
.drawOval((int) Math
.round(selectedObjectData
.getLocation().x
) - selectedObject
.getSize() - sight
,
106 (int) Math
.round(selectedObjectData
.getLocation().y
) - selectedObject
.getSize() - sight
,
107 2 * (selectedObject
.getSize() + sight
),
108 2 * (selectedObject
.getSize() + sight
));
110 synchronized (objectsTable
)
112 for (Iterator iterator
= objectsTable
.keySet().iterator(); iterator
.hasNext();)
114 final WorldObject object
= (WorldObject
) iterator
.next();
115 // don't paint carried objects
116 if (!(object
instanceof CarriableObject
&& ((CarriableObject
) object
).getCarriedBy() != null))
117 paintObject(object
, g
);
123 * Paints an individual object in the world
125 * @param object The object to paint
126 * @param g The graphics object on which to paint the object
128 protected void paintObject(final WorldObject object
, final Graphics g
)
130 final WorldObjectData data
= getData(object
);
131 if (object
.equals(selectedObject
))
132 g
.setColor(selectedObjectColor
);
134 g
.setColor(object
.getColor());
137 g
.fillOval((int) Math
.round(data
.getLocation().x
) - object
.getSize(),
138 (int) Math
.round(data
.getLocation().y
) - object
.getSize(),
139 object
.getSize() * 2,
140 object
.getSize() * 2);
142 // paint any objects the current object is carrying
143 if (object
instanceof CarrierAgent
)
145 final CarriableObject
[] carriedItems
= ((CarrierAgent
) object
).getInventory();
146 for (int i
= 0; i
< carriedItems
.length
; i
++)
147 paintObject(carriedItems
[i
], g
);
150 if (object
instanceof Agent
)
152 final Agent agent
= (Agent
) object
;
153 final VectorToDraw
[] vectorsToDraw
= agent
.getVectorsToDraw();
154 for (int i
= 0; i
< vectorsToDraw
.length
; i
++)
156 g
.setColor(vectorsToDraw
[i
].color
);
157 final Point endPoint
= data
.getLocation().add(vectorsToDraw
[i
].vector
);
158 g
.drawLine((int) Math
.round(data
.getLocation().x
),
159 (int) Math
.round(data
.getLocation().y
),
160 (int) Math
.round(endPoint
.x
),
161 (int) Math
.round(endPoint
.y
));
167 * Adds an object to a random position in the world. The object will have
168 * an initial speed of 0.
170 * @param object The object to add
171 * @throws CollisionException Thrown if the object cannot be added randomly
172 * to the world in MAX_ADD_OBJECT_ATTEMPTS
174 public void addObject(final WorldObject object
) throws CollisionException
176 addObject(object
, new Vector(0, 0));
180 * Adds an object to a random position in the world with the given initial
183 * @param object The object to add
184 * @param velocity The initial velocity
185 * @throws CollisionException Thrown if the object cannot be added randomly
186 * to the world in MAX_ADD_OBJECT_ATTEMPTS
188 public void addObject(final WorldObject object
, final Vector velocity
) throws CollisionException
190 for (int i
= 0; i
< MAX_ADD_OBJECT_ATTEMPTS
; i
++)
194 addObject(object
, getRandomPoint(object
.getSize()), velocity
);
197 catch (CollisionException e
)
200 catch (OutOfThisWorldException e
)
204 throw new CollisionException("Tried " + MAX_ADD_OBJECT_ATTEMPTS
+ " times to add an object randomly and failed.");
208 * Adds an object to the world at the given position with the given initial
211 * @param object The object to add
212 * @param location The location to add the object
213 * @param velocity The initial velocity
214 * @throws CollisionException Thrown if the location given is at least
215 * partially occupied by another object
216 * @throws OutOfThisWorldException Thrown if the location given will place
217 * at least part of the object outside the
218 * boundaries of the world
220 public void addObject(final WorldObject object
, final Point location
, final Vector velocity
) throws CollisionException
, OutOfThisWorldException
222 if (objectsTable
.containsKey(object
))
223 return; // already have this object
224 if (!inWorld(location
, object
.getSize()))
225 throw new OutOfThisWorldException();
226 final WorldObjectData data
= new WorldObjectData(location
, velocity
, object
);
227 synchronized (objectsTable
)
229 for (Iterator iterator
= objectsTable
.keySet().iterator(); iterator
.hasNext();)
231 final WorldObject otherObject
= (WorldObject
) iterator
.next();
232 final WorldObjectData otherObjectData
= getData(otherObject
);
233 final double distance
= location
.distance(otherObjectData
.getLocation());
234 if (distance
< object
.getSize() + otherObject
.getSize())
235 throw new CollisionException(object
, otherObject
);
237 objectsTable
.put(object
, data
);
238 if (object
instanceof Agent
)
239 ((Agent
) object
).setWorld(this);
244 * Returns a random point in the world
246 * @return A random point in the world
248 protected Point
getRandomPoint()
250 return getRandomPoint(0);
254 * Returns a random point in the world within a given distance from the
257 * @param distance The minimum distance from the boundaries that the point
259 * @return A random point in the world within a given distance from the
262 protected Point
getRandomPoint(final int distance
)
264 return new Point(Math
.random() * (getWorldWidth() - distance
- distance
) + distance
,
265 Math
.random() * (getWorldHeight() - distance
- distance
) + distance
);
269 * Removes an object from the world
271 * @param object The object to remove
273 public void removeObject(final WorldObject object
)
275 synchronized (objectsTable
)
277 objectsTable
.remove(object
);
282 * Returns the location and velocity data for the given object
284 * @param object The object to get the data for
285 * @return The location and velocity data for the given object
287 protected WorldObjectData
getData(final WorldObject object
)
289 if (object
instanceof CarriableObject
&& ((CarriableObject
) object
).getCarriedBy() != null)
290 return getData(((CarriableObject
) object
).getCarriedBy());
292 return (WorldObjectData
) objectsTable
.get(object
);
296 * Returns the velocity for the given object
298 * @param object The object to get the velocity of
299 * @return The velocity for the given object
301 public Vector
getVelocity(final WorldObject object
)
303 return getData(object
).getVelocity();
309 * @param object The object to move
311 private void moveObject(final WorldObject object
)
313 final WorldObjectData data
= getData(object
);
315 // don't move carried objects
316 if (object
instanceof CarriableObject
&& ((CarriableObject
) object
).getCarriedBy() != null)
319 if (object
instanceof Agent
)
321 final Agent agent
= (Agent
) object
;
322 data
.setVelocity(data
.getVelocity().add(agent
.getAcceleration()));
324 if (data
.getVelocity().getLength() == 0)
326 final Point newLocation
= data
.getLocation().add(data
.getVelocity());
327 // check that new location is in the world
328 if (!inWorld(newLocation
, object
.getSize()))
330 bounceOffWall(object
);
335 // check for collisions
336 boolean collision
= false;
337 final WorldObject aObject
= object
;
338 final WorldObjectData a
= data
;
339 for (Iterator iterator
= objectsTable
.keySet().iterator(); iterator
.hasNext();)
341 final WorldObject bObject
= (WorldObject
) iterator
.next();
342 final WorldObjectData b
= getData(bObject
);
343 // Define C as the vector from the center of A to the center of B
344 final Vector c
= new Vector(a
.getLocation(), b
.getLocation());
345 final double lengthOfC
= c
.getLength();
347 // Make sure A is going to travel far enough to hit B
348 final double aVelocityLength
= a
.getVelocity().getLength();
349 final double radiiSum
= aObject
.getSize() + bObject
.getSize();
350 if (aVelocityLength
< lengthOfC
- radiiSum
)
352 // Make sure A is going towards B
353 if (a
.getVelocity().dotProduct(c
) <= 0)
355 // Normalize A's velocity vector and call it N
356 final Vector n
= a
.getVelocity();
358 // Get dot product of N and C. This is the point along A's path
359 // where it will be closest to B. We'll call it D.
360 final double d
= n
.dotProduct(c
);
361 // Use the pythagorean theorem to get the value for the square
362 // of the closest distance on V to B, and call it F.
363 // (ie. the shortest distance from V to B is sqrt(F)
364 final double f
= lengthOfC
* lengthOfC
- d
* d
;
365 // Define T such that "the longest distance A can travel without
366 // hitting B" = D - sqrt(T). Again by the pythagorean theorem
367 final double radiiSumSquared
= radiiSum
* radiiSum
;
368 final double t
= radiiSumSquared
- f
;
369 // Check if closest point is within the two radii of B. To save
370 // time, rather than taking the sqrt of F, we'll just square the
371 // other side of the equation
374 // if A won't travel far enough to collide with B, there's no
376 final double distanceToCollision
= d
- Math
.sqrt(t
);
377 if (aVelocityLength
< distanceToCollision
)
380 collide(aObject
, bObject
);
383 data
.setLocation(newLocation
);
388 * Called by the WorldThread. This method does three things: it allows all
389 * the agents to observe the world, moves all the objects, and repaints the
392 public void incrementTime()
395 for (Iterator iterator
= objectsTable
.keySet().iterator(); iterator
.hasNext();)
397 final WorldObject object
= (WorldObject
) iterator
.next();
398 if (object
instanceof Agent
)
399 ((Agent
) object
).observeWorld();
402 for (int i
= 0; i
< listeners
.size(); i
++)
403 ((WorldListener
) listeners
.get(i
)).timeIncremented(time
);
409 * Returns the number of milliseconds to wait between time cycles
411 * @return The number of milliseconds to wait between time cycles
413 public int getDelay()
419 * Sets the number of milliseconds to wait between time cycles
421 * @param delay The number of milliseconds to wait between time cycles
423 public void setDelay(final int delay
)
429 * Starts the world thread
431 public void startThread()
435 thread
= new WorldThread(this);
438 for (int i
= 0; i
< listeners
.size(); i
++)
439 ((WorldListener
) listeners
.get(i
)).threadStarted();
444 * Stops the world thread
446 public void stopThread()
450 thread
.setRunning(false);
453 for (int i
= 0; i
< listeners
.size(); i
++)
454 ((WorldListener
) listeners
.get(i
)).threadStopped();
459 * Returns whether or not the world thread is running
461 * @return Whether or not the world thread is running
463 public boolean isRunning()
465 return thread
!= null && thread
.getRunning();
469 * Returns all the objects that can be seen by the given agent, normally
470 * called by the given agent himself.
472 * THIS METHOD WILL NOT RETURN OTHER AGENTS! To get seen agents, use
475 * @param agent The agent for which to get the seen objects
476 * @return All the objects the given agent can see
478 public List
getSeenObjects(final Agent agent
)
480 final List seenObjects
= new ArrayList();
481 for (Iterator iterator
= objectsTable
.keySet().iterator(); iterator
.hasNext();)
483 final WorldObject object
= (WorldObject
) iterator
.next();
484 if (!(object
instanceof Agent
) && getDistanceBetweenObjects(agent
, object
) <= agent
.getSight())
485 seenObjects
.add(object
);
491 * Returns all the other agents that can be seen by the given agent,
492 * normally called by the given agent himself.
494 * @param agent The agent for which to return the seen neighbors
495 * @return All the agents that the given agent can see
497 public List
getNeighbors(final Agent agent
)
499 final List neighbors
= new ArrayList();
500 for (Iterator iterator
= objectsTable
.keySet().iterator(); iterator
.hasNext();)
502 final WorldObject object
= (WorldObject
) iterator
.next();
503 if (object
instanceof Agent
&& !object
.equals(agent
) && getDistanceBetweenObjects(agent
, object
) <= agent
.getSight())
504 neighbors
.add(object
);
510 * Returns a vector from the first object to the second object. This is
511 * useful when an agent wants to move towards an object. The returned
512 * vector is the direction in which the agent should move.
514 * Note: The vector returned is not from the center of the first object to the
515 * center of the second object! The vector returned is the vector that, if added
516 * to the first object's location, would place the object so that it was touching
517 * the second object. In other words, it's the vector from the center of the first
518 * object to the center of the object <b>minus</b> the radii of the both objects.
520 * @param a The first object
521 * @param b The second object
522 * @return A vector from the first object to the second object
524 public Vector
getVectorToObject(final WorldObject a
, final WorldObject b
)
526 Vector v
= new Vector(getData(a
).getLocation(), getData(b
).getLocation());
527 v
= v
.setLength(v
.getLength() - a
.getSize() - b
.getSize());
532 * Returns the distance between the two objects
535 * @param b Another object
536 * @return The distance between the two objects
538 protected double getDistanceBetweenObjects(final WorldObject a
, final WorldObject b
)
540 return getVectorToObject(a
, b
).getLength();
544 * Returns the width (in pixels) of the world
546 * @return The width (in pixels) of the world
548 public int getWorldWidth()
550 return getMinimumSize().width
;
554 * Returns the height (in pixels) of the world
556 * @return The height (in pixels) of the world
558 public int getWorldHeight()
560 return getMinimumSize().height
;
564 * Returns the number of elapsed time cycles
566 * @return The number of elapsed time cycles
568 public long getTime()
574 * Imports all the objects from another world into this one
576 * @param world The world to import the objects from
578 public void importObjects(final World world
)
580 objectsTable
= world
.objectsTable
;
582 selectedObject
= null;
583 selectedObjectColor
= null;
589 * Returns whether or not an object with the given size at the given
590 * location would be complete inside the boundaries of the world
592 * @param location The location of the hypothetical object
593 * @param size The size of the hypothetical object
594 * @return Whether or not an object with the given size at the given
595 * location would be complete inside the boundaries of the world
597 protected boolean inWorld(final Point location
, final int size
)
599 return location
.x
>= size
&&
600 location
.x
< getWorldWidth() - size
&&
601 location
.y
>= size
&&
602 location
.y
< getWorldHeight() - size
;
606 * Collides two objects. Using the objects' masses, locations, and
607 * velocities it calculates the objects' velocities after a perfectly
608 * elastic collision (i.e. no momentum or kinetic energy is lost).
611 * @param b Another object
613 private void collide(final WorldObject a
, final WorldObject b
)
615 final WorldObjectData aData
= getData(a
);
616 final WorldObjectData bData
= getData(b
);
618 // First, find the normalized vector n from the center of
619 // A to the center of B
620 final Vector n
= new Vector(aData
.getLocation(), bData
.getLocation());
623 // Find the length of the component of each of the velocities along n.
626 final double a1
= aData
.getVelocity().dotProduct(n
);
627 final double a2
= bData
.getVelocity().dotProduct(n
);
629 // Using the optimized version,
630 // optimizedP = Ê2(a1 - a2)
631 // ÊÊÊÊÊÊÊÊÊÊÊÊÊ-----------
632 // ÊÊÊÊÊÊÊÊÊÊÊÊÊÊÊm1 + m2
633 final double optimizedP
= (2.0 * (a1
- a2
)) / (a
.getMass() + b
.getMass());
635 // Calculate final velocity for A
636 // va' = va - optimizedP * mb * n
637 aData
.setVelocity(aData
.getVelocity().subtract(n
.multiply(optimizedP
* b
.getMass())));
639 // Calculate final velocity for B
640 // vb' = vb + optimizedP * ma * n
641 bData
.setVelocity(bData
.getVelocity().add(n
.multiply(optimizedP
* a
.getMass())));
643 // Notify objects of collision
648 * Bounces the object off the wall. Performs a calculation based on the
649 * object's location and velocity and modifies the object's velocity
650 * appropriately. The collision is perfectly elastic (i.e. no momentum or
651 * kinetic energy is lost)
653 * @param object The object to bounce
655 private void bounceOffWall(final WorldObject object
)
657 final WorldObjectData data
= getData(object
);
658 final Point newLocation
= data
.getLocation().add(data
.getVelocity());
660 // change velocity depending on which wall we've hit
661 if (newLocation
.x
< object
.getSize() || newLocation
.x
> getWorldWidth() - object
.getSize())
663 final Vector velocity
= data
.getVelocity();
665 data
.setVelocity(velocity
);
667 if (newLocation
.y
< object
.getSize() || newLocation
.y
> getWorldHeight() - object
.getSize())
669 final Vector velocity
= data
.getVelocity();
671 data
.setVelocity(velocity
);
676 * Returns the object at the given x and y coordinates. Used for user
677 * interaction and mouse-clicks.
679 * @param x The x coordinate
680 * @param y The y coordinate
681 * @return The object at the given location
683 protected WorldObject
getObjectAt(final int x
, final int y
)
685 final Point point
= new Point(x
, y
);
686 for (Iterator iterator
= objectsTable
.keySet().iterator(); iterator
.hasNext();)
688 final WorldObject object
= (WorldObject
) iterator
.next();
689 final WorldObjectData data
= getData(object
);
690 if (data
.getLocation().distance(point
) <= object
.getSize())
697 * Selects the object at the given location
699 * @param x The x coordinate
700 * @param y The y coordinate
702 public void selectObjectAt(final int x
, final int y
)
704 selectObject(getObjectAt(x
, y
));
708 * Selects the given object
710 * @param object The object to select
712 public void selectObject(final WorldObject object
)
714 selectedObject
= object
;
719 * Sets the color to paint the selected object
721 * @param selectedObjectColor The color to paint the selected object
723 public void setSelectedObjectColor(final Color selectedObjectColor
)
725 this.selectedObjectColor
= selectedObjectColor
;
729 * Sets the color with which to paint the selected object's "sight circle"
731 * @param selectedSight The color with which to paint the selected object's
734 public void setSelectedSight(final Color selectedSight
)
736 this.selectedSight
= selectedSight
;
740 * Drops an item from a CarrierAgent
742 * @param agent The agent dropping the item
743 * @param item The object beind dropped
744 * @throws CollisionException Thrown if the item cannot be placed within the "pickup distance"
746 public void dropItem(final CarrierAgent agent
, final CarriableObject item
) throws CollisionException
749 final WorldObjectData agentData
= getData(agent
);
750 Vector dropVector
= agentData
.getVelocity();
751 dropVector
= dropVector
.setLength(agent
.getSize() + item
.getSize());
752 synchronized (objectsTable
)
756 final Point dropPoint
= agentData
.getLocation().add(dropVector
);
757 boolean collided
= false;
758 for (Iterator iterator
= objectsTable
.keySet().iterator(); iterator
.hasNext();)
760 final WorldObject otherObject
= (WorldObject
) iterator
.next();
761 final WorldObjectData otherObjectData
= getData(otherObject
);
762 final double distance
= dropPoint
.distance(otherObjectData
.getLocation());
763 if (distance
< item
.getSize() + otherObject
.getSize() || !inWorld(dropPoint
, item
.getSize()))
767 if (collisions
> MAX_ADD_OBJECT_ATTEMPTS
)
768 throw new CollisionException(item
, otherObject
);
773 final WorldObjectData itemData
= getData(item
);
774 itemData
.setLocation(dropPoint
);
775 itemData
.setVelocity(agentData
.getVelocity());
779 dropVector
= Vector
.getRandom(agent
.getSize() + item
.getSize());
785 * Registers a listener for this world
787 public void addListener(final WorldListener listener
)
789 listeners
.add(listener
);
794 * Returns the closest object of the given type to the given agent that
795 * is seen by the agent
797 * @param agent The agent to get an object near
798 * @param type The type of objects to search for
799 * @return The closest seen object of the given type to the given agent
801 public WorldObject
getClosestObjectOfType(final Agent agent
, final Class type
)
803 final List objectsOfCorrectType
= new ArrayList();
804 for (Iterator iterator
= objectsTable
.keySet().iterator(); iterator
.hasNext();)
806 final WorldObject object
= (WorldObject
) iterator
.next();
807 if (type
.isInstance(object
) && getDistanceBetweenObjects(agent
, object
) <= agent
.getSight())
808 objectsOfCorrectType
.add(object
);
810 if (objectsOfCorrectType
.isEmpty())
814 Collections
.sort(objectsOfCorrectType
, objectDistanceComparator(agent
));
815 return (WorldObject
) objectsOfCorrectType
.iterator().next();
820 * Returns a comparator to sort other objects by their distance to the given object
822 * @param object The object to compare distances to
823 * @return A comparator to sort other objects by their distance to the given object
825 public Comparator
objectDistanceComparator(final WorldObject object
)
827 return new Comparator()
829 public int compare(final Object o1
, final Object o2
)
831 final Vector v1
= getVectorToObject(object
, (WorldObject
) o1
);
832 final Vector v2
= getVectorToObject(object
, (WorldObject
) o2
);
833 return v1
.compareTo(v2
);