Showing posts with label Java3D. Show all posts
Showing posts with label Java3D. Show all posts

Saturday, July 11, 2009

Animations in Java 3D

Wow, I haven't done much on this blog in a while... I haven't done a whole lot with the game lately but I got it up on github if anyone is interested: http://github.com/caspian311/boardgame.

So I have been messing around a bit with how the pieces move. The way it use to get done was by using keyframes. The problem with that there was no way to tell when you had reached the end of the animation to signal something else to happen. So I finally just switched it over to using Behaviors instead.

From my understanding all that you need to do is extend the javax.media.j3d.Behavior class, set the scheduling bounds (setSchedulingBounds()), then add it to the scene graph. Then you to override two methods initialize() and processStimulus(Enumeration stimuli).

In the iniatilize method you specify what event, or wake up criterion, will trigger the behavior. For animations you can make it based on a certain number of frames has gone by or a specified amount of time has gone by or it an AWT even occurred.

In the processStimulus method you loop through all the stimuli looking for the wake up criterion that you specified. Then you do whatever you want done (move/rotate/whatever something a bit) then if you want to wake up when the next time a criterion happens you call the wakeupOn() method.

Ok I'm not great at describing it, let's just see some code...


public class AnimationBehviour extends Behavior {
private static final float MOVEMENT_SPEED = 0.1f;
private static final int ANIMATION_WAITING = 10;
private final ListenerManager finishedListenerManager
= new ListenerManager();
private final TransformGroup transformGroup;
private final Vector3f currentLocation;
private final Vector3f moveToLocation;
private boolean atLocation;

public AnimationBehviour(Bounds bounds, TransformGroup
transformGroup, Vector3f currentLocation, Vector3f
moveToLocation) {
this.transformGroup = transformGroup;
this.currentLocation = currentLocation;
this.moveToLocation = moveToLocation;

setSchedulingBounds(bounds);
}

@Override
public void initialize() {
wakeupOn(new WakeupOnElapsedTime(10));
}

@SuppressWarnings("unchecked")
@Override
public void processStimulus(Enumeration stimuli) {
while (stimuli.hasMoreElements()) {
WakeupCriterion criterion = (WakeupCriterion)
stimuli.nextElement();
if (criterion instanceof WakeupOnElapsedTime) {
moveCloser();

if (!atLocation) {
wakeupOn(new WakeupOnElapsedTime(ANIMATION_WAITING));
} else {
setEnable(false);
finishedListenerManager.notifyListeners();
}
}
}
}

private void moveCloser() {
Transform3D transformation = new Transform3D();
transformGroup.getTransform(transformation);

if (isFinished(currentLocation)) {
atLocation = true;
} else {
updatePosition(currentLocation, moveToLocation);
transformation.set(currentLocation);
}

transformGroup.setTransform(transformation);
}

private void updatePosition(Vector3f currentPositionVector,
Vector3f endingPointVector) {
float[] currentPosition = new float[3];
currentPositionVector.get(currentPosition);
float[] endingPoint = new float[3];
endingPointVector.get(endingPoint);

for (int i = 0; i < currentPosition.length; i++) {
if (currentPosition[i] < endingPoint[i]) {
currentPosition[i] += MOVEMENT_SPEED;
} else if (currentPosition[i] > endingPoint[i]) {
currentPosition[i] -= 0.1f;
}
}

currentPositionVector.set(currentPosition);
}

private boolean isFinished(Vector3f currentPositionVector) {
boolean finished = false;

if (currentPositionVector.x <= moveToLocation.x + .1
&& currentPositionVector.x >= moveToLocation.x - .1) {
if (currentPositionVector.y <= moveToLocation.y + .1
&& currentPositionVector.y >= moveToLocation.y - .1) {
if (currentPositionVector.z <= moveToLocation.z + .1
&& currentPositionVector.z >= moveToLocation.z - .1) {
finished = true;
}
}
}

return finished;
}

public void addAnimationFinishedListener(
IListener animationFinishedListener) {
finishedListenerManager.addListener(
animationFinishedListener);
}
}


Also, since you are specifying whether or not to continue on each iteration you can add listeners to when the animation is finished and notify them. Here's how I call this class.

public void animateAlongPath(List path) {
this.path = path;
animateNextStep(getNextStep());
}

