Desktop pet development - Luo Xiaohei

Keywords: javafx

preface

I am a person who likes to open the setting interface and try functions one by one. In the past, I tried to set the message notification of CSDN once a day. I thought, "no one pays attention anyway. It's the same as not setting it."
On the 23rd, I received an email saying that someone left me a message. I was very happy. I clicked on the page and suddenly found that I had 600 + unread messages, which frightened me

Thank you very much for your attention (* ^ ▽ ^ *)! This also gives me great motivation, so I haven't been idle in recent days. I work overtime to improve the functions a little bit.
Many people want to try something new with demo. I'm afraid everyone will be disappointed, so I didn't package it into an installation package, because the first version is really very simple

If I really want to run, I'm not stingy, but I don't package it into an executable file. I also need to configure the Java environment to run. Oh, I put all the files of the project on GitHub. Click visit = > Jiang-TaiBai/IXiaoHei <=

Overview of the second edition

Generally, the following contents are made:

  1. Right click menu
  2. Set status class (mood, physical strength, cleanliness, etc.)
  3. Appliance warehouse
  4. Use effect of appliances
  5. Optimized the previous code and set more singleton modes (I don't know if it's good or not, it's easy to cause memory leakage, but the code is better to write, ha ha)

The following figure is a preview. I don't know why the card is playing on CSDN. Unfortunately, the video can't be plugged in (you have to upload the video link to other platforms)

The interface design is not good enough. I want to improve the functions in the early stage, and then settle down to sort out the interface (it's really difficult to find materials!!!)

Realization of right-click menu function


The basic idea is that when you right-click the ImageView Node where Luo Xiaohei is located, open an FXML rendering interface, and the FXML interface has to use a layer of nominalstage (no well named, literal translation is the nominal stage), so as to avoid adding a small logo on the taskbar.
The FXML sets many buttons, and each button corresponds to the method of the Controller.

Wake up right-click menu class

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: Click the right-click menu triggered by the ontology</p>
 *
 * @author Taibai
 */
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) {
        // ======Set the nominal stage to avoid generating a small window in the taskbar======
        final Stage nominalStage = new Stage();
        nominalStage.initStyle(StageStyle.UTILITY);
        nominalStage.setOpacity(0);
        final Stage stage = new Stage();
        stage.initOwner(nominalStage);
        stage.initStyle(StageStyle.TRANSPARENT);    // Sets the window to be transparent and borderless
        stage.setAlwaysOnTop(true);                 // The settings window is always displayed at the top

        // ======Set the position where the menu appears. By default, it appears at the lower right of the cursor, but there are two cases that exceed the edge of the screen======
        Rectangle2D screenRectangle = Screen.getPrimary().getBounds();
        double screenWidth = screenRectangle.getWidth();
        double screenHeight = screenRectangle.getHeight();
        double stageWidth = 138.0;
        double stageHeight = 280.0;
        // If the pop-up right-click menu exceeds the lower edge of the screen, it will expand upward
        if(screenY + stageHeight > screenHeight) screenY -= stageHeight;
        // If the pop-up right-click menu exceeds the right edge of the screen, it will expand to the left
        if(screenX + stageWidth > screenWidth) screenX -= stageWidth;
        stage.setX(screenX);
        stage.setY(screenY);

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

        // ======Get controller instance======
        ContextMenuController controller = loader.getController();   //Gets the instance object of the Controller
        controller.Init(stage, screenX, screenY);

        // ======Load the scene in the stage and set the css style for the scene======
        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());

        // ======Set the hidden stage when the focus is lost======
        stage.focusedProperty().addListener((observable, oldValue, newValue) -> {
            if (!stage.isFocused()) {
                stage.close();
            }
        });

        // ======Display menu======
        nominalStage.show();
        stage.show();
    }

}

Location of the right-click menu

This class is called [wake up right-click menu class], which is triggered by a click event. Therefore, you can obtain the coordinates of the cursor on the screen when the mouse clicks, so just set the coordinates in the upper left corner of the right-click menu.
Of course, just as we right-click on the desktop, if the right-click menu exceeds the screen, it should appear on the other side. Here, the following code is used to solve this requirement:

// ======Set the position where the menu appears. By default, it appears at the lower right of the cursor, but there are two cases that exceed the edge of the screen======
Rectangle2D screenRectangle = Screen.getPrimary().getBounds();
double screenWidth = screenRectangle.getWidth();
double screenHeight = screenRectangle.getHeight();
double stageWidth = 138.0;
double stageHeight = 280.0;
// If the pop-up right-click menu exceeds the lower edge of the screen, it will expand upward
if(screenY + stageHeight > screenHeight) screenY -= stageHeight;
// If the pop-up right-click menu exceeds the right edge of the screen, it will expand to the left
if(screenX + stageWidth > screenWidth) screenX -= stageWidth;
stage.setX(screenX);
stage.setY(screenY);

Close the right-click menu

This function can sacrifice more than a dozen of my hair

I searched the whole network for countless solutions, and finally solved them. The content of JavaFx on the Internet is not as much as Spring. It can be said that it is difficult to do anything~
The solution is to listen to the focused attribute of the stage. Once found! stage.isFocused(), close the stage

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

Right click menu controller class

The right-click menu controller class mainly controls the trigger event of the button. At present, it only realizes feeding and bathing. The reason why I didn't see a doctor is because I didn't find the material

It's all right. The template is set up. The medical module can be completed soon after the materials are available. Other modules still need time to precipitate.

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: Controller in right-click menu</p>
 *
 * @author Taibai
 */
public class ContextMenuController {

    /**
     * The first level menu should be hidden after clicking the button
     */
    private Stage preStage;
    /**
     * The X coordinate in the upper left corner of the open menu is used to open the secondary menu
     */
    private double screenX;
    /**
     * The Y coordinate in the upper left corner of the open menu is used to open the secondary menu
     */
    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) {
        // ======Set the nominal stage to avoid generating a small window in the taskbar======
        final Stage nominalStage = new Stage();
        nominalStage.initStyle(StageStyle.UTILITY);
        nominalStage.setOpacity(0);
        final Stage stage = new Stage();
        stage.initOwner(nominalStage);
        stage.initStyle(StageStyle.TRANSPARENT);    // Sets the window to be transparent and borderless
        stage.setAlwaysOnTop(true);                 // The settings window is always displayed at the top

        // ======Set the position where the menu appears. By default, it appears at the lower right of the cursor, but there are two cases that exceed the edge of the screen======
        stage.setX(screenX);
        stage.setY(screenY);

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

        // ======Get controller instance======
        ItemsWindowController controller = loader.getController();   //Gets the instance object of the Controller
        controller.Init(title, stage);

        // ======Load the scene in the stage and set the css style for the scene======
        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());

        // ======Set the hidden stage when the focus is lost======
        stage.focusedProperty().addListener((observable, oldValue, newValue) -> {
            if (!stage.isFocused()) {
                stage.close();
            }
        });

        // ======Display menu======
        nominalStage.show();
        stage.show();
    }

}

This is almost as like as two peas. The function is to create a menu to close.
This class leads to the ItemsWindowController class, which is the controller that controls the menu

Item display menu bar

UI design of item display menu bar


Users need to select items to trigger the use of items (milk and eggs appear in the first episode ~ they can be replaced with corresponding animation when they have time to deduct them in the future). Then the content in it must not be dead, so I designed such a page (after several experiments T_T, I still don't know how to add a scroll bar to vbox):

From top to bottom:

  • The outermost anchor pane is the panel of the entire item list
  • The words in the Label only need to be replaced and can be reused as a list of food, bath products and drugs
  • Pane... Now think about it. It seems that there is no need to set it. Wait for the third version of optimization
  • ScrollPane is a scrolling pane so that if there are many items in it, you can scroll through them
  • VBox is especially convenient for placing items. You don't have to calculate xy coordinates yourself

Item display menu bar controller class

At present, is too laggy to open the two level menu in the second edition. Generally, the analysis should be caused by frequent deletion of nodes. I hope that the third version can cache the node organization structure.

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: Goods list controller. This window is used to display the list of food, bath supplies and workers</p>
 *
 * @author Taibai
 */
public class ItemsWindowController {

    public static final String FoodTitle = "Food warehouse";
    public static final String BathTitle = "Bath warehouse";
    public static final String DrugTitle = "Drug warehouse";

    @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);
        // Disable left and right rollers
        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() {
        // Xiao Hei won't get sick! (not because I can't find the material QAQ)
    }

}

Load items - take food as an example

