当前位置:首页 » 《休闲阅读》 » 正文

桌面宠物开发——罗小黑(二)_白宝库

16 人参与  2022年02月03日 15:07  分类 : 《休闲阅读》  评论

点击全文阅读


文章目录

  • 前言
  • 第二版概述
  • 右键菜单功能实现
    • 唤醒右键菜单类
    • 右键菜单控制器类
  • 物品展示菜单栏
    • 物品展示菜单栏UI设计
    • 物品展示菜单栏控制器类
  • 物品大家族
    • 物品枚举类(全局变量的更好的选择)
    • 物品实体类
    • 物品的仓库(全局唯一,单例模式)
  • 状态家族
    • 心情值状态类
    • 体力值状态类
    • 总状态(全局唯一,单例模式)
  • 一点点优化
  • 后话

前言

我是一个喜欢打开设置界面一个个功能尝试的人,以前试着设置了CSDN的消息通知是每天一次的,心想“反正也没什么人关注,设置了和没设置一样。”
23日我收到邮件说有人给我留言啦,我挺开心地,点开页面一看,猛地发现我居然有600+的未读消息,可把我给吓坏了
请添加图片描述
真的特别感谢大家的关注(*^▽^*)!这也给了我很大的动力,所以最近几天也没闲着,加班加点把功能一点点完善。
很多人想用demo尝尝鲜,我怕大家会失望所以没有打包成安装包,因为第一版真的特别特别简陋
请添加图片描述
如果真的想运行,我当然毫不吝啬,只是没有打包成可执行文件,还需要自己配置Java环境运行哦,项目的所有文件我都放在GitHub上了,点击访问 => Jiang-TaiBai/IXiaoHei <=

第二版概述

大致上做了以下内容:

  1. 右键菜单
  2. 设置了状态类(心情、体力、清洁度等)
  3. 用具仓库
  4. 用具的使用效果
  5. 优化了之前的代码,并且设置了更多的单例模式(不知道这样做好不好,容易造成内存泄漏,不过代码写起来更爽哈哈)

下图是预览图,不知道为什么CSDN上播放着卡卡的,可惜视频插不了(得要已上传到其他平台的视频链接)
请添加图片描述
界面设计地不够好,本人想着前期先把功能完善吧,等功能确定了,再沉下心来把界面整理一遍(找素材真的特别难!!!)
请添加图片描述

右键菜单功能实现

在这里插入图片描述
基本思想就是当对罗小黑所在的ImageView这个Node右键的时候,打开一个FXML渲染的界面,该FXML界面又得用一层nominalStage(找不到好的命名的,直译是名义上的舞台),这样就避免了在任务栏上多一个小logo。
该FXML设置很多按钮,每一个按钮对应着Controller的方法。

唤醒右键菜单类

package org.taibai.hellohei.menu;

import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.event.EventType;
import javafx.fxml.FXMLLoader;
import javafx.geometry.Rectangle2D;
import javafx.scene.Node;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.input.KeyEvent;
import javafx.scene.paint.Color;
import javafx.stage.Popup;
import javafx.stage.Screen;
import javafx.stage.Stage;
import javafx.stage.StageStyle;
import org.taibai.hellohei.controller.ContextMenuController;
import java.io.IOException;


/**
 * <p>Creation Time: 2021-09-25 04:36:35</p>
 * <p>Description: 点击本体触发的右键菜单</p>
 *
 * @author 太白
 */
public class ContextMenu {

    private static ContextMenu contextMenu;

    private ContextMenu() {

    }

    public static ContextMenu getInstance() {
        if (contextMenu == null) contextMenu = new ContextMenu();
        return contextMenu;
    }

