开发者

Bouncing shape nested in another shape (What is wrong with this code)

开发者 https://www.devze.com 2023-03-06 11:57 出处:网络
I have an application that bounces Shapes around inside a JPanel. Whenever shapes hit a side they will bounce off in the other direction. I am trying to add a new shape called a NestingShape that cont

I have an application that bounces Shapes around inside a JPanel. Whenever shapes hit a side they will bounce off in the other direction. I am trying to add a new shape called a NestingShape that contains zero or more Shapes that bounce around inside it, while the NestingShape bounces around in the JPanel. The children of a NestingShape instance can be either simple Shapes or other NestingShape instances.

Right now I am having trouble with moving the children of a NestingShape within a NestingShape with the move(width, height) method in the NestingShape subclass. I am also having trouble with developing a method in the Shape superclass that can find the parent any given shape. I'll copy and paste the code I have come up with so far for the Shape superclass and NestingShape subclass below and the test c开发者_如何学JAVAases I am using to test the code so far:

Shape superclass:

NOTE: parent() and path() methods are the most relevant methods for this task and the parent() method is the one I'm having trouble implementing. There are a lot of little details such as fFill and count and such that are related to different Shapes that I have developed, and those can be ignored.

package bounce;

import java.awt.Color;
import java.util.List;

/**
 * Abstract superclass to represent the general concept of a Shape. This class
 * defines state common to all special kinds of Shape instances and implements
 * a common movement algorithm. Shape subclasses must override method paint()
 * to handle shape-specific painting.
 * 
 * @author wadfsd
 *
 */
public abstract class Shape {
    // === Constants for default values. ===
    protected static final int DEFAULT_X_POS = 0;

    protected static final int DEFAULT_Y_POS = 0;

    protected static final int DEFAULT_DELTA_X = 5;

    protected static final int DEFAULT_DELTA_Y = 5;

    protected static final int DEFAULT_HEIGHT = 35;

    protected static final int DEFAULT_WIDTH = 25;

    protected static final Color DEFAULT_COLOR = Color.black;

    protected static final String DEFAULT_STRING = "";
    // ===

    // === Instance variables, accessible by subclasses.
    protected int fX;

    protected int fY;

    protected int fDeltaX;

    protected int fDeltaY;

    protected int fWidth;

    protected int fHeight;

    protected boolean fFill;

    protected Color fColor;

    protected int count;

    protected int fState;

    protected int before;

    protected String fString;
    // ===

    /**
     * Creates a Shape object with default values for instance variables.
     */
    public Shape() {
        this(DEFAULT_X_POS, DEFAULT_Y_POS, DEFAULT_DELTA_X, DEFAULT_DELTA_Y, DEFAULT_WIDTH, DEFAULT_HEIGHT, DEFAULT_COLOR, DEFAULT_STRING);
    }

    /**
     * Creates a Shape object with a specified x and y position.
     */
    public Shape(int x, int y) {
        this(x, y, DEFAULT_DELTA_X, DEFAULT_DELTA_Y, DEFAULT_WIDTH, DEFAULT_HEIGHT, DEFAULT_COLOR, DEFAULT_STRING);
    }

    public Shape(int x, int y, String str) {
        this(x, y, DEFAULT_DELTA_X, DEFAULT_DELTA_Y, DEFAULT_WIDTH, DEFAULT_HEIGHT, DEFAULT_COLOR, str);
    }

    /**
     * Creates a Shape object with specified x, y, and color values.
     */
    public Shape(int x, int y, Color c) {
        this(x, y, DEFAULT_DELTA_X, DEFAULT_DELTA_Y, DEFAULT_WIDTH, DEFAULT_HEIGHT, c, DEFAULT_STRING);
    }

    public Shape(int x, int y, Color c, String str) {
        this(x, y, DEFAULT_DELTA_X, DEFAULT_DELTA_Y, DEFAULT_WIDTH, DEFAULT_HEIGHT, c, str);
    }

    /**
     * Creates a Shape instance with specified x, y, deltaX and deltaY values.
     * The Shape object is created with a default width, height and color.
     */
    public Shape(int x, int y, int deltaX, int deltaY) {
        this(x, y, deltaX, deltaY, DEFAULT_WIDTH, DEFAULT_HEIGHT, DEFAULT_COLOR, DEFAULT_STRING);
    }

    public Shape(int x, int y, int deltaX, int deltaY, String str) {
        this(x, y, deltaX, deltaY, DEFAULT_WIDTH, DEFAULT_HEIGHT, DEFAULT_COLOR, str);
    }

