/******************************************************************************* * Copyright (c) 2001, 2010 Mathew A. Nelson and Robocode contributors * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://robocode.sourceforge.net/license/epl-v10.html * * Contributors: * Mathew A. Nelson * - Initial API and implementation * Flemming N. Larsen * - Rewritten * Pavel Savara * - now driven by BattleObserver *******************************************************************************/ package net.sf.robocode.ui.battleview; import net.sf.robocode.battle.snapshot.RobotSnapshot; import net.sf.robocode.robotpaint.Graphics2DSerialized; import net.sf.robocode.robotpaint.IGraphicsProxy; import net.sf.robocode.settings.ISettingsManager; import net.sf.robocode.settings.ISettingsListener; import net.sf.robocode.ui.IImageManager; import net.sf.robocode.ui.IWindowManager; import net.sf.robocode.ui.IWindowManagerExt; import net.sf.robocode.ui.gfx.GraphicsState; import net.sf.robocode.ui.gfx.RenderImage; import net.sf.robocode.ui.gfx.RobocodeLogo; import robocode.control.events.BattleAdaptor; import robocode.control.events.BattleFinishedEvent; import robocode.control.events.BattleStartedEvent; import robocode.control.events.TurnEndedEvent; import robocode.control.snapshot.BulletState; import robocode.control.snapshot.IBulletSnapshot; import robocode.control.snapshot.IRobotSnapshot; import robocode.control.snapshot.ITurnSnapshot; import java.awt.*; import java.awt.event.ComponentAdapter; import java.awt.event.ComponentEvent; import java.awt.geom.*; import java.awt.image.BufferStrategy; import java.awt.image.BufferedImage; import static java.lang.Math.*; import java.util.Random; /** * @author Mathew A. Nelson (original) * @author Flemming N. Larsen (contributor) * @author Pavel Savara (contributor) */ @SuppressWarnings("serial") public class BattleView extends Canvas { private final static String ROBOCODE_SLOGAN = "Build the best, destroy the rest"; private final static Color CANVAS_BG_COLOR = SystemColor.controlDkShadow; private final static Area BULLET_AREA = new Area(new Ellipse2D.Double(-0.5, -0.5, 1, 1)); private final static int ROBOT_TEXT_Y_OFFSET = 24; // The battle and battlefield, private BattleField battleField; private boolean initialized; private double scale = 1.0; // Ground private int[][] groundTiles; private final int groundTileWidth = 64; private final int groundTileHeight = 64; private Image groundImage; // Draw option related things private boolean drawRobotName; private boolean drawRobotEnergy; private boolean drawScanArcs; private boolean drawExplosions; private boolean drawGround; private boolean drawExplosionDebris; private int numBuffers = 2; // defaults to double buffering private RenderingHints renderingHints; // Fonts and the like private Font smallFont; private FontMetrics smallFontMetrics; private final IImageManager imageManager; private final ISettingsManager properties; private final IWindowManagerExt windowManager; private BufferStrategy bufferStrategy; private Image offscreenImage; private Graphics2D offscreenGfx; private final GeneralPath robocodeTextPath = new RobocodeLogo().getRobocodeText(); private static final MirroredGraphics mirroredGraphics = new MirroredGraphics(); private final GraphicsState graphicsState = new GraphicsState(); private IGraphicsProxy[] robotGraphics; public BattleView(ISettingsManager properties, IWindowManager windowManager, IImageManager imageManager) { this.properties = properties; this.windowManager = (IWindowManagerExt) windowManager; this.imageManager = imageManager; battleField = new BattleField(800, 600); new BattleObserver(windowManager); properties.addPropertyListener(new ISettingsListener() { public void settingChanged(String property) { loadDisplayOptions(); if (property.startsWith("robocode.options.rendering")) { initialized = false; validate(); } } }); addComponentListener(new ComponentAdapter() { public void componentResized(ComponentEvent e) { initialized = false; validate(); } }); } @Override public void paint(Graphics g) { final ITurnSnapshot lastSnapshot = windowManager.getLastSnapshot(); if (lastSnapshot != null) { update(lastSnapshot); } else { paintRobocodeLogo((Graphics2D) g); } } public BufferedImage getScreenshot() { BufferedImage screenshot = getGraphicsConfiguration().createCompatibleImage(getWidth(), getHeight()); if (windowManager.getLastSnapshot() == null) { paintRobocodeLogo((Graphics2D) screenshot.getGraphics()); } else { screenshot.getGraphics().drawImage(offscreenImage, 0, 0, null); } return screenshot; } private void update(ITurnSnapshot snapshot) { if (!initialized) { initialize(); } if (offscreenImage == null || !isDisplayable() || (getWidth() <= 0) || (getHeight() <= 0)) { return; } offscreenGfx = (Graphics2D) offscreenImage.getGraphics(); if (offscreenGfx != null) { offscreenGfx.setRenderingHints(renderingHints); drawBattle(offscreenGfx, snapshot); if (bufferStrategy != null) { Graphics2D g = null; try { g = (Graphics2D) bufferStrategy.getDrawGraphics(); g.drawImage(offscreenImage, 0, 0, null); bufferStrategy.show(); } catch (NullPointerException e) {// Occurs sometimes for no reason?! } finally { if (g != null) { g.dispose(); } } } } } private void loadDisplayOptions() { ISettingsManager props = properties; drawRobotName = props.getOptionsViewRobotNames(); drawRobotEnergy = props.getOptionsViewRobotEnergy(); drawScanArcs = props.getOptionsViewScanArcs(); drawGround = props.getOptionsViewGround(); drawExplosions = props.getOptionsViewExplosions(); drawExplosionDebris = props.getOptionsViewExplosionDebris(); renderingHints = props.getRenderingHints(); numBuffers = props.getOptionsRenderingNoBuffers(); } private void initialize() { loadDisplayOptions(); if (offscreenImage != null) { offscreenImage.flush(); offscreenImage = null; } offscreenImage = getGraphicsConfiguration().createCompatibleImage(getWidth(), getHeight()); offscreenGfx = (Graphics2D) offscreenImage.getGraphics(); if (bufferStrategy == null) { createBufferStrategy(numBuffers); bufferStrategy = getBufferStrategy(); } // If we are scaled... if (getWidth() < battleField.getWidth() || getHeight() < battleField.getHeight()) { // Use the smaller scale. // Actually we don't need this, since // the RobocodeFrame keeps our aspect ratio intact. scale = min((double) getWidth() / battleField.getWidth(), (double) getHeight() / battleField.getHeight()); offscreenGfx.scale(scale, scale); } else { scale = 1; } // Scale font smallFont = new Font("Dialog", Font.PLAIN, (int) (10 / scale)); smallFontMetrics = offscreenGfx.getFontMetrics(smallFont); // Initialize ground image if (drawGround) { createGroundImage(); } else { groundImage = null; } initialized = true; } private void createGroundImage() { // Reinitialize ground tiles Random r = new Random(); // independent final int NUM_HORZ_TILES = battleField.getWidth() / groundTileWidth + 1; final int NUM_VERT_TILES = battleField.getHeight() / groundTileHeight + 1; if ((groundTiles == null) || (groundTiles.length != NUM_VERT_TILES) || (groundTiles[0].length != NUM_HORZ_TILES)) { groundTiles = new int[NUM_VERT_TILES][NUM_HORZ_TILES]; for (int y = NUM_VERT_TILES - 1; y >= 0; y--) { for (int x = NUM_HORZ_TILES - 1; x >= 0; x--) { groundTiles[y][x] = (int) round(r.nextDouble() * 4); } } } // Create new buffered image with the ground pre-rendered int groundWidth = (int) (battleField.getWidth() * scale); int groundHeight = (int) (battleField.getHeight() * scale); groundImage = new BufferedImage(groundWidth, groundHeight, BufferedImage.TYPE_INT_RGB); Graphics2D groundGfx = (Graphics2D) groundImage.getGraphics(); groundGfx.setRenderingHints(renderingHints); groundGfx.setTransform(AffineTransform.getScaleInstance(scale, scale)); for (int y = NUM_VERT_TILES - 1; y >= 0; y--) { for (int x = NUM_HORZ_TILES - 1; x >= 0; x--) { Image img = imageManager.getGroundTileImage(groundTiles[y][x]); if (img != null) { groundGfx.drawImage(img, x * groundTileWidth, y * groundTileHeight, null); } } } } private void drawBattle(Graphics2D g, ITurnSnapshot snapShot) { // Save the graphics state graphicsState.save(g); // Reset transform g.setTransform(new AffineTransform()); // Reset clip g.setClip(null); // Clear canvas g.setColor(CANVAS_BG_COLOR); g.fillRect(0, 0, getWidth(), getHeight()); // Calculate border space double dx = (getWidth() - scale * battleField.getWidth()) / 2; double dy = (getHeight() - scale * battleField.getHeight()) / 2; // Scale and translate the graphics AffineTransform at = AffineTransform.getTranslateInstance(dx, dy); at.concatenate(AffineTransform.getScaleInstance(scale, scale)); g.setTransform(at); // Set the clip rectangle g.setClip(0, 0, battleField.getWidth(), battleField.getHeight()); // Draw ground drawGround(g); if (snapShot != null) { // Draw scan arcs drawScanArcs(g, snapShot); // Draw robots drawRobots(g, snapShot); // Draw robot (debug) paintings drawRobotPaint(g, snapShot); } // Draw the border of the battlefield drawBorder(g); if (snapShot != null) { // Draw all bullets drawBullets(g, snapShot); // Draw all text drawText(g, snapShot); } // Restore the graphics state graphicsState.restore(g); } private void drawGround(Graphics2D g) { if (!drawGround) { // Ground should not be drawn g.setColor(Color.BLACK); g.fillRect(0, 0, battleField.getWidth(), battleField.getHeight()); } else { // Create pre-rendered ground image if it is not available if (groundImage == null) { createGroundImage(); } // Draw the pre-rendered ground if it is available if (groundImage != null) { int groundWidth = (int) (battleField.getWidth() * scale) + 1; int groundHeight = (int) (battleField.getHeight() * scale) + 1; int dx = (getWidth() - groundWidth) / 2; int dy = (getHeight() - groundHeight) / 2; final AffineTransform savedTx = g.getTransform(); g.setTransform(new AffineTransform()); g.drawImage(groundImage, dx, dy, groundWidth, groundHeight, null); g.setTransform(savedTx); } } } private void drawBorder(Graphics2D g) { final Shape savedClip = g.getClip(); g.setClip(null); g.setColor(Color.RED); g.drawRect(-1, -1, battleField.getWidth() + 2, battleField.getHeight() + 2); g.setClip(savedClip); } private void drawScanArcs(Graphics2D g, ITurnSnapshot snapShot) { if (drawScanArcs) { for (IRobotSnapshot robotSnapshot : snapShot.getRobots()) { if (robotSnapshot.getState().isAlive()) { drawScanArc(g, robotSnapshot); } } } } private void drawRobots(Graphics2D g, ITurnSnapshot snapShot) { double x, y; AffineTransform at; int battleFieldHeight = battleField.getHeight(); if (drawGround && drawExplosionDebris) { RenderImage explodeDebrise = imageManager.getExplosionDebriseRenderImage(); for (IRobotSnapshot robotSnapshot : snapShot.getRobots()) { if (robotSnapshot.getState().isDead()) { x = robotSnapshot.getX(); y = battleFieldHeight - robotSnapshot.getY(); at = AffineTransform.getTranslateInstance(x, y); explodeDebrise.setTransform(at); explodeDebrise.paint(g); } } } for (IRobotSnapshot robotSnapshot : snapShot.getRobots()) { if (robotSnapshot.getState().isAlive()) { x = robotSnapshot.getX(); y = battleFieldHeight - robotSnapshot.getY(); at = AffineTransform.getTranslateInstance(x, y); at.rotate(robotSnapshot.getBodyHeading()); RenderImage robotRenderImage = imageManager.getColoredBodyRenderImage(robotSnapshot.getBodyColor()); robotRenderImage.setTransform(at); robotRenderImage.paint(g); at = AffineTransform.getTranslateInstance(x, y); at.rotate(robotSnapshot.getGunHeading()); RenderImage gunRenderImage = imageManager.getColoredGunRenderImage(robotSnapshot.getGunColor()); gunRenderImage.setTransform(at); gunRenderImage.paint(g); if (!robotSnapshot.isDroid()) { at = AffineTransform.getTranslateInstance(x, y); at.rotate(robotSnapshot.getRadarHeading()); RenderImage radarRenderImage = imageManager.getColoredRadarRenderImage(robotSnapshot.getRadarColor()); radarRenderImage.setTransform(at); radarRenderImage.paint(g); } } } } private void drawText(Graphics2D g, ITurnSnapshot snapShot) { final Shape savedClip = g.getClip(); g.setClip(null); for (IRobotSnapshot robotSnapshot : snapShot.getRobots()) { if (robotSnapshot.getState().isDead()) { continue; } int x = (int) robotSnapshot.getX(); int y = battleField.getHeight() - (int) robotSnapshot.getY(); if (drawRobotEnergy) { g.setColor(Color.white); int ll = (int) robotSnapshot.getEnergy(); int rl = (int) ((robotSnapshot.getEnergy() - ll + .001) * 10.0); if (rl == 10) { rl = 9; } String energyString = ll + "." + rl; if (robotSnapshot.getEnergy() == 0 && robotSnapshot.getState().isAlive()) { energyString = "Disabled"; } centerString(g, energyString, x, y - ROBOT_TEXT_Y_OFFSET - smallFontMetrics.getHeight() / 2, smallFont, smallFontMetrics); } if (drawRobotName) { g.setColor(Color.white); centerString(g, robotSnapshot.getVeryShortName(), x, y + ROBOT_TEXT_Y_OFFSET + smallFontMetrics.getHeight() / 2, smallFont, smallFontMetrics); } } g.setClip(savedClip); } private void drawRobotPaint(Graphics2D g, ITurnSnapshot turnSnapshot) { int robotIndex = 0; for (IRobotSnapshot robotSnapshot : turnSnapshot.getRobots()) { final Object graphicsCalls = ((RobotSnapshot) robotSnapshot).getGraphicsCalls(); if (graphicsCalls == null || !robotSnapshot.isPaintEnabled()) { continue; } // Save the graphics state GraphicsState gfxState = new GraphicsState(); gfxState.save(g); g.setClip(null); g.setComposite(AlphaComposite.SrcAtop); IGraphicsProxy gfxProxy = getRobotGraphics(robotIndex); if (robotSnapshot.isSGPaintEnabled()) { gfxProxy.processTo(g, graphicsCalls); } else { mirroredGraphics.bind(g, battleField.getHeight()); gfxProxy.processTo(mirroredGraphics, graphicsCalls); mirroredGraphics.release(); } // Restore the graphics state gfxState.restore(g); robotIndex++; } } private IGraphicsProxy getRobotGraphics(int robotIndex) { if (robotGraphics[robotIndex] == null) { robotGraphics[robotIndex] = new Graphics2DSerialized(); robotGraphics[robotIndex].setPaintingEnabled(true); } return robotGraphics[robotIndex]; } private void drawBullets(Graphics2D g, ITurnSnapshot snapShot) { final Shape savedClip = g.getClip(); g.setClip(null); double x, y; for (IBulletSnapshot bulletSnapshot : snapShot.getBullets()) { x = bulletSnapshot.getPaintX(); y = battleField.getHeight() - bulletSnapshot.getPaintY(); AffineTransform at = AffineTransform.getTranslateInstance(x, y); if (bulletSnapshot.getState().getValue() <= BulletState.MOVING.getValue()) { // radius = sqrt(x^2 / 0.1 * power), where x is the width of 1 pixel for a minimum 0.1 bullet double scale = max(2 * sqrt(2.5 * bulletSnapshot.getPower()), 2 / this.scale); at.scale(scale, scale); Area bulletArea = BULLET_AREA.createTransformedArea(at); Color bulletColor; if (properties.getOptionsRenderingForceBulletColor()) { bulletColor = Color.WHITE; } else { bulletColor = new Color(bulletSnapshot.getColor()); } g.setColor(bulletColor); g.fill(bulletArea); } else if (drawExplosions) { if (!bulletSnapshot.isExplosion()) { double scale = sqrt(1000 * bulletSnapshot.getPower()) / 128; at.scale(scale, scale); } RenderImage explosionRenderImage = imageManager.getExplosionRenderImage( bulletSnapshot.getExplosionImageIndex(), bulletSnapshot.getFrame()); explosionRenderImage.setTransform(at); explosionRenderImage.paint(g); } } g.setClip(savedClip); } private void centerString(Graphics2D g, String s, int x, int y, Font font, FontMetrics fm) { g.setFont(font); int width = fm.stringWidth(s); int height = fm.getHeight(); int descent = fm.getDescent(); double left = x - width / 2; double top = y - height / 2; double scaledViewWidth = getWidth() / scale; double scaledViewHeight = getHeight() / scale; double borderWidth = (scaledViewWidth - battleField.getWidth()) / 2; double borderHeight = (scaledViewHeight - battleField.getHeight()) / 2; if (left + width > scaledViewWidth) { left = scaledViewWidth - width; } if (top + height > scaledViewHeight) { top = scaledViewHeight - height; } if (left < -borderWidth) { left = -borderWidth; } if (top < -borderHeight) { top = -borderHeight; } g.drawString(s, (int) (left + 0.5), (int) (top + height - descent + 0.5)); } private void drawScanArc(Graphics2D g, IRobotSnapshot robotSnapshot) { Arc2D.Double scanArc = (Arc2D.Double) ((RobotSnapshot) robotSnapshot).getScanArc(); if (scanArc == null) { return; } final Composite savedComposite = g.getComposite(); g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.2f)); scanArc.setAngleStart((360 - scanArc.getAngleStart() - scanArc.getAngleExtent()) % 360); scanArc.y = battleField.getHeight() - robotSnapshot.getY() - robocode.Rules.RADAR_SCAN_RADIUS; int scanColor = robotSnapshot.getScanColor(); g.setColor(new Color(scanColor, true)); if (abs(scanArc.getAngleExtent()) >= .5) { g.fill(scanArc); } else { g.draw(scanArc); } g.setComposite(savedComposite); } private void paintRobocodeLogo(Graphics2D g) { setBackground(Color.BLACK); g.clearRect(0, 0, getWidth(), getHeight()); g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); g.transform(AffineTransform.getTranslateInstance((getWidth() - 320) / 2.0, (getHeight() - 46) / 2.0)); g.setColor(new Color(0, 0x40, 0)); g.fill(robocodeTextPath); Font font = new Font("Dialog", Font.BOLD, 14); int width = g.getFontMetrics(font).stringWidth(ROBOCODE_SLOGAN); g.setTransform(new AffineTransform()); g.setFont(font); g.setColor(new Color(0, 0x50, 0)); g.drawString(ROBOCODE_SLOGAN, (float) ((getWidth() - width) / 2.0), (float) (getHeight() / 2.0 + 50)); } private class BattleObserver extends BattleAdaptor { public BattleObserver(IWindowManager windowManager) { windowManager.addBattleListener(this); } @Override public void onBattleStarted(BattleStartedEvent event) { battleField = new BattleField(event.getBattleRules().getBattlefieldWidth(), event.getBattleRules().getBattlefieldHeight()); initialized = false; setVisible(true); super.onBattleStarted(event); robotGraphics = new IGraphicsProxy[event.getRobotsCount()]; } @Override public void onBattleFinished(BattleFinishedEvent event) { super.onBattleFinished(event); robotGraphics = null; } public void onTurnEnded(final TurnEndedEvent event) { if (event.getTurnSnapshot() == null) { repaint(); } else { update(event.getTurnSnapshot()); } } } }