    public void show(Node node, double screenX, double screenY) {
        // ====== 设置名义上的stage,避免在任务栏生成一个小窗口 ======
        final Stage nominalStage = new Stage();
        nominalStage.initStyle(StageStyle.UTILITY);
        nominalStage.setOpacity(0);
        final Stage stage = new Stage();
        stage.initOwner(nominalStage);
        stage.initStyle(StageStyle.TRANSPARENT);    // 设定窗口透明且无边框
        stage.setAlwaysOnTop(true);                 // 设置窗口总显示在最前

        // ====== 设置菜单出现的位置,默认出现在光标的右下方,但是有两种超出屏幕边缘的情况 ======
        Rectangle2D screenRectangle = Screen.getPrimary().getBounds();
        double screenWidth = screenRectangle.getWidth();
        double screenHeight = screenRectangle.getHeight();
        double stageWidth = 138.0;
        double stageHeight = 280.0;
        // 如果弹出的右键菜单超出屏幕下边缘,就向上展开
        if(screenY + stageHeight > screenHeight) screenY -= stageHeight;
        // 如果弹出的右键菜单超出屏幕右边缘,就向左展开
        if(screenX + stageWidth > screenWidth) screenX -= stageWidth;
        stage.setX(screenX);
        stage.setY(screenY);

        // ====== 获得fxml文件 ======
        FXMLLoader loader = new FXMLLoader(getClass().getResource("/org/taibai/hellohei/fxml/ContextMenu.fxml"));
        Parent root = null;
        try {
            root = loader.load();
        } catch (IOException e) {
            e.printStackTrace();
        }

        // ====== 获得控制器实例 ======
        ContextMenuController controller = loader.getController();   //获取Controller的实例对象
        controller.Init(stage, screenX, screenY);

        // ====== 在stage中装入scene,并为scene设置css样式 ======
        Scene scene = new Scene(root);
        scene.setFill(Color.TRANSPARENT);
        stage.setScene(scene);
        scene.getStylesheets().addAll(this.getClass().getResource("/org/taibai/hellohei/fxml/ContextMenu.css").toExternalForm());

        // ====== 当失去焦点的时候设置隐藏stage ======
        stage.focusedProperty().addListener((observable, oldValue, newValue) -> {
            if (!stage.isFocused()) {
                stage.close();
            }
        });

        // ====== 展示菜单 ======
        nominalStage.show();
        stage.show();
    }

}

右键菜单的位置

这个类叫做【唤醒右键菜单类】,是通过点击事件触发的,因此可以获得鼠标点击时候光标在屏幕上的坐标,所以只要设置城右键菜单左上角坐标就行。
当然啦,就像我们在桌面上右击一样,如果右键菜单超出了屏幕就应当在另一侧出现,这里用如下代码解决了这个需求:

// ====== 设置菜单出现的位置,默认出现在光标的右下方,但是有两种超出屏幕边缘的情况 ======
Rectangle2D screenRectangle = Screen.getPrimary().getBounds();
double screenWidth = screenRectangle.getWidth();
double screenHeight = screenRectangle.getHeight();
double stageWidth = 138.0;
double stageHeight = 280.0;
// 如果弹出的右键菜单超出屏幕下边缘,就向上展开
if(screenY + stageHeight > screenHeight) screenY -= stageHeight;
// 如果弹出的右键菜单超出屏幕右边缘,就向左展开
if(screenX + stageWidth > screenWidth) screenX -= stageWidth;
stage.setX(screenX);
stage.setY(screenY);

关闭右键菜单

这个功能可献祭了我十几根头发
茂密

搜遍了全网找了无数种解决方法,终于给解决了,网上JavaFx的内容没有Spring多,可谓是寸步难行呐~
解决方法就是对stage的focused属性进行监听,一旦发现!stage.isFocused(),就把stage关闭

stage.focusedProperty().addListener((observable, oldValue, newValue) -> {
    if (!stage.isFocused()) {
        stage.close();
    }
});

右键菜单控制器类

右键菜单控制器类主要控制的就是按钮的触发事件,目前只实现了喂食、洗澡,没有看病的原因是因为我没找到素材
请添加图片描述
没事,模板搭好了,看病模块只需要等素材到手就可以很快弄好啦,其他模块还是需要时间沉淀的。

package org.taibai.hellohei.controller;

import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.geometry.Rectangle2D;
import javafx.scene.Node;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.paint.Color;
import javafx.stage.Screen;
import javafx.stage.Stage;
import javafx.stage.StageStyle;

import java.io.IOException;


/**
 * <p>Creation Time: 2021-09-25 10:38:00</p>
 * <p>Description: 右键菜单的控制器</p>
 *
 * @author 太白
 */
public class ContextMenuController {

    /**
     * 点击按钮后应当隐藏一级菜单
     */
    private Stage preStage;
    /**
     * 打开的菜单左上角X坐标,为了打开二级菜单
     */
    private double screenX;
    /**
     * 打开的菜单左上角Y坐标,为了打开二级菜单
     */
    private double screenY;

    public void Init(Stage stage, double screenX, double screenY) {
        this.preStage = stage;
        this.screenX = screenX;
        this.screenY = screenY;
    }

    @FXML
    public void eat() {
        preStage.close();
        showItemsWindow(ItemsWindowController.FoodTitle);
    }

    @FXML
    public void bath() {
        preStage.close();
        showItemsWindow(ItemsWindowController.BathTitle);
    }

    private void showItemsWindow(String title) {
        // ====== 设置名义上的stage,避免在任务栏生成一个小窗口 ======
        final Stage nominalStage = new Stage();
        nominalStage.initStyle(StageStyle.UTILITY);
        nominalStage.setOpacity(0);
        final Stage stage = new Stage();
        stage.initOwner(nominalStage);
        stage.initStyle(StageStyle.TRANSPARENT);    // 设定窗口透明且无边框
        stage.setAlwaysOnTop(true);                 // 设置窗口总显示在最前

        // ====== 设置菜单出现的位置,默认出现在光标的右下方,但是有两种超出屏幕边缘的情况 ======
        stage.setX(screenX);
        stage.setY(screenY);

        // ====== 获得fxml文件 ======
        FXMLLoader loader = new FXMLLoader(getClass().getResource("/org/taibai/hellohei/fxml/ItemsWindow.fxml"));
        Parent root = null;
        try {
            root = loader.load();
        } catch (IOException e) {
            e.printStackTrace();
        }

        // ====== 获得控制器实例 ======
        ItemsWindowController controller = loader.getController();   //获取Controller的实例对象
        controller.Init(title, stage);

        // ====== 在stage中装入scene,并为scene设置css样式 ======
        Scene scene = new Scene(root);
        scene.setFill(Color.TRANSPARENT);
        stage.setScene(scene);
        scene.getStylesheets().addAll(this.getClass().getResource("/org/taibai/hellohei/fxml/ItemsWindow.css").toExternalForm());

        // ====== 当失去焦点的时候设置隐藏stage ======
        stage.focusedProperty().addListener((observable, oldValue, newValue) -> {
            if (!stage.isFocused()) {
                stage.close();
            }
        });

        // ====== 展示菜单 ======
        nominalStage.show();
        stage.show();
    }

}

这个和【唤醒右键菜单类】几乎一模一样,毕竟实现的功能都是创建一个菜单到关闭的过程。
该类引出了ItemsWindowController类,该类就是控制该菜单的控制器

物品展示菜单栏

物品展示菜单栏UI设计

在这里插入图片描述
用户需要选择物品才能触发使用物品(牛奶和鸡蛋在第一集中就出现啦~等以后有时间扣下来就可以换成相对应的动画了。),那么里面的内容肯定不能是死的,所以我设计了如此页面(经过无数次试验 T_T,一直搞不懂怎么给vbox加一个滚动条):
在这里插入图片描述
从上到下依次是:

  • 最外面的AnchorPane是整个物品清单的面板
  • Label里的文字只需要更换,就能复用为食物、沐浴用品、药物清单
  • Pane……现在想想好像确实没有设置的必要,等第三版优化吧
  • ScrollPane是滚动窗格,这样如果里面有很多物品可以滚动浏览了
  • VBox用来放置物品特别方便,不用自己计算xy坐标了

