前言
代码演示环境:
- 软件环境:Windows 10
- 开发工具:Visual Studio Code
- JDK版本:OpenJDK 15
尽管这些代码是10几年前的写的,然而依然可能在古代操作系统和Java最新开源版本中失常运行。
界面和交互
AWT事件模型
如果一个人玩橡棋就像一个人玩游戏时没有交互一样,会十分无聊,所以玩家最大的乐趣就是与电脑或者人的交互。那么首先玩家得与电脑交互—键盘与鼠标的交互,在JDK 1.4版本还提供了手柄的驱动让玩家与电脑交互。
AWT有本人的事件散发线程—该线程散发所有品种的事件,比方鼠标点击和键盘事件,这些事件都来自于操作系统。
那么AWT在哪里散发这些事件?在一个特定的组件呈现一种事件时候发。AWT会查看是否有该事件的监听器存在—监听器是一个对象,它专门从另外一个对象接管事件,在这种状况下,事件就会来自于AWT事件散发器线程了。每种事件都有对应的监听器,比方输出事件,咱们有KeyListener接口来对象。上面形容的是事件的工作流程:
- 用户按下键
- 操作系统发送键盘事件给Java运行时
- java运行时产生事件对象,而后增加到AWT的事件队列中去
- AWT事件分发送线程调配事件对象给任何一个KeyListeners
- KeyListener获取键盘事件,并且做它想做的事
咱们能够应用AWTEventListener类,它能够用来调试解决
Toolkit.getDefaultToolkit().addAWTEventListener(new AWTEventListener(){ public void eventDispatched(AWTEevent event){ System.out.println(event); }}, -1);
留神:以上代码只能用来调试,但不能应用在实在的游戏中。
键盘输入
在一个游戏中,咱们会应用大量的键盘,比方光标键来挪动人物的地位,以及应用键盘管制武器。上面咱们应用KeyListener来监听键盘事件,并且解决这些事件。
Window window = screen.getFullScreenWindow();window.addKeyListener(keyListener);
KeyListener接口定义了keyPressed(), KeyReleased()和KeyTyped()办法。“typed”事件呈现一个键盘第一次按下之后,而后反复点击该键盘。该事件对于游戏来基本上没有应用,所以咱们只关注键盘的press和release事件。
以上办法都有一个KeyEvent事件参数,该事件对象能够让咱们察看哪个键盘被按下和开释掉—应用虚构键盘代码(virtual key code)。虚构键盘是Java定义的代码,用来示意每个键盘的键,然而它不与理论的字符雷同,比方Q和q是不同字符,然而它们有雷同的key code值。所有的虚构键盘都是以VK_xxx示意,比方Q键应用KeyEvent.VK_Q示意,大多数状况下,咱们能够依据虚构键来推判理论对应的键。留神:Window类的setFocusTraversalKeysEnabled(false)办法是让按键聚焦在转换键事件上,转换键能够批改以后按键的焦点,而后能够让焦点移到另外的组件中去。比方,在一个web网页中,咱们可能按了Tab键,让光标从一个表单域移到另外一个表单域组件中去。Tab键的事件由AWT的焦点转换代码封装,然而咱们可获取Tab键的事件,所以这个办法容许咱们能够这样应用。除了Tab键,咱们能够应用Alt键来产生激活记忆行为(activate nmemonic)。比方按Alt+F是激活File菜单行为。因为AWT会认为在Alt之后按下的键会被疏忽,所以如果不想有这种后果咱们会呼叫KeyEvent的consume()办法不让AWT疏忽该行为。在确认没有其它对象解决Alt键(或者没有润饰键激活记忆),那么咱们把Alt键看成一个一般的键。
键盘演示代码-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.*;/** 性能:书写一个键盘测试类,用来阐明键盘事件的应用 备注:该类继承GameCore引擎类,而后实现键盘监听器接口,以解决键盘操作事件。 */public class KeyTest extends GameCore { private LinkedList messages = new LinkedList();//应用一个双向链表来保留事件 /** 重写你父类的init办法,以初始化本类的实例。 */ public void init(){ super.init(); //设置屏幕为全屏幕显示 Window window = screen.getFullScreenWindow(); //容许输出TAB键和其它特定键 window.setFocusTraversalKeysEnabled(false); //给以后全屏幕增加键盘监听器 window.addKeyListener(this); //向汇合中增加音讯 addMessage("键盘输入测试,按Escape键退出程序。"); } /* 实现监听器接口定义的办法 */ public void keyPressed(KeyEvent event){ int keyCode = event.getKeyCode(); //如果按了esc键 if(keyCode == KeyEvent.VK_ESCAPE){ stop();//那么设置后果标识位 }else{ //否则解决按下事件 addMessage("按下了:" + KeyEvent.getKeyText(keyCode)); //event.consume();//确定该键不解决任何事件 } } public void keyReleased(KeyEvent event){ int keyCode = event.getKeyCode(); addMessage("开释了:" + KeyEvent.getKeyText(keyCode)); //event.consume(); } public void keyTyped(KeyEvent event){ //event.consume(); } public synchronized void addMessage(String message){ messages.add(message); //如果汇合的大小大于或者等于屏幕的高度除了字体大小 if(messages.size() >= screen.getHeight() / FONT_SIZE){ messages.remove(0); //那么删除汇合中的第 } } /** 绘制汇合听元素,其中RenderingHints类定义和治理键和关联值的汇合,它容许 应用程序将输出参数作为其它类应用的算法抉择,这些类用来执行出现和图片解决服务。 */ public synchronized void draw(Graphics2D g){ Window window = screen.getFullScreenWindow(); //应用指定的算法实现图像的显示--要求“文本抗锯齿提醒键”和"文本抗锯齿提醒值" g.setRenderingHint( RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON); //绘制背景图像 g.setColor(window.getBackground()); g.fillRect(0,0,screen.getWidth(),screen.getHeight()); //绘制须要显示的音讯 g.setColor(window.getForeground()); int y = FONT_SIZE; //绘制文字在互屏幕中去 for(int i = 0; i < messages.size(); i++){ g.drawString((String)messages.get(i),5,y); y += FONT_SIZE; } }}
关联外围代码-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.*;/** 性能:书写一个抽象类,用来测试它的子类实现draw办法 备注: 该类是一个引擎,它规定了子类实现游戏的动作:重写update办法和draw办法。客户端类只须要实现 gameLoop中的update办法与draw办法即可。如果须要实现与用户的交互,那么只须要向子类增加相应 的监听器即可。 */public abstract class GameCore extends ActionAdapter{ protected static final int FONT_SIZE = 54; private boolean isRunning; protected ScreenManager screen; //有屏幕治理 protected InputManager inputManager;//有输出管理器 //用来保留引擎的组件,比方InputComponent等 protected java.util.List list; //应用时用来再初始化 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; } /** 示意游戏完结 */ public void stop(){ isRunning = false; } /** 呼叫init()和gameLoop()办法 */ public void run(){ try{ init(); gameLoop(); }finally{ screen.restoreScreen(); } } //默认的初始化行为 public void init(){ //1. 指定一个屏幕管理器对象 screen = new ScreenManager(); //2. 而后确定以后计算机的显卡 DisplayMode displayMode = screen.findFirstCompatibleMode(POSSIBLE_MODES); //3. 设置全屏幕显示模型--它是子类获取全屏幕的前提 screen.setFullScreen(displayMode); //4.上面是获取全屏幕中的默认字体款式与色彩 Window window = screen.getFullScreenWindow(); window.setFont(new Font("Dialog", Font.PLAIN, FONT_SIZE)); window.setBackground(Color.blue); window.setForeground(Color.white); //5. 示意以后游戏运行中 isRunning = true; } public Image loadImage(String fileName){ return new ImageIcon(fileName).getImage(); } /** 如果stop办法被呼叫,那么进行呼叫该办法。默认的gameLoop()行为。 */ private void gameLoop(){ //获取以后的工夫 long startTime = currentTimeMillis(); //初始化游戏开始的以后 long currentTime = startTime; //如果isRunning为true值 while(isRunning){//那么让游戏循环持续 //1. 以后游戏进行工夫--其中elapsedTime值的大小是由以后 // 主线程sleep的值(Thread.sleep(20))来确定的! long elapsedTime = currentTimeMillis() - currentTime; out.println("以后工夫:" + currentTime + ",游戏的进行的工夫:" + elapsedTime); currentTime += elapsedTime; //2. 依据以后游戏的进行工夫来进行游戏动画的更新--须要子类重写(指定的动作) update(elapsedTime); Graphics2D g = screen.getGraphics(); draw(g);//绘制图片--须要子类重写(指定的动作) g.dispose(); screen.update();//应用双缓存技术刷新屏幕 try{ Thread.sleep(20); }catch(InterruptedException e){ e.printStackTrace(); } }//否则不作为! } /** 性能:该办法须要由子类实现,以实现特定的动画成果。具体的动画成果,须要依据需要形容来实现。 能够写成形象办法作为框架来应用! */ public void update(long elapsedTime){ //do nothing } /** 性能:定义一个形象办法,要求子类必须实现该办法,以便可能在屏幕中显示进去。该办法必须实现 */ public abstract void draw(Graphics2D g);}
键盘输入运行成果
鼠标输出
鼠标有三种事件:
- 鼠标按钮点击事件
- 鼠标挪动事件
- 鼠标滚动事件
鼠标演示代码-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.*;/** 性能:书写一个类用来测试监听鼠标的行为 备注:继承游戏引擎GameCore父类,而后实现键盘监听器,鼠标相干的监听器(包含鼠标挪动、 鼠标滚轴监听器) */public class MouseTest extends GameCore { private static final int TRAIL_SIZE = 10; //绘制重影10个 private static final Color[] COLORS = { //设置字体的前景色彩 Color.white, Color.black, Color.yellow, Color.magenta }; private LinkedList trailList; private boolean trailMode; private int colorIndex; /** 重写init办法以初始化该类的实例 */ public void init(){ super.init(); trailList = new LinkedList(); Window window = screen.getFullScreenWindow(); //给以后全屏幕增加鼠标和键盘的监听器 window.addMouseListener(this); window.addMouseMotionListener(this); window.addMouseWheelListener(this); window.addKeyListener(this); } /** 性能:重写/实现draw的形象办法,以实现鼠标的draw动作。 */ public synchronized void draw(Graphics2D g){ int count = trailList.size(); //是否间断绘制以后挪动的鼠标 if(count > 1 && !trailMode){ count = 1;//只绘制第一个Point对象字样 } //1. 获取以后的全屏幕 Window window = screen.getFullScreenWindow(); //2. 而后向该全屏幕绘制背景--必须先有这一步 g.setColor(window.getBackground()); g.fillRect(0,0,screen.getWidth(),screen.getHeight()); //3. 接着绘制指令--指绘制文本须要抗锯齿成果 g.setRenderingHint( RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON); g.setColor(window.getForeground()); //4. 开始绘制文本到全屏幕中 g.drawString("鼠标测试。按Escape键退出程序。", 5, FONT_SIZE); //绘制鼠标--依据鼠标以后的地位来绘制句文字--绘制"你好!Java世界。"重影成果 for(int i = 0; i < count; i++){ Point point = (Point)trailList.get(i); g.drawString("你好!Java世界。",point.x, point.y); } } //判断是否为重影显示“你好!Java世界。”字样 public void mousePressed(MouseEvent event){ trailMode = !trailMode; } //重写鼠标进入事件 public void mouseEntered(MouseEvent event){ mouseMoved(event); } //重写鼠标拖拽事件 public void mouseDragged(MouseEvent event){ mouseMoved(event); } //重写鼠标退出事件 public void mouseExited(MouseEvent event){ mouseMoved(event); } /** 重写鼠标挪动事件,用来保留以后鼠标的挪动的坐标值,这些坐标的个数必须小于TRAIL_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(); } } /** 重写鼠标的滚轴事件,用来解决屏幕中前景显示的色彩。 */ 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]); } //重写键盘的按下事件,以便退出应用程序 public void keyPressed(KeyEvent event){ //如果按下了Esc键,那么屏幕进入游戏前的显示模型,并完结程序。 if(event.getKeyCode() == KeyEvent.VK_ESCAPE){ stop(); } } }
Point对象被保留到trailList汇合中,该对象有x和y的坐标值,并且最多能够保留10个坐标值。如果鼠标挪动在持续,那么draw办法会给每个Point绘制一个“hello world!”字样,否则只绘制第一个Point对象,点击鼠标会批改trail模型。
在以上代码中,咱们Robot类挪动鼠标,然而鼠标挪动事件可能不会立刻呈现,所以代码会查看鼠标挪动事件是否定位在屏幕地方。如果是这样,那么把它认为是一种重置地方的事件,而理论的事件被疏忽掉;否则该事件被当作一般的鼠标挪动事件处理。
对于鼠标的样子,咱们能够应用Java API创立本人的款式,创立时须要应用Toolkit类的createCustomerCursor()办法来实现
在游戏中咱们能够呼叫Toolkit类截取一个不可见的光标,而后呼叫setCursor()办法:
Window window = screen.getFullScreenWindow();window.setCursor(invisibleCursor);
之后,咱们能够呼叫Cursor类的getPredefinedCursor()办法来复原原来的光标款式:
Cursor normalCursor = Cursor.getPredefineCursor(Cursor.DEFAULT_CURSOR);
创立输出管理器
后面解说了罕用的输出事件,以及它们的解决。上面咱们把它们放在一起,就能够创立一个输出管理器。然而,在封装之前,咱们先要阐明后面的代码的缺点。
首先,咱们应该留神到synchronized润饰的办法。记住:所有的事件都是从AWT事件散发线程中产生的,该线程不是主线程!显然,咱们不批改游戏状态(批改妖怪的地位),所以这些同步办法必定不可能让这些事件产生。而在咱们的理论游戏中,咱们必须解决游戏循环中的特定的点(point)。
所以,为了解决这个问题,咱们须要设置标识位(boolean变量)来标识,这个标识变量的批改产生键盘按下事件。比方jumpIsPressed布尔值能够在keyPressed()办法中设置和批改,而后在前面的游戏循环(game loop)中查看该变量是否被设置了,而后再依据这个标识呼叫相应的代码来解决游戏的行为。对于有些行为,比方“跳”、“挪动”等动作,每个玩家有不同的喜好,所以咱们须要让玩家来设置键盘的性能,这样咱们须要隐射这些通用的游戏行为,于是类InputManager是控件玩家输出行为:
- 解决所有的键盘和鼠标事件,包含相干的鼠标行为
- 保留这些事件,这样咱们能够当咱们须要时准确查问这些事件,而不批改AWT事件散发线程中的游戏状态
- 查看初始化过的键盘按下事件,而后查看该键值是否曾经被其它的键位占用了
- 隐射键盘到游戏的通用行为,比方把空格键隐射成为“跳”的行为
- 能够让用户任何配置键盘的行为
以上性能咱们应用GameAction类来封装,其中isPressed()是判断键盘的行为,而getAmount()是判断鼠标挪动了多少。最初,这些办法由InputManager来呼叫,比方在这个类的press()和release()办法中呼叫GameAction中的接口办法。
演示代码-GameAction
package com.az.arklis.engine;/** 性能:该类是用户初始行为的形象(定义),比方跳和挪动。该类由InputManager类用来隐射 键盘和鼠标的行为。 备注:所谓游戏输出行为包含在游戏循环中的特定点的输出,咱们能够设置一个boolean变量用来示意一个 键是否按下了。比方设置一个jumpIsPressed布尔变量,把这个变量放到keyPressed()办法中,咱们来判断 当按下space键之后,咱们查看jumpIsPressed是否为true值,如果是true值,那么让玩家执行跳的动作。 除了游戏中跳之外,玩家还能够设置初始的动作键,比方挪动,咱们能够设置光标键来示意,以及A键 和D键也示意左右挪动。假如咱们心愿玩家本人定义游戏中的行为键,那么,在程序中咱们必须实现这些 游戏行为的隐射性能。咱们实现InputManager类来形象这些行为。总之,咱们心愿该类InputManager能够 实现以下性能: 1、解决所有键和鼠标事件,包含鼠标的绝对挪动 2、保留所有上述行为的事件队列,而不是批改AWT事件散发线程的状态 3、查看键的初始按下行为,以及查看这些键是否被其它对象占用 4、隐射所有的游戏行为,比方隐射space键为游戏中的跳的动作 5、实现能够让玩家本人批改游戏键 而GameAction类是用来专门隐射游戏中的行为的,也就是形象游戏行为的设置性能。比方,形象玩家 的初始行为(跳或者挪动)。该类被InputManager类应用来隐射键盘和鼠标的行为。 */public class GameAction{ //一般行为--针对isPressed()办法返回的true值来示意,即示意一个键曾经被占用。 public static final int NORMAL = 0; /* 初始化按键行为,isPressed()办法返回true值的状况是:只有该键第一次被被按下之后,并且不是该键 在被开释之后再按下的状态。 */ public static final int DETECT_INITIAL_PRESS_ONLY = 1; private static final int STATE_RELEASED = 0;//标识是否被开释 private static final int STATE_PRESSED = 1; //标识是否解决按下的状态 private static final int STATE_WAITING_FOR_RELEASE = 2; //标识是否期待开释的状态 private String name;//保留游戏行为的名称 private int behavior; //示意游戏的行为 private int amount; //计数器 private int state; //以后状态标识 /** 在构造方法中初始化成员变量--游戏行为名称,以及一般状态。 */ public GameAction(String name){ this(name,NORMAL); } public int getBehavior(){ return behavior; } /** 该构造方法指定了游戏的行为 */ public GameAction(String name, int behavior){ this.name = name; this.behavior = behavior; reset();//回到开释状态,而后计数器清零 } public String getName(){ return name; } public void setName(String name){ this.name = name; } public void reset(){ state = STATE_RELEASED; amount = 0; } /** 性能:开关该GameAction行为--等同于press之后release行为 */ public synchronized void tap(){ press(); release(); } /** 性能:标识键盘被点击事件 */ public synchronized void press(){ press(1); } /** 性能:示意该键被指定点击的次数,鼠标挪动到指定地位 */ 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; }}
最初,咱们创立一个InputManager类用来治理所有输出,并发期待不见光标和相干的鼠标行和等。另外该类有隐射键盘和鼠标事件到GameAction类中,当咱们按下一个键盘时,该类的代码查看GameAction是否有键盘被隐射了,如果有那么呼叫GameAction类的中press()办法。
那么在这个类中怎么隐射?咱们应用一个GameAction数组来解决,每个下标对应一个虚构键代码,最大虚构键的只能小于或者等于600数值,也就是说GameAction数组的长度是600.
图片起源:http://www.diuxie.com/ 游戏下载