package com.indyforge.twod.engine.graphics.rendering.scenegraph; import java.awt.Canvas; import java.awt.Color; import java.awt.Frame; import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.Toolkit; import java.awt.event.ComponentAdapter; import java.awt.event.ComponentEvent; import java.awt.event.FocusEvent; import java.awt.event.FocusListener; import java.awt.event.KeyEvent; import java.awt.event.KeyListener; import java.awt.image.BufferStrategy; import java.awt.image.BufferedImage; import java.io.IOException; import java.lang.reflect.InvocationTargetException; import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.SocketAddress; import java.net.UnknownHostException; import java.util.List; import java.util.concurrent.LinkedBlockingDeque; import com.indyforge.foxnet.rmi.InvokerManager; import com.indyforge.foxnet.rmi.LookupException; import com.indyforge.foxnet.rmi.pattern.change.AdminSessionServer; import com.indyforge.foxnet.rmi.pattern.change.Change; import com.indyforge.foxnet.rmi.pattern.change.Changeable; import com.indyforge.foxnet.rmi.pattern.change.ChangeableQueue; import com.indyforge.foxnet.rmi.pattern.change.Session; import com.indyforge.foxnet.rmi.pattern.change.SessionServer; import com.indyforge.foxnet.rmi.pattern.change.impl.DefaultChangeableQueue; import com.indyforge.foxnet.rmi.pattern.change.impl.DefaultSessionServer; import com.indyforge.foxnet.rmi.transport.network.Broadcaster; import com.indyforge.foxnet.rmi.transport.network.ConnectionManager; import com.indyforge.twod.engine.graphics.GraphicsRoutines; import com.indyforge.twod.engine.resources.assets.AssetManager; import com.indyforge.twod.engine.util.task.Task; import com.indyforge.twod.engine.util.task.TaskQueue; /** * This class supports active rendering which is the most performant way to * render if you use java2d. * * Of course active rendering has its price: * * The nice Swing layout system and the input system could cause some issues. * (Mixing Swing and AWT is never a good idea!) * * @author Christopher Probst * @see Scene */ public final class SceneProcessor implements Changeable<SceneProcessor>, KeyListener, FocusListener { /** * @see Broadcaster#receiveBroadcast(int, int, int) */ public static List<Object> receiveBroadcast(int port, int maxResults, int timeoutmillies) throws IOException, ClassNotFoundException { return Broadcaster.receiveBroadcast(port, maxResults, timeoutmillies); } /** * @see Broadcaster#receiveBroadcast(SocketAddress, int, int) */ public static List<Object> receiveBroadcast(SocketAddress target, int maxResults, int timeoutmillies) throws IOException, ClassNotFoundException { return Broadcaster.receiveBroadcast(target, maxResults, timeoutmillies); } public static final String REMOTE_NAME = "session_server"; /* * Used for tasks which should be processed in the main thread. */ private final TaskQueue taskQueue = new TaskQueue( new LinkedBlockingDeque<Task>()); /* * ************************************************************************ * NETWORK PART BEGINS **************************************************** * ************************************************************************ */ /* * Used to lock the access. */ private final Object netLock = new Object(); /* * The connection manager of this scene processor. */ private ConnectionManager connectionManager; /* * The network mode of this processor. */ private NetworkMode networkMode; /* * The message broadcaster. */ private Broadcaster broadcaster; /* * Will be set by the server to "reset" the network time. The user can * calculate the active network time using this timestamp. */ private long networkInitTimestamp = -1; /* * This flag is used for applying queued changes. */ private boolean synchronousQueue = false; /* * ************************************************************************ * NETWORK CLIENT PART **************************************************** * ************************************************************************ */ /* * Here we can store the invoker manager of this scene processor. */ private InvokerManager invokerManager; /* * Here we can store the session of this scene processor. */ private Session<SceneProcessor> session; /* * The changeable session instances. */ private ChangeableQueue<SceneProcessor> changeableClient; private ChangeableQueue<SceneProcessor> changeableServer; /* * ************************************************************************ * NETWORK SERVER PART **************************************************** * ************************************************************************ */ /* * Here we can store the admin session server of this scene processor. */ private AdminSessionServer<SceneProcessor> adminSessionServer; /* * ************************************************************************ * NETWORK PART ENDS ****************************************************** * ************************************************************************ */ /* * The frame of this scene processor or null (If headless). */ private final Frame frame; /* * The canvas of this scene processor or null (If headless). */ private final Canvas canvas; /* * This is the thread-safe buffer strategy for rendering. * * VOLATILE: The var is set in the EDT. */ private volatile BufferStrategy bufferStrategy; /* * The last thread which processed this scene processor. */ private volatile Thread lastProcessorThread; /* * The fast offscreen image. Ultimate hardware performance. There is no * better way to render graphics in java2d. */ private BufferedImage offscreen; /* * The shutdown requested flag. */ private volatile boolean shutdownRequested = false, /* * The only render with focus flag. */ onlyRenderWithFocus = true; /* * The scene which we want to render. */ private Scene root; /* * Used for timing. */ private long curTime, lastTime = -1; /* * Thread safe vars for rendering. * * VOLATILE: These vars are set in the EDT. */ private volatile int peerWidth, peerHeight; /** * * @author Christopher Probst * */ public enum NetworkMode { Server, Client, Offline } /** * Creates a new offline scene processor. * */ public SceneProcessor() { this(NetworkMode.Offline, null, -1, -1); } /** * Creates a new offline scene processor. * * @param title * The title of the frame. (Ignored if headless mode) * @param width * The width of the frame. (Ignored if headless mode) * @param height * The height of the frame. (Ignored if headless mode) */ public SceneProcessor(String title, int width, int height) throws InterruptedException, InvocationTargetException { this(NetworkMode.Offline, title, width, height); } /** * Creates a new scene processor using the given network mode. * * @param networkMode * The network mode of this processor. */ public SceneProcessor(NetworkMode networkMode) { this(networkMode, null, -1, -1); } /** * Creates a new scene processor using the given network mode. * * @param networkMode * The network mode of this processor. * @param title * The title of the frame. (Ignored if headless mode) * @param width * The width of the frame. (Ignored if headless mode) * @param height * The height of the frame. (Ignored if headless mode) */ public SceneProcessor(NetworkMode networkMode, String title, int width, int height) { // Set the network mode networkMode(networkMode); /* * Very important! The twod-engine decides between headless and * not-headless mode. This is very useful for server systems. In * headless mode no resources like images, sound, etc. are really * loaded. Only the references to these resources are used. */ if (!AssetManager.isHeadless()) { /* * Create a new canvas implementation if not headless. */ canvas = new Canvas() { /** * */ private static final long serialVersionUID = 1L; /* * (non-Javadoc) * * @see java.awt.Canvas#addNotify() */ @Override public void addNotify() { super.addNotify(); // Double buffered should be fine createBufferStrategy(2); // Create the strategy bufferStrategy = getBufferStrategy(); } }; // The canvas is rendered by us! canvas.setIgnoreRepaint(true); // This canvas can receive input canvas.setFocusable(true); // Add resize listener canvas.addComponentListener(new ComponentAdapter() { @Override public void componentResized(ComponentEvent e) { // Threading issues... peerWidth = canvas.getWidth(); peerHeight = canvas.getHeight(); } }); // Add listener canvas.addKeyListener(this); canvas.addFocusListener(this); /* * Create a new frame using this scene processor. */ try { frame = title != null && width > 0 && height > 0 ? GraphicsRoutines .createFrame(this, title, width, height) : null; } catch (Exception e1) { throw new RuntimeException("Failed to create scene processor " + "frame. Reason: " + e1.getMessage(), e1); } } else { // Headless... frame = null; canvas = null; bufferStrategy = null; offscreen = null; peerWidth = -1; peerHeight = -1; } } /** * @return the frame (which contains the canvas) of this scene processor or * null in headless mode. */ public Frame frame() { return frame; } /** * @return the canvas of this scene processor or null in headless mode. */ public Canvas canvas() { return canvas; } /** * @return the network mode of this processor. */ public NetworkMode networkMode() { synchronized (netLock) { return networkMode; } } /** * Sets the network mode and releases old resources. * * @param networkMode * The new network mode. * @return */ public SceneProcessor networkMode(NetworkMode networkMode) { synchronized (netLock) { if (networkMode == null) { throw new NullPointerException("networkMode"); } else if (hasNetworkResources()) { releaseNetworkResources(); } // Init the connection manager connectionManager = (this.networkMode = networkMode) != NetworkMode.Offline ? new ConnectionManager( networkMode == NetworkMode.Server ? true : false) : null; return this; } } /** * @return true if this processor has any active network resources, * otherwise false. */ public boolean hasNetworkResources() { synchronized (netLock) { return (connectionManager != null ? !connectionManager.isDisposed() : false) || broadcaster != null; } } /** * Resets the network. All active connections (broadcasting is not affected * by this method) will be terminated and internal bindings will be removed. * Please note that the network is still able to work after this method. * <p> * If you want to DESTROY the active network please use * {@link SceneProcessor#releaseNetworkResources()}. */ public void resetNetwork() { synchronized (netLock) { if (networkMode != NetworkMode.Offline) { // Shutdown all connections connectionManager.channels().close().awaitUninterruptibly(); networkInitTimestamp = -1; // Unbind all bindings connectionManager.staticReg().unbindAll(); // Reset server stuff adminSessionServer = null; // Reset client stuff invokerManager = null; session = null; changeableClient = null; changeableServer = null; } } } /** * Releases all active network resources and sets the network mode to * offline. */ public void releaseNetworkResources() { synchronized (netLock) { if (hasNetworkResources()) { if (connectionManager != null) { connectionManager.dispose(); connectionManager = null; } networkMode = NetworkMode.Offline; networkInitTimestamp = -1; closeBroadcaster(); // Delete server stuff adminSessionServer = null; // Delete client stuff invokerManager = null; session = null; changeableClient = null; changeableServer = null; } } } /** * Disposes this scene processor which is not able to work after calling * this method. */ public void dispose() { releaseNetworkResources(); if (frame != null) { frame.dispose(); } } /** * Connects to the given server. If the scene processor is already * connected, the old connection will be cancelled first. * * @param socketAddress * The host address. * @return this for chaining; * @throws IOException * If the connection attempt failed. */ public SceneProcessor openClient(SocketAddress socketAddress) throws IOException { synchronized (netLock) { if (networkMode != NetworkMode.Client) { throw new IllegalStateException("Wrong network mode"); } else if (invokerManager != null) { // Reset the network resetNetwork(); } try { // Try to open the connection invokerManager = connectionManager.openClient(socketAddress); } catch (IOException e) { /* * Reset the network if the connection failed. */ resetNetwork(); // Throw again! throw e; } return this; } } /** * Connects to the given server. If the scene processor is already * connected, the old connection will be cancelled first. * * @param host * The host address. * @param port * The port. * @return this for chaining; * @throws IOException * If the connection attempt failed. */ public SceneProcessor openClient(String host, int port) throws IOException { return openClient(new InetSocketAddress(host, port)); } /** * Resets the network time. */ public void resetNetworkTime() { synchronized (netLock) { if (networkMode == NetworkMode.Offline) { throw new IllegalStateException("Wrong network mode"); } // Init the local network time networkInitTimestamp = System.currentTimeMillis(); } } /** * @return the synchronous queue flag. */ public boolean isSynchronousQueue() { synchronized (netLock) { return synchronousQueue; } } /** * Sets the synchronous queue flag. * * @param synchronousQueue * If true, the queued changes will be applied synchronously, * otherwise they are applies asynchronously. * @return this for chaining. */ public SceneProcessor synchronousQueue(boolean synchronousQueue) { synchronized (netLock) { this.synchronousQueue = synchronousQueue; return this; } } /** * @return the network time. */ public float networkTime() { synchronized (netLock) { if (networkMode == NetworkMode.Offline) { throw new IllegalStateException("Wrong network mode"); } else if (networkInitTimestamp < 0) { return -1f; } else { return (System.currentTimeMillis() - networkInitTimestamp) * 0.001f; } } } /** * Opens a server with the given port. If the scene processor has already an * open server the old one will be disposed. * * @param port * The port. * @return this for chaining. */ public SceneProcessor openServer(int port) { synchronized (netLock) { if (networkMode != NetworkMode.Server) { throw new IllegalStateException("Wrong network mode"); } else if (adminSessionServer != null) { // Reset network resetNetwork(); } // Create a new instance adminSessionServer = new DefaultSessionServer<SceneProcessor>(this); // Bind the server instance connectionManager.staticReg().bind(REMOTE_NAME, adminSessionServer); // Open the server channel connectionManager.openServer(port); return this; } } /** * * Opens a broadcaster using all known ip addresses of this computer. * * @param broadcastPort * The broadcasting port. * @param networkPort * The network port. * @return this for chaining. * @throws UnknownHostException * If an host exception occurs. * @throws IOException * If an IO exception occurs. */ public SceneProcessor openBroadcaster(int broadcastPort, int networkPort) throws UnknownHostException, IOException { // At first get all addresses InetAddress[] addresses = InetAddress.getAllByName(InetAddress .getLocalHost().getHostName()); // Create new array of inet socket addresses InetSocketAddress[] inetAddresses = new InetSocketAddress[addresses.length]; // Copy for (int i = 0; i < addresses.length; i++) { inetAddresses[i] = new InetSocketAddress(addresses[i], networkPort); } // Open broadcaster return openBroadcaster(broadcastPort, inetAddresses); } /** * Opens a broadcaster. * * @param broadcastPort * The broadcasting port. * @param message * The broadcasted message. * @return this for chaining. * @throws IOException * If an IO exception occurs. */ public SceneProcessor openBroadcaster(int broadcastPort, Object message) throws IOException { synchronized (netLock) { if (networkMode == NetworkMode.Offline) { throw new IllegalStateException("Wrong network mode"); } else if (broadcaster != null) { closeBroadcaster(); } // Try to create a new broadcaster broadcaster = new Broadcaster(broadcastPort, message); // Start the broadcaster broadcaster.start(); return this; } } /** * @return true if this scene processor has broadcaster, otherwise false. */ public boolean hasBroadcaster() { synchronized (netLock) { return broadcaster != null; } } /** * Closes the broadcaster. * * @return this for chaining. */ public SceneProcessor closeBroadcaster() { synchronized (netLock) { if (broadcaster != null) { broadcaster.interrupt(); } broadcaster = null; return this; } } /** * @return the broadcaster. */ public Broadcaster broadcaster() { synchronized (netLock) { return broadcaster; } } /** * Links this scene processor with a session server. * * @param name * The user name of this scene processor. * @return a {@link Session} implementation. * @throws LookupException * If the lookup failed. */ @SuppressWarnings("unchecked") public Session<SceneProcessor> linkClient(String name) throws LookupException { synchronized (netLock) { if (invokerManager == null) { throw new IllegalStateException("No invoker manager. " + "Please open the client first."); } else if (session != null) { return session; } // Try to lookup the server SessionServer<SceneProcessor> server = (SessionServer<SceneProcessor>) invokerManager .lookupProxy(REMOTE_NAME); /* * Open a session for this scene processor and save it. */ if ((session = server.openSession(this, name)) != null) { /* * Init both changeable instances. */ changeableClient = new DefaultChangeableQueue<SceneProcessor>( session.client()); changeableServer = new DefaultChangeableQueue<SceneProcessor>( session.server()); return session; } else { throw new IllegalStateException("Null session returned"); } } } /** * @return true if this scene processor has an admin session server, * otherwise false. */ public boolean hasAdminSessionServer() { synchronized (netLock) { return adminSessionServer != null; } } /** * @return the admin session server. */ public AdminSessionServer<SceneProcessor> adminSessionServer() { synchronized (netLock) { return adminSessionServer; } } /** * @return true if this scene processor has a session, otherwise false. */ public boolean hasSession() { synchronized (netLock) { return session != null; } } /** * @return the session. */ public Session<SceneProcessor> session() { synchronized (netLock) { return session; } } /** * @return the changeable client (client-side). */ public ChangeableQueue<SceneProcessor> changeableClient() { synchronized (netLock) { return changeableClient; } } /** * @return the changeable server (client-side). */ public ChangeableQueue<SceneProcessor> changeableServer() { synchronized (netLock) { return changeableServer; } } /** * @return the scene root. */ public Scene root() { return root; } /* * (non-Javadoc) * * @see * com.indyforge.foxnet.rmi.pattern.change.Changeable#applyChange(com.indyforge * .foxnet.rmi.pattern.change.Change) */ @Override public void applyChange(final Change<SceneProcessor> change) { if (Thread.currentThread().equals(lastProcessorThread)) { change.apply(this); } else { taskQueue.tasks().offer(new Task() { /** * */ private static final long serialVersionUID = 1L; /* * (non-Javadoc) * * @see com.indyforge.twod.engine.util.task.Task#update(float) */ @Override public boolean update(float tpf) { applyChange(change); return true; } }); } } /** * Sets the given root scene. * * @param root * The root scene. * @return this for chaining. */ public SceneProcessor root(Scene root) { // Remove old link if (this.root != null) { this.root.processor = null; } // Save the root this.root = root; // Connect if not null... if (root != null) { // Set processor root.processor = this; // The time must be resetted. resetTime(); } return this; } /** * @return the task deque. */ public TaskQueue taskQueue() { return taskQueue; } /** * Resets the processor time. */ public void resetTime() { lastTime = -1; } /** * @return the shutdown-requested flag. */ public boolean isShutdownRequested() { return shutdownRequested; } /** * Sets the shutdown-requested flag. * * @param shutdownRequested * If true the shutdown will be initiated. * @return this for chaining. */ public SceneProcessor shutdownRequest(final boolean shutdownRequested) { if (Thread.currentThread().equals(lastProcessorThread)) { this.shutdownRequested = shutdownRequested; } else { taskQueue.tasks().offer(new Task() { /** * */ private static final long serialVersionUID = 1L; /* * (non-Javadoc) * * @see com.indyforge.twod.engine.util.task.Task#update(float) */ @Override public boolean update(float tpf) { shutdownRequest(shutdownRequested); return true; } }); } return this; } /** * @return the only-render-with-focus flag. */ public boolean isOnlyRenderWithFocus() { return onlyRenderWithFocus; } /** * Sets the only-render-with-focus flag. * * @param onlyRenderWithFocus * If true the processor will stop rendering without focus. */ public void onlyRenderWithFocus(boolean onlyRenderWithFocus) { this.onlyRenderWithFocus = onlyRenderWithFocus; } /** * Starts the game loop. This method blocks until shutdown is requested or * an exception is thrown. This method {@link SceneProcessor#dispose() * disposes} this scene processor when returning. * * @param maxFPS * The maximum frames per second. A value < 1 means no max. * @return this for chaining. * @throws Exception * If some kind of error occurs. */ public SceneProcessor start(int maxFps) throws Exception { try { while (!isShutdownRequested()) { // Process the whole scene! process(maxFps); } return this; } finally { // Dispose the scene processor dispose(); } } /** * Process the whole scene. * * @param maxFPS * The maximum frames per second. A value < 1 means no max. * @throws Exception * If some kind of error occurs. */ public void process(int maxFPS) throws Exception { // Save the current thread lastProcessorThread = Thread.currentThread(); // Do the time stuff... lastTime = lastTime == -1 ? System.currentTimeMillis() : curTime; // Get active time curTime = System.currentTimeMillis(); // The passed time float tpf = (curTime - lastTime) * 0.001f; // Execute all pending tasks taskQueue.update(tpf); // If root is null we cannot process... if (root == null) { // Sleep the given amount of time if (maxFPS > 0) { Thread.sleep(1000 / maxFPS); } return; } // Was the scene processed ? boolean processed = false; /* * Do only render if NOT in headless mode! */ if (canvas != null) { // Read into local cache int w = peerWidth, h = peerHeight; // Load from cache BufferStrategy ptr = bufferStrategy; if (w > 0 && h > 0 && ptr != null && (!onlyRenderWithFocus || canvas.isFocusOwner())) { /* * Advanced image rendering. */ // Create new... if (offscreen == null || offscreen.getWidth() != w || offscreen.getHeight() != h) { // Create new compatible image for fast rendering offscreen = GraphicsRoutines.createImage(w, h); } // Create new graphics Graphics2D g2d = offscreen.createGraphics(); try { // Black is a good background g2d.setBackground(Color.BLACK); g2d.clearRect(0, 0, w, h); // Simulate the scene root.simulate(g2d, w, h, tpf); // Scene is processed now processed = true; } finally { // Dispose g2d.dispose(); } // Get the graphics context Graphics frameBuffer = ptr.getDrawGraphics(); try { // Always clear frameBuffer.setColor(Color.black); frameBuffer.fillRect(0, 0, w, h); // Render the offscreen frameBuffer.drawImage(offscreen, 0, 0, null); } finally { // Dispose the buffer frameBuffer.dispose(); } // Render to frame if not lost if (!ptr.contentsLost()) { ptr.show(); } // Synchronize with toolkit Toolkit.getDefaultToolkit().sync(); } } /* * If the scene was still not processed yet... */ if (!processed) { // Simulate the scene without drawing root.simulate(null, -1, -1, tpf); } /* * Send all queued changes if not offline... */ synchronized (netLock) { if (networkMode != NetworkMode.Offline) { if (synchronousQueue) { if (hasAdminSessionServer()) { adminSessionServer.composite().applyQueuedChanges(); } else if (hasSession()) { changeableClient.applyQueuedChanges(); changeableServer.applyQueuedChanges(); } } else { if (hasAdminSessionServer()) { adminSessionServer.composite().applyQueuedChangesLater( null); } else if (hasSession()) { changeableClient.applyQueuedChangesLater(null); changeableServer.applyQueuedChangesLater(null); } } } } // Check! if (maxFPS > 0) { // Calc the frame duration long frameDuration = System.currentTimeMillis() - curTime; // Calc the minimum duration long minDuration = 1000 / maxFPS; // Wait a bit if (frameDuration < minDuration) { // Sleep Thread.sleep(minDuration - frameDuration); } } } /* * (non-Javadoc) * * @see java.awt.event.FocusListener#focusGained(java.awt.event.FocusEvent) */ @Override public void focusGained(FocusEvent e) { } /* * (non-Javadoc) * * @see java.awt.event.FocusListener#focusLost(java.awt.event.FocusEvent) */ @Override public void focusLost(FocusEvent e) { taskQueue.tasks().offer(new Task() { /** * */ private static final long serialVersionUID = 1L; /* * (non-Javadoc) * * @see com.indyforge.twod.engine.util.task.Task#update(float) */ @Override public boolean update(float tpf) { if (root != null) { // Clear keyboard root.clearKeyboardState(); } return true; } }); } /* * (non-Javadoc) * * @see java.awt.event.KeyListener#keyPressed(java.awt.event.KeyEvent) */ @Override public void keyPressed(final KeyEvent e) { taskQueue.tasks().offer(new Task() { /** * */ private static final long serialVersionUID = 1L; /* * (non-Javadoc) * * @see com.indyforge.twod.engine.util.task.Task#update(float) */ @Override public boolean update(float tpf) { if (root != null) { root.pressed(e.getKeyCode(), Boolean.TRUE); } return true; } }); } /* * (non-Javadoc) * * @see java.awt.event.KeyListener#keyReleased(java.awt.event.KeyEvent) */ @Override public void keyReleased(final KeyEvent e) { taskQueue.tasks().offer(new Task() { /** * */ private static final long serialVersionUID = 1L; /* * (non-Javadoc) * * @see com.indyforge.twod.engine.util.task.Task#update(float) */ @Override public boolean update(float tpf) { if (root != null) { root.pressed(e.getKeyCode(), Boolean.FALSE); } return true; } }); } /* * (non-Javadoc) * * @see java.awt.event.KeyListener#keyTyped(java.awt.event.KeyEvent) */ @Override public void keyTyped(KeyEvent e) { } }