物品展示菜单栏控制器类

目前第二版打开二级菜单会很卡,大致上分析应该是频繁地增加删除节点导致的,希望第三版能将节点组织结构缓存下来,毕竟里面的内容除了文字其他没啥大的更改的。

package org.taibai.hellohei.controller;

import javafx.collections.ObservableList;
import javafx.fxml.FXML;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.Label;
import javafx.scene.control.ScrollPane;
import javafx.scene.layout.*;
import javafx.stage.Stage;
import org.taibai.hellohei.items.bath.BathItem;
import org.taibai.hellohei.items.food.FoodItem;
import org.taibai.hellohei.items.ItemWarehouse;
import org.taibai.hellohei.state.TotalState;

import java.util.Map;

/**
 * <p>Creation Time: 2021-09-27 00:32:16</p>
 * <p>Description: 货物列表控制器,该窗口用于展示食物、洗澡用品、打工列表</p>
 *
 * @author 太白
 */
public class ItemsWindowController {

    public static final String FoodTitle = "食物仓库";
    public static final String BathTitle = "沐浴仓库";
    public static final String DrugTitle = "药品仓库";

    @FXML
    public Label title;
    private String titleText;
    @FXML
    public Pane itemPane;
    @FXML
    public ScrollPane scrollPane;
    @FXML
    public VBox vbox;
    private Stage stage;

    public void Init(String title, Stage stage) {
        this.stage = stage;
        this.titleText = title;
        this.title.setText(title);
        vbox.setAlignment(Pos.TOP_CENTER);
        vbox.setSpacing(10);
        // 禁用左右滚轴
        scrollPane.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
        loadItems();
    }

    private void loadItems() {
        switch (titleText) {
            case FoodTitle:
                loadFoodItems();
                break;
            case BathTitle:
                loadBathItems();
                break;
            case DrugTitle:
                loadDrugItems();
        }
    }

    private void loadFoodItems() {
        Map<String, FoodItem> foodItemList = ItemWarehouse.getInstance().getFoodItemMap();
        for (Map.Entry<String, FoodItem> entry : foodItemList.entrySet()) {
            if (entry.getValue().getItemNum() != 0) {
                vbox.getChildren().add(entry.getValue().toItemAnchorPane());
            }
        }
        ObservableList<Node> children = vbox.getChildren();
        children.forEach(c -> c.setOnMouseReleased(e -> {
            if (TotalState.getInstance().getStaminaState().canIncrease()) {
                stage.close();
            }
        }));
    }

    private void loadBathItems() {
        Map<String, BathItem> bathItemList = ItemWarehouse.getInstance().getBathItemMap();
        for (Map.Entry<String, BathItem> entry : bathItemList.entrySet()) {
            if (entry.getValue().getItemNum() != 0) {
                vbox.getChildren().add(entry.getValue().toItemAnchorPane());
            }
        }
        ObservableList<Node> children = vbox.getChildren();
        children.forEach(c -> c.setOnMouseReleased(e -> {
            if (TotalState.getInstance().getCleanlinessState().canIncrease()) {
                stage.close();
            }
        }));
    }

    private void loadDrugItems() {
        // 小黑是不会生病的!(不是因为我找不到素材 QAQ)
    }

}

装载物品 loadItems ——拿食物举例

用户拥有总仓库ItemWarehouse,可以查看自己所拥有的所有物品,因此在这里只需要遍历仓库里的所有东西(这里就算是0个也算仓库里有这个物品,只是不会显示出来而已),如果物品数量大于等于1,就加入到vbox中待显示。
这里我将vbox每一个元素也就是anchorPane的生成封装在了FoodItem类中,这样也精简了代码量,提高了anchorPane的复用度(同一个对象,只更改文字就能再次使用)

