Lab 13:Project 3 交互
介绍#
本实验会帮助你开始项目第二阶段:交互性。希望你在做本实验时,已经在世界生成方面取得了显著进展。我们并不要求你在完成本实验时已经结束第一阶段,但你(希望)应当已经接近完成。
与 Lab 12 一样,本实验不会直接向你的项目贡献任何实际代码;它会帮助你更加熟悉项目所需的有用工具,并教你一些最终可能会使用的编程范式。
记忆游戏#
为了准备制作你的游戏,我们会使用 StdDraw 和 java.util.Random 构建一个简单的记忆游戏。这个游戏很像电子玩具 Simon ↗,不过它在电脑上运行,并使用键盘而不是 4 个彩色按钮。游戏目标是:一个随机生成的目标字符串会逐个字母短暂显示在屏幕上,随后玩家需要把它输入出来。
目标字符串起初只有一个字母;每当玩家成功输入一个字符串后,游戏都会通过加长目标字符串而变得更难。
最终,我们希望 MemoryGame.java 拥有一个能够启动可玩记忆游戏的 main 方法;但与其直接跳进游戏实现,不如先尝试拆解运行游戏所需完成的任务。对于这个记忆游戏,大致如下:
- 创建游戏窗口。
- 随机生成目标字符串。
- 每次一个字符地在屏幕上显示目标字符串。
- 等待玩家输入,直到其输入的字符数量与目标字符串中的字符数量相同。
- 如果玩家输入与目标字符串匹配,则回到第 2 步,但使用一个更长的随机目标字符串;如果不匹配,则打印游戏结束消息并退出。
一般而言,良好的编码实践是:先构建用途明确的小过程,再使用这些基础过程组合出更复杂的方法。最终,你只需要在 main 方法中写几行代码,就能构建出像游戏或文本编辑器这样复杂的东西。查看 MemoryGame.java,你会看到我们已经写好了几个方法头,每个方法会处理上面的一项任务。
对于 Project 3,我们强烈建议你采用这种过程:识别游戏的步骤,并把它拆分为独立方法。它会为你的开发提供一条清晰的前进道路,也会为单元测试提供明确的分界点。
实验结束时,你会得到一个功能类似下面 GIF 的程序:

generateRandomString#
第一个任务:我们需要能够随机生成一个指定长度的字符串。如上面简要提到的那样,应当使用 java.util.Random 完成随机生成。
修改 MemoryGame 构造函数,创建一个 Random 对象,并使用程序的第一个参数作为种子。之后,补全 generateRandomString,使它使用你的 Random 对象生成一个随机字符串,其长度由输入 n 指定。因为我们只希望生成由小写字符组成的字符串,所以为了方便你,我们提供了私有字段 CHARACTERS。
你可以选择直接使用 java.util.Random 的方法,也可以使用 byow.Core.RandomUtils 中有帮助的工具方法。所有工具方法都把一个 Random 对象作为参数,因此它们在底层确实都使用了相应的 java.util.Random 方法;如果你好奇这些工具如何工作,可以查看它们!
在使用 char 和 String 类型的变量时,你可能会遇到问题。下面是关于它们的三个有用知识点:
-
在 Java 中,
Character或char使用单引号包裹,如'B';而String对象使用双引号包裹,如"and can be longer"。 -
可以使用
+运算符把char添加到String中:
javaString favClass = "CS 61" + 'B'; -
可以通过下面的方法把
char转换为String:
javaString B = Character.toString('B');
drawFrame#
现在有了随机字符串,我们需要能够每次一个字母地在屏幕上显示它。为此,我们会使用 Princeton 的 StdDraw 库。它已经位于 javalib 文件夹中,因此如果你通过 pom.xml 正确导入了项目,就能够访问 StdDraw 为我们提供的所有有用方法。与 Lab 12 不同,这一次你会直接使用 StdDraw,而不是依赖瓦片引擎。
我们使用 StdDraw 库,是因为它相当轻量,并且容易上手;但使用它时,你应当了解这个库的一些怪异之处。尤其是,每当我们想更改屏幕上显示的内容时,都必须清除整个屏幕,并重新绘制希望显示的全部内容。因此,拥有这样一个方法会极其有用:它先清除画布,绘制下一帧所需的一切,然后显示画布。
最终,drawFrame 会成为我们的这个方法;但现在先保持简单。我们知道自己需要在屏幕上显示字符串,并且字符串应当足够醒目。补全 drawFrame:让它清除画布,把字体设置为大号粗体(大小 30 很合适),绘制输入字符串并使其位于画布中央,然后把画布显示在屏幕上。现在很适合查看 StdDraw ↗ API,并弄清楚它的确切工作方式。
一些值得查看的有用方法包括:
flashSequence#
使用目前已经构建的内容,我们需要定义一个过程,每次一个字符地呈现目标字符串。编写 flashSequence,让它接收输入字符串,并每次把一个字符显示在屏幕中央。每个字符应当在屏幕上显示 1 秒;字符之间应当有短暂的 0.5 秒间隔,在此期间屏幕为空白。
solicitNCharsInput#
每次一个字符地显示目标字符串后,我们需要等待玩家输入自己的字符串。对于这项任务,我们必须使用 StdDraw 的按键监听 API,读取玩家输入的内容。本实验中相关的方法是 hasNextKeyTyped 和 nextKeyTyped。它们会与 StdDraw 用来存储用户所有已按下并释放的按键的队列交互。
hasNextKeyTyped 会查看队列中是否还留有任何按键;nextKeyTyped 则移除队列前端的按键并返回它。请注意,nextKeyTyped 以 char 的形式返回按键——这是 StdDraw 的另一个怪异之处,并且它使我们无法使用键盘上那些不对应 char 值的按键。
熟悉 StdDraw 如何处理键盘输入后,编写 solicitNCharsInput:使用 StdDraw 读取 n 次按键,并返回与这些按键对应的字符串。此外,随着用户输入按键,目前已经构建出的字符串应当居中显示在屏幕上,让用户能够看到自己已经按下了什么。
一个很不错的附加功能是:允许用户在输入错误时删除字符;但因为 StdDraw 无法处理退格键,我们会直接忽略这个功能,并声称这是为了让游戏更难。
startGame#
就快完成了!现在我们已经定义了所有子过程,是时候把它们组合起来并运行游戏了。startGame 方法应当启动游戏,并开始游戏循环,直到玩家无法正确输入目标字符串。startGame 的代码应当看起来像是把下面这个过程翻译成代码:
- 从第
1轮开始游戏。 - 在屏幕中央显示消息
"Round: ",后面跟上轮数。 - 生成一个随机字符串,其长度等于当前轮数。
- 每次一个字母地显示随机字符串。
- 等待玩家输入一个与目标字符串长度相同的字符串。
- 检查玩家是否输入正确。
- 如果输入正确,把轮数增加
1,然后从第 2 步重复。 - 如果输入错误,结束游戏,并在屏幕中央显示消息
"Game Over! You made it to round:",后面跟上玩家失败时的轮数。
- 如果输入正确,把轮数增加
完成这些内容后,你应当能够运行 MemoryGame.java 并玩自己的游戏!它非常简陋,当然也不漂亮,但下一节会稍微改善这一点。
有帮助的 UI#
此时,你已经有了一个可以玩的、功能正常的游戏,但它的呈现方式绝对算不上令人愉快。最重要的是,如果有人在完全不了解背景的情况下拿到这个游戏,就会很难弄清楚正在发生什么。因为游戏非常简单,我们可以通过在顶部显示一条消息来解决这个问题,告诉玩家应该做什么:"Watch!" 或 "Type!"。
既然已经做到这里,另一个值得显示的内容是当前轮数,让玩家知道目标字符串有多长;并且,因为我们是友善的人,最好还给玩家显示一句鼓励的话。最终,我们希望得到类似下面的内容:

因为这是对用户界面(UI)的更改,所以需要修改 drawFrame,让它具备这一功能。向 drawFrame 方法添加内容,使得只要游戏尚未结束,就在屏幕顶部绘制一个栏,其中包含:左侧显示轮数,即 "Round: " + round number;中央显示当前任务("Watch!" 或 "Type!");右侧显示一句鼓励的话。
对于鼓励语,你应当从某个好话集合中随机选择一句显示。为了方便你,我们提供了包含一些短语的私有字段 ENCOURAGEMENT,但你可以自由添加自己的短语。
提交与评分#
本实验没有 Gradescope 评分器。只要在 4/16 11:59 PM 前提交 Phase 1 Review Form ↗,你就会获得本实验的 64 分。
即使没有评分器,我们仍强烈建议你确保自己理解这里的所有方法,并创建一个完全能够工作的解答。这样,当你在完成 Project 3: BYOW 时对某个渲染方面感到困惑,就可以把本实验当作一个小型示例,快速提醒自己这个库是如何工作的。
原始页面:https://sp21.datastructur.es/materials/lab/lab13/lab13 ↗