private void animateNextStep(Vector3f currentLocation) {
final Vector3f moveToLocation = getNextStep();
if (moveToLocation != null) {
AnimationBehviour animationBehaviour =
new AnimationBehviour(bounds, transformGroup,
currentLocation, moveToLocation);
animationBehaviour.addAnimationFinishedListener(
new IListener() {
public void fireEvent() {
parentGroup.removeChild(animationGroup);
animateNextStep(moveToLocation);
}
});

animationGroup.addChild(animationBehaviour);
parentGroup.addChild(animationGroup);
}
}

private Vector3f getNextStep() {
Vector3f nextStep = null;
if (pathIndex < path.size()) {
nextStep = path.get(pathIndex);
pathIndex++;
}
return nextStep;
}

So the piece will walk through the path of locations specified and when the animation is done for a given step it will notify the next step animation.

Saturday, February 21, 2009

Alright, so I blew through the first two stores that I was attempting:

1. User opens the application and sees the game board. Game board is a chess board (8 x 8 - alternating black and white squares) background is a gray. Camera is looking at the center from above and toward one side.

2. The user has one piece (a blue ball) on the board that is located on one side of the board on one of the center squares.

And up until now there was really no design to it. I started coding a class that had the capability of drawing on the 3D canvas and just kept going. Neither of these stories contain any user interaction yet so I was having a hard time coming up with testable code.

I eventually saw that there was a bit of logic needed for creating the checkered game board. So I thought I'd extract out something that would need to know how to do that. So I started going into an MVP pattern. I wanted the view to get something that it could use to create the proper rows and columns with the right alternating colors without too much logic. It wound up looking like this:

public void constructGrid(GameGridData data) {
for (int x = 0; x < data.getTileData().length; x++) {
for (int z = 0; z < data.getTileData()[0].length; z++) {
Tile tile = new Tile(data.getTileData()[x][z]);
board.addChild(tile);
}
}
}

The Tile class was an abstraction I had done to encapsulate the creation of the geometry and details of creating the individual squares. The TileData is a bean that I had to construct to keep the back-end models from knowing anything about the Java3D APIs. The GameGridData has the algorithm needed to get all the positions and colors in the right data structure (the TileData bean) that the view needed. I have a feeling that GameGridData may morph into an abstract class where subclasses will be specific for Chess boards or terrain looking grids or vast spans of black space. Once that was all pulled apart, I was able to construct a Model that generated these TileData beans and a Presenter that could communicate between the two.

Now what I have is a bunch of smaller classes that don't contain "view code" all of which are very testable! So that's where the first real signs of a design started to form. I had a need to be able to test what I was doing and no real good way of isolating the code that needed testing. So by separating out what was just calls to the framework's API (or my abstractions around the framework) and the logic needed for the correct calls, I was able to write a few tests and get things a bit more agile.

The third story actually got me into some user interaction:

3. The user can select a square on the board and the ball will move to that square. Movement is shown and not just a sudden change in location.

Selecting an object in Java3D is a bit more complicated than in Swing. Picking an object is basically translating a point that your mouse picked on the screen to a ray or cone that extends from the point down into the canvas and then seeing what objects intersect with that ray or cone. So my abstractions around the actual tiles in the grid by my Tile class paid off when I found out that the API will return the Node or Shape3D object that was in the intersection path. So I was able to retrieve the same TileData bean that I used to create the selected Tile object and then notify the presenters that are listening to the view.

And this is what it wound up looking like:

final PickCanvas pickCanvas = new PickCanvas(canvas3D, board);
pickCanvas.setMode(PickInfo.PICK_GEOMETRY);
pickCanvas.setTolerance(4.0f);

canvas3D.addMouseListener(new MouseAdapter() {
@Override
public void mouseClicked(MouseEvent mouseEvent) {
pickCanvas.setShapeLocation(mouseEvent);
PickInfo pickClosest = pickCanvas.pickClosest();
if (pickClosest != null) {
Tile tile = (Tile) pickClosest.getNode();
selectedTile = tile.getTileData();
tileSelectedListeners.notifyListeners();
}
}
});


So then I needed a way to move the user's game piece once the position selection took place. I made some similar refactorings to the code that created the user's piece with a MVP pattern. And then I let the model from the game grid and the model from the user piece be able to communicate with each other. And then the piece model notified it's presenter which in turn told the view to move the piece to the correct location.

public UserPieceModel(final IGameGridModel gameGridModel) {
gameGridModel.addPositionSelectedListener(new IListener() {
public void fireEvent() {
currentPosition = gameGridModel.getSelectedPosition();
adjustCurrentPositionForHeight();
modelListenerManager.notifyListeners();
}
});
}