Map<String, FoodItem> foodItemList = ItemWarehouse.getInstance().getFoodItemMap();
        for (Map.Entry<String, FoodItem> entry : foodItemList.entrySet()) {
            if (entry.getValue().getItemNum() != 0) {
                vbox.getChildren().add(entry.getValue().toItemAnchorPane());
            }
        }

同时为每一个物品增加一个点击事件,当点击物品就代表要使用该商品(当然如果能用的话),要使用就默认关闭该窗口:

ObservableList<Node> children = vbox.getChildren();
        children.forEach(c -> c.setOnMouseReleased(e -> {
            if (TotalState.getInstance().getStaminaState().canIncrease()) {
                stage.close();
            }
        }));

物品大家族

物品枚举类(全局变量的更好的选择)

这里拿食物举例,每一个食物应当包括如下信息:

  • 食物的名称
  • 食物的ID
  • 食物的图片资源路径
  • 吃某个食物能增加多少体力
  • 吃这个食物对应的动作图片资源路径(未来想做成动作队列,这样三个或三个以上连续的动作就能实现了,无需自定义GIF了)
package org.taibai.hellohei.items.food;

/**
 * <p>Creation Time: 2021-09-27 17:22:01</p>
 * <p>Description: Item列表,列出所有食物、洗澡用品、药物</p>
 *
 * @author 太白
 */
public enum FoodEnum {

    EGG("鸡蛋", "FOOD_001", "foods/egg.png", 10, "eat drumstick.gif"),
    MILK("牛奶", "FOOD_002", "foods/milk.png", 5, "eat drumstick.gif");

    /**
     * 食物名称
     */
    private final String name;
    /**
     * 食物的唯一性ID
     */
    private final String id;
    /**
     * 食物的图片资源路径
     */
    private final String path;
    /**
     * 吃一个这个可以增加多少饱腹度(一般叫饥饿值,感觉不太对)
     */
    private final int buff;
    /**
     * 吃东西的图片资源路径
     */
    private final String actionPath;

    public static final String pathPrefix = "/org/taibai/hellohei/img/";

    FoodEnum(String name, String id, String path, int buff, String actionPath) {
        this.name = name;
        this.id = id;
        this.path = path;
        this.buff = buff;
        this.actionPath = actionPath;
    }

    public String getName() {
        return name;
    }

    public String getId() {
        return id;
    }

    public String getPath() {
        return pathPrefix + path;
    }

    public int getBuff() {
        return buff;
    }

    public String getActionPath() {
        return pathPrefix + actionPath;
    }
}

物品实体类

一个物品应当有如下两个信息:

  1. 该物品是什么: foodEnum
  2. 该物品有多少: itemNum

另外我将物品在物品展示菜单的窗格复用了,这样只需要修改label就能在不重建AnchorPane的情况下复用原来的窗格了。

package org.taibai.hellohei.items.food;

import javafx.geometry.Pos;
import javafx.scene.control.Label;
import javafx.scene.image.ImageView;
import javafx.scene.layout.AnchorPane;
import org.taibai.hellohei.constant.Constant;
import org.taibai.hellohei.img.ResourceGetter;
import org.taibai.hellohei.state.TotalState;
import org.taibai.hellohei.ui.Action;
import org.taibai.hellohei.ui.ActionExecutor;
import org.taibai.hellohei.ui.InterfaceFunction;

/**
 * <p>Creation Time: 2021-09-27 17:09:50</p>
 * <p>Description: 供以吃东西、洗澡、看病的物品</p>
 *
 * @author 太白
 */
public class FoodItem {

    private FoodEnum foodEnum;
    private int itemNum;
    private AnchorPane anchorPane;
    private Label label;