The user owns the total warehouse ItemWarehouse and can view all the items he owns. Therefore, he only needs to traverse all the items in the warehouse (even if there are 0 items here, there are such items in the warehouse, but they will not be displayed). If the number of items is greater than or equal to 1, they will be added to vbox to be displayed.
Here, I encapsulate the generation of each element of vbox, that is, anchorPane, in the FoodItem class, which also simplifies the amount of code and improves the reusability of anchorPane (the same object can be used again only by changing the text)

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());
            }
        }

At the same time, add a click event for each item. When you click an item, it means you want to use the commodity (of course, if it can be used), and the window will be closed by default:

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

Item family

Item enumeration class (better choice for global variables)

Take food as an example. Each food should include the following information:

  • Name of food
  • Food ID
  • Picture resource path of food
  • How much physical strength can you increase by eating a certain food
  • The action picture resource path corresponding to eating this food (in the future, we want to make an action queue so that three or more consecutive actions can be realized without customizing GIF)
package org.taibai.hellohei.items.food;

/**
 * <p>Creation Time: 2021-09-27 17:22:01</p>
 * <p>Description: Item List, list all food, bath supplies and drugs</p>
 *
 * @author Taibai
 */
public enum FoodEnum {

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

    /**
     * Food name
     */
    private final String name;
    /**
     * Unique ID of food
     */
    private final String id;
    /**
     * Picture resource path of food
     */
    private final String path;
    /**
     * How much satiety can you increase by eating one of these
     */
    private final int buff;
    /**
     * Picture resource path for eating
     */
    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;
    }
}

Item entity class

An item should have the following two information:

  1. What is the item: foodEnum
  2. How many items are there: itemNum

In addition, I reuse the items in the pane of the item display menu, so that I can reuse the original pane without rebuilding the AnchorPane by modifying the label.

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: Articles for eating, bathing and seeing a doctor</p>
 *
 * @author Taibai
 */
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();
    }

    /**
     * Create an AnchorPane to display in ItemsWindow, and add a click event at the same time
     * Click to generate the action of using the item and subtract an item
     *
     * @return Created AnchorPane
     */
    public AnchorPane toItemAnchorPane() {
        label.setText(foodEnum.getName() + "*" + itemNum);
        return anchorPane;
    }

    /**
     * Only the number of objects changes each time, so there is no need to create new objects again, otherwise it will be particularly time-consuming
     */
    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); //Vertically and horizontally centered
        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("You can't eat any more~", 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("Really delicious", Constant.UserInterface.SayingRunTime);
        // Increase physical strength
        TotalState.getInstance().getStaminaState().increase(foodEnum.getBuff());
    }

    /**
     * Increase the number of item s by num
     *
     * @param num Increased quantity
     */
    public void increase(int num) {
        this.itemNum += num;
    }

    /**
     * Reduce the number of item s by num
     *
     * @param num Reduced quantity
     */
    public void decrease(int num) {
        this.itemNum -= num;
        itemNum = Math.max(0, itemNum);
    }

    /**
     * How many more items are there to get this item
     *
     * @return item Number of
     */
    public int getItemNum() {
        return itemNum;
    }
}

Warehouse of items (globally unique, single instance mode)

Of course, it's simple to write a dead warehouse for a single machine. There are 10 items by default.
In the future, you can access the back-end and synchronize the data locally after login. This is why you need to set the item ID to uniquely identify the item.

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 Stock</p>
 *
 * @author Taibai
 */
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() {
        // There are 10 by default, which can be persisted after the backend system is written
        for (FoodEnum foodEnum : FoodEnum.values()) {
            foodItemMap.put(foodEnum.getId(), new FoodItem(foodEnum, 10));
        }
        // There are also 10 bath supplies by default
        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;
    }
}

State Family

Mood value status class

The mood value is that clicking Xiaohei can make Xiaohei happy, and there must be an expression of happiness, right? It is not only the change of internal data, but also the rising text expression (cloud is used here instead, and no good-looking material can be found for the time being)

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: Xiaohei's mood value</p>
 *
 * @author Taibai
 */
public class EmotionState {

    /**
     * Mood value, value is [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();
    }

    /**
     * Mood value decreased
     */
    public void reduce() {
        emotion = Math.max(Min_Value, emotion - Reduce_Step);
    }