    /**
     * Creates a Shape instance with specified x, y, deltaX, deltaY and color values.
     * The Shape object is created with a default width and height.
     */
    public Shape(int x, int y, int deltaX, int deltaY, Color c) {
        this(x, y, deltaX, deltaY, DEFAULT_WIDTH, DEFAULT_HEIGHT, c, DEFAULT_STRING);
    }

    public Shape(int x, int y, int deltaX, int deltaY, Color c, String str) {
        this(x, y, deltaX, deltaY, DEFAULT_WIDTH, DEFAULT_HEIGHT, c, str);
    }

    /**
     * Creates a Shape instance with specified x, y, deltaX, deltaY, width and
     * height values. The Shape object is created with a default color.
     */
    public Shape(int x, int y, int deltaX, int deltaY, int width, int height) {
        this(x, y, deltaX, deltaY, width, height, DEFAULT_COLOR, DEFAULT_STRING);
    }

    public Shape(int x, int y, int deltaX, int deltaY, int width, int height, String str) {
        this(x, y, deltaX, deltaY, width, height, DEFAULT_COLOR, str);
    }

    public Shape(int x, int y, int deltaX, int deltaY, int width, int height, Color c) {
        this(x, y, deltaX, deltaY, width, height, c, DEFAULT_STRING);
    }

    /**
     * Creates a Shape instance with specified x, y, deltaX, deltaY, width,
     * height and color values.
     */
    public Shape(int x, int y, int deltaX, int deltaY, int width, int height, Color c, String str) {
        fX = x;
        fY = y;
        fDeltaX = deltaX;
        fDeltaY = deltaY;
        fWidth = width;
        fHeight = height;
        fFill = false;
        fColor = c;
        count = 0;
        fState = 1;
        before = 0;
        fString = str;
    }

    /**
     * Moves this Shape object within the specified bounds. On hitting a 
     * boundary the Shape instance bounces off and back into the two- 
     * dimensional world and logs whether a vertical or horizontal wall
     * was hit for the DynamicRectangleShape.
     * @param width width of two-dimensional world.
     * @param height height of two-dimensional world.
     */
    public void move(int width, int height) {
        int nextX = fX + fDeltaX;
        int nextY = fY + fDeltaY;

        if (nextY <= 0) {
            nextY = 0;
            fDeltaY = -fDeltaY;
            fFill = false;
            count++;
        } else if (nextY + fHeight >= height) {
            nextY = height - fHeight;
            fDeltaY = -fDeltaY;
            fFill = false;
            count++;
        }

        // When Shape hits a corner the vertical wall fFill value overrides the horizontal
        if (nextX <= 0) {
            nextX = 0;
            fDeltaX = -fDeltaX;
            fFill = true;
            count++;
        } else if (nextX + fWidth >= width) {
            nextX = width - fWidth;
            fDeltaX = -fDeltaX;
            fFill = true;
            count++;
        }

        fX = nextX;
        fY = nextY;
    }

    public void text(Painter painter, String str) {
        painter.drawCentredText(str, fX, fY, fWidth, fHeight);
    }

    /**
     * Returns the NestingShape that contains the Shape that method parent
     * is called on. If the callee object is not a child within a
     * NestingShape instance this method returns null.
     */
    public NestingShape parent() {
        // Related to NestingShape
    }

    /**
     * Returns an ordered list of Shape objects. The first item within the
     * list is the root NestingShape of the containment hierarchy. The last
     * item within the list is the callee object (hence this method always
     * returns a list with at least one item). Any intermediate items are
     * NestingShapes that connect the root NestingShape to the callee Shape.
     * E.g. given:
     * 
     *   NestingShape root = new NestingShape();
     *   NestingShape intermediate = new NestingShape();
     *   Shape oval = new OvalShape();
     *   root.add(intermediate);
     *   intermediate.add(oval);
     *   
     * a call to oval.path() yields: [root,intermediate,oval] 
     */
    public List<Shape> path() {
        // Related to NestingShape
    }

    /**
     * Method to be implemented by concrete subclasses to handle subclass
     * specific painting.
     * @param painter the Painter object used for drawing.
     */
    public abstract void paint(Painter painter);

    /**
     * Returns this Shape object's x position.
     */
    public int x() {
        return fX;
    }

    /**
     * Returns this Shape object's y position.
     */
    public int y() {
        return fY;
    }

    /**
     * Returns this Shape object's speed and direction.
     */
    public int deltaX() {
        return fDeltaX;
    }

    /**
     * Returns this Shape object's speed and direction.
     */
    public int deltaY() {
        return fDeltaY;
    }

    /**
     * Returns this Shape's width.
     */
    public int width() {
        return fWidth;
    }