    public FoodItem(FoodEnum foodEnum, int itemNum) {
        this.foodEnum = foodEnum;
        this.itemNum = itemNum;
        init();
    }

    /**
     * 创建一个AnchorPane供以在ItemsWindow显示,同时要添加点击事件
     * 点击后将产生用该物品的动作、并且减去一个物品
     *
     * @return 创建出来的AnchorPane
     */
    public AnchorPane toItemAnchorPane() {
        label.setText(foodEnum.getName() + "*" + itemNum);
        return anchorPane;
    }

    /**
     * 每次只是个数的变化,所以不需要再次创建新的对象,否则会特别耗时
     */
    private void init() {
        anchorPane = new AnchorPane();
        ImageView imageView = new ImageView(ResourceGetter.getInstance().get(foodEnum.getPath()));
        imageView.setFitWidth(86);
        imageView.setFitHeight(86);
        label = new Label(foodEnum.getName() + "*" + itemNum);
        label.setLayoutX(0);
        label.setLayoutY(66);
        label.setMinWidth(86);
        label.setMinHeight(20);
        label.setAlignment(Pos.CENTER); //垂直水平居中
        label.setStyle("-fx-background-color: rgba(0, 0, 0, 0.6); -fx-text-fill: white");
        anchorPane.getChildren().addAll(imageView, label);
        anchorPane.setOnMousePressed(e -> {
            useItem();
        });
    }

    private void useItem() {
        if (itemNum <= 0) return;
        if (!TotalState.getInstance().getStaminaState().canIncrease()) {
            InterfaceFunction.getInstance().say("不能再吃啦~", Constant.UserInterface.SayingRunTime);
            return;
        }
        decrease(1);
        Action action = Action.creatTemporaryInterruptableAction(
                foodEnum.getActionPath(),
                Constant.UserInterface.ActionRunTime * 2,
                Constant.ImageShow.mainImage
        );
        ActionExecutor.getInstance().execute(action);
        InterfaceFunction.getInstance().say("真好吃", Constant.UserInterface.SayingRunTime);
        // 增加体力值
        TotalState.getInstance().getStaminaState().increase(foodEnum.getBuff());
    }

    /**
     * 将该item数量增加num个
     *
     * @param num 增加的数量
     */
    public void increase(int num) {
        this.itemNum += num;
    }

    /**
     * 将该item数量减少num个
     *
     * @param num 减少的数量
     */
    public void decrease(int num) {
        this.itemNum -= num;
        itemNum = Math.max(0, itemNum);
    }

    /**
     * 得到该item还有多少个
     *
     * @return item的数量
     */
    public int getItemNum() {
        return itemNum;
    }
}

物品的仓库(全局唯一,单例模式)

单机的写死的仓库当然简单啦,这里默认每个物品都有10个。
未来这里可以接入后端,登录后同步数据到本地,这也是为什么要设置物品ID,为了能唯一标识物品。

package org.taibai.hellohei.items;

import org.taibai.hellohei.items.bath.BathEnum;
import org.taibai.hellohei.items.bath.BathItem;
import org.taibai.hellohei.items.food.FoodEnum;
import org.taibai.hellohei.items.food.FoodItem;

import java.util.HashMap;
import java.util.Map;

/**
 * <p>Creation Time: 2021-09-27 17:35:14</p>
 * <p>Description: Item存量</p>
 *
 * @author 太白
 */
public class ItemWarehouse {

    private static ItemWarehouse itemWarehouse;

    private final Map<String, FoodItem> foodItemMap = new HashMap<>();
    private final Map<String, BathItem> bathItemMap = new HashMap<>();

    private ItemWarehouse() {
        // 这里默认有10个,等后端系统写好后就可以持久化了
        for (FoodEnum foodEnum : FoodEnum.values()) {
            foodItemMap.put(foodEnum.getId(), new FoodItem(foodEnum, 10));
        }
        // 这里也默认有10个洗澡用品
        for (BathEnum bathEnum : BathEnum.values()) {
            bathItemMap.put(bathEnum.getId(), new BathItem(bathEnum, 10));
        }
    }

