关于java:Java游戏编程不完全详解3

60次阅读

共计 12092 个字符,预计需要花费 31 分钟才能阅读完成。

前言
代码演示环境:

  • 软件环境: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/ 游戏下载

正文完
 0