    /**
     * Returns this Shape's height.
     */
    public int height() {
        return fHeight;
    }

    /**
     * Returns a String whose value is the fully qualified name of this class 
     * of object. E.g., when called on a RectangleShape instance, this method 
     * will return "bounce.RectangleShape".
     */
    public String toString() {
        return getClass().getName();
    }
}

NestingShape subclass:

NOTE: Having trouble with the move() method

package bounce;

import java.util.ArrayList;
import java.util.List;

public class NestingShape extends Shape {
    private List<Shape> nest = new ArrayList<Shape>();

    /**
     * Creates a NestingShape object with default values for state.
     */
    public NestingShape() {
        super();
    }

    /**
     * Creates a NestingShape object with specified location values, default values for other
     * state items.
     */
    public NestingShape(int x, int y) {
        super(x,y);
    }

    /**
     * Creates a NestingShape with specified values for location, velocity and direction.
     * Non-specified state items take on default values.
     */
    public NestingShape(int x, int y, int deltaX, int deltaY) {
        super(x,y,deltaX,deltaY);
    }

    /**
     * Creates a NestingShape with specified values for location, velocity, direction, width, and
     * height.
     */
    public NestingShape(int x, int y, int deltaX, int deltaY, int width, int height) {
        super(x,y,deltaX,deltaY,width,height);
    }

    /**
     * Moves a NestingShape object (including its children) with the bounds specified by arguments
     * width and height.
     */
    public void move(int width, int height) {   
        int nextX = fX + fDeltaX;
        int nextY = fY + fDeltaY;

        if (nextY <= 0) {
            nextY = 0;
            fDeltaY = -fDeltaY;
            fFill = false;
            count++;
        } else if (nextY + fHeight >= height) {
            nextY = height - fHeight;
            fDeltaY = -fDeltaY;
            fFill = false;
            count++;
        }

        if (nextX <= 0) {
            nextX = 0;
            fDeltaX = -fDeltaX;
            fFill = true;
            count++;
        } else if (nextX + fWidth >= width) {
            nextX = width - fWidth;
            fDeltaX = -fDeltaX;
            fFill = true;
            count++;
        }

        fX = nextX;
        fY = nextY;

        // Move children
        for (int i = 0; i < shapeCount(); i++) {
            Shape shape = shapeAt(i);

            int nextXChild = shape.fX + shape.fDeltaX;
            int nextYChild = shape.fY + shape.fDeltaY;

            if (nextYChild <= 0) {
                nextYChild = 0;
                shape.fDeltaY = -shape.fDeltaY;
            } else if (nextYChild + shape.fHeight >= fHeight) {
                nextYChild = fHeight - shape.fHeight;
                shape.fDeltaY = -shape.fDeltaY;
            }

            if (nextXChild <= 0) {
                nextXChild = 0;
                shape.fDeltaX = -shape.fDeltaX;
            } else if (nextXChild + fWidth >= width) {
                nextXChild = fWidth - shape.fWidth;
                shape.fDeltaX = -shape.fDeltaX;
            }

            shape.fX = nextXChild;
            shape.fY = nextYChild;
        }
    }

    /**
     * Paints a NestingShape object by drawing a rectangle around the edge of its bounding box.
     * The NestingShape object's children are then painted.
     */
    public void paint(Painter painter) {
        painter.drawRect(fX,fY,fWidth,fHeight);
        painter.translate(fX,fY);
        for (int i = 0; i < shapeCount(); i++) {
            Shape shape = shapeAt(i);
            shape.paint(painter);
        }
        painter.translate(0,0);
    }

    /**
     * Attempts to add a Shape to a NestingShape object. If successful, a two-way link is
     * established between the NestingShape and the newly added Shape. Note that this method
     * has package visibility - for reasons that will become apparent in Bounce III.
     * @param shape the shape to be added.
     * @throws IllegalArgumentException if an attempt is made to add a Shape to a NestingShape
     * instance where the Shape argument is already a child within a NestingShape instance. An
     * IllegalArgumentException is also thrown when an attempt is made to add a Shape that will
     * not fit within the bounds of the proposed NestingShape object.
     */
    void add(Shape shape) throws IllegalArgumentException {
        if (contains(shape)) {
            throw new IllegalArgumentException();
        } else if (shape.fWidth > fWidth || shape.fHeight > fHeight) {
            throw new IllegalArgumentException();
        } else {
            nest.add(shape);
        }
    }