    public static ItemWarehouse getInstance() {
        if (itemWarehouse == null) itemWarehouse = new ItemWarehouse();
        return itemWarehouse;
    }

    public Map<String, FoodItem> getFoodItemMap() {
        return foodItemMap;
    }

    public Map<String, BathItem> getBathItemMap() {
        return bathItemMap;
    }
}

状态家族

心情值状态类

心情值就是点击小黑就能让小黑开心,开心就得有个表示对吧,不仅是内部数据的更改,还有升起的文字表示(这里用云代替,暂时找不到好看的素材)
请添加图片描述

package org.taibai.hellohei.state;

import javafx.animation.*;
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-25 02:59:11</p>
 * <p>Description: 小黑的心情值</p>
 *
 * @author 太白
 */
public class EmotionState {

    /**
     * 心情值,取值为[0, 100]
     */
    private int emotion = 60;
    public static final int Reduce_Step = 5;
    public static final int Increase_Step = 10;
    public static final int Max_Value = 100;
    public static final int Min_Value = 0;
    private ImageView imageView;

    private final ResourceGetter resourceGetter = ResourceGetter.getInstance();

    public EmotionState() {
        imageView = new ImageView();
    }

    /**
     * 心情值降低
     */
    public void reduce() {
        emotion = Math.max(Min_Value, emotion - Reduce_Step);
    }

    /**
     * 心情值增加
     */
    public void increase() {
        if (emotion < Max_Value) showIncreasedAnimation();
        emotion = Math.min(Max_Value, emotion + Increase_Step);
        System.out.printf("[EmotionState::increase]-当前心情=%d\n", emotion);
    }

    /**
     * 展示心情增加的动画
     */
    private void showIncreasedAnimation() {
        Image increasingImg = resourceGetter.get(Constant.ImageShow.emotionIncreasingImage);
        imageView.setImage(increasingImg);
        imageView.setStyle("-fx-background:transparent;");
        // 设置相对于父容器的位置
        imageView.setX(0);
        imageView.setY(0);
        imageView.setLayoutX(60);
        imageView.setLayoutY(0);
        imageView.setFitHeight(80);         // 设置图片显示的大小
        imageView.setFitHeight(80);
        imageView.setPreserveRatio(true);   // 保留width:height比例
        imageView.setVisible(true);

        double millis = Constant.UserInterface.ActionRunTime * 1000;
        // 位移动画
        TranslateTransition translateTransition = new TranslateTransition(Duration.millis(millis), imageView);
        translateTransition.setInterpolator(Interpolator.EASE_BOTH);
        translateTransition.setFromY(40);
        translateTransition.setToY(0);
        // translateTransition.play();
        // 淡入淡出动画
        FadeTransition fadeTransition = new FadeTransition(Duration.millis(millis), imageView);
        fadeTransition.setFromValue(1.0);
        fadeTransition.setToValue(0);
        // 并行执行动画
        ParallelTransition parallelTransition = new ParallelTransition();
        parallelTransition.getChildren().addAll(
                fadeTransition,
                translateTransition
        );
        parallelTransition.play();
    }

    public ImageView getImageView() {
        return imageView;
    }
}

大致上就是让一张图片显示,并且添加两个动画:向上移动、淡入淡出
让这两个动画并行显示,就得到图中的效果啦。

体力值状态类

俺其实不想叫做体力值的,以前一直叫“饥饿度”,但最近一细想,吃东西会导致“饥饿度”下降,虽然符合逻辑但是还是有点不符合逻辑?但是叫“饱腹度”、“饱胀度”好像更不顺口?

package org.taibai.hellohei.state;

