Getting started with jPCT - a free 3d engine for Java

jPCT is a 3d engine for Java that allows you to use OpenGL and Software renders, develop 3d applications for desktop, Internet and Android. So let's try it in action by writing a little game on it.
image

Get jPCT here . Unzip the archive to a place convenient for you. In it you will find jar files, documentation and the necessary native libraries. Next, launch Eclipse and choose to create a new project. Enter the name of the project, click next. On the Libraries tab, select Add external jar add jpct.jar, lwjgl.jar, lwjgl_util.jar. For jpct, specify the location of javadoc, and for lwjgl, the location of the native library.
image

Let's try to make a little game about a mouse running through a maze in search of cheese. As probably any 3d jPCT engine provides the classes Object3D, Camera, World. jPCT supports several formats of 3d models, I will not list them all here, if you are interested then refer to the documentation. 3ds models are loaded using the static method of the Loader load3DS class which will return you an array of 3ds objects in the scene. Before loading 3d objects, it is desirable to load textures, then texturing will come automatically, provided that the texture names correspond to the information received from the 3ds file. Textures are loaded as follows:

final TextureManager texMan = TextureManager.getInstance();
texMan.addTexture( "TextureName.png", new Texture ( "TextureFileName.png" ) );

After creating the objects, you need to add them to the world using the addObject method of the World class. A program can have several World objects, but at the same time each object can belong to only one of them. Before you display objects, you need to “build” them, while calculating the normals, the bounding rectangle, etc. This can be done by calling the buildAllObjects () method of the World class. Created the world, now as they say, let there be light! We add light, as you probably already guessed with the addLight method, while indicating its coordinates and the intensity of red, green and blue. All coordinates in jPCT are set mainly in the form of SimpleVector (class jPCT), although the “old” form with three float coordinates has been preserved.
The way we all see this is determined by the camera. The camera can be moved, rotated, directed at the object and directed parallel to the z axis of the object (align). By the way, in jPCT, the x axis is directed to the right, the z axis is from the observer, and the y axis is down (under the legs).

As I said, jPCT supports software rendering and OpenGL. This is how the framebuffer is created: Then, when we don’t need it, we will return it to its previous state: Drawing the world into the buffer is very simple: As you may have guessed, everything around will be blue. In addition to the actual rendering, jpct has classes for handling user input. For keyboard input, this is the KeyMapper class. You simply create a new KeyMapper object and then at any time you can get the state of the keys by calling poll (),

final FrameBuffer buffer = new FrameBuffer(800, 600, FrameBuffer.SAMPLINGMODE_NORMAL);
buffer.disableRenderer(IRenderer.RENDERER_SOFTWARE);
buffer.enableRenderer(IRenderer.RENDERER_OPENGL);



buffer.disableRenderer(IRenderer.RENDERER_OPENGL);
buffer.dispose();



buffer.clear(java.awt.Color.BLUE);
theWorld.renderScene(buffer);
theWorld.draw(buffer);
buffer.update();
buffer.displayGLOnly();





There is also a collision detector. In our game, the mouse will hit the walls and at the end, a collision of the mouse and cheese will cause the end of the game. Therefore, for the mouse object, we call the setCollisionMode method (Object3D.COLLISION_CHECK_SELF). and for cheese and walls setCollisionMode (Object3D.COLLISION_CHECK_OTHERS). The cheese will still notify us of a collision so that we know that the game is over (addCollisionListener (this)). Now every time the mouse moves, you need to call the method of the Object3D class checkForCollisionEllipsoid (SimpleVector - move, SimpleVector - an ellipsoid that describes the mouse, 8 - recursion depth). In addition to the ellipsoid, you can use a sphere and a cube - respectively, the methods checkForCollisionSpherical (...), checkForCollision (...). This is all that is in jPCT from physics, if you need something more, you can look at, for example, jBullet.

And finally, the whole code:

import java.awt.Color;
import java.awt.event.KeyEvent;
import java.io.BufferedReader;
import java.io.FileReader;
import java.util.ArrayList;
import java.util.List;
import java.util.StringTokenizer;
import com.threed.jpct.*;
public class RatDungeon implements CollisionListener {
	private final Object3D rat;
	private final Object3D cheese;
	private Camera camera;
	private final Camera attached = new Camera();
	private final World theWorld = new World();
	private final KeyMapper keyMapper;
	private boolean forward = false;
	private boolean backward = false;
	private boolean left = false;
	private boolean right = false;
	private boolean doLoop = true;
        private final SimpleVector ellipsoid = new SimpleVector(15, 18, 32);
        private final float SPEED = 2.0f;
        private final float MAXSPEED = 3.0f;
	private boolean switchCamera = false;
	private final int high_camera = 1;
	private final int attached_camera = 2;
	private int cameraType = high_camera;
	private final float cellSize = 60f;
	private boolean gameOver = false;
	public RatDungeon() throws Exception {
             textures ();
	     rat = loadObject( "Nave.3ds" );
	     rat.scale(0.3f);
	     rat.setCollisionMode( Object3D.COLLISION_CHECK_SELF );
	     theWorld.addObject(rat);
	     cheese = loadObject( "queso.3DS" );
	     cheese.scale(0.8f);
	     cheese.setCollisionMode( Object3D.COLLISION_CHECK_OTHERS );
	     cheese.addCollisionListener(this);
	     theWorld.addObject(cheese);
	     loadWorld();
	     Config.linearDiv = 700;
	     Config.lightDiscardDistance = 650;
	     Config.collideOffset = 800;
	     theWorld.addLight( new SimpleVector (175, -250, -175), 15, 15, 15 );
	     theWorld.addLight( new SimpleVector (350, -250, 0), 15, 15, 15 );
	     theWorld.addLight( new SimpleVector (0, -250, 0), 15, 15, 15 );
	     theWorld.addLight( new SimpleVector (350, -250, -350), 15, 15, 15 );
	     theWorld.addLight( new SimpleVector (0, -250, -350), 15, 15, 15 );
	     theWorld.buildAllObjects();
	     keyMapper = new KeyMapper();
	}
	private void loop() throws Exception {
		 final FrameBuffer buffer = new FrameBuffer(800, 600, FrameBuffer.SAMPLINGMODE_NORMAL);
		 buffer.disableRenderer(IRenderer.RENDERER_SOFTWARE);
		 buffer.enableRenderer(IRenderer.RENDERER_OPENGL);
		 moveCamera();
		 final long delay = 20;
		 long previous = System.currentTimeMillis();
		 while( doLoop ) {
			 buffer.clear(java.awt.Color.BLUE);
			 theWorld.renderScene(buffer);
			 theWorld.draw(buffer);
			 buffer.update();
			 buffer.displayGLOnly();
			 final long current = System.currentTimeMillis();
			 if( current - previous > delay )
			 {
				 pollControls();
				 move();
		                 moveCamera();
		                 if( switchCamera ) {
					 switchCamera = false;
					 if( cameraType == high_camera ) {
						 cameraType = attached_camera;
						 theWorld.setCameraTo(attached);
					 }
					 else if( cameraType == attached_camera ) {
						 cameraType = high_camera;
						 theWorld.setCameraTo(camera);
					 }
		                 }
		                 previous = current;
		                 if( gameOver ) {
		        	         System.out.println( "Game over!!!" );
		        	         gameOver = false;
		                 }
			 }
		 }
		 buffer.disableRenderer(IRenderer.RENDERER_OPENGL);
		 buffer.dispose();
		 System.exit(0);
	}
	private void loadWorld() throws Exception {
		final BufferedReader in = new BufferedReader( new FileReader( "level.txt" ) );
		String s;
		int width = 0, height = 0;
		int ratX = 0, ratY = 0;
		int cheeseX = 0, cheeseY = 0;
		int line_num = -1;
		final List< int[] > walls = new ArrayList();
		while( ( s = in.readLine() ) != null ) {
			if( s.startsWith("rat", 0) ) {
				final StringTokenizer st = new StringTokenizer(s);
				st.nextToken();
				ratX = Integer.parseInt( st.nextToken() );
				ratY = Integer.parseInt( st.nextToken() );
			}
			else if( s.startsWith("cheese", 0) ) {
				final StringTokenizer st = new StringTokenizer(s);
				st.nextToken();
				cheeseX = Integer.parseInt( st.nextToken() );
				cheeseY = Integer.parseInt( st.nextToken() );
			}
			else {
				if( ( s.indexOf('_') != -1 ) || ( s.indexOf('|') != -1 ) ) {
					if( width == 0 )
						width = s.length()/2;
					if( s.indexOf('|') != -1 )
						line_num++;
					for( int i = 0; i < s.length(); i++ ) {
						if( s.charAt(i) == '_' ) {
							final int coords[] = { i/2, line_num, i/2, line_num + 1 };
							walls.add( coords );
						}
						else if( s.charAt(i) == '|' ) {
							final int coords[] = { i/2 - 1, line_num, i/2, line_num + 1 };
							walls.add( coords );
						}
					}
				}
			}
		}
		height = line_num + 1;
		createTable( width, height, walls );
		rat.translate( cellSize*ratX, 0, - cellSize*ratY );
		cheese.translate( cellSize*cheeseX, -5, - cellSize*cheeseY );
		camera = theWorld.getCamera ();
	        camera.rotateX( (-1.1f-0.14f) );
	        camera.setPosition( new SimpleVector( cellSize*(width/2), -cellSize*(height + 2), - cellSize*height ) );
	}
	private void move() {
		SimpleVector moveRes = new SimpleVector(0, 0, 0);
        if( forward ) {
        	final SimpleVector t = rat.getZAxis();
                t.scalarMul(SPEED);
                moveRes.add(t);
        }
        if( backward ) {
        	final SimpleVector t = rat.getZAxis();
                t.scalarMul(-SPEED);
                moveRes.add(t);
        }
        if( left ) {
        	rat.rotateY( (float) Math.toRadians(-1.8f) );
        }
        if( right ) {
        	rat.rotateY( (float) Math.toRadians(1.8f) );
        }
        if( moveRes.length() > MAXSPEED ) {
        	moveRes.makeEqualLength( new SimpleVector(0, 0, MAXSPEED) );
        }
                moveRes = rat.checkForCollisionEllipsoid(moveRes, ellipsoid, 8);
                rat.translate(moveRes);
                moveRes = new SimpleVector(0, 0, 0);
	}
	private void moveCamera() {
		 attached.setPositionToCenter(rat);
                 attached.align(rat);
                 attached.rotateCameraX( (float) Math.toRadians(30) );
                 attached.moveCamera( Camera.CAMERA_MOVEOUT, 150 );
	}
	private void pollControls() {
		KeyState ks = null;
		while( ( ks = keyMapper.poll() ) != KeyState.NONE ) {
	        if(ks.getKeyCode() == KeyEvent.VK_ESCAPE) {
	        	doLoop = false;
	        }
	        if(ks.getKeyCode() == KeyEvent.VK_C ) {
	        	if( cameraType == high_camera )
	        		switchCamera = true;
	        }
	        if(ks.getKeyCode() == KeyEvent.VK_D ) {
	        	if( cameraType == attached_camera )
	        		switchCamera = true;
	        }
	        if( ks.getKeyCode() == KeyEvent.VK_UP ) {
	            forward = ks.getState();
	        }
	        if (ks.getKeyCode() == KeyEvent.VK_DOWN) {
	                backward = ks.getState();
	        }
	        if (ks.getKeyCode() == KeyEvent.VK_LEFT) {
	                left = ks.getState();
	        }
	        if (ks.getKeyCode() == KeyEvent.VK_RIGHT) {
	                right = ks.getState();
		}
		if (org.lwjgl.opengl.Display.isCloseRequested()) {
		        doLoop = false;
		}
	}
	private void createTable( final int sizeX, final int sizeY, final List< int[] > walls ) {
		for( int x = 0; x < sizeX; x++ ) {
			 for( int y = 0; y < sizeY; y++ ) {
				 final Object3D cell = Primitives.getBox( cellSize/2, 0.1f );
				 cell.translate( x*cellSize, 0, - y*cellSize );
				 cell.rotateY( (float )Math.PI/4 );
				 if( ( x%2 + y%2 ) == 1 )
					 cell.setAdditionalColor( new Color( 0, 250, 0 ) );
				 else
					 cell.setAdditionalColor( new Color( 0, 0, 250 ) );
				 cell.setLighting( Object3D.LIGHTING_NO_LIGHTS );
				 theWorld.addObject(cell);
			 }
		}
		final Color wallColor = new Color( 230, 10, 10 );
		for( int i = 0; i < walls.size(); i++ ) {
			 final int cell1X = walls.get(i)[0];
			 final int cell1Y = walls.get(i)[1];
			 final int cell2X = walls.get(i)[2];
			 final int cell2Y = walls.get(i)[3];
			 final int x = cell2X;
			 final int y = cell1Y;
			 final Object3D wall = Primitives.getBox( cellSize/2, 0.1f );
			 wall.rotateY( (float )Math.PI/4 );
			 wall.translate( cellSize*x, -cellSize/2, -cellSize*y );
			 if( cell1X < cell2X ) {
			 	wall.rotateZ( (float )Math.PI/2 );
			 	wall.translate( new SimpleVector( -cellSize/2, 0, 0 ) );
			 }
			 else {
			 	wall.rotateX( (float )Math.PI/2 );
			 	wall.translate( new SimpleVector( 0, 0, -cellSize/2 ) );
			 }
			 wall.setAdditionalColor( wallColor );
			 wall.setCollisionMode( Object3D.COLLISION_CHECK_OTHERS );
			 theWorld.addObject(wall);
		}
	}
	private Object3D loadObject( final String fileName ) {
		final Object3D objParts [] = Loader.load3DS( fileName, 1f );
		Object3D ret = new Object3D (0);
		for( int i = 0; i < objParts.length; i++ ) {
			 final Object3D part = objParts [i];
			 part.setCenter( new SimpleVector(0, 0, 0) );
			 part.rotateX( (float)-Math.PI/2 );
			 part.rotateMesh();
			 part.setRotationMatrix( new Matrix() );
			 if( (i&1) == 1 ) {
				 part.setTransparency(0);
			 }
			 ret = Object3D.mergeObjects (ret, part);
		}
		return ret;
	}
	private void textures() {
		final TextureManager texMan = TextureManager.getInstance();
		texMan.addTexture( "ROJO.JPG", new Texture ( "ROJO.JPG" ) );
		texMan.addTexture( "QUESO.JPG", new Texture ( "QUESO.JPG" ) );
	}
	@Override
	public void collision(final CollisionEvent ce) {
		gameOver = true;
	}
	@Override
	public boolean requiresPolygonIDs() {
		return false;
	}
	public static void main(final String[] args) throws Exception {
		new RatDungeon().loop();
	}
}


In the future I plan to talk about porting the game to the Internet (java applet) and on Android with minimal code changes.

Links:
jPCT wiki jPCT
Projects
Source and files

Also popular now: