传送门
简介
我原本想通过Java做一个小游戏合集,但目前只有俄罗斯方块是完整做好的。其实就小游戏而言,除最顶层的逻辑略有不同以外,很多底层逻辑是通用的,所以我想通过这个俄罗斯方块小游戏跟大家分享我对游戏引擎
和面向对象
的理解。
首先先展示下成品:
因为演示的时间比较长,文件比较大,我故意抽了些帧
,游戏实际上还是很流畅
的。
对于游戏引擎而言再简陋,让帧率
可调
和稳定
的基本功能还是要有的。
源码分享在文章最后。
小工具
通过演示的画面大家可以看到我是直接双击运行jar文件而不是通过命令行java -jar name.jar
来运行。这是因为我用C语言写了一个小工具。其底层其实还是java -jar name.jar
。但是它使得runnable jar都可以被直接双击运行,方便许多。在这里我先分享下那个小工具:
//visual studio环境下需要这个宏定义才可以使用类似于strcat这样的所谓不安全函数
//不是visual studio环境的视情况而定
//因为visual studio官方推荐的安全函数strcat_s使用起来很繁琐
//所以笔者还是习惯使用strcat
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include<stdlib.h>
#include<string.h>
int main(int argn, char** args) {
//程序被双击执行时的第一个参数默认是程序的绝对路径
//所以参数数量大于一表示是双击了其他文件
//或者传参了
//而不是直接双击工具程序
//或者通过命令行执行时不传参
if (argn > 1) {
//这里即将构造出java -jar ...命令然后执行
//这的1000表示java -jar那句命令总长度不超过999
//最后一位是\0占位
//如果出现文件名称很长而不兼容的极端情况可以把1000改大
//大多数情况下1000够用了
char* cmd = new char[1000];
memset(cmd, 0, sizeof(char) * 1000);
strcat(cmd, "java -jar ");
strcat(cmd, args[1]);
//执行命令
system(cmd);
}
return 0;
}
把这个小工具输出为.exe。以visual studio为例,
生成→生成解决方案就可以得到.exe文件
成功提示的上一行说明了目标文件被输出到了哪里。
接下来直接对着jar文件双击
在弹出这个提示时选择我们刚才制作好的小工具(我这是JarRunner.exe)并点击左下角的始终,以后runnable jar就都可以直接双击打开了。
实现
简易引擎
游戏依赖于一定的框架环境
会使得后续的代码更加简洁
,逻辑
更加清晰
,所以这里先实现一个简单的游戏引擎。
引擎模块
的文件结构如上图。
因为Java没有直接把KeyCode和按键关联的API,所以需要用一个KeyCode类
来专门记录,以便后续使用。当然这不可能是手敲出来的,能够直接CV最好,但笔者在开发时没有找到能够直接CV的全面代码,只找到了全面的形式化描述
。我后面会讲述怎么通过工具类
把一些形式化描述转换为这个文件。此外这个模块还需要一个引擎类
和配置类
作为所有引擎实例
和配置实例
的基类。往往一个引擎类
需要对应一个配置类
作为构造函数的参数去配置它。所以在当前项目中:一个引擎实例
往往由引擎类
和配置类
两个文件组成,他们的基类是这里的GameEngine
和EngineConfugure
。
KeyCode的内容
以下是KeyCode的具体内容,有需要的可以直接从我这里复制。
package com.test.gameengine;
public class KeyCode {
public static final int _1 = 49;
public static final int _2 = 50;
public static final int _3 = 51;
public static final int _4 = 52;
public static final int _5 = 53;
public static final int _6 = 54;
public static final int _7 = 55;
public static final int _8 = 56;
public static final int _9 = 57;
public static final int _0 = 48;
public static final int A = 65;
public static final int B = 66;
public static final int C = 67;
public static final int D = 68;
public static final int E = 69;
public static final int F = 70;
public static final int G = 71;
public static final int H = 72;
public static final int I = 73;
public static final int J = 74;
public static final int K = 75;
public static final int L = 76;
public static final int M = 77;
public static final int N = 78;
public static final int O = 79;
public static final int P = 80;
public static final int Q = 81;
public static final int R = 82;
public static final int S = 83;
public static final int T = 84;
public static final int U = 85;
public static final int V = 86;
public static final int W = 87;
public static final int X = 88;
public static final int Y = 89;
public static final int Z = 90;
public static final int BACKSPACE = 8;
public static final int TAB = 9;
public static final int ENTER = 10;
public static final int SHIFT = 16;
public static final int CTRL = 17;
public static final int ALT = 18;
public static final int CAPS_LOCK = 20;
public static final int ESC = 27;
public static final int SPACE = 32;
public static final int WINDOWS = 524;
public static final int ARROW_LEFT = 37;
public static final int ARROW_UP = 38;
public static final int ARROW_RIGHT = 39;
public static final int ARROW_DOWN = 40;
public static final int COMMA = 44;
public static final int MINIS = 45;
public static final int DOT = 46;
public static final int DIAGONAL = 47;
public static final int SEMICOLON = 59;
public static final int EQUALS_SIGN = 61;
public static final int SQUARE_BRACKETS_LEFT = 91;
public static final int ANTI_DIAGONAL = 92;
public static final int SQUARE_BRACKETS_RIGHT = 93;
public static final int BACK_QUOTE = 192;
public static final int QUOTE = 222;
public static final int PAUSE = 19;
public static final int PAGE_UP = 33;
public static final int PAGE_DOWN = 34;
public static final int END = 35;
public static final int HOME = 36;
public static final int DELETE = 127;
public static final int SCROLL_LOCK = 145;
public static final int INSERT = 155;
public static final int NUMPAD_0 = 96;
public static final int NUMPAD_1 = 97;
public static final int NUMPAD_2 = 98;
public static final int NUMPAD_3 = 99;
public static final int NUMPAD_4 = 100;
public static final int NUMPAD_5 = 101;
public static final int NUMPAD_6 = 102;
public static final int NUMPAD_7 = 103;
public static final int NUMPAD_8 = 104;
public static final int NUMPAD_9 = 105;
public static final int NUMPAD_ASTERISK = 106;
public static final int NUMPAD_PLUS = 107;
public static final int NUMPAD_MINIS = 109;
public static final int NUMPAD_DOT = 110;
public static final int NUMPAD_DIAGONAL = 111;
public static final int NUM_LOCK = 144;
public static final int F1 = 112;
public static final int F2 = 113;
public static final int F3 = 114;
public static final int F4 = 115;
public static final int F5 = 116;
public static final int F6 = 117;
public static final int F7 = 118;
public static final int F8 = 119;
public static final int F9 = 120;
public static final int F10 = 121;
public static final int F11 = 122;
public static final int F12 = 123;
}
EngineConfugure
EngineConfugure
描述了游戏实例的最基本属性
:最大帧率
、窗口尺寸
和窗口是否允许调整大小
。将来这个类将作为GameEngine构造函数里必要的参数之一。
package com.test.gameengine;
public class EngineConfugure {
private int maxFrameRate;
private int windowHeight;
private int windowWidth;
private boolean resizable;
public EngineConfugure(int maxFrameRate,int windowWidth,int windowHeight,boolean resizable) {
super();
this.maxFrameRate = maxFrameRate;
this.windowHeight=windowHeight;
this.windowWidth=windowWidth;
this.resizable=resizable;
}
public int getMaxFrameRate() {
return maxFrameRate;
}
public int getWindowHeight() {
return windowHeight;
}
public int getWindowWidth() {
return windowWidth;
}
public boolean isResizable() {
return resizable;
}
public void setMaxFrameRate(int maxFrameRate) {
this.maxFrameRate = maxFrameRate;
}
public void setWindowHeight(int windowHeight) {
this.windowHeight = windowHeight;
}
public void setWindowWidth(int windowWidth) {
this.windowWidth = windowWidth;
}
}
GameEngine
GameEngine的作用是主要模仿Unity
实现一个对awake
、start
和update
函数的简单调度
,这里只是调度模式模仿Unity,在具体实现上是有着天差地别的,有兴趣的读者可以自行研究。并且封装了一个简易的事件系统。
具体代码如下(讲解看注释):
package com.test.gameengine;
import java.awt.Toolkit;
import java.awt.event.FocusEvent;
import java.awt.event.FocusListener;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.WindowEvent;
import java.awt.event.WindowListener;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.Scanner;
import javax.swing.JFrame;
public class GameEngine implements WindowListener,FocusListener,MouseListener,KeyListener{
private ArrayList<Integer>presses=new ArrayList<>();//记录本帧按下了哪些按键
private ArrayList<Integer>releases=new ArrayList<>();//记录本帧释放了哪些按键
private boolean lastFocued=false;//上一帧是否聚焦到窗口
protected boolean hasFocuced = false;//本帧是否聚焦到窗口
protected boolean hasClicked=false;//本帧是否点击了窗口
private boolean hasShutDown=false;//引擎的关机标志
private Runnable afterShutDown;//引擎最后时刻的回调
private int maxFrameRate;//最大帧率
private float deltaTime;//上一帧耗时
private float minFrameTime;//当前帧率下一帧最小耗时
protected JFrame mainFrame;//游戏窗口
//由一个GameConfugure对象和JFrame对象构造
//分别指定基本属性和在哪个JFrame窗口里运行
public GameEngine(EngineConfugure conf,JFrame mainFrame) {
this.setMaxFrameRate(conf.getMaxFrameRate());
//如果游戏实例有窗口这对其初始化
if(mainFrame!=null) {
//设置窗口尺寸为指定的尺寸并居中
mainFrame.setBounds((getScreenWidth()-conf.getWindowWidth())/2,(getScreenHeight()-conf.getWindowHeight())/2,conf.getWindowWidth(),conf.getWindowHeight());
//默认使用自由布局
mainFrame.setLayout(null);
mainFrame.setResizable(conf.isResizable());
//使得关闭窗口的事件被拦截,方便控制关闭和执行回调
mainFrame.setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE);
//添加各种事件回调以构建简单的事件系统
mainFrame.addWindowListener(this);
mainFrame.addFocusListener(this);
mainFrame.addMouseListener(this);
mainFrame.addKeyListener(this);
mainFrame.requestFocus();
}
this.mainFrame=mainFrame;
}
//设置帧率的函数,出于计时精度考虑,为保证帧率稳定,加上最高60的限制
//同时计算出每帧的最小耗时
//当一帧的所需执行时间小于最小耗时时会等待,以保证帧率稳定
public final void setMaxFrameRate(int maxFrameRate) {
if(maxFrameRate>60||maxFrameRate<=0) {
maxFrameRate=60;
}
this.maxFrameRate=maxFrameRate;
this.minFrameTime=1.0f/maxFrameRate;
}
//获得精确到毫秒级的时间
public static final long getNanos() {
return System.nanoTime();
}
//执行一帧的游戏循环
private void runCycle(Runnable action) {
long before=getNanos();
action.run();
long after=getNanos();
//t为本帧所需耗时
float t=(float)((after*1.0-before)*1e-9);
float left=minFrameTime-t;
//若小于最小耗时时则等待
if(left>0) {
try {
Thread.sleep((long)(left*1e3));
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
//记录本帧耗时
deltaTime=(float)((getNanos()-before)*1e-9);
}
public void awake() {
}
public void start() {
}
public void update() {
}
//出于满足事件系统和其他行为的需求
//要在每帧开始和结束时分别执行一些代码
//所以将update封装为 _update
//实际被游戏循环调度的是这个方法
private void _update() {
if(hasFocuced!=lastFocued) {
onFocusChange(hasFocuced);
}
lastFocued=hasFocuced;
update();
presses.clear();
releases.clear();
}
public int getMaxFrameRate() {
return maxFrameRate;
}
public float getDeltaTime() {
return deltaTime;
}
public float getMinFrameTime() {
return minFrameTime;
}
//开始引擎基本声明周期即awake,start,update
protected void startUp() {
runCycle(()->awake());
runCycle(()->start());
while(true) {
if(hasShutDown) {
if(afterShutDown!=null) {
afterShutDown.run();
}
break;
}
runCycle(()->_update());
}
}
//显示游戏窗口
protected void showFrame() {
this.mainFrame.setVisible(true);
}
//获取屏幕宽高
public static final int getScreenWidth() {
return Toolkit.getDefaultToolkit().getScreenSize().width;
}
public static final int getScreenHeight() {
return Toolkit.getDefaultToolkit().getScreenSize().height;
}
//发出关机命令时的回调
protected void onShutDown() {
}
//发出关机命令
//将关机标识设为true
//由游戏循环检测可做到安全结束
public void shutDown(Runnable afterShutdown) {
onShutDown();
hasShutDown=true;
this.afterShutDown=afterShutdown;
}
//整个项目公用的标准输入读取器
public static final Scanner stdin=new Scanner(System.in);
@Override
public void windowOpened(WindowEvent e) {
// TODO Auto-generated method stub
}
//拦截窗口关闭行为
@Override
public void windowClosing(WindowEvent e) {
// TODO Auto-generated method stub
if(!couldCloseWindow()) {
return;
}
this.mainFrame.dispose();
shutDown(null);
System.out.println("游戏已退出!");
System.out.println();
}
@Override
public void windowClosed(WindowEvent e) {
// TODO Auto-generated method stub
}
@Override
public void windowIconified(WindowEvent e) {
// TODO Auto-generated method stub
}
@Override
public void windowDeiconified(WindowEvent e) {
// TODO Auto-generated method stub
}
@Override
public void windowActivated(WindowEvent e) {
// TODO Auto-generated method stub
}
@Override
public void windowDeactivated(WindowEvent e) {
// TODO Auto-generated method stub
}
protected boolean couldCloseWindow() {
return true;
}
private static Date date=new Date();
private static SimpleDateFormat sdf=new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
//获取时间戳
public static final String getDateTime() {
return sdf.format(date);
}
//当聚焦、点击事件被捕获时,记录
@Override
public void focusGained(FocusEvent e) {
// TODO Auto-generated method stub
hasFocuced=true;
}
@Override
public void focusLost(FocusEvent e) {
// TODO Auto-generated method stub
hasFocuced=false;
}
@Override
public void mouseClicked(MouseEvent e) {
// TODO Auto-generated method stub
hasClicked=true;
}
@Override
public void mousePressed(MouseEvent e) {
// TODO Auto-generated method stub
}
@Override
public void mouseReleased(MouseEvent e) {
// TODO Auto-generated method stub
}
@Override
public void mouseEntered(MouseEvent e) {
// TODO Auto-generated method stub
}
@Override
public void mouseExited(MouseEvent e) {
// TODO Auto-generated method stub
}
protected void onFocusChange(boolean now) {
}
@Override
public void keyTyped(KeyEvent e) {
// TODO Auto-generated method stub
}
//当按键按下和抬起时,记录
//这些记录将在_update里被维护管理
@Override
public void keyPressed(KeyEvent e) {
// TODO Auto-generated method stub
this.presses.add(e.getKeyCode());
}
@Override
public void keyReleased(KeyEvent e) {
// TODO Auto-generated method stub
this.releases.add(e.getKeyCode());
}
//以下有一些事件系统的接口函数
//本帧是否按下
protected boolean isPress(int keyCode) {
return presses.contains(keyCode);
}
//本帧最后按下的是否是
protected boolean isLastPress(int keyCode) {
return presses.get(presses.size()-1)==keyCode;
}
//只在between里考虑,本帧最后按下的受否是
protected boolean isLastPress(int keyCode,int...between) {
ArrayList<Integer>be=new ArrayList<>();
for(int i:between) {
be.add(i);
}
for(int i=presses.size()-1;i>=0;i--) {
int now=presses.get(i);
if(now==keyCode) {
return true;
}else if(be.contains(now)){
return false;
}
}
return false;
}
//抬起同按下
protected boolean isRelease(int keyCode) {
return releases.contains(keyCode);
}
protected boolean isLastRelease(int keyCode) {
return releases.get(releases.size()-1)==keyCode;
}
protected boolean isLastRelease(int keyCode,int...between) {
ArrayList<Integer>be=new ArrayList<>();
for(int i:between) {
be.add(i);
}
for(int i=releases.size()-1;i>=0;i--) {
int now=releases.get(i);
if(now==keyCode) {
return true;
}else if(be.contains(now)) {
return false;
}
}
return false;
}
protected boolean getHasShutdown() {
return this.hasShutDown;
}
}
至此,能够调度游戏循环、具有简单事件系统的简易游戏引擎就完成了。
构建KeyCode.java的小工具
身为程序员我们要养成自己用制作小工具
的习惯。现在来通过一个工具类
实现把一段对于KeyCode的形式化
(不是用代码的)描述转换为
KeyCode.java文件里的代码
。
首先大佬博客里有对KeyCode的形式化描述。如:
现在通过一个工具类KeyCodeUtil把它给转化为代码描述。
把有关描述文件
KeyCodeStatement放在容易找到
的地方,我这里和KeyCodeUtil放在同一个文件夹下。
把大佬博客里的东西CV进Statement后还得略作修改,最后修改为如下所示:
_1 --> 49
_2 --> 50
_3 --> 51
_4 --> 52
_5 --> 53
_6 --> 54
_7 --> 55
_8 --> 56
_9 --> 57
_0 --> 48
A --> 65
B --> 66
C --> 67
D --> 68
E --> 69
F --> 70
G --> 71
H --> 72
I --> 73
J --> 74
K --> 75
L --> 76
M --> 77
N --> 78
O --> 79
P --> 80
Q --> 81
R --> 82
S --> 83
T --> 84
U --> 85
V --> 86
W --> 87
X --> 88
Y --> 89
Z --> 90
Backspace --> 8
Tab --> 9
Enter --> 10
Shift --> 16
Ctrl --> 17
Alt --> 18
Caps_Lock --> 20
Esc --> 27
Space --> 32
Windows --> 524
ARROW_LEFT --> 37
ARROW_UP --> 38
ARROW_RIGHT --> 39
ARROW_DOWN --> 40
comma --> 44
Minis --> 45
dot --> 46
diagonal --> 47
semicolon --> 59
equals_Sign --> 61
square_brackets_left --> 91
anti_diagonal --> 92
square_brackets_right --> 93
back_quote --> 192
quote --> 222
Pause --> 19
Page_Up --> 33
Page_Down --> 34
End --> 35
Home --> 36
Delete --> 127
Scroll_Lock --> 145
Insert --> 155
NumPad_0 --> 96
NumPad_1 --> 97
NumPad_2 --> 98
NumPad_3 --> 99
NumPad_4 --> 100
NumPad_5 --> 101
NumPad_6 --> 102
NumPad_7 --> 103
NumPad_8 --> 104
NumPad_9 --> 105
NumPad_asterisk --> 106
NumPad_plus --> 107
NumPad_minis --> 109
NumPad_dot --> 110
NumPad_diagonal --> 111
Num_Lock --> 144
F1 --> 112
F2 --> 113
F3 --> 114
F4 --> 115
F5 --> 116
F6 --> 117
F7 --> 118
F8 --> 119
F9 --> 120
F10 --> 121
F11 --> 122
F12 --> 123
描述中的大小写无所谓
,后期由程序
来保证
最终结果的规范性
。
package com.test.editorutils;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.ArrayList;
public class KeyCodeUtil {
public static void main(String[] args) throws IOException {
//描述文件名称
final String fromName="KeyCodeStatement.txt";
//分割标志
final String pointSign=" --> ";
//获取当前类的路径
//这个只能在IDE里用
final String parentPath=KeyCodeUtil.class.getResource("").getPath();
File from=new File(parentPath,fromName);
ArrayList<String>fromLines=readToLines(from);
ArrayList<String>keyCodeNames=new ArrayList<>();
ArrayList<Integer>keyCodeValues=new ArrayList<>();
//开始解析描述文件
for(String i:fromLines) {
if(i.length()==0) {
continue;
}
int pIndex=i.indexOf(pointSign);
if(pIndex==-1) {
System.out.println("bad statement '"+i+"'");
return;
}
String left=i.substring(0,pIndex);
String right=i.substring(pIndex+pointSign.length());
if(left.length()==0) {
System.out.println("bad key code name statement");
return;
}
int code;
try {
code = Integer.parseInt(right);
} catch (Exception e) {
System.out.println("bad num statement '"+right+"'");
return;
}
keyCodeNames.add(left);
keyCodeValues.add(code);
}
if(keyCodeNames.size()==0) {
System.out.println("blank statements");
return;
}
String res="public static final int "+keyCodeNames.get(0).toUpperCase()+" = "+keyCodeValues.get(0)+";";
for(int i=1;i<keyCodeNames.size();i++) {
res+="\npublic static final int "+keyCodeNames.get(i).toUpperCase()+" = "+keyCodeValues.get(i)+";";
}
System.out.println(res);
}
//按行缓存文件
private static ArrayList<String> readToLines(File target) throws IOException{
ArrayList<String>lines=new ArrayList<>();
InputStream is=new FileInputStream(target);
InputStreamReader isr=new InputStreamReader(is);
BufferedReader br=new BufferedReader(isr);
String line;
while((line=br.readLine())!=null) {
lines.add(line);
}
br.close();
return lines;
}
}
工具类的运行结果:
把结果拷贝进KeyCode类。
配置解析器实现
本项目采用通过解析配置文件
conf.xml的方式实现项目配置。
解析器文件结构:
接口
interfaces下放的是这个模块会用到的接口。
StepGoable接口是用于描述某种以固定帧率为时间间隔
推进游戏的引擎实例
的的配置类
要实现的接口,我们接下来要做的俄罗斯方块就满足那个模型。因为方块可能是每10帧或者20帧下落一层。之所以不把帧率调低而每帧下落是出于事件捕获考虑,因为封装事件系统是每帧响应一次的。如是做可以使得方块在一次下落的时间内可以进行多次的移动和变形操作,更加符合需求。
package com.test.gameengine.confreaders.beans.interfaces;
import com.test.gameengine.confreaders.beans.EngineConfugure;
public interface StepGoable {
//默认多少帧推进一次
int defaultFramesPerStep = 10;
int getFramesPerStep();
void setFramesPerStep(int frames);
default SetCallBack getSetFramesPerStepByString() {
return (value, trace, warnMsg, wrongMsg) -> EngineConfugure.setIntByString(value, trace, warnMsg, wrongMsg,
ivalue -> this.setFramesPerStep(ivalue), ivalue -> ivalue > 0);
}
}
SetCallback是用来描述在解析过程中修改对象值
的回调的方法接口
。
package com.test.gameengine.confreaders.beans.interfaces;
import java.util.ArrayList;
public interface SetCallBack {
void set(String value,ArrayList<String>trace,ArrayList<String>warnMsg,ArrayList<String>wrongMsg);
}
java beans
beans下是储存解析结果的java bean,同时也是解析规则的指定者。
所有具体java bean继承于com.test.gameengine.confreaders.beans.EngineConfugure
。而com.test.gameengine.confreaders.beans.EngineConfugure
又继承自com.test.gameengine.EngineConfugure
。
package com.test.gameengine.confreaders.beans;
import java.util.ArrayList;
import org.w3c.dom.Node;
import com.test.gameengine.confreaders.beans.interfaces.SetCallBack;
import com.test.games.interfaces.Action;
import com.test.games.interfaces.Func1;
public abstract class EngineConfugure extends com.test.gameengine.EngineConfugure {
public static final int defaultMaxFrames = 30;
public static final int defaultWindowWidth = 500;
public static final int defaultWindowHeight = 500;
//指定了什么标签将被认定为是在配置这个配置
public abstract String getName();
public EngineConfugure(int maxFramesRate, int windowWidth, int windowHeight) {
super(maxFramesRate, windowWidth, windowHeight, false);
}
public EngineConfugure() {
// TODO Auto-generated constructor stub
super(defaultMaxFrames, defaultWindowWidth, defaultWindowHeight, false);
}
//当且仅当getSetXXXByString的方法存在时
//<XXX>Value</XXX>标签才会被识别
//识别后会把Value作为第一个参数调用SetCallBack的set回调
//以达到配置的目的
public SetCallBack getSetMaxFramesByString() {
return (value, trace, warnMsg, wrongMsg) -> setIntByString(value, trace, warnMsg, wrongMsg, (ivalue) -> {
super.setMaxFrameRate(ivalue);
}, (ivalue) -> ivalue > 0);
}
public SetCallBack getSetWindowWidthByString() {
return (value, trace, warnMsg, wrongMsg) -> setIntByString(value, trace, warnMsg, wrongMsg, (ivalue)->super.setWindowWidth(ivalue), (ivalue)->ivalue>0);
}
public SetCallBack getSetWindowHeightByString() {
return (value, trace, warnMsg, wrongMsg) -> setIntByString(value, trace, warnMsg, wrongMsg, (ivalue)->super.setWindowHeight(ivalue), (ivalue)->ivalue>0);
}
//当这个方法为返回true时将会拦截该标签节点的解析
//在这里面可以另作解析
//以保证灵活性
public boolean extraAnlzOfConfugure(Node node,ArrayList<String>trace,ArrayList<String>warnMsg,ArrayList<String> worngMsg) {
return false;
}
public static final String getTrace(ArrayList<String> trace) {
String res = "配置";
if (trace.size() > 0) {
res += "‘" + trace.get(0) + "’";
}
for (int i = 1; i < trace.size(); i++) {
res += "->" + "‘" + trace.get(i) + "’";
}
return res;
}
//封装配置一个整形变量的行为
//因为这在个项目中它最常用
@SafeVarargs
public static final void setIntByString(String value, ArrayList<String> trace, ArrayList<String> warnMsg,
ArrayList<String> wrongMsg, Action<Integer> setInt, Func1<Integer, Boolean>... conditions) {
try {
int ivalue = Integer.parseInt(value);
boolean flag = true;
for (Func1<Integer, Boolean> i : conditions) {
if (!i.invoke(ivalue)) {
flag = false;
break;
}
}
if (flag) {
setInt.invoke(ivalue);
} else {
throw new Exception();
}
} catch (Exception e) {
warnMsg.add(getTrace(trace) + "不合法,将被忽略");
}
}
}
主体
confreader目录下是解析器主体
,负责注册
和解析
配置。为什么需要注册呢?因为我没有找到让Java像其他语言一样反射获取整个package下所有类信息的方案,所以就没有办法通过像规定特定包下的都为配置java bean,或者采用注解来标识的方法实现知道谁是配置的java bean。所以在解析前要先注册谁是配置java bean。以明确解析规则。
注册器
package com.test.gameengine.confreader;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import com.test.gameengine.confreaders.beans.*;
public class ConfugureRegister {
private ConfugureRegister() {};
public static final ArrayList<Class<? extends EngineConfugure>> classes=new ArrayList<>();
private static boolean hasRegist=false;
//执行注册
public static final void regist() {
//避免重复注册
if(hasRegist) {
return;
}
hasRegist=false;
classes.add(GeneralConfugure.class);
classes.add(RusianBlockConfugure.class);
}
//获取注册过的java bean的Class对象
public static final Class<? extends EngineConfugure> tryGet(String name) {
for(Class<? extends EngineConfugure>i:classes) {
try {
String iName=i.getConstructor().newInstance().getName();
if(iName.equals(name)) {
return i;
}
} catch (InstantiationException | IllegalAccessException | IllegalArgumentException
| InvocationTargetException | NoSuchMethodException | SecurityException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
return null;
}
}
解析器
package com.test.gameengine.confreader;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import org.w3c.dom.DOMException;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;
import com.test.gameengine.confreaders.beans.EngineConfugure;
import com.test.gameengine.confreaders.beans.interfaces.SetCallBack;
import com.test.games.interfaces.Action;
public class ConfugureReader {
private static ArrayList<EngineConfugure> confs = new ArrayList<>();
private ConfugureReader() {
};
//遍历xml子节点
public static final void throughNodeList(NodeList list, Action<Node> action) {
for (int i = 0; i < list.getLength(); i++) {
Node now = list.item(i);
if (now.getNodeName().startsWith("#")) {
continue;
}
action.invoke(now);
}
}
//解析一个配置文件
private static final void read(File read, ArrayList<String> wrongMsg, ArrayList<String> warnMsg)
throws ParserConfigurationException, SAXException, IOException {
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
DocumentBuilder db = dbf.newDocumentBuilder();
NodeList list = null;
try {
Document doc = db.parse(read);
list = doc.getDocumentElement().getChildNodes();
} catch (Exception e) {
wrongMsg.add("错误的配置文件,找不到根节点");
return;
}
ArrayList<String>trace=new ArrayList<>();
throughNodeList(list,(node) -> {
Class<? extends EngineConfugure> now;
trace.add(node.getNodeName());
if ((now = ConfugureRegister.tryGet(node.getNodeName())) != null) {
try {
EngineConfugure conf = now.getConstructor().newInstance();
throughNodeList(node.getChildNodes(),(subNode) -> {
trace.add(subNode.getNodeName());
Method method;
try {
if (!conf.extraAnlzOfConfugure(subNode,trace,warnMsg,wrongMsg)) {
method = now.getMethod("getSet" + subNode.getNodeName() + "ByString");
SetCallBack callback=(SetCallBack) method.invoke(conf);
callback.set(subNode.getTextContent(), trace, warnMsg, wrongMsg);
}
} catch (SecurityException | NoSuchMethodException | IllegalAccessException
| IllegalArgumentException | InvocationTargetException | DOMException e) {
// TODO Auto-generated catch block
warnMsg.add("无效的配置‘"+node.getNodeName()+"’->‘"+subNode.getNodeName()+"’");
}
trace.remove(1);
});
confs.add(conf);
} catch (InstantiationException | IllegalAccessException | IllegalArgumentException
| InvocationTargetException | NoSuchMethodException | SecurityException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}else {
warnMsg.add("无效的配置‘"+node.getNodeName()+"’");
}
trace.remove(0);
});
}
//尝试解析src/conf.xml或者conf.xml
//能够找到src/conf.xml说明在IDE环境(笔者是Eclipse环境,其他环境可能存在差异但需要实现的逻辑相同)
//不在则尝试解析程序同目录下的conf.xml
//也不存在则什么都不做按游戏配置的默认值运行
public static final void read(ArrayList<String> wrongMsg, ArrayList<String> warnMsg) {
File srcConf = new File("src/conf.xml");
if (srcConf.exists()) {
try {
read(srcConf, wrongMsg, warnMsg);
} catch (ParserConfigurationException | SAXException | IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}else {
File outConf = new File("conf.xml");
if (outConf.exists()) {
try {
read(outConf, wrongMsg, warnMsg);
} catch (ParserConfigurationException | SAXException | IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
//尝试获取解析到的配置实例,若不存在则返回配置默认实例
@SuppressWarnings("unchecked")
public static final <T extends EngineConfugure> T getConfugure(Class<T> _class) {
for (EngineConfugure i : confs) {
if (i.getClass().equals(_class)) {
return (T) i;
}
}
try {
return (T) _class.getConstructor().newInstance();
} catch (InstantiationException | IllegalAccessException | IllegalArgumentException | InvocationTargetException
| NoSuchMethodException | SecurityException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return null;
}
}
至此,配置解析器已经成功实现。
一些方法接口和通用类
在开始实现具体的游戏实例前再对一些接下来会用到方法接口
和通用类
作下介绍。
描述没有返回值传1个参数的回调。
package com.test.games.interfaces;
public interface Action<Tin> {
void invoke(Tin in);
}
描述有返回值传0个参数的回调。
package com.test.games.interfaces;
public interface Func0<Tout> {
Tout invoke();
}
描述有返回值传1个参数的回调。
package com.test.games.interfaces;
public interface Func1<Tin,Tout> {
Tout invoke(Tin in);
}
以上的方法接口的设计也是参照C#/Unity的UnityAction<…>和Fun<…>
描述一个整数区间的类。
package com.test.general;
public class IntVector2 {
private int x;
private int y;
public IntVector2(int x,int y) {
this.x=x;
this.y=y;
}
public int getX() {
return this.x;
}
public int getY() {
return this.y;
}
}
点阵地图模板
接下来正式开始实现俄罗斯方块的引擎实例。
我的本意是开发一些列小游戏
的,所以我在游戏引擎基类→具体引擎实例
之间加入了实例模板
这一层结构。使得相似的逻辑可以被多个实例引用避免
冗余
和多次编写
带来的不良后果。
因为这个项目里的俄罗斯方块是基于点阵地图
的,所以这里先实现一个点阵地图
的实例模板
。实例模板
和引擎实例
一样由配置类
和引擎类
组成。
配置类
package com.test.gameengine.confreaders.beans;
import java.util.ArrayList;
import com.test.gameengine.confreaders.beans.interfaces.SetCallBack;
public abstract class MappableConfugure extends EngineConfugure {
//定义一些配置的默认值,当找不到配置文件时游戏将按这些值运行
public static final int defaultMapWidth = 20;
public static final int defaultMapHeight = 20;
public static final String defaultBlank = "-";
public static final String defaultFiller = "@";
public static final int defaultSignSize = 20;
private int mapWidth = defaultMapWidth;//地图宽度
private int mapHeight = defaultMapHeight;//地图高度
private String blank=defaultBlank;//地图空白符号
private String filler=defaultFiller;//地图填充符号
private int signSize=defaultSignSize;//地图中符号的字号大小
public int getMapWidth() {
return mapWidth;
}
public void setMapWidth(int mapWidth) {
this.mapWidth = mapWidth;
}
public SetCallBack getSetMapWidthByString() {
return (value, trace, warnMsg, wrongMsg) -> setIntByString(value, trace, warnMsg, wrongMsg, (ivalue) -> {
this.setMapWidth(ivalue);
}, (ivalue) -> ivalue > 0);
}
public int getMapHeight() {
return mapHeight;
}
public void setMapHeight(int mapHeight) {
this.mapHeight = mapHeight;
}
public SetCallBack getSetMapHeightByString() {
return (value, trace, warnMsg, wrongMsg) -> setIntByString(value, trace, warnMsg, wrongMsg, (ivalue) -> {
this.setMapHeight(ivalue);
}, (ivalue) -> ivalue > 0);
}
public static final int[] parseZOArray(String statement) throws Exception {
ArrayList<Integer> res = new ArrayList<>();
for (int i = 0; i < statement.length(); i++) {
String now = statement.substring(i, i + 1);
int v = Integer.parseInt(now);
if(v!=0&&v!=1) {
throw new Exception("该方法只能解析0和1");
}
res.add(v);
}
int size=res.size();
int []rres=new int[size];
for(int i=0;i<size;i++) {
rres[i]=res.get(i);
}
return rres;
}
public String getBlank() {
return this.blank;
}
public void setBlank(String blank) {
this.blank=blank;
}
public SetCallBack getSetBlankByString() {
return (value, trace, warnMsg, wrongMsg) -> this.setBlank(value);
}
public String getFiller() {
return this.filler;
}
public void setFiller(String filler) {
this.filler=filler;
}
SetCallBack getSetFillerByString() {
return (value, trace, warnMsg, wrongMsg) -> this.setFiller(value);
}
public int getSignSize() {
return this.signSize;
}
public void setSignSize(int signSize) {
this.signSize=signSize;
}
public SetCallBack getSetSignSizeByString() {
return (value, trace, warnMsg, wrongMsg) -> EngineConfugure.setIntByString(value, trace, warnMsg, wrongMsg,
ivalue -> this.setSignSize(ivalue), ivalue -> ivalue > 0);
}
}
引擎类
package com.test.gameengine.moduels;
import java.awt.Color;
import java.awt.Font;
import java.awt.GridLayout;
import java.util.Random;
import javax.swing.JFrame;
import javax.swing.JLabel;
import com.test.gameengine.GameEngine;
import com.test.gameengine.confreaders.beans.MappableConfugure;
import com.test.games.interfaces.Func1;
import com.test.general.IntVector2;
public class MappableEngine extends GameEngine {
private int mapWidth;//地图宽度
private int mapHeight;//地图高度
private String blank;//地图空白用什么符号表示
private String filler;//非空白用什么表示
private int signSize;//每个符号的自号是多少
protected String map[][];//地图内容
private JLabel lmap[][];//地图基本单元
public MappableEngine(MappableConfugure conf, JFrame mainFrame) {
super(conf, mainFrame);
// TODO Auto-generated constructor stub
this.mapWidth=conf.getMapWidth();
this.mapHeight=conf.getMapHeight();
this.blank=conf.getBlank();
this.filler=conf.getFiller();
this.signSize=conf.getSignSize();
this.map=new String[mapHeight][mapWidth];
this.lmap=new JLabel[mapHeight][mapWidth];
}
public int getMapWidth() {
return mapWidth;
}
public int getMapHeight() {
return mapHeight;
}
public String getBlank() {
return blank;
}
public String getFiller() {
return filler;
}
public int getSignSize() {
return signSize;
}
//用final修饰awake使得子类不能再重写
//而是重写afterAwake
//以保护模板的基本功能
@Override
public final void awake() {
// TODO Auto-generated method stub
super.awake();
initMainFrameNMap();
afterAwake();
}
protected void afterAwake() {}
//将地图的内容填入基本单元
protected void print() {
for (int i = 0; i < this.mapHeight; i++) {
for (int j = 0; j < this.mapWidth; j++) {
lmap[i][j].setText(map[i][j]);
}
}
}
//采用网格布局,将整个窗口分成height*wigth
//height和width分别对应几行、几列
//在每个单元格里填充上JLabel并缓存到基本单元的列表里
//设置基本单元的字体、字号
//把整个地图内容列表都有空白符号填充
private void initMainFrameNMap() {
mainFrame.setLayout(new GridLayout(this.mapHeight, this.mapWidth));
for (int i = 0; i < mapHeight * this.mapWidth; i++) {
JLabel now = new JLabel("", JLabel.CENTER);
Font font = new Font("Aria", Font.PLAIN, signSize);
now.setFont(font);
int r = i / this.mapWidth;
int c = i % this.mapWidth;
map[r][c]=this.getBlank();
lmap[r][c] = now;
mainFrame.add(now);
}
this.showFrame();
}
//获取地图中的任意一个点坐标
protected IntVector2 getRandomInMap(@SuppressWarnings("unchecked") Func1<IntVector2,Boolean>...conditions) {
int xmax=this.map.length;
int ymax=this.map[0].length;
Random random=new Random();
int xrandom=random.nextInt();
int yrandom=random.nextInt();
IntVector2 res = new IntVector2(xrandom%xmax, yrandom%ymax);
for(Func1<IntVector2, Boolean>i:conditions) {
if(!i.invoke(res)) {
return getRandomInMap(conditions);
}
}
return res;
}
}
具体游戏实例
和实例模板一样,具体实例也主要由配置类
和引擎类
组成他们在本质上是一样的,实例模板实现的功能还是比较抽象,具体实例则具体到某个游戏的实现了。除此以外,我们把每次落下的方块叫做障碍物,出于描述游戏有哪些障碍物的需求,还需要一个障碍物类
和障碍物池
以构建障碍物系统
。我们发现在构建障碍物池是需要用到一些繁琐的赋值。因此我们可以通过一个工具类
把0101的形式化描述
给转换为赋值语句
。
障碍物系统
障碍物类
package com.test.games.rusianblock;
public class Barrier {
private int shapeIndex=0;//当前形状是形状数组里的哪一个
private int [][][]shapes;//一个二维的01矩阵表达一个形状,因为可以变形,所以是三维数组,其下标含义为【第几个形状】【行】【列】
public Barrier(int[][][] shapes) {
super();
this.shapes = shapes;
}
//添加形状
public void addShape(int [][]nshape) {
int len=shapes.length;
int [][][]t=new int[len+1][][];
for(int i=0;i<len;i++) {
t[i]=shapes[i];
}
t[len]=nshape;
shapes=t;
}
public Barrier() {
shapes=new int[0][][];
}
//获取当前形状
public int[][] getShapeNow() {
return shapes[shapeIndex];
}
//变形
public int[][] changeShape() {
shapeIndex++;
shapeIndex%=shapes.length;
return getShapeNow();
}
//计算这次变形后的障碍但实际不变性
//用来判定变形是否合法
//以阻止不合法变形的发生
public Barrier virtualChange() {
Barrier res=new Barrier(shapes);
res.shapeIndex=shapeIndex;
res.changeShape();
return res;
}
public int[][][] getShapes() {
return shapes;
}
}
工具类
package com.test.editorutils;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
public class ShapeUtil {
public static void main(String[] args) throws IOException {
//我还是把描述文件放在同一个目录下
File target = new File(ShapeUtil.class.getResource("").getPath(), "ShapeStatement.txt");
InputStream is = new FileInputStream(target);
InputStreamReader isr = new InputStreamReader(is);
@SuppressWarnings("resource")
BufferedReader bf = new BufferedReader(isr);
String line;
int[][][] buffer = new int[4][][];//下标含义【第几个形状】【行】【列】
int pos = -1;//当前填充到第几个形状
int posR=0;//当前填充到第几行
int posC=0;//当前填充到第几列
int C=0;//形状规模,即正方形的边长
String name="shapes";//没有指定名称的命令时的默认名称
//解析整个描述文件
while ((line = bf.readLine()) != null) {
//跳过空行
if(line.length()==0) {
continue;
}
//以#开头视为是命令
//规定这个小工具的命令统一是两个字符
//后面紧跟参数
//注意是紧跟没有空格
if (line.startsWith("#")) {
//行列复位
posR=0;
posC=0;
String cmd = line.substring(1, 3);
switch (cmd) {
//构建命令
case "de":
if(C==0) {
System.out.println("lacking si statement");
return;
}
pos++;
buffer[pos]=new int[C][C];
break;
//复制命令
case "as":
if(C==0) {
System.out.println("lacking si statement");
return;
}
pos++;
String num = line.substring(3);
if (num.length() == 0) {
System.out.println("null num statement");
return;
}
int index;
try {
index = Integer.parseInt(num);
} catch (Exception e) {
System.out.println("bad num statement");
return;
}
if (!(index >= 0)) {
System.out.println("out num statement");
}
buffer[pos] = buffer[index];
break;
//命名命令
case "na":
name=line.substring(3);
break;
//规模声明命令
case "si":
try {
C=Integer.parseInt(line.substring(3));
if(C<=0) {
System.out.println("too small statement in si");
return;
}
}catch (Exception e) {
System.out.println("bad num statement in si");
return;
}
break;
default:
System.out.println("bad cmd");
return;
}
continue;
}
//不以#开头则不是命令
//填充行
for (int i = 0; i < line.length(); i++) {
String now = line.substring(i, i + 1);
switch (now) {
case "0":
if(posC==C) {
posC=0;
posR++;
}
if(posR==C) {
System.out.println("too more statement in R");
return;
}
buffer[pos][posR][posC++]=0;
break;
case "1":
if(posC==C) {
posC=0;
posR++;
}
if(posR==C) {
System.out.println("too more statement in R");
return;
}
buffer[pos][posR][posC++]=1;
break;
default:
System.out.println("bad barier statement ‘"+now+"’");
return;
}
}
}
bf.close();
for(int i=0;i<buffer.length;i++) {
int [][]now=buffer[i];
for(int j=0;j<now.length;j++) {
int []_now=now[j];
String d=name+"["+i+"]["+j+"]=new int[]{"+_now[0];
for(int k=1;k<_now.length;k++) {
d+=","+_now[k];
}
d+="};";
System.out.println(d);
}
System.out.println();
}
}
}
工具类会用到的描述形如:
#nashapesC
#si3
#de
000
111
001
#de
010
010
110
#de
000
100
111
#de
110
010
010
命令说明:
#naXXX
表示最后结果中的变量名为XXX。
#si
表示障碍物规模即边长,出于正确性考虑我们约定障碍物的描述都为正方形。
#de
表示要定义新的障碍物。
还有一个这里没有出现的命令。
#asX
表示复制索引为X障碍物作为自身内容。
注:
#de
和#as
都是构建性的语句都会占有一个索引,也就是说#as
也可以复制另一个#as的内容。
就实例描述而言,会得到结果:
障碍物池
package com.test.games.rusianblock;
import java.util.ArrayList;
import java.util.Random;
public class BarrierPool {
//由配置文件构建的障碍物列表
//其元素会在构造函数里被全部添加到总障碍物列表里
public static final ArrayList<Barrier>extraBarriers=new ArrayList<>();
//随机获取一个障碍物
public Barrier getRandomBarrier(){
int index=new Random().nextInt();
if(index<0) {
index*=-1;
}
return new Barrier(barriers.get(index%barriers.size()).getShapes());
}
//总的障碍物列表
ArrayList<Barrier>barriers=new ArrayList<>();
private int [][][]shapesA=new int[1][2][2];
private int [][][]shapesB=new int[2][4][4];
private int [][][]shapesC=new int[4][3][3];
//以下一系列init...方法
//是构建一些基本障碍物
//其余障碍物是通过配置文件实现的
//这么做主要是验证配置文件解析系统的可靠性
//这里面的每个模块笔者都做过一定调试
//都能基本保证可靠
public BarrierPool() {
initA();
initB();
initC();
for(Barrier i:extraBarriers) {
barriers.add(i);
}
}
private void initShapesA() {
for(int i=0;i<2;i++) {
for(int j=0;j<2;j++) {
shapesA[0][i][j]=1;
}
}
}
private void initA() {
initShapesA();
Barrier a=new Barrier(shapesA);
barriers.add(a);
}
private void initShapesB() {
shapesB[0][0]=new int[]{0,0,0,0};
shapesB[0][1]=new int[]{1,1,1,1};
shapesB[0][2]=new int[]{0,0,0,0};
shapesB[0][3]=new int[]{0,0,0,0};
shapesB[1][0]=new int[]{0,1,0,0};
shapesB[1][1]=new int[]{0,1,0,0};
shapesB[1][2]=new int[]{0,1,0,0};
shapesB[1][3]=new int[]{0,1,0,0};
}
private void initB() {
initShapesB();
Barrier b=new Barrier(shapesB);
barriers.add(b);
}
private void initShapesC() {
shapesC[0][0]=new int[]{0,0,0};
shapesC[0][1]=new int[]{1,1,1};
shapesC[0][2]=new int[]{0,0,1};
shapesC[1][0]=new int[]{0,1,0};
shapesC[1][1]=new int[]{0,1,0};
shapesC[1][2]=new int[]{1,1,0};
shapesC[2][0]=new int[]{0,0,0};
shapesC[2][1]=new int[]{1,0,0};
shapesC[2][2]=new int[]{1,1,1};
shapesC[3][0]=new int[]{1,1,0};
shapesC[3][1]=new int[]{1,0,0};
shapesC[3][2]=new int[]{1,0,0};
}
private void initC() {
initShapesC();
Barrier c=new Barrier(shapesC);
barriers.add(c);
}
}
游戏实例
配置类
相对于点阵地图模板的配置类只是多了个拦截
解障碍物配置
和实现了StepGoable
接口。
package com.test.gameengine.confreaders.beans;
import java.util.ArrayList;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import com.test.gameengine.confreaders.beans.interfaces.StepGoable;
import com.test.games.rusianblock.Barrier;
import com.test.games.rusianblock.BarrierPool;
import com.test.games.utils.XMLUtil;
public class RusianBlockConfugure extends MappableConfugure implements StepGoable{
//定义一些常量,避免直接使用字符串而导致拼写错误
public static final String barriersCmd = "barriers";
public static final String barrierCmd = "barrier";
public static final String shapeCmd = "shape";
public static final String lineCmd = "line";
public static final String NAME = "rusianBlock";
@Override
public String getName() {
// TODO Auto-generated method stub
return NAME;
}
private int framesPerStep = defaultFramesPerStep;
//记录由配置文件解析出来的障碍物
private ArrayList<Barrier> barriers = new ArrayList<>();
@Override
public int getFramesPerStep() {
return framesPerStep;
}
@Override
public void setFramesPerStep(int framesPerStep) {
this.framesPerStep = framesPerStep;
}
public ArrayList<Barrier> getBarriers() {
return barriers;
}
//拦截解析障碍物配置
@Override
public boolean extraAnlzOfConfugure(Node node, ArrayList<String> trace, ArrayList<String> warnMsg,
ArrayList<String> wrongMsg) {
// TODO Auto-generated method stub
//不为障碍物节点则不拦截
if (!node.getNodeName().equals(barriersCmd)) {
return false;
}
//遍历所有节点,解析每个形状
NodeList barriers = node.getChildNodes();
for (int i = 0; i < barriers.getLength(); i++) {
//缓存当前子节点,当前节点是描述单个障碍物的
Node nowBarrier = barriers.item(i);
String barrierNodeName = nowBarrier.getNodeName();
//跳过无意义行
if(barrierNodeName.startsWith("#")) {
continue;
}
//获取障碍物名称
String barrierName=null;
try {
barrierName=nowBarrier.getAttributes().getNamedItem("name").getNodeValue();
}catch(Exception e) {
barrierName="未知";
}
trace.add(barrierNodeName + "(" + barrierName + ")");
//检测节点名称的合法性
if (!barrierNodeName.equals(barrierCmd)) {
warnMsg.add(getTrace(trace) + "未定义,将被忽略");
trace.remove(trace.size() - 1);
continue;
}
//定义用来缓存当前障碍物的对象
Barrier pre = new Barrier();
//开始为当前障碍物解析每个形状
NodeList shapes = nowBarrier.getChildNodes();
for (int j = 0; j < shapes.getLength(); j++) {
//缓存当前子节点,当前子节点是一个形状
Node nowShape = shapes.item(j);
//跳过无意义行
if(nowShape.getNodeName().startsWith("#")) {
continue;
}
//获取形状名称
String shapeNodeName = nowShape.getNodeName();
String shapeName=null;
try {
shapeName=nowShape.getAttributes().getNamedItem("name").getNodeValue();
}catch(Exception e) {
shapeName="未知";
}
trace.add(shapeNodeName + "(" + shapeName + ")");
//检测节点名称的合法性
if (!shapeNodeName.equals(shapeCmd)) {
warnMsg.add(getTrace(trace) + "未定义,将被忽略");
trace.remove(trace.size() - 1);
continue;
}
//开始填充当前形状
NodeList lines = nowShape.getChildNodes();
int shapeSize = XMLUtil.getEffectiveLength(lines);//获取有意以行数量以确定形状规模
int shape[][]=new int[shapeSize][shapeSize];//形状填充到这里
int lineNum=-1;//记录行号,使得报错更容易看懂
for (int k = 0; k < lines.getLength(); k++) {
//缓存当前节点,当前节点表示一行
Node nowLine = lines.item(k);
//跳过无意义行
if(nowLine.getNodeName().startsWith("#")) {
continue;
}
lineNum++;
trace.add(nowLine.getNodeName() + lineNum);
//检查内容合法性
String lineContent = nowLine.getTextContent().trim();
if (!lineContent.matches("^[01]+$")) {
wrongMsg.add(getTrace(trace) + "内容与指定模式^[01]+$不匹配");
}
//检查长度合法性
if (lineContent.length() != shapeSize) {
wrongMsg.add(getTrace(trace) + "的长度与行数不相等。长度:"+lineContent.length()+",内容‘"+lineContent+"’;行数:"+shapeSize);
}
//若以上检查存在错误则跳过
//这里认为存在以上两种错误是违法原则的严重错误
//一些小的错误只是爆出警告
//以上是报错错误
if (wrongMsg.size() > 0) {
trace.remove(trace.size() - 1);
continue;
}
//到这则表示行描述合法,将其解析为01数字数组
try {
int []array=parseZOArray(lineContent);
shape[lineNum]=array;
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
trace.remove(trace.size() - 1);
}
//检查是否存在全是空的形状,这将导致游戏卡死
//也视作是严重错误
int tn=0;
for(int []l:shape) {
for(int m:l) {
tn+=m;
}
}
//若到这都未发生错误,则将这个形状加入到当前障碍物的缓存
if(tn==0) {
warnMsg.add(getTrace(trace)+"非法,这将会导致游戏卡死,将被忽略");
}else {
pre.addShape(shape);
}
trace.remove(trace.size() - 1);
}
if(pre.getShapes().length==0) {
warnMsg.add(getTrace(trace)+"描述为空,将被忽略");
}else {
BarrierPool.extraBarriers.add(pre);
}
trace.remove(trace.size() - 1);
}
return true;
}
}
引擎类
package com.test.games.rusianblock;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.util.ArrayList;
import javax.swing.JFrame;
import com.test.gameengine.KeyCode;
import com.test.gameengine.confreaders.beans.RusianBlockConfugure;
import com.test.gameengine.moduels.MappableEngine;
public class RusianBlockEngine extends MappableEngine {
private boolean isGameOver = false;//游戏是否结束的标志
private BarrierPool bPool = new BarrierPool();//创建障碍物池的对象
private Barrier now;//当前正在下落的障碍物
private int posR;//当前障碍物的左上角在哪行
private int posC;//当前障碍物的左上角在哪一列
private ArrayList<Integer> lastRs=new ArrayList<>();//障碍物上一次填充的行号
private ArrayList<Integer> lastCs=new ArrayList<>();//障碍物上一次填充的列号
private int operation=-1;//当前操作指令
private static final int OPERATION_LEFT = 0;
private static final int OPERATION_RIGHT = 1;
private int frameDuration;//距离上一次推进已经经过了多少帧
private final int origianlFramesPerStep;//原来经过多少帧推进一次
private int nextFramesPerStep;//从下一帧多少帧推进一次
private int framesPerStep;//当前多少帧推进一次
private boolean changeShape=false;//是否发出变形指令
private int scores;//总分
public RusianBlockEngine(RusianBlockConfugure conf, JFrame mainFrame) {
super(conf, mainFrame);
this.origianlFramesPerStep=conf.getFramesPerStep();
this.framesPerStep=conf.getFramesPerStep();
this.nextFramesPerStep=conf.getFramesPerStep();
// TODO Auto-generated constructor stub
//启动游戏循环
super.startUp();
}
@Override
protected void afterAwake() {
// TODO Auto-generated method stub
super.afterAwake();
//原来封装事件系统难以满足这里的需求
//再注册点事件
this.mainFrame.addKeyListener(new KeyListener() {
@Override
public void keyTyped(KeyEvent e) {
// TODO Auto-generated method stub
}
@Override
public void keyReleased(KeyEvent e) {
// TODO Auto-generated method stub
switch (e.getKeyCode()) {
case KeyCode.ARROW_UP:
break;
//放开下键后推进速度复原
case KeyCode.ARROW_DOWN:
nextFramesPerStep = origianlFramesPerStep;
break;
case KeyCode.ARROW_LEFT:
break;
case KeyCode.ARROW_RIGHT:
break;
case KeyCode.SPACE:
break;
}
}
@Override
public void keyPressed(KeyEvent e) {
// TODO Auto-generated method stub
switch (e.getKeyCode()) {
//按上键发出变形命令
case KeyCode.ARROW_UP:
changeShape = true;
break;
//按下键加快推进速度
case KeyCode.ARROW_DOWN:
nextFramesPerStep = 1;
break;
//按左键发出左移命令
case KeyCode.ARROW_LEFT:
operation = OPERATION_LEFT;
break;
//按右键发出右移命令
case KeyCode.ARROW_RIGHT:
operation = OPERATION_RIGHT;
break;
case KeyCode.SPACE:
break;
}
}
});
//先阻塞游戏,在检测到点击后在正式开始
System.out.println("请点击游戏窗口开始游戏!");
while (true) {
if (hasClicked||this.getHasShutdown()) {
break;
}
try {
Thread.sleep(100);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
if(!this.getHasShutdown()) {
System.out.println("游戏开始!");
System.out.println("当前分数" + scores);
}
}
@Override
public void start() {
}
@Override
public void update() {
//如果游戏还能继续则
//响应操作,这里能体现为什么不能用低帧率替代多帧率推进
//操作每帧响应是为了让游戏操作的结果更加稳定
//重绘
if (gameOn()) {
anlzOperation();
paintBarrier();
}
//游戏结束则
//阻塞游戏
//在检测到一次点击时发出引擎关机指令
//并在引擎运行的最后时刻关闭窗口
//且在这是会禁用叉号的关闭
//这个会在在重写couldCloseWindow回调后
//由引擎调度实现
if (isGameOver) {
hasClicked=false;
System.out.println("请点击游戏窗口以关闭");
while(true){
if(hasClicked) {
break;
}
try {
Thread.sleep(100);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
shutDown(()->{
this.mainFrame.dispose();
});
}
}
//重写指定在什么情况下不允许通过叉号关闭窗口
@Override
protected boolean couldCloseWindow() {
// TODO Auto-generated method stub
return !isGameOver;
}
private int getScore(int lineNum) {
return (int) Math.pow(2, lineNum) - 1;
}
//结算消除填满一行的
private void settle() {
ArrayList<Integer> pres = new ArrayList<>();
//记录所有满行的到pres
for (int i = this.getMapHeight() - 1; i >= 0; i--) {
boolean flag = false;
boolean mflag = true;
String[] now = map[i];
for (int j = 0; j < now.length; j++) {
if (now[j].equals(this.getFiller())) {
flag = true;
} else {
mflag = false;
}
}
if (!flag) {
break;
}
if (mflag) {
pres.add(i);
}
}
//若有消除计算并提示获取的分数
int lineNum = pres.size();
int gotScore = getScore(lineNum);
scores += gotScore;
if (lineNum > 0) {
System.out.println(getDateTime());
System.out.println("此此消除" + lineNum + "行,得分" + gotScore);
System.out.println("当前总分" + scores);
}
//从后往前消除满的行
//注意不能直接从前往后
for (int i = lineNum - 1; i >= 0; i--) {
removeLine(pres.get(i));
}
}
//消除行
private void removeLine(int index) {
for (int i = index; i >= 1; i--) {
for (int j = 0; j < this.getMapWidth(); j++) {
map[i][j] = map[i - 1][j];
}
}
for (int i = 0; i < this.getMapWidth(); i++) {
map[0][i] = this.getBlank();
}
}
//响应操作
private void anlzOperation() {
switch (operation) {
case OPERATION_LEFT:
if (isCould(now, posR, posC - 1)) {
posC--;
}
break;
case OPERATION_RIGHT:
if (isCould(now, posR, posC + 1)) {
posC++;
}
break;
}
operation = -1;
}
//判断若这里有障碍物是否合法
private boolean isCould(Barrier b, int posR, int posC) {
int shape[][] = b.getShapeNow();
int len = shape.length;
for (int i = 0; i < len; i++) {
int r = posR + i;
for (int j = 0; j < len; j++) {
if (shape[i][j] == 1) {
int c = posC + j;
if (!couldFill(r, c)) {
return false;
}
if (!isLast(r, c) && map[r][c].equals(this.getFiller())) {
return false;
}
}
}
}
return true;
}
//判断这个点上一帧是否被障碍物填充
private boolean isLast(int r, int c) {
for (int i = 0; i < lastRs.size(); i++) {
if (lastRs.get(i) == r && lastCs.get(i) == c) {
return true;
}
}
return false;
}
//消耗并响应变形命令
private void consumeChangeShape() {
if (changeShape) {
if (isCould(now.virtualChange(), posR, posC)) {
now.changeShape();
}
changeShape = false;
}
}
//不考虑纵向边界的情况下在这里存在障碍物是否合法
private boolean isWithinCould(Barrier b, int posR, int posC) {
int shape[][] = b.getShapeNow();
int len = shape.length;
for (int i = 0; i < len; i++) {
int r = posR + i;
if (r < 0) {
continue;
}
for (int j = 0; j < len; j++) {
if (shape[i][j] == 1) {
int c = posC + j;
if (!couldFill(r, c)) {
return false;
}
if (!isLast(r, c) && map[r][c].equals(this.getFiller())) {
return false;
}
}
}
}
return true;
}
//游戏基本行为,返回游戏能否继续
private boolean gameOn() {
framesPerStep = nextFramesPerStep;
if (now == null) {
now = bPool.getRandomBarrier();
posR = 0;
calOffset();
calPosC();
} else {
consumeChangeShape();
frameDuration++;
if (frameDuration >= framesPerStep) {
frameDuration = 0;
if (isCould(now, posR + 1, posC)) {
posR++;
} else if (posR < 0 && isWithinCould(now, posR + 1, posC)) {
posR++;
} else {
if (!isCould(now, posR, posC)) {
System.out.println("游戏结束,得分" + scores);
System.out.println();
isGameOver = true;
return false;
}
settle();
now = null;
lastRs.clear();
lastCs.clear();
return false;
}
}
}
return true;
}
//在基于障碍物对地图内容做修改后绘制整张地图
private void paintBarrier() {
for (int i = 0; i < lastRs.size(); i++) {
map[lastRs.get(i)][lastCs.get(i)] = this.getBlank();
}
lastRs.clear();
lastCs.clear();
int[][] shape = now.getShapeNow();
for (int i = 0; i < shape.length; i++) {
int[] now = shape[i];
for (int j = 0; j < now.length; j++) {
if (now[j] == 1) {
int r = i + posR;
int c = j + posC;
if (tryFill(r, c)) {
lastRs.add(r);
lastCs.add(c);
}
}
}
}
print();
}
//尝试填充,返回是否运行被填充
private boolean tryFill(int r, int c) {
boolean could = couldFill(r, c);
if (could) {
try {
map[r][c] = this.getFiller();
} catch (Exception e) {
System.out.println(r + ":" + c);
throw e;
}
}
return could;
}
//是否能够被填充
private boolean couldFill(int r, int c) {
if (r >= 0 && r < this.getMapHeight() && c >= 0 && c < this.getMapWidth()) {
return true;
}
return false;
}
//计算新障碍物居中左上角要在第几列
private void calPosC() {
posC = (this.getMapWidth() - now.getShapeNow().length) / 2;
}
//计算低端有多少空行
//以使得新物体生成时正好底端紧贴地图顶端
private void calOffset() {
int res = 0;
int shape[][] = now.getShapeNow();
int len = shape.length;
for (int i = len - 1; i >= 0; i--) {
for (int j : shape[i]) {
if (j == 1) {
posR += res;
posR -= len;
return;
}
}
res++;
}
}
}
尾声
控制台部分
现在整个游戏已经呼之欲出啦。最后制作下控制台部分,即开始是让选游戏那部分。这里出于验证引擎系统的通用性将其视作一个没有窗口的引擎实例。
配置类
因为控制台引擎更不没有窗口,所以不需要专门弄一个配置,直接使用配置的基类配置下最基本的最大帧率就行了。其实有没有这一项配置也不重要,这里是为了用实践验证引擎通用性故意为之,其实完全可以选择更加简单的方案来实现。
菜单打印工具
这样的东西叫菜单:
因为我的本意是做一系列小游戏,可能后期会有打印很复杂的菜单的需求,所以这里将菜单打印
功能封装起来,使其为打印大规模菜单提供便利。
package com.test.games;
public interface CatchCallback {
void invoke(Exception e);
}
package com.test.games;
import java.util.ArrayList;
import java.util.Scanner;
public class NameList {
private ArrayList<String>names=new ArrayList<>();
public NameList(String ...names) {
for(String i:names) {
this.names.add(i);
}
}
public void printMenu(String title,Scanner scanner,CatchCallback catchCallback,Runnable ...callbacks) throws Exception {
if(names.size()!=callbacks.length) {
throw new Exception("回调数量和名称数量不匹配");
}
while(true) {
System.out.println(title+":");
for(int i=0;i<names.size();i++) {
System.out.println(i+")"+names.get(i));
}
String opt=scanner.nextLine();
int iopt=-1;
try {
iopt=Integer.parseInt(opt);
if(iopt>=0&&iopt<callbacks.length) {
callbacks[iopt].run();
}else {
throw new Exception("未知的选项s");
}
}catch(Exception e) {
catchCallback.invoke(e);
}
}
}
}
引擎类
package com.test.games;
import java.util.Scanner;
import javax.swing.JFrame;
import com.test.gameengine.GameEngine;
import com.test.gameengine.confreader.ConfugureReader;
import com.test.gameengine.confreaders.beans.GeneralConfugure;
import com.test.gameengine.confreaders.beans.RusianBlockConfugure;
import com.test.gameengine.confreaders.beans.SnakeConfugure;
import com.test.games.rusianblock.RusianBlockEngine;
import com.test.games.snake.SnakeEngine;
class MainPageEngine extends GameEngine {
public MainPageEngine(GeneralConfugure generalConfugure) {
super(generalConfugure, null);
// TODO Auto-generated constructor stub
this.startUp();
}
@Override
public void awake() {
// TODO Auto-generated method stubs
super.awake();
}
@Override
public void update() {
// TODO Auto-generated method stub
super.update();
try {
mainFrame = new JFrame();
new NameList("俄罗斯方块", "贪吃蛇").printMenu("请选择", new Scanner(System.in), e -> System.out.println("输入有误!请重新输入"),
() -> {
try {
new RusianBlockEngine(ConfugureReader.getConfugure(RusianBlockConfugure.class), mainFrame);
} catch (Exception e) {
e.printStackTrace();
}
}, () -> {
try {
new SnakeEngine(ConfugureReader.getConfugure(SnakeConfugure.class), mainFrame);
} catch (Exception e) {
e.printStackTrace();
}
});
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
源码分享
为了节约大家的下载次数,分享在蓝奏云网盘,密码:6666。