preface
Code demonstration environment:
- Software environment: Windows 10
- Development tool: Visual Studio Code
- JDK version: OpenJDK 15
Although these codes were written more than 10 years ago, they can still run normally in the modern operating system and the latest open source version of Java.
Interface and interaction
AWT event model
If a person plays oak chess, it will be very boring like a person without interaction when playing games, so the greatest fun of players is to interact with computers or people. First, the player has to interact with the computer - the interaction between the keyboard and the mouse. JDK version 1.4 also provides the driver of the handle to allow the player to interact with the computer.
AWT has its own event distribution thread - this thread distributes all kinds of events, such as mouse click and keyboard events, which come from the operating system.
So where does AWT distribute these events? Distributed when an event occurs in a particular component. AWT will check whether there is a listener for this event - a listener is an object that specifically receives events from another object. In this case, the event will come from the AWT event distributor thread. Each event has a corresponding listener, such as an input event. We have a KeyListener interface to the object. The following describes the workflow of the event:
- The user presses the key
- The operating system sends keyboard events to the Java runtime
- java runtime generates event objects and adds them to the event queue of AWT
- The AWT event sending thread allocates the event object to any KeyListeners
- KeyListener gets keyboard events and does what it wants to do
We can use the AWTEventListener class, which can be used to debug processing
Toolkit.getDefaultToolkit().addAWTEventListener(new AWTEventListener(){ public void eventDispatched(AWTEevent event){ System.out.println(event); } }, -1);
Note: the above code can only be used for debugging, but can not be used in real games.
keyboard entry
In a game, we will use a large number of keyboards, such as cursor keys to move the position of characters, and use the keyboard to control weapons. Next, we use KeyListener to listen for keyboard events and handle them.
Window window = screen.getFullScreenWindow(); window.addKeyListener(keyListener);
The KeyListener interface defines keyPressed(), KeyReleased() and KeyTyped() methods. The "typed" event occurs when a keyboard is pressed for the first time and then clicked repeatedly. This event is basically not used in the game, so we only focus on the press and release events of the keyboard.
The above methods have a KeyEvent event event parameter, which allows us to observe which keyboard is pressed and released - using the virtual key code. Virtual keyboard is a Java defined code used to represent the keys of each keyboard, but it is not the same as the actual characters. For example, Q and Q are different characters, but they have the same key code value. All virtual keyboards are based on VK_xxx indicates that, for example, the Q key uses KeyEvent VK_ Q means that in most cases, we can deduce the actual corresponding key according to the virtual key. Note: the setFocusTraversalKeysEnabled(false) method of Window class is to focus the key on the conversion key event. The conversion key can modify the focus of the current key, and then move the focus to another component. For example, in a web page, we may press the Tab key to move the cursor from one form field to another form field component. The event of Tab key is encapsulated by the focus conversion code of AWT, but we can get the event of Tab key, so this method allows us to use it like this. In addition to the Tab key, we can use the ALT key to generate activate memory behavior. For example, pressing Alt+F activates the File menu behavior. Because AWT will think that the key pressed after ALT will be ignored, if we don't want this result, we will call the consume() method of KeyEvent to prevent AWT from ignoring this behavior. After confirming that no other object handles the ALT key (or no modifier key activates memory), we regard the ALT key as an ordinary key.
Keyboard demonstration code KeyTest
package com.funfree.arklis.input; import java.awt.event.*; import java.awt.*; import java.util.LinkedList; import com.funfree.arklis.util.*; import com.funfree.arklis.engine.*; /** Function: write a keyboard test class to explain the use of keyboard events Note: this class inherits the GameCore engine class, and then implements the keyboard listener interface to handle keyboard operation events. */ public class KeyTest extends GameCore { private LinkedList messages = new LinkedList();//Use a two-way linked list to save events /** Override the init method of your parent class to initialize the instance of this class. */ public void init(){ super.init(); //Set the screen to full screen display Window window = screen.getFullScreenWindow(); //Allows you to enter the TAB key and other specific keys window.setFocusTraversalKeysEnabled(false); //Adds a keyboard listener to the current full screen window.addKeyListener(this); //Add a message to the collection addMessage("Keyboard input test, press Escape Key to exit the program."); } /* Method for implementing listener interface definition */ public void keyPressed(KeyEvent event){ int keyCode = event.getKeyCode(); //If you press esc if(keyCode == KeyEvent.VK_ESCAPE){ stop();//Then set the result identification bit }else{ //Otherwise, the press event is handled addMessage("Pressed:" + KeyEvent.getKeyText(keyCode)); //event.consume();// Make sure that the key does not handle any events } } public void keyReleased(KeyEvent event){ int keyCode = event.getKeyCode(); addMessage("Released:" + KeyEvent.getKeyText(keyCode)); //event.consume(); } public void keyTyped(KeyEvent event){ //event.consume(); } public synchronized void addMessage(String message){ messages.add(message); //If the size of the collection is greater than or equal to the height of the screen, except for the font size if(messages.size() >= screen.getHeight() / FONT_SIZE){ messages.remove(0); //Then delete the second item in the set } } /** Draws a collection element, where the RenderingHints class defines and manages a collection of keys and associated values, which allows The application selects the input parameters as the algorithm used by other classes to perform rendering and image processing services. */ public synchronized void draw(Graphics2D g){ Window window = screen.getFullScreenWindow(); //Use the specified algorithm to display the image -- require "text anti aliasing prompt key" and "text anti aliasing prompt value" g.setRenderingHint( RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON); //Draw background image g.setColor(window.getBackground()); g.fillRect(0,0,screen.getWidth(),screen.getHeight()); //Draw the message to be displayed g.setColor(window.getForeground()); int y = FONT_SIZE; //Draw text on the screen for(int i = 0; i < messages.size(); i++){ g.drawString((String)messages.get(i),5,y); y += FONT_SIZE; } } }
Associated core code - GameCore
package com.funfree.arklis.engine; import static java.lang.System.*; import java.awt.*; import com.funfree.arklis.util.*; import javax.swing.ImageIcon; import java.util.*; import com.funfree.arklis.input.*; /** Function: write an abstract class to test its subclass to implement the draw method remarks: This class is an engine, which specifies the actions of subclasses to implement the game: rewrite the update method and draw method. The client class only needs to implement gameLoop The update method and the draw method in. If you need to interact with users, you only need to add the corresponding to the subclass Just use your monitor. */ public abstract class GameCore extends ActionAdapter{ protected static final int FONT_SIZE = 54; private boolean isRunning; protected ScreenManager screen; //With screen management protected InputManager inputManager;//With input manager //Used to save engine components, such as InputComponent protected java.util.List list; //Used for reinitialization when in use public void setList(java.util.List list){ this.list = list; } public java.util.List getList(){ return list; } private static final DisplayMode[] POSSIBLE_MODES = { new DisplayMode(1280,800,32,0), new DisplayMode(1280,800,24,0), new DisplayMode(1280,800,16,0), new DisplayMode(1024,768,32,0), new DisplayMode(1024,768,24,0), new DisplayMode(1024,768,16,0), new DisplayMode(800,600,32,0), new DisplayMode(800,600,24,0), new DisplayMode(800,600,16,0) }; public ScreenManager getScreenManager(){ return screen; } /** Indicates the end of the game */ public void stop(){ isRunning = false; } /** Call init() and gameLoop() methods */ public void run(){ try{ init(); gameLoop(); }finally{ screen.restoreScreen(); } } //Default initialization behavior public void init(){ //1. Specify a screen manager object screen = new ScreenManager(); //2. Then determine the graphics card of the current computer DisplayMode displayMode = screen.findFirstCompatibleMode(POSSIBLE_MODES); //3. Set the full screen display model -- it is the premise for subclasses to obtain the full screen screen.setFullScreen(displayMode); //4. The following is to obtain the default font style and color in the full screen Window window = screen.getFullScreenWindow(); window.setFont(new Font("Dialog", Font.PLAIN, FONT_SIZE)); window.setBackground(Color.blue); window.setForeground(Color.white); //5. Indicates that the game is currently running isRunning = true; } public Image loadImage(String fileName){ return new ImageIcon(fileName).getImage(); } /** If the stop method is called, stop calling the method. The default gameLoop() behavior. */ private void gameLoop(){ //Get current time long startTime = currentTimeMillis(); //Initializes the current at the beginning of the game long currentTime = startTime; //If isRunning is true while(isRunning){//So let the game cycle continue //1. Current game progress time -- where the elapsedTime value is determined by the current // The value of the main thread sleep (Thread.sleep(20)) is determined! long elapsedTime = currentTimeMillis() - currentTime; out.println("Current time:" + currentTime + ",Time of the game:" + elapsedTime); currentTime += elapsedTime; //2. Update the game animation according to the current game time -- subclass rewriting (specified action) is required update(elapsedTime); Graphics2D g = screen.getGraphics(); draw(g);//Draw picture -- subclass override required (specified action) g.dispose(); screen.update();//Refresh screen using dual cache technology try{ Thread.sleep(20); }catch(InterruptedException e){ e.printStackTrace(); } }//Otherwise, do not act! } /** Function: this method needs to be implemented by subclasses to achieve specific animation effects. The specific animation effect needs to be realized according to the demand description. You can write abstract methods as a framework to use! */ public void update(long elapsedTime){ //do nothing } /** Function: define an abstract method. Subclasses must implement the method so that it can be displayed on the screen. This method must be implemented */ public abstract void draw(Graphics2D g); }
Keyboard input operation effect
Mouse input
There are three kinds of mouse events:
- Mouse button click event
- Mouse movement event
- Mouse scroll event
Mouse demonstration code - MouseTest
package com.funfree.arklis.input; import java.awt.event.*; import java.awt.*; import java.util.LinkedList; import com.funfree.arklis.util.*; import com.funfree.arklis.engine.*; /** Function: write a class to test the behavior of monitoring the mouse Note: inherit the parent class of game engine GameCore, and then implement keyboard listener and mouse related listener (including mouse movement Mouse roller listener) */ public class MouseTest extends GameCore { private static final int TRAIL_SIZE = 10; //Draw 10 ghosts private static final Color[] COLORS = { //Sets the foreground color of the font Color.white, Color.black, Color.yellow, Color.magenta }; private LinkedList trailList; private boolean trailMode; private int colorIndex; /** Override the init method to initialize an instance of the class */ public void init(){ super.init(); trailList = new LinkedList(); Window window = screen.getFullScreenWindow(); //A listener that adds a mouse and keyboard to the current full screen window.addMouseListener(this); window.addMouseMotionListener(this); window.addMouseWheelListener(this); window.addKeyListener(this); } /** Function: Rewrite / implement the abstract method of draw to realize the draw action of mouse. */ public synchronized void draw(Graphics2D g){ int count = trailList.size(); //Whether to draw the currently moving mouse continuously if(count > 1 && !trailMode){ count = 1;//Draw only the first Point object } //1. Get the current full screen Window window = screen.getFullScreenWindow(); //2. Then draw the background to the full screen - this step must be done first g.setColor(window.getBackground()); g.fillRect(0,0,screen.getWidth(),screen.getHeight()); //3. Then draw instruction -- it means that anti aliasing effect is required to draw text g.setRenderingHint( RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON); g.setColor(window.getForeground()); //4. Start drawing text into the full screen g.drawString("Mouse test. Press Escape Key to exit the program.", 5, FONT_SIZE); //Draw the mouse -- draw a sentence according to the current position of the mouse -- draw "Hello! Java world." Ghosting effect for(int i = 0; i < count; i++){ Point point = (Point)trailList.get(i); g.drawString("Hello! Java The world.",point.x, point.y); } } //Determine whether to display "Hello, Java World" for ghosting word public void mousePressed(MouseEvent event){ trailMode = !trailMode; } //Override mouse entry event public void mouseEntered(MouseEvent event){ mouseMoved(event); } //Override mouse drag events public void mouseDragged(MouseEvent event){ mouseMoved(event); } //Override mouse exit event public void mouseExited(MouseEvent event){ mouseMoved(event); } /** Override the mouse movement event to save the coordinate values of the current mouse movement. The number of these coordinates must be less than trail_ Value of size */ public synchronized void mouseMoved(MouseEvent event){ Point point = new Point(event.getX(), event.getY()); trailList.addFirst(point); while(trailList.size() > TRAIL_SIZE){ trailList.removeLast(); } } /** Rewrite the scroll event of the mouse to handle the color of the foreground display in the screen. */ public void mouseWheelMoved(MouseWheelEvent event){ colorIndex = (colorIndex + event.getWheelRotation()) % COLORS.length; if(colorIndex < 0){ colorIndex += COLORS.length; } Window window = screen.getFullScreenWindow(); window.setForeground(COLORS[colorIndex]); } //Override the keyboard press event to exit the application public void keyPressed(KeyEvent event){ //If the Esc key is pressed, the screen enters the display model before the game and ends the program. if(event.getKeyCode() == KeyEvent.VK_ESCAPE){ stop(); } } }
The Point object is saved in the trailList collection. The object has x and y coordinate values, and can save up to 10 coordinate values. If the mouse movement continues, the draw method will draw a "hello world!" for each Point Otherwise, only the first Point object will be drawn, and clicking the mouse will modify the trail model.
In the above code, our Robot class moves the mouse, but the mouse movement event may not appear immediately, so the code will check whether the mouse movement event is located in the center of the screen. If so, consider it as an event to reset the center, and the actual event is ignored; Otherwise, the event is treated as a normal mouse movement event. For the appearance of the mouse, we can use the Java API to create our own style. When creating, we need to use the createCustomerCursor() method of the Toolkit class
In the game, we can call the Toolkit class to intercept an invisible cursor, and then call the setCursor() method:
Window window = screen.getFullScreenWindow(); window.setCursor(invisibleCursor);
After that, we can call the getPredefinedCursor() method of Cursor class to restore the original Cursor style:
Cursor normalCursor = Cursor.getPredefineCursor(Cursor.DEFAULT_CURSOR);
Create input manager
Previously, we explained the common input events and their processing. We can create them together in one input manager. However, before encapsulation, we need to explain the defects of the previous code.
First, we should pay attention to the method of synchronized modification. Remember: all events are generated from the AWT event distribution thread, which is not the main thread! Obviously, we don't modify the game state (modify the monster's position), so these synchronization methods can't make these events happen. In our actual game, we have to deal with specific points in the game cycle.
Therefore, in order to solve this problem, we need to set the identification bit (boolean variable) to identify. The keyboard press event occurs when the identification variable is modified. For example, the jumpIsPressed Boolean value can be set and modified in the keyPressed() method, and then check whether the variable is set in the later game loop, and then call the corresponding code according to this ID to deal with the behavior of the game. For some behaviors, such as "jump" and "move", each player has different hobbies, so we need to let the player set the function of the keyboard, so we need to allude to these general game behaviors. Therefore, class InputManager is the control player input behavior:
- Handle all keyboard and mouse events, including related mouse behavior
- Save these events so that we can accurately query these events when we need them without modifying the game state in the AWT event distribution thread
- Check the initialized keyboard press event, and then check whether the key value has been occupied by other keys
- Mapping keyboard to game common behavior, such as mapping the spacebar into "jump" behavior
- Allows users to configure any keyboard behavior
We use the GameAction class to encapsulate the above functions, where isPressed() is to judge the behavior of the keyboard, and getAmount() is to judge how much the mouse moves. Finally, these methods are called by InputManager, such as calling the interface method in GameAction in the press() and release() methods of this class.
Demo code - GameAction
package com.az.arklis.engine; /** Function: this class is the abstraction (definition) of the user's initial behavior, such as jumping and moving. This class is used by the InputManager class for mapping Keyboard and mouse behavior. Note: the so-called game input behavior includes the input at a specific point in the game cycle. We can set a boolean variable to represent a Whether the key is pressed. For example, set a jumpIsPressed boolean variable and put this variable into the keyPressed() method. Let's judge After pressing the space key, we check whether jumpIsPressed is true. If it is true, let the player perform the jump action. In addition to jumping in the game, players can also set the initial action keys, such as moving. We can set the cursor keys to represent it, and the A key And D keys also indicate left-right movement. Suppose we want players to define their own behavior keys in the game, then we must implement these in the program The mapping function of game behavior. We implement the InputManager class to abstract these behaviors. In short, we hope this kind of InputManager can Complete the following functions: 1,Handles all key and mouse events, including the relative movement of the mouse 2,Save the event queue of all the above behaviors instead of modifying the state of the AWT event distribution thread 3,Check the initial press behavior of the keys and whether they are occupied by other objects 4,Insinuate all game behaviors, such as insinuating that the space key is the jumping action in the game 5,Implementation allows players to modify the game keys themselves The GameAction class is used to specifically reflect the behavior in the game, that is, the setting function of abstract game behavior. For example, abstract players The initial act of (jumping or moving). This class is used by the InputManager class to map the behavior of the keyboard and mouse. */ public class GameAction{ //Normal behavior -- represented by the true value returned by the isPressed() method, that is, a key has been occupied. public static final int NORMAL = 0; /* Initialize the key behavior, and the isPressed() method returns the true value: only after the key is pressed for the first time, and it is not the key The state of being pressed after being released. */ public static final int DETECT_INITIAL_PRESS_ONLY = 1; private static final int STATE_RELEASED = 0;//Identify whether it is released private static final int STATE_PRESSED = 1; //Identifies whether the pressed state is processed private static final int STATE_WAITING_FOR_RELEASE = 2; //Identifies the state of waiting to be released private String name;//Save the name of the game behavior private int behavior; //Indicates the behavior of the game private int amount; //Counter private int state; //Current status identification /** Initialize the member variables in the construction method -- the name of the game behavior and the normal state. */ public GameAction(String name){ this(name,NORMAL); } public int getBehavior(){ return behavior; } /** This construction method specifies the behavior of the game */ public GameAction(String name, int behavior){ this.name = name; this.behavior = behavior; reset();//Return to the released state and clear the counter } public String getName(){ return name; } public void setName(String name){ this.name = name; } public void reset(){ state = STATE_RELEASED; amount = 0; } /** Function: switch the GameAction behavior -- equivalent to the release behavior after press */ public synchronized void tap(){ press(); release(); } /** Function: identify the event when the keyboard is clicked */ public synchronized void press(){ press(1); } /** Function: indicates the number of times the key is clicked, and the mouse moves to the specified position */ public synchronized void press(int amount){ if(state != STATE_WAITING_FOR_RELEASE){ this.amount += amount; state = STATE_PRESSED; } } public synchronized void release(){ state = STATE_RELEASED; } public synchronized boolean isPressed(){ return (getAmount() != 0); } public synchronized int getAmount(){ int returnValue = amount; if(returnValue != 0){ if(state == STATE_RELEASED){ amount = 0; }else if(behavior == DETECT_INITIAL_PRESS_ONLY){ state = STATE_WAITING_FOR_RELEASE; amount = 0; } } return returnValue; } }
Finally, we create an InputManager class to manage all inputs and wait for the missing cursor and related mouse lines and so on. In addition, this class has keyboard and mouse events mapped into the GameAction class. When we press a keyboard, the code of this class checks whether there is a keyboard mapped in the GameAction class. If so, call the press() method in the GameAction class.
So how do you insinuate in this class? We use a GameAction array to solve this problem. Each subscript corresponds to a virtual key code. The maximum virtual key can only be less than or equal to 600, that is, the length of the GameAction array is 600
Using the input manager
Next, we create a hero that can move left and right and jump; In addition, we can add pause function to the application, but this is not a real game. Among them, the character needs to show gravity when jumping - the feeling that the character will return to the ground. It should be noted here that the gravity is 9.8m/s, but in the game, this is not important. We use pixels to represent it, so the definition reference is 0.002 pixels / s, which prevents the character from jumping out of the top of the screen.
Demo code - Player
package com.az.arklis.games; import static java.lang.System.*; import com.az.arklis.engine.*; /** Function: write a player class to realize the behavior of jumping and generating gravity Version: ver 1.0.0 */ public class Player extends Sprite{ public static final int STATE_NORMAL = 0; public static final int STATE_JUMPING = 1; //public static final float SPEED = .3F; // Adjust the rate according to the image size public static final float SPEED = 0.6F; //public static final float GRAVITY = .002F; // The acceleration is adjusted to 0 public static final float GRAVITY = .002F; private int floorY; private int state; public Player(Animation animation){ super(animation); state = STATE_NORMAL; } public int getState(){ return state; } public void setState(int state){ this.state = state; } /** Set the position of the floor, regardless of whether the player starts jumping or has landed */ public void setFloorY(int floorY){ this.floorY = floorY; setY(floorY); } /** The action of jumping produced by the player */ public void jump(){ setVelocityY(-1); state = STATE_JUMPING; } /** Update the player's position and action, or set the player's status to NORMAL if the player has landed */ public void update(long elapsedTime){ //Set the rate of horizontal position (descent effect) if(getState() == STATE_JUMPING){ setVelocityY(getVelocityY() + GRAVITY * elapsedTime); } //Mobile player super.update(elapsedTime); //Check whether the player lands if(getState() == STATE_JUMPING && getY() >= floorY){ setVelocityY(0); setY(floorY); setState(STATE_NORMAL); } } }
The Player class is a state based class. It has two states: NORMAL and JUMPING. Because the Player class saves their state, it can check whether the Player is in NORMAL state, JUMPING state, and whether the Player is falling. The program must tell you where the "floor" is. Now that we have the conditions to implement a simple game (using the InputManager class), demonstrate how to move to the Player in the InputManagerTest class, and then let the Player jump. In order to realize the above functions, several gameactions need to be created in the code to realize this function. Each GameAction implies at least one keyboard or mouse event, and finally allows us to pause the game.
Operation effect
Design intuitive user interface
After looking at the above example, let's finish something very important in the game: the user interface. The user interface is not only the movement of keys and mouse; It also includes open menu, screen configuration, enter game and screen buttons. So designing an intuitive, practical and user-friendly interface is a very important part of the game. Because if there is no easy-to-use interface, the game will lose its appreciation.
Then the user interface design principles are as follows:
- Keep the interface simple and tidy. Not all options are presented at once. On the contrary, the most commonly used and useful options should be placed on the home screen for the convenience of players.
- Make sure that each option button is very easy to use. If you need to click many times to find a certain function, it will make players very unhappy.
- Use tool tips. General tips are in the form of pop-up when the mouse passes through a specific object, so that they can tell the player which buttons do functions on the screen and their current state. Tips can be very quick to answer "What's this?" Because Swing has a built-in tooltip function, it is very easy to implement this function.
- Each game behavior responds to a message from the player, such as using sound or waiting for the cursor to represent it.
- Test your user interface. Because some buttons may be the most obvious for us, but they may not be for others. So we need to test how many people are used to our button design. Of course, it is impossible to satisfy everyone, but most people need to get used to it. Remember, in real life, when a player uses our game, we won't tell him / her what to do next!
- When game player tests are conducted, these players are investigated, and they think these buttons are the easiest to use and the most useful. For example, which icons are most easily recognized by them. However, we just listen, regardless of the difficulty of code implementation.
- Re modify the game interface. If it doesn't work, it will take a few days to code and create new icons to make the perfect interface.
- Using Swing components to start the user interface: Swing is a very big topic. In short, Swing is a group of classes that are used to create user interface elements, such as forms, buttons, pop-up menus, drop-down lists, text input boxes, option buttons, labels, etc. For example, we used the JFrame class to realize the full screen display function. In fact, we just use JFrame object to operate Windw and Frame.
Some swing components use their own components to render images, so we can use swing components in our own rendering cycle. This is exciting news because we can integrate all swing functions into full screen games. In this way, we don't have to rebuild the wheel to create the user interface framework! At the same time, swing can customize the UI interface according to the custom style, so we can use swing to realize the personalized interface. Swing has a large number of API s that can be used. Here we are talking about games, not swing components, so we won't discuss the component functions of swing. The skills of using swing components in the game are as follows:
When drawing all Swing components, you only need to call the paintComponents() method of the main panel during the animation cycle:
//Draw our graphics draw(g); //Draw Swing components JFrame frame = screen.getFullScreenWindow(); frame.getLayeredPane().paintComponents(g);
The possible problem is that the content panel actually draws its background, so it will hide all the content under the panel; If you want your own Swing components to be presented in an independent form, you need to set the content panel to be transparent:
If(contentPane instanceof JComponent){ ((JComponent)contentPane).setOpaque(false); }
The second problem is how to deal with Swing's rendering of its own components. For ordinary Swing applications, we don't have to call the paintComponents() method - Swing will automatically render all components in the AWT event distribution thread. Now we have to turn off this function manually to control the rendering time of the component. For example, when we press a button, we make the button look like it was pressed. The component needs to call the repaint() method to redraw the component. Obviously, we don't need component rendering in the AWT distribution thread, because it will conflict with our customized picture rendering, resulting in jitter or other conflicts.
In our code, we need to ignore the potential redraw request. If a button is pressed, it needs to appear in the draw method of the animation loop. To solve this problem, we need to capture the redraw request and ignore it. According to this idea, because all redrawing requests are sent to the RepaintManager class, we use this class to manage redrawing requests, and then distribute these requests to the components that actually need to process redrawing. Therefore, we need to simply use NullRepaintManger to override the RepaintManger class.
Demo code - NullRepaintManer
package com.az.arklis.engine; import static java.lang.System.*; import javax.swing.RepaintManager; import javax.swing.JComponent; /** Function: write a tool class to manage component redrawing requests in Swing remarks: Intercept the redrawing request of the component, and then distribute it to the corresponding component according to the actual animation requirements */ public class NullRepaintManager extends RepaintManager{ /** Install non redraw Manager */ public static void install(){ RepaintManager repaintManager = new NullRepaintManager(); //Dual cache display policy is not allowed repaintManager.setDoubleBufferingEnabled(false); //Modify the current redraw manager to the repaintManager object RepaintManager.setCurrentManager(repaintManager); } /** Function: the specified component is invalid -- subclass override is required to determine */ public void addInvalidComponent(JComponent component){ //do nothing } /** Function: specify the invalid area of a component -- subclass is required to override the determination */ public void addDirtyRegion(JComponent component, int x, int y, int w, int h){ //do nothing } /** Function: specify the invalid area identification of a component -- subclass is required to override the determination */ public void markCompletelyDirty(JComponent component){ //do nothing } /** Function: specify a component drawing area -- subclasses are required to override the determination */ public void paintDirtyRegions(){ //do nothing } }
This class inherits the RepaintManager class, and then overrides the key method - do nothing, so the redrawing event will not be sent to the AWT distribution thread, so we won't look at the jittery component screen.
API code - RepaintManager
/* * Copyright (c) 1997, 2018, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License version 2 only, as * published by the Free Software Foundation. Oracle designates this * particular file as subject to the "Classpath" exception as provided * by Oracle in the LICENSE file that accompanied this code. * * This code is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License * version 2 for more details (a copy is included in the LICENSE file that * accompanied this code). * * You should have received a copy of the GNU General Public License version * 2 along with this work; if not, write to the Free Software Foundation, * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. * * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA * or visit www.oracle.com if you need additional information or have any * questions. */ package javax.swing; import java.awt.*; import java.awt.event.*; import java.awt.image.VolatileImage; import java.security.AccessControlContext; import java.security.AccessController; import java.security.PrivilegedAction; import java.util.*; import java.util.concurrent.atomic.AtomicInteger; import java.applet.*; import jdk.internal.access.JavaSecurityAccess; import jdk.internal.access.SharedSecrets; import sun.awt.AWTAccessor; import sun.awt.AppContext; import sun.awt.DisplayChangedListener; import sun.awt.SunToolkit; import sun.java2d.SunGraphicsEnvironment; import sun.security.action.GetPropertyAction; import com.sun.java.swing.SwingUtilities3; import java.awt.geom.AffineTransform; import sun.java2d.SunGraphics2D; import sun.java2d.pipe.Region; import sun.swing.SwingAccessor; import sun.swing.SwingUtilities2; import sun.swing.SwingUtilities2.RepaintListener; /** * This class manages repaint requests, allowing the number * of repaints to be minimized, for example by collapsing multiple * requests into a single repaint for members of a component tree. * <p> * As of 1.6 <code>RepaintManager</code> handles repaint requests * for Swing's top level components (<code>JApplet</code>, * <code>JWindow</code>, <code>JFrame</code> and <code>JDialog</code>). * Any calls to <code>repaint</code> on one of these will call into the * appropriate <code>addDirtyRegion</code> method. * * @author Arnaud Weber * @since 1.2 */ public class RepaintManager { /** * Whether or not the RepaintManager should handle paint requests * for top levels. */ static final boolean HANDLE_TOP_LEVEL_PAINT; private static final short BUFFER_STRATEGY_NOT_SPECIFIED = 0; private static final short BUFFER_STRATEGY_SPECIFIED_ON = 1; private static final short BUFFER_STRATEGY_SPECIFIED_OFF = 2; private static final short BUFFER_STRATEGY_TYPE; /** * Maps from GraphicsConfiguration to VolatileImage. */ private Map<GraphicsConfiguration,VolatileImage> volatileMap = new HashMap<GraphicsConfiguration,VolatileImage>(1); // // As of 1.6 Swing handles scheduling of paint events from native code. // That is, SwingPaintEventDispatcher is invoked on the toolkit thread, // which in turn invokes nativeAddDirtyRegion. Because this is invoked // from the native thread we can not invoke any public methods and so // we introduce these added maps. So, any time nativeAddDirtyRegion is // invoked the region is added to hwDirtyComponents and a work request // is scheduled. When the work request is processed all entries in // this map are pushed to the real map (dirtyComponents) and then // painted with the rest of the components. // private Map<Container,Rectangle> hwDirtyComponents; private Map<Component,Rectangle> dirtyComponents; private Map<Component,Rectangle> tmpDirtyComponents; private java.util.List<Component> invalidComponents; // List of Runnables that need to be processed before painting from AWT. private java.util.List<Runnable> runnableList; boolean doubleBufferingEnabled = true; private Dimension doubleBufferMaxSize; private boolean isCustomMaxBufferSizeSet = false; // Support for both the standard and volatile offscreen buffers exists to // provide backwards compatibility for the [rare] programs which may be // calling getOffScreenBuffer() and not expecting to get a VolatileImage. // Swing internally is migrating to use *only* the volatile image buffer. // Support for standard offscreen buffer // DoubleBufferInfo standardDoubleBuffer; /** * Object responsible for hanlding core paint functionality. */ private PaintManager paintManager; private static final Object repaintManagerKey = RepaintManager.class; // Whether or not a VolatileImage should be used for double-buffered painting static boolean volatileImageBufferEnabled = true; /** * Type of VolatileImage which should be used for double-buffered * painting. */ private static final int volatileBufferType; /** * Value of the system property awt.nativeDoubleBuffering. */ private static boolean nativeDoubleBuffering; // The maximum number of times Swing will attempt to use the VolatileImage // buffer during a paint operation. private static final int VOLATILE_LOOP_MAX = 2; /** * Number of <code>beginPaint</code> that have been invoked. */ private int paintDepth = 0; /** * Type of buffer strategy to use. Will be one of the BUFFER_STRATEGY_ * constants. */ private short bufferStrategyType; // // BufferStrategyPaintManager has the unique characteristic that it // must deal with the buffer being lost while painting to it. For // example, if we paint a component and show it and the buffer has // become lost we must repaint the whole window. To deal with that // the PaintManager calls into repaintRoot, and if we're still in // the process of painting the repaintRoot field is set to the JRootPane // and after the current JComponent.paintImmediately call finishes // paintImmediately will be invoked on the repaintRoot. In this // way we don't try to show garbage to the screen. // /** * True if we're in the process of painting the dirty regions. This is * set to true in <code>paintDirtyRegions</code>. */ private boolean painting; /** * If the PaintManager calls into repaintRoot during painting this field * will be set to the root. */ private JComponent repaintRoot; /** * The Thread that has initiated painting. If null it * indicates painting is not currently in progress. */ private Thread paintThread; /** * Runnable used to process all repaint/revalidate requests. */ private final ProcessingRunnable processingRunnable; private static final JavaSecurityAccess javaSecurityAccess = SharedSecrets.getJavaSecurityAccess(); /** * Listener installed to detect display changes. When display changes, * schedules a callback to notify all RepaintManagers of the display * changes. */ private static final DisplayChangedListener displayChangedHandler = new DisplayChangedHandler(); static { SwingAccessor.setRepaintManagerAccessor(new SwingAccessor.RepaintManagerAccessor() { @Override public void addRepaintListener(RepaintManager rm, RepaintListener l) { rm.addRepaintListener(l); } @Override public void removeRepaintListener(RepaintManager rm, RepaintListener l) { rm.removeRepaintListener(l); } }); volatileImageBufferEnabled = "true".equals(AccessController. doPrivileged(new GetPropertyAction( "swing.volatileImageBufferEnabled", "true"))); boolean headless = GraphicsEnvironment.isHeadless(); if (volatileImageBufferEnabled && headless) { volatileImageBufferEnabled = false; } nativeDoubleBuffering = "true".equals(AccessController.doPrivileged( new GetPropertyAction("awt.nativeDoubleBuffering"))); String bs = AccessController.doPrivileged( new GetPropertyAction("swing.bufferPerWindow")); if (headless) { BUFFER_STRATEGY_TYPE = BUFFER_STRATEGY_SPECIFIED_OFF; } else if (bs == null) { BUFFER_STRATEGY_TYPE = BUFFER_STRATEGY_NOT_SPECIFIED; } else if ("true".equals(bs)) { BUFFER_STRATEGY_TYPE = BUFFER_STRATEGY_SPECIFIED_ON; } else { BUFFER_STRATEGY_TYPE = BUFFER_STRATEGY_SPECIFIED_OFF; } HANDLE_TOP_LEVEL_PAINT = "true".equals(AccessController.doPrivileged( new GetPropertyAction("swing.handleTopLevelPaint", "true"))); GraphicsEnvironment ge = GraphicsEnvironment. getLocalGraphicsEnvironment(); if (ge instanceof SunGraphicsEnvironment) { ((SunGraphicsEnvironment) ge).addDisplayChangedListener( displayChangedHandler); } Toolkit tk = Toolkit.getDefaultToolkit(); if ((tk instanceof SunToolkit) && ((SunToolkit) tk).isSwingBackbufferTranslucencySupported()) { volatileBufferType = Transparency.TRANSLUCENT; } else { volatileBufferType = Transparency.OPAQUE; } } // There's too much code, so we'll save some here. Because Nuggets said more than words in the browser editor, we'll save a lot /** * If possible this will show a previously rendered portion of * a Component. If successful, this will return true, otherwise false. * <p> * WARNING: This method is invoked from the native toolkit thread, be * very careful as to what methods this invokes! */ boolean show(Container c, int x, int y, int w, int h) { return getPaintManager().show(c, x, y, w, h); } /** * Invoked when the doubleBuffered or useTrueDoubleBuffering * properties of a JRootPane change. This may come in on any thread. */ void doubleBufferingChanged(JRootPane rootPane) { getPaintManager().doubleBufferingChanged(rootPane); } /** * Sets the <code>PaintManager</code> that is used to handle all * double buffered painting. * * @param paintManager The PaintManager to use. Passing in null indicates * the fallback PaintManager should be used. */ void setPaintManager(PaintManager paintManager) { if (paintManager == null) { paintManager = new PaintManager(); } PaintManager oldPaintManager; synchronized(this) { oldPaintManager = this.paintManager; this.paintManager = paintManager; paintManager.repaintManager = this; } if (oldPaintManager != null) { oldPaintManager.dispose(); } } private synchronized PaintManager getPaintManager() { if (paintManager == null) { PaintManager paintManager = null; if (doubleBufferingEnabled && !nativeDoubleBuffering) { switch (bufferStrategyType) { case BUFFER_STRATEGY_NOT_SPECIFIED: Toolkit tk = Toolkit.getDefaultToolkit(); if (tk instanceof SunToolkit) { SunToolkit stk = (SunToolkit) tk; if (stk.useBufferPerWindow()) { paintManager = new BufferStrategyPaintManager(); } } break; case BUFFER_STRATEGY_SPECIFIED_ON: paintManager = new BufferStrategyPaintManager(); break; default: break; } } // null case handled in setPaintManager setPaintManager(paintManager); } return paintManager; } private void scheduleProcessingRunnable(AppContext context) { if (processingRunnable.markPending()) { Toolkit tk = Toolkit.getDefaultToolkit(); if (tk instanceof SunToolkit) { SunToolkit.getSystemEventQueueImplPP(context). postEvent(new InvocationEvent(Toolkit.getDefaultToolkit(), processingRunnable)); } else { Toolkit.getDefaultToolkit().getSystemEventQueue(). postEvent(new InvocationEvent(Toolkit.getDefaultToolkit(), processingRunnable)); } } } /** * PaintManager is used to handle all double buffered painting for * Swing. Subclasses should call back into the JComponent method * <code>paintToOffscreen</code> to handle the actual painting. */ static class PaintManager { /** * RepaintManager the PaintManager has been installed on. */ protected RepaintManager repaintManager; boolean isRepaintingRoot; /** * Paints a region of a component * * @param paintingComponent Component to paint * @param bufferComponent Component to obtain buffer for * @param g Graphics to paint to * @param x X-coordinate * @param y Y-coordinate * @param w Width * @param h Height * @return true if painting was successful. */ public boolean paint(JComponent paintingComponent, JComponent bufferComponent, Graphics g, int x, int y, int w, int h) { // First attempt to use VolatileImage buffer for performance. // If this fails (which should rarely occur), fallback to a // standard Image buffer. boolean paintCompleted = false; Image offscreen; int sw = w + 1; int sh = h + 1; if (repaintManager.useVolatileDoubleBuffer() && (offscreen = getValidImage(repaintManager. getVolatileOffscreenBuffer(bufferComponent, sw, sh))) != null) { VolatileImage vImage = (java.awt.image.VolatileImage)offscreen; GraphicsConfiguration gc = bufferComponent. getGraphicsConfiguration(); for (int i = 0; !paintCompleted && i < RepaintManager.VOLATILE_LOOP_MAX; i++) { if (vImage.validate(gc) == VolatileImage.IMAGE_INCOMPATIBLE) { repaintManager.resetVolatileDoubleBuffer(gc); offscreen = repaintManager.getVolatileOffscreenBuffer( bufferComponent, sw, sh); vImage = (java.awt.image.VolatileImage)offscreen; } paintDoubleBuffered(paintingComponent, vImage, g, x, y, w, h); paintCompleted = !vImage.contentsLost(); } } // VolatileImage painting loop failed, fallback to regular // offscreen buffer if (!paintCompleted && (offscreen = getValidImage( repaintManager.getOffscreenBuffer( bufferComponent, w, h))) != null) { paintDoubleBuffered(paintingComponent, offscreen, g, x, y, w, h); paintCompleted = true; } return paintCompleted; } /** * Does a copy area on the specified region. */ public void copyArea(JComponent c, Graphics g, int x, int y, int w, int h, int deltaX, int deltaY, boolean clip) { g.copyArea(x, y, w, h, deltaX, deltaY); } /** * Invoked prior to any calls to paint or copyArea. */ public void beginPaint() { } /** * Invoked to indicate painting has been completed. */ public void endPaint() { } /** * Shows a region of a previously rendered component. This * will return true if successful, false otherwise. The default * implementation returns false. */ public boolean show(Container c, int x, int y, int w, int h) { return false; } /** * Invoked when the doubleBuffered or useTrueDoubleBuffering * properties of a JRootPane change. This may come in on any thread. */ public void doubleBufferingChanged(JRootPane rootPane) { } /** * Paints a portion of a component to an offscreen buffer. */ protected void paintDoubleBuffered(JComponent c, Image image, Graphics g, int clipX, int clipY, int clipW, int clipH) { if (image instanceof VolatileImage && isPixelsCopying(c, g)) { paintDoubleBufferedFPScales(c, image, g, clipX, clipY, clipW, clipH); } else { paintDoubleBufferedImpl(c, image, g, clipX, clipY, clipW, clipH); } } private void paintDoubleBufferedImpl(JComponent c, Image image, Graphics g, int clipX, int clipY, int clipW, int clipH) { Graphics osg = image.getGraphics(); int bw = Math.min(clipW, image.getWidth(null)); int bh = Math.min(clipH, image.getHeight(null)); int x,y,maxx,maxy; try { for(x = clipX, maxx = clipX+clipW; x < maxx ; x += bw ) { for(y=clipY, maxy = clipY + clipH; y < maxy ; y += bh) { osg.translate(-x, -y); osg.setClip(x,y,bw,bh); if (volatileBufferType != Transparency.OPAQUE && osg instanceof Graphics2D) { final Graphics2D g2d = (Graphics2D) osg; final Color oldBg = g2d.getBackground(); g2d.setBackground(c.getBackground()); g2d.clearRect(x, y, bw, bh); g2d.setBackground(oldBg); } c.paintToOffscreen(osg, x, y, bw, bh, maxx, maxy); g.setClip(x, y, bw, bh); if (volatileBufferType != Transparency.OPAQUE && g instanceof Graphics2D) { final Graphics2D g2d = (Graphics2D) g; final Composite oldComposite = g2d.getComposite(); g2d.setComposite(AlphaComposite.Src); g2d.drawImage(image, x, y, c); g2d.setComposite(oldComposite); } else { g.drawImage(image, x, y, c); } osg.translate(x, y); } } } finally { osg.dispose(); } } private void paintDoubleBufferedFPScales(JComponent c, Image image, Graphics g, int clipX, int clipY, int clipW, int clipH) { Graphics osg = image.getGraphics(); Graphics2D g2d = (Graphics2D) g; Graphics2D osg2d = (Graphics2D) osg; AffineTransform identity = new AffineTransform(); int bw = Math.min(clipW, image.getWidth(null)); int bh = Math.min(clipH, image.getHeight(null)); int x, y, maxx, maxy; AffineTransform tx = g2d.getTransform(); double scaleX = tx.getScaleX(); double scaleY = tx.getScaleY(); double trX = tx.getTranslateX(); double trY = tx.getTranslateY(); boolean translucent = volatileBufferType != Transparency.OPAQUE; Composite oldComposite = g2d.getComposite(); try { for (x = clipX, maxx = clipX + clipW; x < maxx; x += bw) { for (y = clipY, maxy = clipY + clipH; y < maxy; y += bh) { // draw x, y, bw, bh int pixelx1 = Region.clipRound(x * scaleX + trX); int pixely1 = Region.clipRound(y * scaleY + trY); int pixelx2 = Region.clipRound((x + bw) * scaleX + trX); int pixely2 = Region.clipRound((y + bh) * scaleY + trY); int pixelw = pixelx2 - pixelx1; int pixelh = pixely2 - pixely1; osg2d.setTransform(identity); if (translucent) { final Color oldBg = g2d.getBackground(); g2d.setBackground(c.getBackground()); g2d.clearRect(pixelx1, pixely1, pixelw, pixelh); g2d.setBackground(oldBg); } osg2d.setClip(0, 0, pixelw, pixelh); osg2d.translate(trX - pixelx1, trY - pixely1); osg2d.scale(scaleX, scaleY); c.paintToOffscreen(osg, x, y, bw, bh, maxx, maxy); g2d.setTransform(identity); g2d.setClip(pixelx1, pixely1, pixelw, pixelh); AffineTransform stx = new AffineTransform(); stx.translate(pixelx1, pixely1); stx.scale(scaleX, scaleY); g2d.setTransform(stx); if (translucent) { g2d.setComposite(AlphaComposite.Src); } g2d.drawImage(image, 0, 0, c); if (translucent) { g2d.setComposite(oldComposite); } g2d.setTransform(tx); } } } finally { osg.dispose(); } } /** * If <code>image</code> is non-null with a positive size it * is returned, otherwise null is returned. */ private Image getValidImage(Image image) { if (image != null && image.getWidth(null) > 0 && image.getHeight(null) > 0) { return image; } return null; } /** * Schedules a repaint for the specified component. This differs * from <code>root.repaint</code> in that if the RepaintManager is * currently processing paint requests it'll process this request * with the current set of requests. */ protected void repaintRoot(JComponent root) { assert (repaintManager.repaintRoot == null); if (repaintManager.painting) { repaintManager.repaintRoot = root; } else { root.repaint(); } } /** * Returns true if the component being painted is the root component * that was previously passed to <code>repaintRoot</code>. */ protected boolean isRepaintingRoot() { return isRepaintingRoot; } /** * Cleans up any state. After invoked the PaintManager will no * longer be used anymore. */ protected void dispose() { } private boolean isPixelsCopying(JComponent c, Graphics g) { AffineTransform tx = getTransform(g); GraphicsConfiguration gc = c.getGraphicsConfiguration(); if (tx == null || gc == null || !SwingUtilities2.isFloatingPointScale(tx)) { return false; } AffineTransform gcTx = gc.getDefaultTransform(); return gcTx.getScaleX() == tx.getScaleX() && gcTx.getScaleY() == tx.getScaleY(); } private static AffineTransform getTransform(Graphics g) { if (g instanceof SunGraphics2D) { return ((SunGraphics2D) g).transform; } else if (g instanceof Graphics2D) { return ((Graphics2D) g).getTransform(); } return null; } } private class DoubleBufferInfo { public Image image; public Dimension size; public boolean needsReset = false; } /** * Listener installed to detect display changes. When display changes, * schedules a callback to notify all RepaintManagers of the display * changes. Only one DisplayChangedHandler is ever installed. The * singleton instance will schedule notification for all AppContexts. */ private static final class DisplayChangedHandler implements DisplayChangedListener { public void displayChanged() { scheduleDisplayChanges(); } public void paletteChanged() { } private static void scheduleDisplayChanges() { // To avoid threading problems, we notify each RepaintManager // on the thread it was created on. for (AppContext context : AppContext.getAppContexts()) { synchronized(context) { if (!context.isDisposed()) { EventQueue eventQueue = (EventQueue)context.get( AppContext.EVENT_QUEUE_KEY); if (eventQueue != null) { eventQueue.postEvent(new InvocationEvent( Toolkit.getDefaultToolkit(), new DisplayChangedRunnable())); } } } } } } private static final class DisplayChangedRunnable implements Runnable { public void run() { RepaintManager.currentManager((JComponent)null).displayChanged(); } } /** * Runnable used to process all repaint/revalidate requests. */ private final class ProcessingRunnable implements Runnable { // If true, we're wainting on the EventQueue. private boolean pending; /** * Marks this processing runnable as pending. If this was not * already marked as pending, true is returned. */ public synchronized boolean markPending() { if (!pending) { pending = true; return true; } return false; } public void run() { synchronized (this) { pending = false; } // First pass, flush any heavy paint events into real paint // events. If there are pending heavy weight requests this will // result in q'ing this request up one more time. As // long as no other requests come in between now and the time // the second one is processed nothing will happen. This is not // ideal, but the logic needed to suppress the second request is // more headache than it's worth. scheduleHeavyWeightPaints(); // Do the actual validation and painting. validateInvalidComponents(); prePaintDirtyRegions(); } } private RepaintManager getDelegate(Component c) { RepaintManager delegate = SwingUtilities3.getDelegateRepaintManager(c); if (this == delegate) { delegate = null; } return delegate; } }
We show all the 1895 lines of API code. The purpose is to let you read the deeply encapsulated code of OpenJDK 15, so as to have a good positioning and understand the design patterns and underlying principles.
Note: because Swing components are non thread safe, when a Swing component is visible, it is impossible to modify its state in the AWT event distribution thread. If we need to modify the Swing component after it is displayed, we need to do this in the event distribution thread:
EventQueue.invokeAndWait(new Runnable(){ public void run(){ doSomething(); } });
The above code calls the code in the AWT event distributor thread and waits for the code to be executed. In addition, if we don't want to wait for the code to be executed, we call the invokeLater() method to create a simple menu
Now let's improve the InputManagerTest class and add some simple user interfaces: pause, configure, and exit buttons. First, what happens when we click a button? Swing will judge the click, and then check whether the button has a listener. If so, the listener will notify the AWT event distributor thread that the button has been pressed. In the code, we know which components generate event behavior through the getSource() method of ActionEvent.
public void actionPerforme(ActionEvent e){ Object src = e.getSource(); if(src == okButton){ //do something ... } }
Finally, in the user interface, we can use this button to do the following events:
- Add tips - just call the setToolTip("Hello World") method, and the rest is implemented by Swing
- Use icons instead of text in buttons. There must be two different icons, one for the initial state and one for the pressed state
- Hide the default style. If you want the icon to appear as it is, you need to hide the border of the button. When hiding, call the setcontentareafile (false) method to ensure that the background of the knife will not be drawn
- Modify the cursor. Let the cursor turn into a hand when sliding over the button - just call the setCursor() method
- Turn off keyboard focus - call setFocusable(false)
Demo code - MenuTest
package com.funfree.arklis.input; import java.awt.event.*; import static java.lang.System.*; import java.awt.*; import java.util.LinkedList; import com.funfree.arklis.util.*; import com.funfree.arklis.engine.*; import javax.swing.*; /** Function: write a class to test the use of game menus */ public class MenuTest extends InputManagerTest implements ActionListener{ protected GameAction configAction; private JButton playButton; private JButton configButton; private JButton quitButton; private JButton pauseButton; private JPanel playButtonSpace; public void init(){ super.init(); //Do not let Swing components redraw NullRepaintManager.install(); //Create a configuration object configAction = new GameAction("config"); //Create menu button quitButton = createButton("quit","sign out"); playButton = createButton("play","continue"); pauseButton = createButton("pause","suspend"); configButton = createButton("config","Modify settings"); //Create space for play/pause buttons playButtonSpace = new JPanel(); playButtonSpace.setOpaque(false); playButtonSpace.add(pauseButton); //Get full screen object JFrame frame = (JFrame)super.screen.getFullScreenWindow(); Container contentPane = frame.getContentPane(); //Make the panel transparent if(contentPane instanceof JComponent){ ((JComponent)contentPane).setOpaque(false); } //Add components to the screen panel contentPane.setLayout(new FlowLayout(FlowLayout.LEFT)); contentPane.add(playButtonSpace); contentPane.add(configButton); contentPane.add(quitButton); //The display requires the system to list components frame.validate(); } /** Function: rewrite the draw method of InputManagerTest to draw all Swing components */ public void draw(Graphics2D g){ super.draw(g); JFrame frame = (JFrame)super.screen.getFullScreenWindow(); //List the pop-up information (tooltips, pop menus) in the panel frame.getLayeredPane().paintComponents(g); } /** Function: modify the appearance of pause/play button */ public void setPaused(boolean pause){ super.setPaused(pause); //Empty the panel playButtonSpace.removeAll(); //Then, the presentation of the screen is determined according to the current pause state -- if it is currently suspended if(isPaused()){ //Then the release button is displayed playButtonSpace.add(playButton); }else{ //Otherwise, the pause button is displayed playButtonSpace.add(pauseButton); } } public void actionPerformed(ActionEvent event){ Object source = event.getSource(); if(source == quitButton){ super.exit.tap(); }else if(source == configButton){ configAction.tap(); }else if(source == playButton || source == pauseButton){ super.pause.tap(); } } /** Function: this method is a very important auxiliary method - used to create the menu of the game */ private JButton createButton(String name, String toolTip){ //Load picture String imagePath = "images/menu/" + name + ".png"; out.println(imagePath); ImageIcon iconRollover = new ImageIcon(imagePath); int w = iconRollover.getIconWidth(); int h = iconRollover.getIconHeight(); //Sets the style of the cursor for the current button Cursor cursor = Cursor.getPredefinedCursor(Cursor.HAND_CURSOR); //Make the default picture transparent Image image = screen.createCompatibleImage(w, h, Transparency.TRANSLUCENT); Graphics2D g = (Graphics2D)image.getGraphics(); Composite alpha = AlphaComposite.getInstance(AlphaComposite.DST_OVER, .5f); g.setComposite(alpha);//Set transparency g.drawImage(iconRollover.getImage(),0,0,null); g.dispose(); ImageIcon iconDefault = new ImageIcon(image); //Display the pressed picture image = screen.createCompatibleImage(w,h,Transparency.TRANSLUCENT); g = (Graphics2D)image.getGraphics(); g.drawImage(iconRollover.getImage(),2,2,null); g.dispose(); ImageIcon iconPressed = new ImageIcon(image); //Create button object JButton button = new JButton(); button.addActionListener(this); button.setIgnoreRepaint(true); button.setFocusable(false); button.setToolTipText(toolTip); button.setBorder(null); button.setContentAreaFilled(false); button.setCursor(cursor); button.setIcon(iconDefault); button.setRolloverIcon(iconRollover); button.setPressedIcon(iconPressed); return button; } }
In the above example, each button has a PNG image, and other images are generated when the program starts. The default menu image has a little faded style image rendering. This effect is achieved by using the 0.5 transparent effect of AlphaComposite class. The setPause() method of this class is used to set the pause button to be placed in JPanel. The panel also has other menu function buttons. When the user clicks pause and non pause actions, the panel will correctly display the corresponding buttons.
Operation effect
Let the player set the keyboard
If it is necessary for players to project the keyboard or mouse, all players can the game behavior and buttons, as well as the mouse buttons. These buttons are used to represent the game behavior, and the keyboard configuration can have two parts:
- We need to create a configuration object box
- We need to create a special component that allows players to enter keyboard or mouse clicks.
The dialog box lists all possible game behaviors and corresponding instructions. The dialog box itself is a JPanel class, and there can be a series of components, panels and layout managers in the panel.
Creating a special input component is still difficult, because we need the component to show which keys are mapped to the current game behavior, and which keys can be used by players as keys or mouse buttons to modify settings. When all this is done, you also need the component to return the keyboard focus to the main game form.
The idea of creating the input component is to use the JTextField class to receive the player's input. Because the component allows any text to be input, we can let the player input keys or click with the mouse; Then we need to override the input method of JTextField class. Generally, we need to add KeyListener and MouseListener to the implementation class
In the input event, but we need to use another way to obtain the key. Of course, there are other ways to obtain the input event. Because each Swing Component is an instance of the Component class, the Component class has methods processKeyEvent() and processMouseEvent (). These methods are just like the KeyListener and MouseListener methods. We just need to override these methods and let the input event call the enableEvents() method.
Demo code - KeyConfigTest
package com.funfree.arklis.input; import java.awt.*; import java.awt.event.*; import java.util.*; import javax.swing.*; import javax.swing.border.*; import com.funfree.arklis.engine.*; import static java.lang.System.*; /** Function: write a test class to test whether the InputComponent component can be used normally Note: this class inherits the MenuTest class and is used to demonstrate adding a dialog box to allow players to customize game behavior */ public class KeyConfigTest extends MenuTest{ private static final String INSTRUCTIONS = "<html><h4 style='color:#Ff1493 '> click in a behavior input box to modify its key position. " + "<br/>A behavior can have up to three associated keys.<br/>Press Backspace Key clears an added behavior key.</h4></html>"; private JPanel dialog; private JButton okButton; //Override the parent to the original init method to initialize the object public void init(){ super.init(); setList(new ArrayList());//Initializes a collection for loading input behavior //Create GameAction and corresponding mapping key JPanel configPanel = new JPanel(new GridLayout(5,2,2,2)); addActionConfig(configPanel,moveLeft); addActionConfig(configPanel,moveRight); addActionConfig(configPanel,jump); addActionConfig(configPanel,pause); addActionConfig(configPanel,exit); //Create a panel to hold the OK button JPanel bottomPanel = new JPanel(new FlowLayout()); okButton = new JButton(" determine "); okButton.setFocusable(false); okButton.addActionListener(this); bottomPanel.add(okButton); //Create a command panel JPanel topPanel = new JPanel(new FlowLayout()); topPanel.add(new JLabel(INSTRUCTIONS)); //Create a dialog border Border border = BorderFactory.createLineBorder(Color.black); //Create a dialog dialog = new JPanel(new BorderLayout()); dialog.add(topPanel, BorderLayout.NORTH); dialog.add(configPanel,BorderLayout.CENTER); dialog.add(bottomPanel,BorderLayout.SOUTH); dialog.setBorder(border); dialog.setVisible(false); dialog.setSize(dialog.getPreferredSize()); //Put the dialog box in dialog.setLocation( (screen.getWidth() - dialog.getWidth()) / 2, (screen.getHeight() - dialog.getHeight()) / 2); //Set the dialog box as a model dialog box screen.getFullScreenWindow().getLayeredPane().add(dialog,JLayeredPane.MODAL_LAYER); } /** Add a name with game behavior so that InputComponent can use this text to modify the mapped key */ private void addActionConfig(JPanel configPanel, GameAction action){ JLabel label = new JLabel(action.getName(), JLabel.RIGHT); InputComponent input = new InputComponent(action,this); configPanel.add(label); configPanel.add(input); getList().add(input);//Put it in the collection and save it } public void actionPerformed(ActionEvent event){ super.actionPerformed(event); if(event.getSource() == okButton){ configAction.tap();//Hide configuration dialog } } /* Override the checkSystemInput method in the InputManagerTest class to determine whether the dialog box is hidden and displayed */ public void checkSystemInput(){ super.checkSystemInput(); if(configAction.isPressed()){ //Hide or show the configuration dialog box boolean show = !dialog.isVisible(); dialog.setVisible(show); setPaused(show); } } }
Operation effect
It can get up to three human keyboard input restrictions. This takes the Backspace key as a special key. After inputting a key in the InputComponent, if you press the Backspace key and then click the "OK" button, the function key set by the player will be deleted.
summary
In the end game, the user customization of keyboard and mouse is the standard configuration function, and all external transfer input management is also the standard configuration function. Therefore, if we want to develop the end game, it is necessary to control and manage the game input.
From the perspective of Java, because of the deep encapsulation of API, it is very easy for our third-party application developers to realize the input control of computer peripherals.