惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

博客园 - 三生石上(FineUI控件)
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
月光博客
月光博客
博客园 - 【当耐特】
Hugging Face - Blog
Hugging Face - Blog
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
爱范儿
爱范儿
大猫的无限游戏
大猫的无限游戏
S
SegmentFault 最新的问题
博客园_首页
雷峰网
雷峰网
量子位
有赞技术团队
有赞技术团队
博客园 - 叶小钗
博客园 - 聂微东
V
V2EX
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
博客园 - 司徒正美
小众软件
小众软件
The Cloudflare Blog
阮一峰的网络日志
阮一峰的网络日志
Apple Machine Learning Research
Apple Machine Learning Research
Jina AI
Jina AI
人人都是产品经理
人人都是产品经理

山岳库博

自建虚拟局域网实现文明六稳定联机 追光者 快手面经(二〇二四年五月九日) 一文搞懂伪代码 2023 级新生周赛第二周题解 Swpp Backends 官方文档 Butterfly 友链魔改教程 在 MavenCentral 上发布你的项目 MI——高级合成表模块 人狼羊菜过河详细题解 蓝桥杯 2023 训练四 Java 全解 蓝桥杯 2023 训练三 Java 全解 蓝桥杯 2023 训练二 Java 全解 全自动博客部署方案 小白也能用的 SW 构建插件 MI—电力系统工作原理 MI文档——自动化GUI绘制 给博客添加追番页面 网站加载速度优化方案总结 给博客添加自定义的通知悬浮窗
控制台游戏引擎开发文档
空梦 · 2023-04-11 · via 山岳库博

前言

  该引擎设计用途是 JVM 端控制台小游戏开发,也可用于其它用途,本人目前实现了在控制台中显示“Bad Apple”的功能,效果见下方视频:

  目前引擎支持以下功能:

  • 任意位置填充字符串
  • 任意位置修改文本属性
  • 监听键盘和鼠标的按键
  • 无闪屏刷新(多缓冲)

  注意:引擎目前只能在 Windows 平台使用,不支持其它平台!

  源代码见:Github


  下面我们开始说明引擎的使用方法。

引入仓库

  如果你使用gradle管理项目依赖,那么在dependencies语句中添加如下代码:

1
implementation group: 'top.kmar.game', name: 'cg-engine', version: 'x.x.x'

  如果你使用maven管理项目依赖,请添加如下代码:

1
2
3
4
5
<dependency>
<groupId>top.kmar.game</groupId>
<artifactId>cg-engine</artifactId>
<version>x.x.x</version>
</dependency>

  最后的version填写你想使用的版本,截止文档最后一次更新,最新版为1.0.2

  如果你使用sbtivygrapeleiningenbuildr管理依赖,请参考Maven Central

  如果你不使用上面任意一种方法管理项目依赖,请克隆 github 仓库并手动编译代码,然后添加到你的项目中。

  注意:如果你使用 kotlin 开发程序,必须启用K2,否则无法编译,在gradle中添加如下代码即可:

1
2
3
4
5
6
7
compileKotlin {
compilerOptions {
freeCompilerArgs.addAll(
"-Xuse-k2"
)
}
}

初始化和销毁

  使用引擎之前,需要先初始化控制台的信息,GMap.Builder中已经封装好了初始化的方法,调用GMap.Builder.build()时会自动初始化控制台信息。

  我们还可以使用ConsolePrinter.init进行手动初始化, 但是请注意:如果已经通过GMap.Builder进行了初始化操作,请勿通过ConsolePrinter.init重复初始化。

  如果游戏已经结束需要退出控制台,则需调用GMap.Builder.dispose()销毁控制台,正如注释中所说的一样,该函数只有在所有的GMap对象被虚拟机回收或调用close()函数后才会销毁控制台,这个设计是为了防止控制台被销毁后继续通过GMap操作控制台,从而导致程序崩溃。

  如果需要强制销毁,可以手动调用ConsolePrinter.dispose(),这个函数不会进行销毁检查,但是务必注意销毁控制台后不能再通过引擎对控制台进行操作。

  PS:控制台销毁后可以重新初始化。

  代码示例:

1
2
3
4
5
6
7
8
9
10
fun main() {
GMap.Builder.apply {
width = 80
height = 40
}.build().use {
// do something
// 通过某种方法阻塞主线程直到程序结束
}
GMap.Builder.dispose()
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Main {

public static void main(String[] args) {
GMap.BuilderJava.INSTANCE
.setWidth(80)
.setHeight(40);
try (GMap map = GMap.Builder.build()) {
// do something
// 通过某种方法阻塞主线程直到程序结束
}
GMap.Builder.dispose();
}

}

  上述代码使用Builder创建了一个横向 80 个字符,纵向 40 个字符的地图,BuilderJava是为 Java 语言特化的类,用于优化 Java 的调用体验,可以与 Builder类混用。

  接下来我们介绍Builder中各个字段的作用(括号中为缺省值):

  • width - 横向字符数量
  • height - 纵向字符数量
  • fontWidth - 字符宽度(10)
  • cache - 缓存数量(2)
  • ignoreClose - 是否忽略Ctrl + C一类的控制快捷键(false)
  • file - DLL 文件的路径(./libs/utils.dll

  由于控制台限制,字符宽度只能保证字符宽度与设置的值接近,并不能保证恒等于设置的值。理想的拉丁字符宽高比为1:2,中文字符宽高比为1:1,在有些时候会偏离这个比例,如果希望达到完美比例,需要调整fontWidth的值。

  file需要传入一个有效的路径(必须保证目录存在,但是文件可以不存在),当 DLL 文件不存在时,引擎会自动生成 DLL 文件。

键鼠监听

  引擎支持监听键盘和鼠标的按键输入(暂不支持鼠标坐标监听),相关函数均封装在EventListener中,调用pushEvent()函数即可发布一次键盘事件。

  可以通过registry...函数注册事件:

  • registryKeyboardEvent - 注册键盘事件
  • registryMouseEvent - 注册鼠标事件

  两个函数均有对应的remove函数,可以用于移除指定事件。

  下面给出监听事件的示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
fun main() {
EventListener.registryKeyboardEvent(object : IKeyboardListener {

override fun onPressed(code: Int) {
println("pressed: $code")
}

override fun onReleased(code: Int) {
println("released: $code")
}

override fun onActive(code: Int) { }

})
while (true) {
EventListener.pushEvent()
Thread.sleep(10)
}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class Main {

public static void main(String[] args) {
EventListener.registryKeyboardEvent(new IKeyboardListener() {

@Override
public void onPressed(int code) {
System.out.println("pressed: " + code);
}

@Override
public void onReleased(int code) {
System.out.println("released: " + code);
}

@Override
public void onActive(int code) { }

});
while (true) {
EventListener.pushEvent();
try {
Thread.sleep(50);
} catch (InterruptedException ignored) {
}
}
}

}

  请注意:调用pushEvent的时候,引擎只能检测当前按下的按键,无法检测调用pushEvent之前按下的按键,如果两次pushEvent调用的时间间隔过大,会非常容易漏掉一些输入,所以应当适当提高调用频率。

自定义实体

  引擎中所有内容都被设计为“实体”,实体有四个通用属性:

  • visible - 标记实体是否可见,为false时将跳过渲染
  • collisible - 标记实体是否具有碰撞箱,为false时将跳过碰撞箱检测
  • died - 标记实体是否死亡,为true时将自动从地图中移除
  • x/y/width/height - 坐标和尺寸信息

  实体(GEntity)中提供了一些事件:

  • render - 渲染时触发,用于渲染该实体,不可见实体不会触发该事件
  • update - 每次逻辑循环的更新事件
  • beKilled - 被其它实体“击杀”后触发
  • onCollision - 与其它实体碰撞时触发
  • onRemove - 实体被从地图中移除后触发

  实体中还需要实现一些工具函数:

  • getCollision - 获取当前实体指定区域内所有具有碰撞体积的点
  • copy - 深拷贝当前对象

  下面我们给出一个代码示例,在下面的代码中,我们实现了一个矩形可视可碰撞的实体(render函数中的代码后面会说明):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
class BlockEntity(
override var x: Int,
override var y: Int,
override val width: Int,
override val height: Int
) : GEntity {

override val collisible = true
override var died = false
private set

override fun render(graphics: SafeGraphics) {
graphics.fillRect('#', 0, 0, width, height)
}

override fun getCollision(x: Int, y: Int, width: Int, height: Int): Stream<Point2D> {
val builder = Stream.builder<Point2D>()
val right = x + width
val bottom = y + height
for (i in y until bottom) {
for (k in x until right) {
builder.add(Point2D(k, i))
}
}
return builder.build()
}

override fun update(map: GMap, time: Long) {
// do something
}

override fun beKilled(map: GMap, killer: GEntity) {
died = true
}

override fun copy() = BlockEntity(x, y, width, height)

}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
public class BlockEntity implements GEntity {

private boolean died = false;
private int x;
private int y;
private final int width;
private final int height;

public BlockEntity(int x, int y, int width, int height) {
this.x = x;
this.y = y;
this.width = width;
this.height = height;
}

@Override
public void render(@NotNull SafeGraphics graphics) {
graphics.fillRect('#', 0, 0, width, height, -1);
}

@NotNull
@Override
public Stream<Point2D> getCollision(int x, int y, int width, int height) {
Stream.Builder<Point2D> builder = Stream.builder();
int right = x + width;
int bottom = y + height;
for (int i = y; y != bottom; ++y) {
for (int k = x; k != right; ++k) {
builder.add(new Point2D(k, i));
}
}
return builder.build();
}

@Override
public void update(@NotNull GMap map, long time) {
// do something
}

@Override
public void beKilled(@NotNull GMap map, @NotNull GEntity killer) {
died = true;
}

@Override
public boolean getCollisible() {
return true;
}

@Override
public boolean getDied() {
return died;
}

@Override
public int getX() {
return x;
}

@Override
public int getY() {
return y;
}

@Override
public int getWidth() {
return width;
}

@Override
public int getHeight() {
return height;
}

@NotNull
@Override
public GEntity copy() {
return new BlockEntity(x, y, width, height);
}

}

使用画笔

  引擎提供了SafeGraphics类用来绘制图形,其中封装了基础的绘制函数。同时正如其名所说的一样,这个类是一个“安全”的画笔类,它可以确保绘制的内容不会超过为其设定的边界。

  GEntity#render函数会接收一个SafeGraphics的对象,GMap会自动为其设定参数,在实体中使用时,该画笔的绘制坐标是相对于实体本身的,也就是说使用如下代码会在实体的左上角绘制一个#,而非是在地图左上角绘制:

graphics.fillRect(#, 0, 0, 1, 1)

  SafeGraphics中很多函数都提供了缺省参数,不过由于 Java 不支持这个特性,所以如果使用 Java 编写代码,则需要手动填入所有参数。

  至于绘制中所用到的一些概念如下所示:

  • ATTR:
     这是用于控制终端字体颜色、背景颜色等属性的值,所有支持的类型已在ConsolePrinter中列出。需要注意的是,在调用flush函数时,
    同样不会清除上一次设置的 ATTR 信息。

     除 clear 系列函数外的所有函数,传入attr = -1表示无效 attr,打印内容时将忽略 attr 信息。

  • 字符宽度:
     在控制台中,不同字符宽度不同,拉丁文字符宽度为 1,而中文字符宽度为 2,计算字符宽度时,满足char < 0x100的宽度视为 1,否则为 2。

  更多的内容可以查阅ConsolePrinter类中的注释,其中有详细说明。

  一般情况下,我们GMap类会自动为用户生成画笔,并不需要用户自己创建,但是如果想要手动创建画笔对象的话可以参考以下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
fun main() {
GMap.Builder.apply {
width = 80
height = 40
file = File("D:\\Workspace\\jni\\cmake-build-release\\libjni.dll")
}.build().use {
// 创建一个和地图等大的画笔
val g1 = SafeGraphics(it)
// 创建一个绘制起点为 (10, 5),绘制区域大小为 20x10 的画笔,绘制区域受地图尺寸限制
val g2 = SafeGraphics(it, 10, 5, 20, 10)
// 创建一个绘制起点为 (10, 5),绘制区域大小为 20x10 的画笔,绘制区域不受任何限制
val g3 = HalfSafeGraphics(10, 5, 20, 10, ConsolePrinter.index)
// do something
}
GMap.Builder.dispose()
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class Main {

public static void main(String[] args) {
GMap.BuilderJava.INSTANCE
.setWidth(80)
.setHeight(40);
try (GMap map = GMap.Builder.build()) {
// 创建一个和地图等大的画笔
SafeGraphics g1 = SafeGraphics.createSafeGraphics(map);
// 创建一个绘制起点为 (10, 5),绘制区域大小为 20x10 的画笔,绘制区域受地图尺寸限制
SafeGraphics g2 = SafeGraphics.createSafeGraphics(map, 10, 5, 20, 10);
// 创建一个绘制起点为 (10, 5),绘制区域大小为 20x10 的画笔,绘制区域不受任何限制
SafeGraphics g3 = SafeGraphics.createHalfSafeGraphics(10, 5, 20, 10, ConsolePrinter.getIndex());
// do something
}
GMap.Builder.dispose();
}

}

时序控制

  引擎支持用户自己进行时序控制,也支持让引擎接管所有时序控制,调用GMap#start即可使引擎接管所有任务。

  请注意:如果让引擎接管时序控制,逻辑任务和事件任务将不在一个线程中执行,可以使用GMap#runTaskOnLogicThread将一个任务添加到逻辑线程中执行,该函数是线程安全的。

  代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
fun main() {
GMap.Builder.apply {
width = 80
height = 40
}.build().use { map ->
EventListener.registryKeyboardEvent(object : IKeyboardListener {

override fun onPressed(code: Int) {
map.runTaskOnLogicThread {
println("pressed: $code")
true
}
}

override fun onReleased(code: Int) {
map.runTaskOnLogicThread {
println("released: $code")
true
}
}

override fun onActive(code: Int) { }

})
map.start(10, 50) { true }
}
GMap.Builder.dispose()
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public class Main {

public static void main(String[] args) {
GMap.BuilderJava.INSTANCE
.setWidth(80)
.setHeight(40);
try (GMap map = GMap.Builder.build()) {
EventListener.registryKeyboardEvent(new IKeyboardListener() {

@Override
public void onPressed(int code) {
map.runTaskOnLogicThread(() -> {
System.out.println("pressed: $code");
return false;
});
}

@Override
public void onReleased(int code) {
map.runTaskOnLogicThread(() -> {
System.out.println("released: $code");
return false;
});
}

@Override
public void onActive(int code) { }

});
map.start(10, 50, () -> true);
}
GMap.Builder.dispose();
}

}


  以上就是引擎的基本用法,代码注释中我已经尽量详细的描述代码的用法和注意事项,如果还有不懂的地方可以在本篇博文下方发布评论或在 Github 上发布 issue。

创作不易,扫描下方打赏二维码支持一下吧ヾ(≧▽≦*)o