java写游戏脚本(编程一个最简单游戏代码)

java写游戏脚本(编程一个最简单游戏代码)

前言

代码演示环境:

软件环境:Windows 10

开发工具:Visual Studio Code

JDK版本:OpenJDK 15

虽然这些代码是10几年前的写的,但是仍然能够在现代操作系统和Java最新开源版本中正常运行。

界面和交互

AWT事件模型

如果一个人玩橡棋就像一个人玩游戏时没有交互一样,会非常无聊,所以玩家最大的乐趣就是与电脑或者人的交互。那么首先玩家得与电脑交互—键盘与鼠标的交互,在JDK 1.4版本还提供了手柄的驱动让玩家与电脑交互。

那么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);

以上方法都有一个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键看成一个普通的键。

鼠标输入

鼠标有三种事件:

鼠标移动事件

鼠标滚动事件

在以上代码中,我们Robot类移动鼠标,但是鼠标移动事件可能不会立即出现,所以代码会检查鼠标移动事件是否定位在屏幕中央。

如果是这样,那么把它认为是一种重置中央的事件,而实际的事件被忽略掉;否则该事件被当作普通的鼠标移动事件处理。对于鼠标的样子,我们可以使用Java API创建自己的样式,创建时需要使用Toolkit类的createCustomerCursor()方法来实现

在游戏中我们可以呼叫Toolkit类截取一个不可见的光标,然后呼叫setCursor()方法

Window window = screen.getFullScreenWindow();window.setCursor(invisibleCursor);

之后,我们可以呼叫Cursor类的getPredefinedCursor()方法来恢复原来的光标样式:

CursornormalCursor=Cursor.getPredefineCursor(Cursor.DEFAULT_CURSOR);创建输入管理器

前面讲解了常用的输入事件,以及它们的处理。下面我们把它们放在一起,就可以创建一个输入管理器。但是,在封装之前,我们先要说明前面的代码的缺陷。

首先,我们应该注意到synchronized修饰的方法。记住:所有的事件都是从AWT事件分发线程中产生的,该线程不是主线程!显然,我们不修改游戏状态(修改妖怪的位置),所以这些同步方法肯定不可能让这些事件发生。而在我们的实际游戏中,我们必须处理游戏循环中的特定的点(point)。

所以,为了解决这个问题,我们需要设置标识位(boolean变量)来标识,这个标识变量的修改发生键盘按下事件。

比如jumpIsPressed布尔值可以在keyPressed()方法中设置和修改,然后在后面的游戏循环(game loop)中检查该变量是否被设置了,然后再根据这个标识呼叫相应的代码来处理游戏的行为。

对于有些行为,比如“跳”、“移动”等动作,每个玩家有不同的爱好,所以我们需要让玩家来设置键盘的功能,这样我们需要影射这些通用的游戏行为,于是类InputManager是控件玩家输入行为:

处理所有的键盘和鼠标事件,包括相关的鼠标行为

保存这些事件,这样我们可以当我们需要时精确查询这些事件,而不修改AWT事件分发线程中的游戏状态

检查初始化过的键盘按下事件,然后检查该键值是否已经被其它的键位占用了

影射键盘到游戏的通用行为,比如把空格键影射成为“跳”的行为

可以让用户任何配置键盘的行为

以上功能我们使用GameAction类来封装,其中isPressed()是判断键盘的行为,而getAmount()是判断鼠标移动了多少。最后,这些方法由InputManager来呼叫,比如在这个类的press()和release()方法中呼叫GameAction中的接口方法。

演示代码-GameAction

最后,我们创建一个InputManager类用来管理所有输入,并发等待不见光标和相关的鼠标行和等。另外该类有影射键盘和鼠标事件到GameAction类中,当我们按下一个键盘时,该类的代码检查GameAction是否有键盘被影射了,如果有那么呼叫GameAction类的中press()方法。

那么在这个类中怎样影射?我们使用一个GameAction数组来解决,每个下标对应一个虚拟键代码,最大虚拟键的只能小于或者等于600数值,也就是说GameAction数组的长度是600.

使用输入管理器

下面,我们创建一个hero可以左右移动,以及跳跃的行为;另外我们可以该应用程序添加暂停功能,不过这不是一个真正的游戏。

