The goal of this lab is to restructure the Landscape, Simulation, and Agent classes to make it more flexible. This will enable all three previous simulations to be run using the same Landscape and parent Simulation class. My extension was to use it to make a simulation that I named PongSim, since it resembles that game. It has an agent Ball, obstacle Impediment, and resource EnergyChange.
We took a top-down approach to determining how we should structure the classes; that is, we started by thinking about the main method in Simulation in order to determine which class should have what responsibilities. The main method we want to use is very similar to the others I have been using, especially in Lab 3 where I made a parent Simulation class much like we are doing now. It starts by creating a specific type of Simulation based on command line arguments, then initializes it. Then it loops, iterating and updating the Simulation each time through. This gave us three methods for Simulation: initialize(), iterate(), and update().
The constructors for the child simulatons should accept dimensions. I decided to make the constructors rather than initialize() accept the dimensions since the dimensions of a Simulation should never change (it makes more sense to start a new simulation if you change the size of the landscape), but I will want to use initialize() to reset a simulation. Since different simulations will populate the Landscape differently, initialize must be implemented in every child class. Indeed, it makes no sense for the parent Simulation to implement initialize() at all, so I decided to make it an abstract method, so Simulation must be abstract, too.
iterate(), however, is common to all sorts of simulations, if you delegate tasks intellegently. If all types of agents (and Resources and Obstacles) have their own update methods that cause them to perform a single iteration, then all the simulation has to do is cycle through the agents/resources/obstacles and have them call their update methods. The simulation gets the list of agents/resources/obstacles from the Landscape class. It actually does not get the list, but instead gets an iterator. This enables Landscape to give access to the agents in a random order. The agents get to update twice so that it will be possible to implement something like Conway's game of life, where decisions are made before anything changes. iterate() is therefore a very simple class, and writing it showed the need for individual update methods for agents and iterators in Landscape.
update(Long millis) was also a simple method. It just calls the LandscapeDisplay's update method and waits.
That's all there is to Simulation. To clarify, it has private instance variables for the Landscape and LandscapeDisplay; public methods iterate(), update(), and getLandscape(); a main method to run any type of simulation; and an abstract initialize() method.
I extended Simulation to make PongSim. Its only constructor parameters are width, height, and scale, which it supers to Simulation to use to make a Landscape and LandscapeDisplay of appropriate sizes. Its only other method is initialize(). This grabs a referance to Landscape using getLandscape(), and then adds to it a single Ball with random attributes, ten random Impediments, Impediments around the border, and ten random EnergyChanges. That is all.
The next class to deal with is Landscape. No longer is it a simple grid, but a continuous field. This opens many opportunities for freerer movement, and also much trouble for identifying nearby 2-D objects. But that is really not all Landscape's problem to solve.
The constructor for Landscape needs only a width and height, which I have as ints, though I could have made them doubles since all points in the landscape are doubles.
Making Simulation had shown the need for three ArrayLists, one for each Agents, Obstacles, and Resources. Each has its own method to get***Iterator(). Since we decided that it didn't matter in what order the obstacles and resources updated, their getters return the iterators defined by ArrayList. We want Agents to update in a random order, though, so I had to define an AgentIterator class within Landscape. It makes an ArrayList<Integer> of indices, and each time next() is called, a random index is removed and the corrosponding agent is returned.
Landscape also needed methods to add(Type t) the different types of objects, as I used in PongSim's initialize(). Corrospondingly, it will also need remove(Type t) methods, which willl simply use ArrayList's remove(Object o) method. Be thankful for the java library! I overloaded the add and remove so that each has three definitions, one for each Agents, Obstacles, and Resources (rather than Type t, the arguments are Agent a, for instance).
Additionally, Landscape has accessors for its height and width, as well as a draw(Graphics g, double scale). This is called from the LandscapeDisplay class (which I will not treat in detail, it is a very simple GUI that paints its main frame by calling landscape.draw(g,scale)). The Landscape, in turn, makes all Obstacles, Resources, and Agents draw themselves into the Graphics. In lab, we said that the objects would want to be told where and how big to draw themselves. I decided that this was actually not information that the Landscape knows. I want my different sorts of Agents to take up different amounts of space, so I have the landscape only send the scale, and have each object use that to calculate its correct position and size.
Landscape is versitle, and does not need to be extended for my pong simulation. I did, however, add a few methods to it. The Ball needs to know if its motion will make it run into an Obstacle, so there is a Landscape method getObstaclesIn(Rectangle section) that returns an ArrayList of any Obstacles in the Rectangle (a class a made that holds arrays for x and y coordinates). Since Landscape doesn't know the shape of the the Obstacles, it has to ask every Obstacle if it is inside or intersects the Rectangle(so, there's a method that all Obstacles must implement). Likewise, the Ball needs to know if it is going to slide over an EnergyChange, so Landscape has a getResourcesIn(Rectangle section) that acts just like the Obstacle method (so Resources need isIn(Rectangle r) methods as well). It only follows that the Ball might want to know if it would collide with another ball, so I added getAgentsIn(Rectangle section) for good measure. I think that I may want to replace Rectangle with some other class, such as Area, or Shape, that would allow me to look in a circular radius about a point. Or else I can just make a getNeighbors(x,y,radius) for that, it will depend on what simulations I end up performing.
And that's it for Landscape, it doesn't have any very hard tasks to perform since there can be so much variation in agents/resources/obstacles that it can't do much.
These are all defined as interfaces. They all have methods to setPosition, getX, and getY. Since probably all ARO would implement these in the same way, I created an abstract class PositionedObject that has two private instance double fields, one for x and one for y. It implements the three methods, but cannot check for validity since it doesn't know the Landscape. All interfaces also have two draw methods. One takes only two arguments, a Graphics and a double scale; the other needs a Graphics, initial x and y positions, and dx and dy values (all ints). The first is useful for the Landscape to call since it doesn't need to know anything about the specific object in order to call it. The scond is what we came up with in class, and maybe it will be needed later. For now, it is useful to call from the other draw method; the ARO can calculate how big and where to draw itself and then send the second draw method its results. In order to enable my getXXXIn(Rectangle r), all also have isIn(Rectangle r). The Agent interface also has primaryUpdate(Landscape l), secondaryUpdate(Landscape l). Resources has an update(Landscape l), and so does Obstacle. Resources have setType and getType methods. Additionaly, Obstacle has an encompasses(x,y). This method may be redundant with my isIn(r) methods, but it is sort of the reverse, and could possibly be useful. The problem with it is that it suggests that I might be interested in whether or not a specific point is in the Obstacle. I'm more interested in whether or not any point in a shape is in the obstacle. Certainly, treating Agents as points would make things SO much easier, but it also simplifies it TOO much. Anyway, the method is there in case I ever can treat things as points, or if I can figure out to test some finite number of points.
It's easier to understand the interfaces with some examples. Ball extends PositionedObject and implements Agent. It does not need to provide implementation for the positioning methods since it inherits them from PositionedObject, though it must access its x and y positions using thegetters since I made them private (I react to protected's strange behavior by just not using it). The constructor takes an x and y position, a speed, a direction, and a mass, all doubles. It supers the x and y postion, and saves the other variables in its own private fields, as well as defining the diameter to be .9*mass. draw(Graphics g, double scale) calculates the x and y draw positions by multiplying the Ball's x and y positions minus the radius by the scale. The dx and dy are both the diameter. That way, when the other draw method calls g.drawOval, it has the correct coordinates for the upper left corner and the diameter. The landscape doesn't know that Ball is a circle, so it can only call the first draw method. A Ball does not need to wait for others to move or think before doing anything, so its secondaryUpdate is empty. In primaryUpdate, it figures out where it wants to go(based on its velocity)and makes a rectangle that covers most of the area it will travel, as shown as the grey in the diagram.
If there is an Impediment (a type of linear obstacle) in the way, then it changes its angle so that it reflects off the surface. Then it checks the rectangle for an EnergyChange (a type of resource). If there is one, then it changes its speed so that its KE changes the appropriate amount. Only this doesn't work because I haven't yet figured out how to check if a circle is in a Rectangle. Anyway, afterward it sets its position to a new value. It doesn't check the validity because the impediments that surround the landscape should keep it in. In reality, it does occasionally escape, I think at the corners, but it is a rarely occuring bug. Since my simulation only has one ball, I left the isIn(r) empty.
As an example of an obstacle, my simulation has Impediments, constructed with x and y coordinates for their centers, a length, and a slope. They are stationary walls, so the update is empty. When it draws itself, it doesn't make use of the second method, but instead calculates the coordinates of its endpoints and draws a line in the first.
It does implement encompasses(x,y), but, as a line, it only encompasses a few very specific points, so it will almost NEVER return true. It also implements isIn(Rectangle r). This finds the intersection point for each line that makes up the rectangle and the Impediment. It then checks if that intersection point lies on the segments, and returns true if it does. Unfortunately, it should also return true if the Impediment completely lies inside the rectangle, but it does not. This shouldn't be a problem, though, since impediments are almost always too big to fit in the area covered by a Ball in one iteration.
As an example of a Resource, I an EnergyChange that adds or subtracts energy from the Ball. An EnergyChange is constructed with a typeName, xPos, yPos, and the number of Joules it changes. It draws itself in basically the same way that Ball does, but it uses fillOval, and makes it red if the change is negative and green if it is positive, and it uses its magnitude, rather than a mass, to determine size. Its getType and setType are typical getter and setter methods. Its update has a 1/100 chance of increasing the magnitude by a random amount between 0 and 1, and a 1/100 chance of decreasing. Unfortunately, my LandscapeDisplay is not repainting itself correctly; it is cumulative, so you can only really see them grow. It has an accessor for its magnitude. The only other method is isIn(Rectangle r). It actually calls a method I added to Rectangle that checks if a single point(in this case, the center of the EnergyChange) is "looslyWrapped" in it. This was a simplification I made because I didn't know how to do better. It checks that the x coordinate is to the left of at least one of the x-coordinates of the rectangle and to the right of at least one of the x-coordinates of the rectangle, and it does an analogous check for the y's. This works pretty well; watching it, it reacts to an EnergyChange when it looks like it should.
And here is a picture
The Ball starts in the lower left, and you can follow it as it makes its way to the top, reflecting off the Impediments on the sides and one random one. Then it comes down on a - EnergyChange,so it slows down, as you can tell since the circles are closer together (that is a good thing about the LandscapeDisplay not refreshing itself). Then it richoches offseveral impediments before it reaches a + EnergyChange, at which point it speeds up again I took this screenshot shortly after it hit the green so that you could still follow the path.
When it crosses lots of greens, it starts to go so fast that it is more likely to find some way to escape the bounds. I'm not sure how, but it does.
For an overview of the classes (their data fields and the names and return types of their methods), see the attatchment
Since I've been creating some degree of inheritance on the other projects, it was nice to finally make one that I can (hopefully) keep for several weeks. Between Gridworld and projects 2-4, I'd gotten quite good at dealing with objects in a grid, so the change is both exciting and a little intimidating; I think I'm going to be doing a lot more geometry.
I did make my 'Pong' interactive, and even managed to make it playable. I removed the bottom bound, and added a UserPaddle that extends Impediment. It implements update so that it moves to the left or right based on the number of times a user presses the left and right keys. The LandscapeDisplay listens for these, then calls sim.react(String command), which is sent to PongSim, where it calls user.move(1) or -1. move() updates an instance variable move, and that is what is used in update(), and it is reset to 0 each time. In the lab time for lab 6, I learned what was 'wrong' with my LandscapeDisplay class. I had though that something was 'right' in the previous labs that was not in this one. It turns out that the reason the previous labs worked is that they ended up recoloring the entire landscape since the tiles covered every pixel. To solve the problem, I have my Landscape.draw() draw a filled white rectangle before it adds the objects. So now it works, as shown!
The left border does extend all the way down, it just is slightly angled (since I can't have an undefined slope) so that the lower half is not in the picture. The simulation starts in the lower right, and the ball travels up to a positive EnergyChange. Then it bounces around, and I followed it with the UserPaddle for a while. When it actually comes down to the bottom, I almost missed it, but I held down the right arrow and the paddle moved quickly enough, though I overshot and it went off thescreen. It doesn't matter, though; I simply moved it right again until it came back.