/**
 * <p>Creation Time: 2021-09-28 00:51:27</p>
 * <p>Description: 体力值</p>
 *
 * @author 太白
 */
public class StaminaState {

    private int stamina = 60;

    public static final int Reduce_Step = 2;
    public static final int Max_Value = 100;
    public static final int Min_Value = 0;

    /**
     * 体力值降低
     */
    public void reduce() {
        stamina = Math.max(Min_Value, stamina - Reduce_Step);
    }

    /**
     * 体力值增加
     *
     * @param num 增加的量
     */
    public void increase(int num) {
        stamina = Math.min(Max_Value, stamina + num);
        System.out.printf("[StaminaState::increase(%d)]-当前体力值=%d\n", num, stamina);
    }

    /**
     * 是否还能增加
     *
     * @return 能否增加
     */
    public boolean canIncrease() {
        return stamina < Max_Value;
    }

}

总状态(全局唯一,单例模式)

之后取得状态都是从总状态取来的,所以设置为单例模式也能确保子状态是唯一的。

package org.taibai.hellohei.state;

import javax.swing.text.html.ImageView;

/**
 * <p>Creation Time: 2021-09-25 02:58:15</p>
 * <p>Description: 小黑的所有状态</p>
 *
 * @author 太白
 */
public class TotalState {

    private static TotalState totalState;

    private final EmotionState emotionState;
    private final StaminaState staminaState;
    private final CleanlinessState cleanlinessState;

    private TotalState() {
        emotionState = new EmotionState();
        staminaState = new StaminaState();
        cleanlinessState = new CleanlinessState();
    }

    public static TotalState getInstance() {
        if (totalState == null) totalState = new TotalState();
        return totalState;
    }

    public EmotionState getEmotionState() {
        return emotionState;
    }

    public StaminaState getStaminaState() {
        return staminaState;
    }

    public CleanlinessState getCleanlinessState() {
        return cleanlinessState;
    }

}

一点点优化

之前写代码考虑不周全,作为全局共享的对象设置成单例模式确实会省不少事情,就不用在构造对象的时候把对象传来传去的了。
这里将显示GIF的ImageView与显示ImageView的舞台Stage都设置成了单例模式,我称之为MainNode,意思是主要的节点

package org.taibai.hellohei.ui;


import javafx.scene.image.ImageView;
import javafx.stage.Stage;

/**
 * <p>Creation Time: 2021-09-27 22:37:00</p>
 * <p>Description: 动作的展示窗口,包括主界面的ImageView(展示GIF),Stage(控制整个程序的显隐、关闭等)
 *  毕竟是全局唯一的对象,所以设置为单例模式,全局拿到的就是唯一的对象</p>
 *
 * @author 太白
 */
public class MainNode {

    private static MainNode mainNode;
    private final ImageView imageView;
    private final Stage stage;

    private MainNode() {
        imageView = new ImageView();
        stage = new Stage();
    }

    public static MainNode getInstance() {
        if (mainNode == null) mainNode = new MainNode();
        return mainNode;
    }

    public ImageView getImageView() {
        return imageView;
    }

    public Stage getStage() {
        return stage;
    }
}

并且将之前所有用到该ImageView/Stage的对象都重新修改了一遍,保证全局拿到的是同一个对象。

后话

十月后事情开始变多了,比赛也开始多起来了,报名的软考也要如约而至了,希望闲暇时能继续推进项目的开发
项目GitHub仓库地址 => Jiang-TaiBai/IXiaoHei <=

国庆节快到了,预祝大家国庆节快乐呀~
请添加图片描述


点击全文阅读


本文链接:http://zhangshiyu.com/post/34237.html

物品  太白  设置  
<< 上一篇 下一篇 >>

  • 评论(0)
  • 赞助本站

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。

关于我们 | 我要投稿 | 免责申明

Copyright © 2020-2022 ZhangShiYu.com Rights Reserved.豫ICP备2022013469号-1