    /**
     * Removes a particular Shape from a NestingShape instance. Once removed, the two-way link
     * between the NestingShape and its former child is destroyed. This method has no effect if
     * the Shape specified to remove is not a child of the NestingShape. Note that this method
     * has package visibility - for reasons that will become apparent in Bounce III.
     * @param shape the shape to be removed.
     */
    void remove(Shape shape) {
        int index = indexOf(shape);
        nest.remove(index);
    }

    /**
     * Returns the Shape at a specified position within a NestingShape. If the position specified
     * is less than zero or greater than the number of children stored in the NestingShape less
     * one this method throws an IndexOutOfBoundsException.
     * @param index the specified index position.
     */
    public Shape shapeAt(int index) throws IndexOutOfBoundsException {
        if (index < 0 || index >= shapeCount()) {
            throw new IndexOutOfBoundsException();
        } else {
            Shape shape = nest.get(index);
            return shape;
        }
    }

    /**
     * Returns the number of children contained within a NestingShape object. Note this method is
     * not recursive - it simply returns the number of children at the top level within the callee
     * NestingShape object.
     */
    public int shapeCount() {
        int number = nest.size();
        return number;
    }

    /**
     * Returns the index of a specified child within a NestingShape object. If the Shape specified
     * is not actually a child of the NestingShape this method returns -1; otherwise the value
     * returned is in the range 0 .. shapeCount() - 1.
     * @param shape the shape whose index position within the NestingShape is requested.
     */
    public int indexOf(Shape shape) {
        int index = nest.indexOf(shape);
        return index;
    }

    /**
     * Returns true if the shape argument is a child of the NestingShape object on which this method
     * is called, false otherwise.
     */
    public boolean contains(Shape shape) {
        boolean child = nest.contains(shape);
        return child;
    }
}

TestNestingShape test cases:

package bounce;

import java.util.List;

import junit.framework.TestCase;

/**
 * Class to test class NestingShape according to its specification.
 */
public class TestNestingShape extends TestCase {

    private NestingShape topLevelNest;
    private NestingShape midLevelNest;
    private NestingShape bottomLevelNest;
    private Shape simpleShape;

    public TestNestingShape(String name) {
        super(name);
    }

    /**
     * Creates a Shape composition hierarchy with the following structure:
     *   NestingShape (topLevelNest)
     *     |
     *     --- NestingShape (midLevelNest)
     *           |
     *           --- NestingShape (bottomLevelNest)
     *           |
     *           --- RectangleShape (simpleShape)
     */
    protected void setUp() throws Exception {
        topLevelNest = new NestingShape(0, 0, 2, 2, 100, 100);
        midLevelNest = new NestingShape(0, 0, 2, 2, 50, 50);
        bottomLevelNest = new NestingShape(5, 5, 2, 2, 10, 10);
        simpleShape = new RectangleShape(1, 1, 1, 1, 5, 5);

        midLevelNest.add(bottomLevelNest);
        midLevelNest.add(simpleShape);
        topLevelNest.add(midLevelNest);
    }

    /**
     * Checks that methods move() and paint() correctly move and paint a 
     * NestingShape's contents.
     */
    public void testBasicMovementAndPainting() {
        Painter painter = new MockPainter();

        topLevelNest.move(500, 500);
        topLevelNest.paint(painter);
        assertEquals("(rectangle 2,2,100,100)(rectangle 2,2,50,50)(rectangle 7,7,10,10)(rectangle 2,2,5,5)", painter.toString());
    }

    /**
     * Checks that method add successfuly adds a valid Shape, supplied as 
     * argument, to a NestingShape instance. 
     */
    public void testAdd() {
        // Check that topLevelNest and midLevelNest mutually reference each other.
        assertSame(topLevelNest, midLevelNest.parent());
        assertTrue(topLevelNest.contains(midLevelNest));

        // Check that midLevelNest and bottomLevelNest mutually reference each other.
        assertSame(midLevelNest, bottomLevelNest.parent());
        assertTrue(midLevelNest.contains(bottomLevelNest));
    }

    /**
     * Check that method add throws an IlegalArgumentException when an attempt 
     * is made to add a Shape to a NestingShape instance where the Shape 
     * argument is already part of some NestingShape instance.
     */
    public void testAddWithArgumentThatIsAChildOfSomeOtherNestingShape() {
        try {
            topLevelNest.add(bottomLevelNest);
            fail();
        } catch(IllegalArgumentException e) {
            // Expected action. Ensure the state of topLevelNest and 
            // bottomLevelNest has not been changed.
            assertFalse(topLevelNest.contains(bottomLevelNest));
            assertSame(midLevelNest, bottomLevelNest.parent());
        }
    }