So this story is just about wrapped up. I currently just have the user's piece suddenly jumping to the new location but the story has some more specific requirements: "Movement is shown and not just a sudden change in location" I made it that way intentionally because I know nothing of Java3D's animation APIs. So now I'm just doing a quick spike to determine how to do that and then I'll be able to finish this story up and move on to the next.

So overall I think things are progressing nicely. I wasn't liking where this was going at first with a whole bunch of un-testable UI code but now it seems like I've got the start of a design that allows me to test what I'm creating. And really that's the point of this blog. I called it Emergent Development because that's what good software development should be. You start going and you realize you need something so that you can make it more testable, more loosely coupled, more flexible and so you interject a pattern or two so you can test your stuff and just keep going. So your design comes from need not from a over thought-out UML diagram that was created long before any real code started. Design comes as you need it, no sooner.

Monday, February 9, 2009

Setuping up the Project

Ok, so I have a rough idea for the end result of my game and I started to write up some user stories.


Main Concept:

This is a board game where two players have a set of "pieces" that they can move around the board and attack the other player's pieces. The moves will be turn based: each turn the player gets to move one piece and cause one piece to perform an action. Actions may attack the other teams pieces which will may result in the termination of the attacked piece.



Ok, I'm hoping that with a little modification the game platform can provide the functionality for both a chess game and a Final Fantasy Tactics (great game by Squaresoft fyi for those who haven't played) style game.

User Stories:

User opens the application and sees the game board. Game board is a chess board (8 x 8 - alternating black and white squares) background is a gray. Camera is looking at the center from above and toward one side.

The user has one piece (a blue ball) on the board that is located on one side of the board.

The user can select a square on the board and the ball will move to that square. Movement is shown and not just a sudden change in location.

All movements are logged.

User can have eight balls. To move one, the user must select the ball to move first, then select where to move the piece.

The opponent will have 8 pieces also but of a different color.

User cannot move onto a square that is already occupied.

After the user moves a piece, the opponent will move one of its pieces in a random direction (no piece can move off of the board).

Pieces can move up to three square away.

When a user selects a piece to be moved, the squares that are within range will change color to indicate the possible ending positions for that piece. Attempts to move to a square that is not showing the indication is ignored.

User can choose to attack a piece belonging to the opponent if that piece is within 2 squares from the user's piece. To attack, the user selects the piece that they wish to attack with, then select the opponent's piece that is the target. The attack is logged.

Attacks are logged to the same log that the movements were logged.

Some visual representation of an attack between two pieces is shown.

Pieces have a sense of "health". Each piece can take two attacks before it is destroyed. Destroyed pieces simply disappear from the board.

Add an indicator for the health of the piece.

The pieces can be of different types:
- large, can only move 1 spaces but takes 3 hits to destroy
- medium, can move 2 and takes 2 hits to destroy
- small, can move 3 and takes 1 hit to destroy

Balls bounce in place while they wait their move.

The smaller the ball, the faster it bounces.

The pieces can be loaded in from an external model(s) - maybe spaceships.

When the game starts, the user can choose to play the CPU or play another player.

Network enabled games.

The user's stats are kept from game to game: wins, losses.

When the user starts the game they are asked who they
are (username) so as to keep building on their stats.

The user's pieces' stats are kept: number of opponent pieces destroyed

Piece rank based on number of opponent pieces destroyed.

Upgradeable pieces based on number of opponent pieces destroyed.




It's a small backlog and it'll grow. And the stories that are further out are a bit more vague, but they'll become more clear when they start coming to the front.



Starting Development

In the meantime, I decided to setup my continuous integration environment. I am developing this in Java (the graphics done in Java3D) using Maven for my build and Continuum for my continuous integration. Java was chosen because I know it the best and I'm a bit more familiar with Java3D than other 3D programming APIs. Maven was chosen for the use of it's dependency management. I've written a couple of standalone applications in the past and I've refactored out some useful tools dealing with things like error handling and whatnot. And with Maven, it's real easy to include the dependency in the pom and not have to worry about it.

So I got the project all setup with automated builds going and email notifications upon failure. I really think that this is crucial for any project starting up. Get your CI environment up and running. Now I don't have any other developers joining my project at the moment, but non-the-less, it is vital to make sure that whatever you have in your repository is stable (tests pass) at all times.

Interesting bit on the email notifications... GMail has been gracious enough to allow external smtp access to their servers. So it made setting up the mail notifications really simple.



And now I'm all ready to start tackling the first story...