其中,在人物跳跃时需要表现重力—人物会回到地面的感觉。这里需要说明的是,地球引力(gravity)是9.8米/秒,但是游戏中,这个不重要,我们使用像素来表示,所以定义引用为0.002像素/秒,这样做防止人物跳出屏幕的顶端。

Player类是基于状态的类(state-based),它有两种状态:NORMAL和JUMPING状态。因为Player类保存了它们状态,所以它可以检查玩家是否普通状态,还是跳跃状态,以及玩家是否下落的状态。程序中必须告知”floor”在哪里。现在我们已经有了实现简单游戏的条件(使用InputManager类),在类InputManagerTest类中演示怎样移到玩家,然后让玩家跳跃。为实现以上功能,在代码中需要创建几个GameAction来实现该功能,每个GameAction至少影射了一个键盘或者鼠标事件,最后允许我们暂停游戏。

运行效果

设计直观的用户界面

那么用户界面设计原则如下:

保证界面简单和整洁。不是所有选项都一次呈现出来,相反,应该把最常用的、最有用的选项放在主屏幕中,以方便玩家使用。

使用提示贴士(tool tips)。一般提示贴士是鼠标经过特定对象时弹出来的形式,这样它们可以告诉玩家哪些按钮在屏幕中做功能,以及它们的当前的状态。提示贴士可以非常快捷的回答“What’s this?”的问题,因为Swing中有一个内置tooltip功能,所以非常容易实现该功能。

每个游戏行为都响应玩家一个信息,比如使用声音或者等待光标来表示等。

测试你的用户界面。因为有些按钮对于我们来说可能是最明显的,但是对于别人可能就不是了。所以,我们需要测试有多少人习惯我们这样的按钮设计方式。当然,不可能让所有人都满意,但是需要大多数人习惯都可行。记住,在实现生活中,当玩家使用我们的游戏时,我们不会告诉他/她下一步应该做什么!

当游戏界面测试之后,调查这些玩家,他们认为这些按钮哪些最容易使用和最有用。比如哪些图标最容易让他们辨认等。但是,我们只是听,而不用考虑代码实现上的难度。

重新修改游戏界面,如果不能运行,那么需要花几天时间来编码,以及创建新的图标,以做出完美的界面。

使用Swing组件来开始用户界面:Swing是一个非常大的话题,简而言之,Swing是一组类,它们被用来创建用户界面元素,比如窗体、按钮、弹出菜单、下拉列表文本输入框、选项按钮和标签等。比如,前面我们使用JFrame类来实现全屏幕显示功能。实际上,我们使用JFrame对象来操作Windw和Frame而已。

一些Swing组件使用自己的组件来呈现画面,所以,我们可以在自己的呈现循环中使用Swing组件。这是一个令人振奋的消息,因为我们可以把所有的Swing功能都整合到全屏幕游戏中去。

这样,我们可以不必重新造轮子来创建用户界面框架了!同时,Swing可以根据自定义样式来客制化UI界面,所以,我们可以使用Swing实现个性化的界面。Swing有大量的API可以使用,而这里讲解的是游戏,不是Swing组件,所以不会讨论Swing的组件功能。在游戏中使用Swing组件的技巧如下:

绘制所有的Swing组件时,只需要在动画循环过程中,呼叫主面板的paintComponents()方法即可:

//绘制我们的图形draw(g);//绘制Swing组件JFrame frame = screen.getFullScreenWindow();frame.getLayeredPane().paintComponents(g);

有可能出现的问题是:内容面板实际上绘制它的背景,所以它会隐藏面板下面的所有内容;如果希望自已Swing组件以独立的形式呈现,那么需要把内容面板设置为透明:

If(contentPane instanceof JComponent){ ((JComponent)contentPane).setOpaque(false);}

第二问题是:处理Swing怎样呈现自己的组件问题,因为普通的Swing应用,我们不必呼叫paintComponents()方法—Swing会在AWT事件分发线程中自动呈现所有的组件。

而现在我们必须手动关闭这个功能,从而达到控制组件的呈现时机,比如按一个按钮时,我们让该按钮呈现被按下的样子,该组件需要呼叫repaint()方法来重绘组件的样子。显然,我们不需要AWT分发线程中出现组件呈现,因为这与我们的自定义的画面呈现会发生冲突,会产生抖动或者其它的冲突现象。

