相信閱讀過上期文章,動手能力強的朋友們已經自己跑出來介面了。所以這期我要講的是互動部分,也就是對於滑鼠點選事件的響應,包括計時計數對點選事件以及一些狀態量的影響。
回憶下第一期介紹的掃雷規則和操作,遊戲從開局到結束可能會涉及到哪些情況呢?我認為比較重要的就是明確什麼情況下游戲已經結束,結束代表的是勝利還是失敗。對此我定義了一個遊戲狀態量,他有位置、勝利和失敗三種可選值,如下:
// 遊戲狀態相關 [1:獲勝, 0:未知, -1:失敗]
public static byte WIN = 1;
public static byte UNSURE = 0;
public static byte LOSS = -1;
public static byte STATE = UNSURE;
很顯然遊戲只要還未結束,就應該保持在未知狀態。那麼哪些情況會影響到狀態量的取值,就需要我們逐個分析了。
根據規則,當我們把除地雷以外的所有格子均點開後便取得勝利,所以右鍵點選並不會對遊戲狀態造成影響。那我們僅需在每次左鍵點選處理中進行格子數統計,符合要求就修改遊戲狀態為勝利,點選到地雷便修改為失敗。另外每次點選都需要更新相關格子的顯示,所以這兩項任務可以放在一起進行,做法如下:
// 更新點選過的資料
mineSweeper.clickCell(row, column);
執行完後就對遊戲狀態進行判斷,如果沒有點選到地雷,執行 STATE == UNSURE 部分:
if (STATE == UNSURE) {
// 統計非雷格子已點開數目
int count = 0;
for (int i = 0; i < GAME.height; ++i) {
for (int j = 0; j < GAME.width; ++j) {
if (map[i][j] > BOUND) {
Button btn = (Button) buttons.get(i * GAME.width + j);
count += 1;
int value = map[i][j] - 100;
if (value != BLANK) {
// 消除空白填充
btn.setPadding(new Insets(0.0));
// 設定粗體和字型顏色
btn.setFont(Font.font("Arial", FontWeight.BOLD, GAME.numSize));
btn.setTextFill(NUMS[value - 1]);
btn.setText(value + "");
}
btn.setStyle("-fx-border-color: #737373; -fx-opacity: 1; -fx-background-color: #ffffff");
btn.setDisable(true);
}
}
}
// 判斷全部非雷格子是否全部點開
if (count + GAME.bomb == GAME.width * GAME.height) {
STATE = WIN;
}
}
否則執行 STATE == LOSS 部分:
if (STATE == LOSS) {
// 遊戲失敗, 顯示所有地雷位置
for (int i = 0; i < GAME.height; ++i) {
for (int j = 0; j < GAME.width; ++j) {
if (map[i][j] == BOMB) {
Button btn = (Button) buttons.get(i * GAME.width + j);
btn.setStyle("-fx-background-color:#ffffff; -fx-background-size: contain; -fx-background-image: url(" + UNEXPLODED_IMG + ")");
}
}
}
button.setStyle("-fx-background-color:#ffffff; -fx-background-size: contain; -fx-background-image: url(" + EXPLODED_IMG + ")");
}
看上去似乎所有任務都完成了,真的是這樣嗎?別忘了還有計時功能,時間超出指定範圍也可以認為是遊戲失敗。上期說過計時計數這塊有自定義控制元件,這期它依舊不是主角,但是我會大致說明下它的工作方式。如果你還記得遊戲介面那兩個黑框框是GridPane佈局的話,顯示出的數位就是其中的控制元件外觀。我使用的是三位數,也就是說每個佈局中都含有三個數位自定義控制元件,根據數值不同排列組合表示不同整數。
首先來講計時,這裡JavaFX提供的有時間軸類,直接拿來用非常方便。我們可以設定事件觸發的間隔,對應到掃雷裡自然是每秒觸發一次。事件中要做的就是判斷遊戲狀態和是否超時,下面給出程式碼以供參考。
涉及到的量:
// 時間計數和超時範圍
public static int TIMER = 0;
public static int OVERTIME = 999;
// 計時器
public static Timeline TIMELINE = null;
計時事件:
TIMELINE = new Timeline(
new KeyFrame(Duration.seconds(1), event -> {
TIMER += 1;
// 超時自動判負
if (TIMER >= OVERTIME) {
STATE = LOSS;
}
// 遊戲勝負已確定
if (STATE != UNSURE) {
String path = WIN_IMG;
TIMELINE.stop();
if (STATE == LOSS) {
path = LOSS_IMG;
} else {
// 自定義模式不計入成績
if (GAME != GameEnum.CUSTOM) {
Platform.runLater(() -> showDialog());
}
}
reset.setStyle("-fx-background-size: contain; -fx-background-image: url(" + path + ")");
}
ledTime[0].switchSkin(TIMER / 100);
ledTime[1].switchSkin(TIMER % 100 / 10);
ledTime[2].switchSkin(TIMER % 10);
})
);
接下來是計數功能,數位顯示原理同上,主要是互動。這個數位表示的是遊戲中剩餘可用標記數 REST_FLAG,它的值通過左右鍵點選改變。它的改變規則具體如下:
- 該數值初始大小等於地雷數目。
- 右鍵點選未知格子時,如果先前沒有標記,那麼值減去1,標記旗幟;如果已有旗幟標記,值不變,替換為問號標記;如果已有問號標記,值加上1,去除格子上的標記。
- 左鍵點選有標記的格子時,不管是哪種標記,值統統加上1,去除標記。
接下來需要考慮如何監聽 REST_FLAG 值的變化,通過查閱資料,我找到了一種方案 ReadOnlyIntegerWrapper。該類提供了一個方便的類來定義唯讀屬性。它建立兩個同步的屬性。一個屬性是唯讀的,可以傳遞給外部使用者。另一個屬性是可讀寫的,只能在內部使用。最重要的是可以對它設定監聽器,在值發生變化時執行一些操作,實現如下:
// 建立具有可觀察特性的整數變數
rest = new ReadOnlyIntegerWrapper(REST_FLAG);
// 新增監聽器, 在變數值變化時執行相應的操作, 下同
ChangeListener<? super Number> restListener = (observable, oldValue, newValue) -> {
// 在變數值變化時執行相應的操作
ledMark[0].switchSkin(REST_FLAG / 100);
ledMark[1].switchSkin(REST_FLAG % 100 / 10);
ledMark[2].switchSkin(REST_FLAG % 10);
};
// 將監聽器繫結到rest屬性
rest.addListener(restListener);
這些工作完成後,我們再來考慮一個有關計時的問題。什麼時機開始計時較為合適呢?是進入遊戲介面,還是第一次點選格子?我認為後者更符合要求。當然這個全看個人設計,如果採用後者的方案的話,也需要設定對應的值來監聽,比如下面這種:
// 遊戲是否開局, 即格子是否被點選過 [1:是, 0:否]
public static int YES = 1;
public static int NO = 0;
public static int CLICKED = NO;
然後把上邊提到的監聽事件與之結合:
clicked = new ReadOnlyIntegerWrapper(CLICKED);
ChangeListener<? super Number> clickListener = (observable, oldValue, newValue) -> {
// 已經被點選, 開始計時
TIMER = 0;
// TODO 這裡放入計時監聽事件
TIMELINE.setCycleCount(Animation.INDEFINITE);
TIMELINE.play();
};
clicked.addListener(clickListener);
值發生變化後需要手動呼叫set方法觸發監聽:
// 判斷遊戲是否開局
if (CLICKED == NO) {
CLICKED = YES;
clicked.set(CLICKED);
}
// 觸發監聽, 修改剩餘地雷數顯示
rest.set(REST_FLAG);
截止到這裡,有關遊戲部分就只剩下排行榜功能未介紹了。至於鴿了好幾期都沒說的自定義控制元件,因為我覺得它的實現並不重要,瞭解它的作用一樣能理解前邊的內容,所以就放在最後一期再說吧。
——————————————我———是———分———割———線—————————————
我居然更到第三期了哎,一週之內呀!太勤快了吧!不行,最多再更兩期,我要報仇雪恨般地拖更,拖拖拖拖拖拖拖一拖到明年,大好時光怎麼能天天用來碼文呢?我要打電動去啦,阿偉也攔不住,我說的!