    /**
     * Check that method add throws an IllegalArgumentException when an attempt
     * is made to add a shape that will not fit within the bounds of the 
     * proposed NestingShape object.
     */
    public void testAddWithOutOfBoundsArgument() {
        Shape rectangle = new RectangleShape(80, 80, 2, 2, 50, 50);

        try {
            topLevelNest.add(rectangle);
            fail();
        } catch(IllegalArgumentException e) {
            // Expected action. Ensure the state of topLevelNest and 
            // rectangle has not been changed.
            assertFalse(topLevelNest.contains(rectangle));
            assertNull(rectangle.parent());
        }
    }

    /**
     * Check that method remove breaks the two-way link between the Shape 
     * object that has been removed and the NestingShape it was once part of.
     */
    public void testRemove() {
        topLevelNest.remove(midLevelNest);
        assertFalse(topLevelNest.contains(midLevelNest));
        assertNull(midLevelNest.parent());
    }

    /**
     * Check that method shapeAt returns the Shape object that is held at a
     * specified position within a NestingShape instance.
     */
    public void testShapeAt() {
        assertSame(midLevelNest, topLevelNest.shapeAt(0));
    }

    /**
     * Check that method shapeAt throws a IndexOutOfBoundsException when called
     * with an invalid index argument.
     */
    public void testShapeAtWithInvalidIndex() {
        try {
            topLevelNest.shapeAt(1);
            fail();
        } catch(IndexOutOfBoundsException e) {
            // Expected action.
        }
    }

    /**
     * Check that method shapeCount returns zero when called on a NestingShape
     * object without children.
     */
    public void testShapeCountOnEmptyParent() {
        assertEquals(0, bottomLevelNest.shapeCount());
    }

    /**
     * Check that method shapeCount returns the number of children held within 
     * a NestingShape instance - where the number of children > 0.
     */
    public void testShapeCountOnNonEmptyParent() {
        assertEquals(2, midLevelNest.shapeCount());
    }

    /**
     * Check that method indexOf returns the index position within a
     * NestingShape instance of a Shape held within the NestingShape. 
     */
    public void testIndexOfWith() {
        assertEquals(0, topLevelNest.indexOf(midLevelNest));
        assertEquals(1, midLevelNest.indexOf(simpleShape));
    }

    /**
     * Check that method indexOf returns -1 when called with an argument that
     * is not part of the NestingShape callee object.
     */
    public void testIndexOfWithNonExistingChild() {
        assertEquals(-1, topLevelNest.indexOf(bottomLevelNest));
    }

    /**
     * Check that Shape's path method correctly returns the path from the root
     * NestingShape object through to the Shape object that path is called on.
     */
    public void testPath() {
        List<Shape> path = simpleShape.path();

        assertEquals(3, path.size());
        assertSame(topLevelNest, path.get(0));
        assertSame(midLevelNest, path.get(1));
        assertSame(simpleShape, path.get(2));
    }

    /**
     * Check that Shape's path method correctly returns a singleton list
     * containing only the callee object when this Shape object has no parent.
     */
    public void testPathOnShapeWithoutParent() {
        List<Shape> path = topLevelNest.path();

        assertEquals(1, path.size());
        assertSame(topLevelNest, path.get(0));
    }
}

With the code I have so far when I run the test cases, I am unable to test the testAdd and testRemove related test cases to make sure I'm adding shapes properly because I haven't yet developed the parent() method in the Shape class. But I can't think of a way of implementing the parent method.

Whenever I testBasicMovementAndPainting, I also get a failed test because my current move() method (in the NestingShape class) only moves the children in the first NestingShape and doesn't move the children of the midLevelNest.

It's a bit of a long read and I'm not sure if that provides enough context as there are a lot of other classes in the package that I didn't include but if anyone could help I would really appreciate it.

Thanks.


For the "parent" problem: a Shape needs an extra Shape attribute that points to the outer nesting shape (it's container / parent):

private Shape parent = null;

you can either set it with a constructor or simply add getter/setter methods:

public Shape(Shape parent) {
  this.parent = parent;
}

public void setParent(Shape parent) {
  this.parent = parent;
}

public Shape parent() {
  return parent;
}

Note the problem now is that any shape can be container for other shapes - it is not restricted to NestingShape. But if I declare the parent as a NestingShape, then we have the ugly situation, that Shape depends on NestingShape, its subclass.

Maybe you simply define an extra interface named ShapeContainer which adds container functionality to a Shape, like

public interface ShapeContainer {
  public List<Shape> getChildren();
  // .. more?
}

Then your class signature would look like this

public class NestingShape extends Shape implements ShapeContainer

and the type of the parent field in Shape would be ShapeContainer.

0

精彩评论

暂无评论...
验证码 换一张
取 消