文章目錄
- 寫在前面
- 第一版雛形
- 專案目錄
- 主入口程式
- 全域常量
- 動作
- 動作基本類
- 動作生成者
- 動作執行者
- 事件
- 資源加載器
- 互動平臺
寫在前面
我的QQ寵物已經讀大學啦,每天吃得飽飽的,每次回到家,我學習的時候也會讓她也學習,我想著以后大學我一定有更多的時間陪她,可誰知2018年9月15日她卻回到了自己的故鄉,
時間過得也快,轉眼就大三了,這些年也學了不少知識,愛上了動漫《羅小黑戰記》,誰知又要停更三年呢?羅小黑說:“我想和小白一起學讀書!”,好呀~那就來讀書吧!
想過多種語言來撰寫,比如C#、Python、C++,但還是選擇了自己最熟悉的Java,謝謝燕然都護的博客給的思路,打算做一個更完整的桌面寵物,模仿原來QQ寵物的饑餓度、健康值、心情值、金幣系統、學習成長系統,結合番劇、電影的故事背景添加法力值等等等等,最終做成一款可安裝式的C/S架構的游戲,讓喜歡羅小黑戰記的人在三年的等待時間內都可以來陪伴他~
第一版雛形

專案目錄
使用的IDE是JetBrains Intellij IDEA,新建一個普通的Java專案,這個為專案目錄

主入口程式
整個程式從主入口程式進入,下面是HelloHeiApplication類原始碼:
package org.taibai.hellohei;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.AnchorPane;
import javafx.stage.Stage;
import javafx.stage.StageStyle;
import org.taibai.hellohei.constant.Constant;
import org.taibai.hellohei.event.GlobalEventListener;
import org.taibai.hellohei.img.ResourceGetter;
import org.taibai.hellohei.ui.InterfaceFunction;
import java.io.IOException;
public class HelloHeiApplication extends Application {
/**
* 展示圖片的視窗
*/
private ImageView imageView;
private AnchorPane pane;
private InterfaceFunction interfaceFunction;
/**
* 全域事件監聽,目前支持拖拽、左鍵點擊反饋
*/
private GlobalEventListener globalEventListener;
private final ResourceGetter resourceGetter = ResourceGetter.newInstance();
@Override
public void start(Stage primaryStage) throws IOException {
primaryStage.initStyle(StageStyle.UTILITY);
primaryStage.setOpacity(0); // 設定父級透明度為0
Stage stage = new Stage();
stage.initOwner(primaryStage); // 將 primaryStage 設定為歸屬物件,即父級視窗
initImageView();
// 互動功能平臺
interfaceFunction = new InterfaceFunction(stage, imageView);
// 面板
pane = new AnchorPane(interfaceFunction.getMessageBox(), interfaceFunction.getImageView());
pane.setStyle("-fx-background:transparent;");
// 開啟全域事件
globalEventListener = new GlobalEventListener(stage, imageView, pane);
initStage(stage);
primaryStage.show();
stage.show();
interfaceFunction.setTray(stage); //添加系統托盤
}
public static void main(String[] args) {
launch(args);
}
private void initImageView() {
Image image = resourceGetter.get(Constant.ImageShow.mainImage);
this.imageView = new ImageView(image);
imageView.setX(0);
imageView.setY(0);
imageView.setLayoutX(0);
imageView.setLayoutY(50);
imageView.setFitHeight(Constant.ImageShow.ImageHeight); // 設定圖片顯示的大小
imageView.setFitHeight(Constant.ImageShow.ImageWidth);
imageView.setPreserveRatio(true); // 保留width:height比例
imageView.setStyle("-fx-background:transparent;"); // 透明背景
}
private void initStage(Stage stage) {
Scene scene = new Scene(pane, 400, 400);
scene.setFill(null);
scene.getStylesheets().add(getClass().getResource("application.css").toExternalForm());
stage.setScene(scene);
// 設定表單的初始位置
stage.setX(850);
stage.setY(400);
stage.setAlwaysOnTop(true);// 視窗總顯示在最前
// 修改任務欄圖示
stage.getIcons().add(resourceGetter.get(Constant.ImageShow.iconImage));
stage.initStyle(StageStyle.TRANSPARENT);// 背景透明
stage.setOnCloseRequest(event -> {
event.consume();
interfaceFunction.exit();
});
}
}
依次說明一下:
- primaryStage并非正在的stage,在start方法中又新建了一個
Stage實體,并且讓primaryStage隱藏,這樣做的目的是就不會在任務欄里面顯示行程了,否則強迫癥患者會很難受的, ImageView將作為整個程式展示的視窗,一系列動作也只是替換gif圖片罷了- 互動平臺
InterfaceFunction提供了用戶的一系列互動動作,例如顯示隱藏、退出、切換狀態,以后的各種功能也將在互動平臺擴展 - 全域事件監聽者
GlobalEventListener:考慮到各事件之間可能會相互干擾,于是開了一個類去集中管理,意在解決重復觸發點擊事件、拖動時不觸發點擊事件等問題,這樣也讓start方法更輕便一些, - 最后將互動平臺加入系統托盤,于是你可以在任務欄里像找到QQ程式一樣找到
小黑后臺
全域常量
雖然全域常量不太好,目前功能單一,全域常量有助于除錯,希望在后面能選擇更好的解決方案
package org.taibai.hellohei.constant;
/**
* <p>Creation Time: 2021-09-21 18:00:49</p>
* <p>Description: 各種常量,集中管理</p>
*
* @author 太白
*/
public class Constant {
public static class ImageShow {
/**
* 主體的長與寬
*/
public static final int ImageHeight = 100;
public static final int ImageWidth = 100;
public static final String mainImage = "/org/taibai/hellohei/img/licking the claw.gif";
public static final String byeImage = "/org/taibai/hellohei/img/bye.gif";
public static final String iconImage = "/org/taibai/hellohei/img/icon.png";
public static final String guitarImage = "/org/taibai/hellohei/img/playing guitar.gif";
}
public static class UserInterface {
/**
* 互動時間,例如點擊羅小黑會回應一個動作,該動作持續RunTime
*/
public static final int RunTime = 3;
/**
* 碎碎念
*/
public static final String[] selfTalking = {
"嘿咻~",
"點我~",
"小白,這個字怎么念呀",
"想吃甘蔗了……",
"在干嘛呢~"
};
}
}
動作
動作基本類
一個動作應該有如下屬性
- path: 該動作是什么
- time: 執行多少時間
- isTemporaryAction: 是否是暫時的,比如點擊后觸發的動作是展示顯示的,而恢復到默認狀態是持續的
- recoverPath: 如果是暫時的那么應該恢復到什么動作
- interruptable: 是否可中斷的,例如退出影片是不可中斷的,而在做普通影片是可中斷的,這樣退出影片就得以顯示
package org.taibai.hellohei.ui;
/**
* <p>Creation Time: 2021-09-22 11:49:27</p>
* <p>Description: 動作</p>
*
* @author 太白
*/
public class Action {
/**
* 當前動作
*/
private final String path;
/**
* 動作維持時間,如果為-1則保持該動作
*/
private final double time;
/**
* 是否是暫時的動作
*/
private final boolean isTemporaryAction;
/**
* 如果是暫時的動作,則應當在該時間內恢復到這個動作
*/
private String recoverPath;
/**
* 是否可中斷
*/
private final boolean interruptable;
/**
* 若動作是持續的,則維持時間為 PerpetualTime
*/
public static final double PerpetualTime = -1.0;
private Action(String path, double time, boolean isTemporaryAction, String recoverPath, boolean interruptable) {
this.path = path;
this.time = time;
this.isTemporaryAction = isTemporaryAction;
this.recoverPath = recoverPath;
this.interruptable = interruptable;
}
private Action(String path, double time, boolean isTemporaryAction, boolean interruptable) {
this.path = path;
this.time = time;
this.isTemporaryAction = isTemporaryAction;
this.interruptable = interruptable;
}
/**
* 創建暫時的、可中斷的動作
*
* @param path 動作路徑
* @param time 持續時間
* @param recoverPath 恢復動作路徑
* @return 創建的動作實體
*/
public static Action creatTemporaryInterruptableAction(String path, double time, String recoverPath) {
return new Action(path, time, true, recoverPath, true);
}
/**
* 創建持續的、可中斷的動作
*
* @param path 動作路徑
* @return 創建的動作實體
*/
public static Action creatContinuousInterruptableAction(String path) {
return new Action(path, PerpetualTime, false, true);
}
/**
* 創建短暫的、不可中斷的動作,例如退出影片
*
* @param path 動作路徑
* @param time 持續時間
* @param recoverPath 恢復動作路徑
* @return 創建的動作實體
*/
public static Action creatTemporaryUninterruptibleAction(String path, double time, String recoverPath) {
return new Action(path, time, true, recoverPath, false);
}
/**
* 創建持續的、不可中斷的動作,比較苛刻展示想不到案例
*
* @param path 動作路徑
* @return 創建的動作實體
*/
public static Action creatContinuousUninterruptibleAction(String path) {
return new Action(path, PerpetualTime, false, false);
}
public String getPath() {
return path;
}
public double getTime() {
return time;
}
public boolean isTemporaryAction() {
return isTemporaryAction;
}
public String getRecoverPath() {
return recoverPath;
}
public boolean isInterruptable() {
return interruptable;
}
}
并且采納《Effective Java》“隱藏”了建構式,并且提供公開的介面來構造四種型別的動作,分別是
creatTemporaryInterruptableAction:暫時的、可中斷的動作creatContinuousInterruptableAction:持續的、可中斷的動作creatTemporaryUninterruptibleAction:暫時的、不可中斷的動作creatContinuousUninterruptibleAction:持續的、不可中斷的動作
動作生成者
一個動作的產生是隨機的,如果放在動作類或者動作執行類不太妥當,因此將其獨立管理,構建了一個名為ActionGenerator的動作生成者類
package org.taibai.hellohei.ui;
import org.taibai.hellohei.constant.Constant;
import java.util.HashMap;
import java.util.Map;
/**
* <p>Creation Time: 2021-09-21 18:15:02</p>
* <p>Description: 獲取一個新的互動動作以及互動動作的關閉</p>
*
* @author 太白
*/
public class ActionGenerator {
/**
* 動作編號
*/
private int actionIndex = NoAction;
private static final Map<Integer, String> resource = new HashMap<Integer, String>() {{
put(1, Constant.ImageShow.guitarImage);
}};
private static final int MinIndex = 1;
private static final int MaxIndex = 1;
public static final int NoAction = 0;
/**
* 隨機生成一個動作編號,這里當動作編號不為0時說明動作還未結束
*
* @return 當且僅當上一個動作未結束時,回傳false,且不生成新動作
*/
public boolean generateNewActionIndex() {
if (actionIndex != NoAction) return false;
actionIndex = (int) (Math.random() * (MaxIndex - MinIndex + 1) + MinIndex);
return true;
}
/**
* 結束動作時必須呼叫該API,約定的
*
* @return 是否關閉,若早已關閉也回傳false
*/
public void close() {
actionIndex = NoAction;
}
/**
* 獲得動作的GIF資源
*
* @return 動作GIF資源檔案相對路徑
*/
public String getActionPath() {
if (resource.containsKey(actionIndex))
return resource.get(actionIndex);
return null;
}
}
這里約定一個動作的開啟必須要關閉,不關閉將不會再生成動作,
動作執行者
動作的執行是互相影響的,例如連續點擊不應該連續觸發動作等,因此將其獨立出來,構建了一個動作執行者類ActionExecutor
package org.taibai.hellohei.ui;
import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.util.Duration;
import org.taibai.hellohei.constant.Constant;
import org.taibai.hellohei.img.ResourceGetter;
/**
* <p>Creation Time: 2021-09-22 11:49:12</p>
* <p>Description: 動作執行者</p>
*
* @author 太白
*/
public class ActionExecutor {
private ImageView imageView;
private Action curAction;
private final ResourceGetter resourceGetter = ResourceGetter.newInstance();
private final ActionGenerator actionGenerator = new ActionGenerator();
private static ActionExecutor actionExecutor;
private Timeline timeline;
public static ActionExecutor newInstance(ImageView imageView) {
if (actionExecutor == null) actionExecutor = new ActionExecutor(imageView);
return actionExecutor;
}
private ActionExecutor(ImageView imageView) {
this.imageView = imageView;
}
public boolean execute(Action action) {
// 如果上一個動作不可中斷,那么動作執行失敗
if (curAction != null && !curAction.isInterruptable()) return false;
Image actionImage = resourceGetter.get(action.getPath());
imageView.setImage(actionImage);
curAction = action;
if (timeline != null) timeline.pause();
// 如果當前動作是暫時的,則還需要恢復到某一個動作
if (action.isTemporaryAction()) {
timeline = new Timeline(new KeyFrame(Duration.seconds(action.getTime()), e -> executeContinuousInterruptableActionAction(action.getRecoverPath())));
timeline.play();
}
return true;
}
public boolean executeClickAction() {
boolean ok = actionGenerator.generateNewActionIndex();
if (ok) {
execute(Action.creatTemporaryInterruptableAction(
actionGenerator.getActionPath(),
Constant.UserInterface.RunTime,
Constant.ImageShow.mainImage));
}
return ok;
}
/**
* 立即執行一個可中斷的、持續的動作
*/
private void executeContinuousInterruptableActionAction(String path) {
curAction = null;
timeline = null;
actionGenerator.close();
Action action = Action.creatContinuousInterruptableAction(path);
execute(action);
}
}
動作的執行是影響全域的,因此將其設計為單例模式,這樣全域拿到的就是同一個物件,所產生的影響也是全域同步的,
事件
事件起初遇到了點麻煩,就是拖動也會觸發點擊事件,如果在主入口程式設定會很繁瑣,因此我將事件管理劃到了一個類中,這樣拖動時記錄初始坐標,松開滑鼠時只需要判斷坐標值是不是一樣的,如果是一樣的就說明在原地,執行點擊事件(就是逗小黑玩),雖然有可能拖動到同一個地方,但用戶既然要拖拽肯定是想移動一個位置,所以不大可能回到原來的位置(就算故意移回到原位也很困難是吧~)
package org.taibai.hellohei.event;
import javafx.scene.image.ImageView;
import javafx.scene.layout.AnchorPane;
import javafx.stage.Stage;
import org.taibai.hellohei.constant.Constant;
import org.taibai.hellohei.img.ResourceGetter;
import org.taibai.hellohei.ui.Action;
import org.taibai.hellohei.ui.ActionExecutor;
import org.taibai.hellohei.ui.ActionGenerator;
/**
* <p>Creation Time: 2021-09-22 12:50:52</p>
* <p>Description: 全域事件監聽者</p>
*
* @author 太白
*/
public class GlobalEventListener {
private final Stage stage;
private final ImageView imageView;
private final AnchorPane anchorPane;
/**
* 動作執行者,觸發的動作需要托付給動作執行者執行
*/
private final ActionExecutor actionExecutor;
private double xOffset = 0;
private double yOffset = 0;
private double preScreenX = 0;
private double preScreenY = 0;
public GlobalEventListener(Stage stage, ImageView imageView, AnchorPane anchorPane) {
this.stage = stage;
this.imageView = imageView;
this.anchorPane = anchorPane;
this.actionExecutor = ActionExecutor.newInstance(imageView);
enableDrag();
enableClick();
}
/**
* 激活拖動
*/
private void enableDrag() {
anchorPane.setOnMousePressed(e -> {
xOffset = e.getSceneX();
yOffset = e.getSceneY();
});
anchorPane.setOnMouseDragged(e -> {
stage.setX(e.getScreenX() - xOffset);
stage.setY(e.getScreenY() - yOffset);
});
}
/**
* 點擊隨機觸發一個動作
*/
private void enableClick() {
imageView.setOnMousePressed(e -> {
preScreenX = e.getScreenX();
preScreenY = e.getScreenY();
});
imageView.setOnMouseReleased(e -> {
if (e.getScreenX() == preScreenX && e.getScreenY() == preScreenY) {
actionExecutor.executeClickAction();
}
});
}
}
資源加載器
整個程式的運作都需要GIF圖片的顯示,因此需要用一個類去加載GIF圖片,這里使用類級別與屬性級別的單例模式,降低了創建類所需要的時間,當然如果使用HashMap容易導致記憶體泄漏,因此使用WeekHashMap
擴展閱讀 WeekHashMap
和HashMap一樣,WeakHashMap 也是一個散串列,它存盤的內容也是鍵值對(key-value)映射,而且鍵和值都可以是null,不過WeakHashMap的鍵是“弱鍵”,在 WeakHashMap 中,當某個鍵不再正常使用時,會被從WeakHashMap中被自動移除,更精確地說,對于一個給定的鍵,其映射的存在并不阻止垃圾回收器對該鍵的丟棄,這就使該鍵成為可終止的,被終止,然后被回收,某個鍵被終止時,它對應的鍵值對也就從映射中有效地移除了,
package org.taibai.hellohei.img;
import javafx.scene.image.Image;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.WeakHashMap;
/**
* <p>Creation Time: 2021-09-21 18:35:46</p>
* <p>Description: 資源加載器</p>
*
* @author 太白
*/
public class ResourceGetter {
private static final Map<String, Image> images = new WeakHashMap<>();
private static ResourceGetter singleton;
public static ResourceGetter newInstance() {
if (singleton == null) singleton = new ResourceGetter();
return singleton;
}
private ResourceGetter() {
}
public Image get(String path) {
if (!images.containsKey(path)) {
images.put(path, new Image(Objects.requireNonNull(this.getClass().getResourceAsStream(path))));
}
return images.get(path);
}
}
互動平臺
目前的互動功能僅僅只有碎碎念、顯示隱藏,希望后面能擴充點功能,互動平臺開啟一個執行緒,隨機事件后觸發一次互動功能,比如開啟碎碎念功能后,將在隨機事件后彈出訊息框,
package org.taibai.hellohei.ui;
import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.application.Platform;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.control.Label;
import javafx.scene.image.ImageView;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import javafx.scene.shape.Polygon;
import javafx.stage.Stage;
import javafx.util.Duration;
import org.taibai.hellohei.constant.Constant;
import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.util.Objects;
import java.util.Random;
/**
* <p>Creation Time: 2021-09-21 19:00:47</p>
* <p>Description: 互動功能</p>
*
* @author 太白
*/
public class InterfaceFunction {
private final ImageView imageView;
private final ActionExecutor actionExecutor;
private final Stage stage;
private VBox messageBox;
private CheckboxMenuItem itemSay = new CheckboxMenuItem("碎碎念");
private final String greet = "好久不見鴨,想你了~";
public InterfaceFunction(Stage stage, ImageView imageView) {
this.stage = stage;
this.imageView = imageView;
this.actionExecutor = ActionExecutor.newInstance(imageView);
this.messageBox = new VBox();
initMessage();
say(greet, 8);
// 開啟隨機事件
RandomEvent randomEvent = new RandomEvent();
new Thread(randomEvent).start();
}
/**
* 初始化訊息框
*/
private void initMessage() {
Label bubble = new Label();
//設定氣泡的寬度,如果沒有這句,就會根據內容多少來自適應寬度
bubble.setPrefWidth(100);
bubble.setWrapText(true); //自動換行
bubble.setStyle("-fx-background-color: rgba(255,255,255,0.7); -fx-background-radius: 8px;");
bubble.setPadding(new Insets(7)); //標簽的內邊距的寬度
bubble.setFont(new javafx.scene.text.Font(14));
bubble.setTextFill(Color.web("#000000"));
Polygon triangle = new Polygon(0.0, 0.0, 8.0, 10.0, 16.0, 0.0);//分別設定三角形三個頂點的X和Y
triangle.setFill(new Color(1, 1, 1, 0.7));
// VBox.setMargin(triangle, new Insets(0, 50, 0, 0));//設定三角形的位置,默認居中
messageBox.getChildren().addAll(bubble, triangle);
messageBox.setAlignment(Pos.BOTTOM_CENTER);
messageBox.setStyle("-fx-background:transparent;");
//設定相對于父容器的位置
messageBox.setLayoutX(0);
messageBox.setLayoutY(0);
messageBox.setVisible(true);
}
/**
* 退出
*/
public void exit() {
// 展示告別影片
double time = 1.5;
actionExecutor.execute(Action.creatTemporaryUninterruptibleAction(Constant.ImageShow.byeImage, time, Constant.ImageShow.mainImage));
// 要用Platform.runLater,不然會報錯Not on FX application thread;
Platform.runLater(() -> say("再見~", Constant.UserInterface.SayingRunTime));
// 影片結束后執行退出
new Timeline(new KeyFrame(
Duration.seconds(time),
ae -> System.exit(0)))
.play();
}
/**
* 說一句話
*
* @param msg 訊息
* @param duration 持續時間
*/
public void say(String msg, int duration) {
Label lbl = (Label) messageBox.getChildren().get(0);
lbl.setText(msg);
messageBox.setVisible(true);
//設定氣泡的顯示時間
new Timeline(new KeyFrame(
Duration.seconds(duration),
ae -> {
messageBox.setVisible(false);
}))
.play();
}
/**
* 添加系統托盤
*
* @param stage 舞臺
*/
public void setTray(Stage stage) {
SystemTray tray = SystemTray.getSystemTray();
//托盤圖示
BufferedImage image;
try {
// 為托盤添加一個右鍵彈出選單
PopupMenu popMenu = new PopupMenu();
popMenu.setFont(new Font("微軟雅黑", Font.PLAIN, 14));
MenuItem itemShow = new MenuItem("顯示");
itemShow.addActionListener(e -> Platform.runLater(() -> stage.show()));
MenuItem itemHide = new MenuItem("隱藏");
// 要先setImplicitExit(false),否則stage.hide()會直接關閉stage
// stage.hide()等同于stage.close()
itemHide.addActionListener(e -> {
Platform.setImplicitExit(false);
Platform.runLater(stage::hide);
});
MenuItem itemExit = new MenuItem("退出");
itemExit.addActionListener(e -> exit());
popMenu.add(itemSay);
popMenu.addSeparator();
popMenu.add(itemShow);
popMenu.add(itemHide);
popMenu.add(itemExit);
//設定托盤圖示
image = ImageIO.read(Objects.requireNonNull(getClass().getResourceAsStream(Constant.ImageShow.iconImage)));
TrayIcon trayIcon = new TrayIcon(image, "小黑", popMenu);
trayIcon.setToolTip("小黑");
trayIcon.setImageAutoSize(true);//自動調整圖片大小,這步很重要,不然顯示的是空白
tray.add(trayIcon);
} catch (IOException | AWTException e) {
e.printStackTrace();
}
}
public ImageView getImageView() {
return imageView;
}
public VBox getMessageBox() {
return messageBox;
}
class RandomEvent implements Runnable {
@Override
public void run() {
while (true) {
Random rand = new Random();
//隨機發生自動事件,以下設定間隔為9~24秒,要注意這個時間間隔包含了影片播放的時間
long time = (rand.nextInt(15) + 10) * 1000;
if (itemSay.getState()) {
//隨機選擇要說的話,因為目前只有兩個寵物,所以可以用三目運算子
String str = Constant.UserInterface.selfTalking[rand.nextInt(5)];
Platform.runLater(() -> say(str, Constant.UserInterface.SayingRunTime));
}
try {
Thread.sleep(time);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
之后功能還會繼續擴充,苦命考研狗,先去學習了~
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/302506.html
標籤:java

