Survey
* Your assessment is very important for improving the workof artificial intelligence, which forms the content of this project
* Your assessment is very important for improving the workof artificial intelligence, which forms the content of this project
3D Java Game Programming – Episode 15 Building a Java First-Person Shoote Episode 15– Textures + More! In this episode CHERNO discusses how to display the FPS counter on the screen and the addition of a texture to the scene. The changes are rather minimal but the difference in how the application looks is rather impressive. Figure 1 - Episode 15 version of Minefront The code changes only took place in two older Java classes (Display.java and Render3D.java) and the introduction of a new class to handle the reading in of the Texture. Adding FPS to the Screen The application made use of typical graphics methods to draw a string (g.drawString()) and set the color and font type. The change required the introduction of a new variable fps that was constantly updated in the run() method. Here is the entire Java class with new portions highlighted. Table 1- Display.java for Episode 15 package com.mime.minefront; import java.awt.Canvas; 1 3D Java Game Programming – Episode 15 import import import import import import import import import import java.awt.Color; java.awt.Cursor; java.awt.Font; java.awt.Graphics; java.awt.Insets; java.awt.Point; java.awt.Toolkit; java.awt.image.BufferStrategy; java.awt.image.BufferedImage; java.awt.image.DataBufferInt; import javax.swing.JFrame; import javax.swing.SwingUtilities; import com.mime.minefront.graphics.Screen; import com.mime.minefront.input.Controller; import com.mime.minefront.input.InputHandler; public class Display extends Canvas implements Runnable{ private static final long serialVersionUID = 1L; public static final int WIDTH = 800; public static final int HEIGHT = 600; public static final String TITLE = "Minefront Pre-Alpha 0.02"; public static final int FRAMES_PER_SECOND = 60; private Thread thread; private boolean running = false; // indicates if the game is running or not private private private private Screen screen; Game game; BufferedImage img; int[] pixels; private InputHandler input; private int newX = 0; private int oldX = 0; private int fps = 0; public Display() { screen = new Screen(WIDTH, HEIGHT); img = new BufferedImage(WIDTH, HEIGHT, BufferedImage.TYPE_INT_RGB); pixels = ((DataBufferInt)img.getRaster().getDataBuffer()).getData(); game = new Game(); input = new InputHandler(); addKeyListener(input); addMouseListener(input); addMouseMotionListener(input); addFocusListener(input); } 2 3D Java Game Programming – Episode 15 private void start() { if (running) { return; } running = true; thread = new Thread(this); thread.start(); } private void stop() { if (!running) { return; } running = false; try { thread.join(); } catch (Exception e) { e.printStackTrace(); System.exit(0); } } private void tick() { game.tick(input.key); } private void render() { BufferStrategy bs = this.getBufferStrategy(); if (bs == null) { createBufferStrategy(3); return; } screen.render(game); for (int i = 0; i < WIDTH * HEIGHT; i++) { pixels[i] = screen.pixels[i]; } Graphics g = bs.getDrawGraphics(); g.drawImage(img, 0, 0, WIDTH, HEIGHT, null); g.setFont(new Font("Verdana", Font.BOLD, 20)); g.setColor(Color.yellow); g.drawString("FPS: " + fps, 20, 40); g.dispose(); bs.show(); } @Override public void run() { int frames = 0; frames drawn in one second // holds the number of 3 3D Java Game Programming – Episode 15 double unprocessedSeconds = 0; // accumulates time long previousTime = System.nanoTime(); // start counting double secondsPerTick = 1 / (double)FRAMES_PER_SECOND; defining 60 ticks in a second or in other words // We are // declaring that we want 60 frames / second int tickCount = 0; assume 60 ticks signal that we // If 60 ticks (or 1 second) we // should render a frame boolean ticked = false; frame should be rendered // our signal indicator that a while(running) { long currentTime = System.nanoTime(); // holds the time now long passedTime = currentTime - previousTime; // holds the time between now and when stop // watch started previousTime = currentTime; // remember for the next time check unprocessedSeconds = unprocessedSeconds + (passedTime / 1000000000.0); // time elapsed while (unprocessedSeconds > secondsPerTick) { // Has 1/60 sec elapsed, i.e. a tick tick(); // do it! unprocessedSeconds -= secondsPerTick; // remember the time that went over a tick ticked = true; // signal to render a frame tickCount++; if (tickCount % FRAMES_PER_SECOND == 0) { // approximately a second has passed, output fps = frames; //previousTime += 1000; // fudge factor ??? tickCount = 0; frames = 0; } } if (ticked) { render(); // handles rendering things to the screen frames++; ticked = false; } newX = InputHandler.MouseX; if (newX > oldX) { Controller.turnRight = true; } else if (newX < oldX) { Controller.turnLeft = true; } else { Controller.turnLeft = false; 4 3D Java Game Programming – Episode 15 Controller.turnRight = false; } oldX = newX; } } public void resizeToInternalSize(JFrame frame, int internalWidth, int internalHeight) { Insets insets = frame.getInsets(); final int newWidth = internalWidth + insets.left + insets.right; final int newHeight = internalHeight + insets.top + insets.bottom; Runnable resize = new Runnable() { public void run() { setSize(newWidth, newHeight); } }; if (SwingUtilities.isEventDispatchThread()) { try { SwingUtilities.invokeAndWait(resize); } catch (Exception e) { // ignore ...but will be no no if using Sonar! } } else { resize.run(); } validate(); } public static void main(String[] args) { BufferedImage cursor = new BufferedImage(16,16, BufferedImage.TYPE_INT_ARGB); Cursor blank = Toolkit.getDefaultToolkit().createCustomCursor(cursor, new Point(0,0), "blank"); Display game = new Display(); JFrame frame = new JFrame(); frame.add(game); frame.getContentPane().setCursor(blank); frame.setTitle(TITLE); frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); frame.setSize(WIDTH, HEIGHT); frame.setLocationRelativeTo(null); frame.setResizable(false); frame.setVisible(true); // ensure the display canvas is the right size! game.resizeToInternalSize(frame, WIDTH, HEIGHT); game.start(); } } 5 3D Java Game Programming – Episode 15 The changes above inserts at the top-left corner the number of frames per second the screen is updating. Using Paint.NET to create the texture We will use the free program Paint.NET to create a texture or graphics image and to use as a texture. What is a texture? An image (an array of pixels) used to represent or wallpaper a 3D surface is a texture. The texture is usually has two-dimensional qualities such as color and brightness. A texture is used to wrap around any 3-dimensional object (called texture mapping). You can easily find and download Paint.NET at http://www.getpaint.net. When you start it up you will see the UI. Tool Bar Image List Main Editing Area (canvas) Figure 2 - Paint.NET UI 6 3D Java Game Programming – Episode 15 Paint.NET started out as a senior project to create a paint tool to replace Microsoft Windows Paint program. In addition, it focused on using .NET technology using the C# programming language. The tool was made freely available to the general public (donations are encouraged) and it seems to be quite popular among the programming community that creates indie or game jam games. The four floating child windows – Tools, History, Layers, and Colors are all movable. You can close them or toggle them on and off by using the keys F5, F6, F7, and F8 respectively. The manner in which the texture was created was rather simple. 1. 2. 3. 4. Open Paint.NET Select the Image menu item and click on Resize to display the Resize dialog box Turn off “Maintain aspect ratio” if the checkbox is clicked Enter 8 for the Width and the Height Figure 3 - Resizing our texture 5. The canvas will be rather too small to work with so click on the Zoom In icon. I changed it to 2400%. 7 3D Java Game Programming – Episode 15 Zoom into the canvas Gradient tool Select gray color Figure 4 - Our small texture 6. 7. 8. 9. Select View menu and select Pixel Grid. Click on the gradient tool from the tool bar Select dark gray as the Primary color by clicking on it from the Color dialog Move the mouse to the bottom-right corner of the canvas (your 8x8 grid) and click the leftmouse button and drag it diagonally to the top-left corner 8 3D Java Game Programming – Episode 15 Pencil tool Exchange color Figure 5 - Creating your texture 10. Save the above image as floor.png. click “OK” in “Save Configuration” dialog window. 11. Now click on the double arrow under the dialog box that has “Primary” as its selection. This will swap the white with the gray. 12. Select the “pencil” tool from the Tools dialog and color the texture image as shown below. 9 3D Java Game Programming – Episode 15 Figure 6 - Update floor texture 13. Select “Save As” from the file menu and save the image as floor2.png. Updating the Code to use the new Texture Create a new class under com.mime.minefront.graphics named Texture.java. Table 2 - Texture.java package com.mime.minefront.graphics; import java.awt.image.BufferedImage; import javax.imageio.ImageIO; public class Texture { public static Render floor = loadBitmap("/textures/floor2.png"); public static Render loadBitmap(String fileName) { try { 10 3D Java Game Programming – Episode 15 BufferedImage image = ImageIO.read(Texture.class.getResource(fileName)); int width = image.getWidth(); int height = image.getHeight(); Render result = new Render(width, height); image.getRGB(0, 0, width, height, result.pixels, 0, width); return result; } catch (Exception e) { System.out.println("CRASH!"); throw new RuntimeException(e); } } } The main purpose of the class is to read in the image that corresponds to the texture into a Render object (which essentially saves it into a pixels array). The image is extracted and saved into the array of pixels in the Render object. The key BufferedImage method is: public int[] getRGB(int startX, int startY, int w, int h, int[] rgbArray, int offset, int scansize) The startX and startY is the top-left location in the image to start moving into the pixel array. The next two parameters the width and height (w and h) or the number of pixels across and the number of pixels down in the image are set to the actual width and height of the image. The rgbArray holds the translated pixels from the image (with the correct display colors). The offset specifies the offset into the array where the pixels from the source image shall be moved into. The resulting Render object is returned. Another thing to point out about the Texture object is that the creation of the class shall automatically read in the texture image and get it ready for processing. This means that the Texture class only processes one and only one image at a Time. A more general class to handle more than one texture for a game would have been a better design since it would allow us to easily re-use in future game development projects. Accessing the texture resource You will need to create a new directory for the game textures. The convention is to create a directory named resources or res to hold all game resources such as images, sounds and data files. We will place 11 3D Java Game Programming – Episode 15 our two images (floor.png and floor2.png) into a project directory named resources/textures as shown below. Figure 7 - Package Explorer in Episode 15 You will need to perform the following steps in order to include the resources directory in the classpath so that the line: BufferedImage image = ImageIO.read(Texture.class.getResource(fileName)); Will work. 1. Highlight the Project name “Minefront” 2. Right-click and select “Build Path” and from the context sub-menu select “Configure Build Path” 3. Click on the “Add Class Folder” button 12 3D Java Game Programming – Episode 15 Figure 8 - Adding a folder to the classpath 4. Click on the checkbox next to the /resources directory. Using the New Texture The only other change left is to replace the line that sets the pixel contents, note the highlighted change below: Table 3 - Render3D.java for Episode 15 package com.mime.minefront.graphics; import com.mime.minefront.Game; public class Render3D extends Render { public double[] zBuffer; private double renderDistance = 5000; public Render3D(int width, int height) { super(width, height); zBuffer = new double[width* height]; } public void floor(Game game) { double floorPosition = 8.0; double ceilingPosition = 8.0; double forward = game.controls.z; 13 3D Java Game Programming – Episode 15 double right = game.controls.x; double rotation = game.controls.rotation; double cosine = Math.cos(rotation); double sine = Math.sin(rotation); for (int y=0; y < height; y++) { double ceiling = (y + - height / 2.0) / height; double z = (ceiling != 0 ? (floorPosition / ceiling) : 0.0); if (ceiling < 0 ) { z = ceilingPosition / -ceiling; } for (int x = 0; x < width; x++) { double depth = (x - width / 2.0) / height; depth *= z; double xx = depth * cosine + z * sine; // + right; double yy = z * cosine - depth * sine; // + forward; int xPix = (int)(xx + right); int yPix = (int)(yy + forward); int pixelPosition = x + y * width; zBuffer[pixelPosition] = z; //pixels[pixelPosition] = ( ((xPix & 15) << 4) | ((yPix & 15)) << 12) & 0x00ffffff; pixels[pixelPosition] = Texture.floor.pixels[(xPix & 7) (yPix & 7 ) * 8]; if (z > 500 || y == (height / 2)) { pixels[pixelPosition] = 0; } } } } public void renderDistanceLimiter() { for (int i=0; i < width * height; i++) { int color = pixels[i]; int brightness = (int) (renderDistance / zBuffer[i]); if (brightness < 0) brightness = 0; if (brightness > 255) brightness = 255; int r = (color >> 16) & 0xff; int g = (color >> 8) & 0xff; int b = color & 0xff; r = r * brightness / 255; g = g * brightness / 255; b = b * brightness / 255; 14 + 3D Java Game Programming – Episode 15 pixels[i] = r << 16 | g << 8 | b; } } } The algorithm used to tile the floor and ceiling is very smart. The 3 rightmost bits of the xPix value (a value from 000-111 or 0-7) and the 3 rightmost bits of the yPix as well are used to access the pixel position to draw to the screen in our Texture.floor.pixels array! That’s it….try replacing the texture used with other 8x8 images to see different effects and rooms. 15