Java游戏编程不完全详解-3

前言
代码演示环境:

  • 软件环境: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);
}

键盘输入运行效果

Java游戏编程不完全详解-3_第1张图片

鼠标输入

鼠标有三种事件:

  • 鼠标按钮点击事件
  • 鼠标移动事件
  • 鼠标滚动事件

鼠标演示代码-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模型。

Java游戏编程不完全详解-3_第2张图片

在以上代码中,我们Robot类移动鼠标,但是鼠标移动事件可能不会立即出现,所以代码会检查鼠标移动事件是否定位在屏幕中央。如果是这样,那么把它认为是一种重置中央的事件,而实际的事件被忽略掉;否则该事件被当作普通的鼠标移动事件处理。
对于鼠标的样子,我们可以使用Java API创建自己的样式,创建时需要使用Toolkit类的createCustomerCursor()方法来实现

image.png

在游戏中我们可以呼叫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/ 游戏下载

你可能感兴趣的:(java游戏开发)