在我们的代码中,需要忽略潜在的重绘请求,如果一个按钮被按下之后的话,然后需要在动画循环的draw方法中出现。为解决这个问题,需要我们捕获重绘请求,然后忽略它。

根据此思路,因为所有的重绘请求都发送到RepaintManager类,所以我们通过该类来管理重绘请求,然后把这些请求分发到实际需要处理重绘的组件上去。于是,我们需要简单使用NullRepaintManger来重写RepaintManger类即可实现。

演示代码-NullRepaintManer

该类继承RepaintManager类,然后重写关键方法—什么都不做,于是重绘事件不会被发送到AWT分发线程中去,所以我们不会看抖动的组件画面。

我们从1895行的API代码全部展示出来,目的是想让大家阅读一下OpenJDK 15版本的深度封装的代码,从而让自己有一个很好的定位,以及对设计模式和底层原理有所了解。

**注意:**因为Swing组件是非线程安全的,所以当一个Swing组件可见时,我们不可能在AWT事件分发线程中修改它的状态。如果我们需要在Swing组件显示之后修改它,那么需要在事件分发线程这样做:

EventQueue.invokeAndWait(new Runnable(){ public void run(){ doSomething(); }});

以上代码是在AWT事件分发器线程器呼叫代码,然后等待这些代码执行完成,另外,如果我们不想等这些代码执行完成,那么呼叫invokeLater()方法来实现,创建一个简单的菜单(Creating a Simple Menu)

如果有,那么监听器会通知AWT事件分发器线程,说明该按钮被按下了。在代码中,我们通过ActionEvent的getSource()方法知道哪些组件产生了事件行为。

public void actionPerforme(ActionEvent e){ Object src = e.getSource(); if(src == okButton){ //do something … }}

最后,在用户界面中,我们可以该按钮做以下事件:

添加提示贴士—只需要呼叫setToolTip(“Hello World”)方法 ,然后剩下由Swing来实现

使用图标,而不是在按钮中使用文本。必须两个不同的图标,一个表示初始状态,一个表示按下状态

隐藏默认的样式。如果需要让图标原样出现,那么需要隐藏按钮的边框,隐藏时呼叫setContentAreaFiled(false)方法,以保证挥刀的背景不会被绘制

修改光标。让光标在滑过按钮时变成手样—呼叫setCursor()方法即可

关闭键盘焦点—呼叫setFocusable(false)

在上面示例中,每个按钮有一个PNG图片,其它图片是在程序启动时生成。默认菜单图片有一点faded样式的图片呈现,实现这样种效果是使用AlphaComposite类的0.5透明效果。

运行效果

让玩家设置键盘

如果需要让玩家影射键盘或者鼠标,所有玩家可以游戏行为和按钮,以及鼠标按钮,这些按钮是被用来表示游戏行为的,而键盘配置可以有两部分:

我们需要创建配置对象框

对话框列出所有可能的游戏行为,以及相应的指令,该对话框本身是一个JPanel类,而该面板中的可以有一系列组件、面板和布局管理器。

创建一个特殊的输入组件还是比较困难的,因为我们需要该组件能显示哪些键是影射到当前游戏行为的,哪些键可以让玩家作为按键或者鼠标键来修改设置。当这一切完成之后,还需要该组件把键盘焦点送回主游戏窗体。

输入事件中去,但是这时需要我们使用另外的方式来获取键,当然还其它的方式可以获取输入事件。因为每个Swing组件都是Component类的实例,所以Component类有方法processKeyEvent()和processMouseEvent方法。

这些方法就像KeyListener和MouseListener方法一样,我们只需要重写这些方法,然后让输入事件呼叫enableEvents()方法。

总结

在端游中,键盘、鼠标的用户自定义是标准的配置功能,所有外调输入管理也是标准配置的功能,因此,如果我们要做端游开发,那么对游戏输入控制和管理是必须的。

从Java的角度来说,因为有API的深度封装,所以我们第三方应用开发人员是非常容易实现计算机外设的输入控制的。

发表评论

登录后才能评论