    /**
     * Mood value increases
     */
    public void increase() {
        if (emotion < Max_Value) showIncreasedAnimation();
        emotion = Math.min(Max_Value, emotion + Increase_Step);
        System.out.printf("[EmotionState::increase]-Current mood=%d\n", emotion);
    }

    /**
     * Animation showing increased mood
     */
    private void showIncreasedAnimation() {
        Image increasingImg = resourceGetter.get(Constant.ImageShow.emotionIncreasingImage);
        imageView.setImage(increasingImg);
        imageView.setStyle("-fx-background:transparent;");
        // Sets the position relative to the parent container
        imageView.setX(0);
        imageView.setY(0);
        imageView.setLayoutX(60);
        imageView.setLayoutY(0);
        imageView.setFitHeight(80);         // Set the size of the picture display
        imageView.setFitHeight(80);
        imageView.setPreserveRatio(true);   // Keep width:height scale
        imageView.setVisible(true);

        double millis = Constant.UserInterface.ActionRunTime * 1000;
        // Displacement animation
        TranslateTransition translateTransition = new TranslateTransition(Duration.millis(millis), imageView);
        translateTransition.setInterpolator(Interpolator.EASE_BOTH);
        translateTransition.setFromY(40);
        translateTransition.setToY(0);
        // translateTransition.play();
        // Fade in and fade out animation
        FadeTransition fadeTransition = new FadeTransition(Duration.millis(millis), imageView);
        fadeTransition.setFromValue(1.0);
        fadeTransition.setToValue(0);
        // Parallel execution animation
        ParallelTransition parallelTransition = new ParallelTransition();
        parallelTransition.getChildren().addAll(
                fadeTransition,
                translateTransition
        );
        parallelTransition.play();
    }

    public ImageView getImageView() {
        return imageView;
    }
}

Basically, let a picture show, and add two animations: move up, fade in and fade out
Let the two animations be displayed in parallel to get the effect in the figure.

Physical strength status class

In fact, I don't want to call it physical strength. I used to call it "hunger", but recently, when I think about it, eating will lead to the decline of "hunger". Although it is logical, it is still a little illogical? But what about "satiety" and "fullness"?

package org.taibai.hellohei.state;

/**
 * <p>Creation Time: 2021-09-28 00:51:27</p>
 * <p>Description: Physical strength value</p>
 *
 * @author Taibai
 */
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;

    /**
     * Reduced physical strength
     */
    public void reduce() {
        stamina = Math.max(Min_Value, stamina - Reduce_Step);
    }

    /**
     * Physical strength increased
     *
     * @param num Increased amount
     */
    public void increase(int num) {
        stamina = Math.min(Max_Value, stamina + num);
        System.out.printf("[StaminaState::increase(%d)]-Current physical strength=%d\n", num, stamina);
    }

    /**
     * Can it be increased
     *
     * @return Can increase
     */
    public boolean canIncrease() {
        return stamina < Max_Value;
    }

}

Total status (globally unique, singleton mode)

After that, the obtained states are obtained from the total state, so setting the singleton mode can also ensure that the sub states are unique.

package org.taibai.hellohei.state;

import javax.swing.text.html.ImageView;

/**
 * <p>Creation Time: 2021-09-25 02:58:15</p>
 * <p>Description: All States of Xiaohei</p>
 *
 * @author Taibai
 */
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;
    }

}

A little optimization

Before writing the code, we didn't think carefully. Setting the globally shared object into the singleton mode will really save a lot of things, so we don't have to pass the object around when constructing the object.
Here, the ImageView displaying GIF and the Stage displaying ImageView are set to the singleton mode. I call it MainNode, which means the main node

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: Action display window, including ImageView (display GIF) and stage (control the display, hiding, closing, etc. of the whole program) of the main interface
 *  After all, it is a globally unique object, so if it is set to singleton mode, the globally unique object will be obtained</p>
 *
 * @author Taibai
 */
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;
    }
}

In addition, all previous objects using the ImageView/Stage are modified again to ensure that the same object is obtained globally.

Later words

After October, things began to change, competitions began to increase, and the soft test for registration should come as promised. I hope to continue to promote the development of the project in my spare time
Project GitHub warehouse address = > Jiang-TaiBai/IXiaoHei <=

National Day is coming. I wish you a happy National Day~

Posted by mehaj on Mon, 27 Sep 2021 11:37:49 -0700