Skip to content

Technical‐Documentation

Ondřej Čopák edited this page May 7, 2025 · 1 revision

Project Structure

The game is about a unicorn collecting coins, avoiding enemies, and moving on a real-time tile-based map. The goal is to provide readable and easily modifiable code, enabling new features to be implemented quickly. The code fully leverages Java’s OOP features to maintain generality.

The code is separated into a Model-View structure. This project is not platform-dependent, meaning it can run on any platform with JavaFX support. The game is designed to be played on a computer, but it can also run on Android devices using the JavaFX port for Android.

Javadoc can be found in the doc folder or online: View Javadoc HTML

Model

There are primarily four types of classes:

  1. GameObject Classes – for items, characters, etc.
  2. "Static" Classes – for functionality like file management, JSON handling, physics, etc.
  3. Enums – for game parameters and to simplify the process of adding new items.
  4. Interfaces – for structuring the project.

GameObject

The GameObject class is the main class, with the most descendants. It contains functions like getTexture(), tick(), position(), etc., used for rendering the scene. Before a level starts, all objects (Characters, Items, Tiles, Start, Goal, Arrows) are collected and passed to the View as a List. Each frame, the View updates every object and uses their position and texture to render them on the screen.

Characters

Character collisions are calculated each frame based on nearby tiles, arrows, and other characters. The collision box is calculated using the current FPS and character speed to ensure no collisions are skipped. Before the start or after reaching the goal, characters become invincible and cannot be affected by arrows.

Items

Items are created using the ItemFactory, which identifies what item is being instantiated. This approach maintains the generality of the code and supports OOP principles, making it easy to add new items. Each item has its own effect, which is handled in GamePhysics using [ItemName].isActive().

Textures

Each GameObject must have a texture. All textures are managed in the TextureEnum. If any required texture is missing from the file system, a default “missing texture” is applied. Objects can have multiple textures, and textures are switched at each tick to support animations.

All textures except for the logo, fences, background video, and tiles are from the original game. The tiles and logo were made by me, and the background video is gameplay footage from the game.

View

There are currently four "scenes":

  1. Menu – Play / Continue / Select Profile / Level Editor / Exit Game
  2. ProfileManager – For selecting different profiles
  3. LevelSelector – For choosing or editing levels
  4. Level – The level itself

The scenes are implemented as VBoxes, switched through the AppViewManager class, which handles changing frames within a single scene. This approach avoids issues related to window size, position, or fullscreen mode during scene transitions. The game always runs in a 16:9 aspect ratio. If the application window isn't 16:9, black bars are added to maintain the aspect ratio.

Menu

At startup, the game checks for a saved level to continue. If one exists, the player is given the option to continue from the first non-completed level.

Profile Manager

On the first launch (or if no profile is selected), the player is redirected here. Players can create new profiles and view how many levels have been completed under each profile.

Level Selector

Here, players can view all levels, with completed ones highlighted in green. If the player enters the level editor, they can also create new levels or select existing ones to edit.

Level

The level is rendered each frame (default 60 FPS). The level frame includes a GameLoop that manages actions like Play, Pause, Restart, and SpeedUp. A HUD is displayed below the level, showing all necessary options and stats. Since tiles aren't buttons, arrows are placed based on calculated positions using all known information about screen size, position, and tile map dimensions. At the end of a level (win or lose), a popup appears allowing the player to restart, proceed to the next level, or return to the previous screen.

UML Diagrams

These UML diagrams (generated in IntelliJ IDEA) illustrate the project structure. They include all the classes except for the tests, which are not relevant to the project.

To see the Javadoc of the project, you can check the doc folder in the root of the project or visit the HTML page of Javadoc.

No Dependencies

uml-norelations

With Dependencies

uml-relations

Gameplay

The exact gameplay is described in the user manual.

The goal of this project is to make the user experience as smooth as possible, with controls only by mouse clicking, or optionally using F11 to switch to fullscreen mode.

Level Editor

The level editor works similarly to playing a level. The only difference is that the scene renders only when objects are added, rotated, or removed. The level editor uses the same technology for placing arrows as for placing tiles, with the difference that the object to be placed must be selected in the HUD located at the bottom of the screen. Once the object is selected and the mouse is clicked, LevelEditorUtils checks whether the object can be placed based on its requirements.

The level is saved if the PathFinder finds a path from the start to the goal while collecting all the coins during playtesting.

Animated Textures

All character items and other objects have animated textures. The only exceptions are tiles and tile fences. I made all the objects have the same number of frames to maintain consistency.

Rules:

  1. For character static movement animation in each direction, there are 16 animated frames.
  2. For any rotation of item/character/arrow, there are 9 animated frames (10 degrees per frame).
  3. The goal also has 9 animated frames in each direction, totaling 36 for a full rotation, multiplied by 4 for all possible directions.

The animations are rendered every other frame by default. This can be changed in the static properties in the Character and Item classes. Speeding up the game also speeds up the animations.

These textures are not handmade but used from the original game.

Animated Background Video

In the Menu (all frames except for the Level and Level Editor), there is a background video playing blurred gameplay footage, similar to Factorio. If the video fails to load, a black screen is shown instead. To fix this, simply restart the game. The video is looped, and each time you enter the menu it starts from a random position. The video is excluded from the main render management to avoid resetting it when the screen is resized.

Music

The game features background music and sound effects. The music volume can be changed in the settings or for each audio track in the SoundEnum class. All sounds are taken from the original game.

There are three background music tracks:

  1. Main menu
  2. Level
  3. Level editor

Several sound effects exist for collecting items, winning, losing, killing wolves, and other actions.

Buttons play a sound effect when clicked. This is handled by the ButtonSoundInjector, which injects an action listener into all buttons with the .button class to trigger the sound effect.

Physics

The physics are handled by the GamePhysics class. It is a "static" class that manages all game physics.

This class includes multiple functions to check collisions, process tile interactions, and perform other physics-related tasks. Each time a character's tick() method is called, physics checks are also executed.

The physics system handles:

  1. For every sheep, determining the physical events to apply — collisions with walls, arrows, items, enemies, or other sheep.
  2. If a sheep collides with an item, it calls the item's static use() function, marking the item as active.
  3. If an active item is used, it applies its effect to the sheep (or wolf).
  4. If characters are too close to each other, they are slowed down to space them evenly.

The "unstruggle" effect shown below demonstrates this. Its purpose is to ensure that when two arrows point toward each other and characters get too close, they don’t remain clumped after the arrows are destroyed, which would look awkward.

unstruggle


Implementation

This section describes the most important parts of the project and how they are implemented. It works best in combination with the rest of this manual—refer to specific topics as needed. Also, check the UML diagrams above for class structure and the generated Javadoc for class-level details. As always, the best way to understand the code is to read the code.

Threads

The game uses multiple threads:

  • JavaFX Thread: Responsible for rendering UI and handling user input.
  • GameLoop Thread: Handles game logic and rendering the actual level.

In each game loop iteration, all game objects are updated via tick(). Simultaneously, the JavaFX thread renders the scene. To prevent concurrency issues, CopyOnWriteArrayList is used—a thread-safe version of ArrayList that allows safe concurrent reads/writes.

When the user pauses, stops, or resets the game, the main thread first waits for the GameLoop to finish (using join() for safety), before proceeding with further actions.

Mouse click events, such as adding, rotating, or deleting arrows, are also handled in the JavaFX thread, again using CopyOnWriteArrayList to avoid conflicts.

Folder Management

The project stores its data in the resources directory, organized by data type:

The /levels, /profiles, and /textures directories can be relocated to support portability across platforms.

/datasaves/
   /levels/
   /profiles/
      /ProfileName1/
      /ProfileName2/
      ...
/sounds/
   /music/
   /sfx/
/textures/
   /editor/
   /level/
      /arrows/
      /backgrounds/
      /characters/
      /items/
      /tiles/
      /objects/
         /goal/
         /start/

Rendering

The GameLoop class controls the game's core update-render cycle in a dedicated daemon thread. Using a fixed frame rate, it updates game state (tick()) and schedules rendering using Platform.runLater().

Game speed is controlled by a multiplier (0.5x, 1x, 2x, 4x) that determines how often full update frames occur. At higher speeds, frames are skipped to maintain the same FPS.

In each frame, the HUD (lives, time, coins, etc.) is updated, and win/loss conditions are checked. Pausing and resuming is safely handled without creating duplicate threads. resetLevel() fully unloads and reloads the level while keeping sync with rendering.

Objects are rendered in order of priority. Time-sensitive logic (like countdowns or timeout audio cues) is handled in tick(). The loop is designed to avoid ConcurrentModificationException using proper synchronization and defensive coding.

Object Static Properties

Most renderable objects have static fields for tracking global state—such as item counts or how many sheep reached the goal.

These values reset whenever a new level is loaded to avoid incorrect carry-over.

Common classes like GameObject or Character also define static final constants (e.g., FPS or animation frame count) used in rendering.

From Game Launcher to Level Win

  1. The Launcher class starts the application, initializing paths and resources.

  2. The main menu is shown.

  3. When a level is selected, it’s loaded and rendered via the View system.

  4. After completing a level:

    • Static values like coin counts are reset.
    • The next level is loaded.
    • The GameLoop continues uninterrupted, switching the current view.

Used Technology

Key technologies and tools used in the project:

  • Java 21
  • IntelliJ IDEA — IDE for development and generating Javadoc
  • Maven — Project build and dependency management
  • JavaFX 21.0.2 — GUI and media framework
  • JUnit 5 (5.10.2) — Unit/integration testing
  • org.json (20250107) — JSON parsing and writing
  • Java Util Logging — Logging with custom redirection (LoggerStdOutErrHandler)
  • PowerPoint — Texture creation (yes, really)
  • Vegas PRO 21.0 — Trailer editing
  • GitHub Copilot / ChatGPT — Used for Javadoc generation and text refinement (human-reviewed)

Dynamically Resizable Window

The game window supports dynamic resizing while preserving a 16:9 aspect ratio using a single JavaFX canvas. Resize listeners adjust canvas dimensions and UI overlays in real time.

When the "frame" changes:

  • The old root Pane is replaced with the new one.
  • The scene is redrawn.

All UI elements (buttons, labels, fonts) scale accordingly. If a video is present, it is resized—not reloaded—for performance. Frame changes are made using:

AppViewManager.get().switchTo(new Frame());

Each new frame's constructor must configure the layout (e.g., using a VBox).

Data

Game data is saved and loaded as JSON using org.json.

Level Data

Story levels are in resources/datasaves/levels/story, while custom levels are in each profile’s folder.

{
  "updatedTime": 0,            -> time of last level update, utc timestamp
  "creator": "ProfiPoint",     -> name of the profile of the creator
  "goal": [5,0,90],            -> goal position and rotation
  "mapSize": [24,12],          -> size of the map width and height
  "creationTime": 0,           -> time of level creation, utc timestamp
  "maxArrows": 20,             -> limit of arrows
  "defaultLevel": false,       -> false = custom level, true = story level
  "start": [1,1,180],          -> start position and rotation
  "enemies": {                 -> array of enemies width_tile-height_tile, rotation
    "5-2": 270
  },
  "timeLimit": 180,            -> time limit in seconds
  "tiles": {                   -> array of tiles width_tile-height_tile, texture
    "0-1": 2,
    "1-0": 1
  },
  "creatorUpdated": "",        -> name of the profile of the last editor
  "goalSheep": 8,              -> number of sheep to get in goal for win
  "items": [                   -> list of items [width_tile-height_tile, type, duration]
    [4,3,0,1],                 -> 0 = coin
    [2,2,1,15],                -> 1 = rage
    [3,2,2,15]                 -> 2 = freeze
  ],
  "sheep": 8                   -> number of sheep to spawn
}

Profile Data

Each profile contains:

  • 1.json ... n.json: Custom levels
  • _DATA.json: Completed levels
{
  "completedCustom": [1],      -> id of completed custom levels
  "completedStory": [1,2,6,3]  -> id of completed story levels
}
  • _SETTINGS.json: Profile settings
{
  "music": 50,                 -> volume of the music
  "sfx": 50,                   -> volume of the sound effects
  "fps": 60,                   -> fps of the game
  "fullScreen": false          -> true = full screen, false = windowed
}
  • _CURRENT.txt: Active profile name
ProfileName                    -> name of the currently selected profile

Level/profile templates are also included in the same directory.

Testing

Unit and integration tests are implemented using JUnit 5 and are located in src/test/java.

Currently, only the Level class is tested, covering:

  • Loading and saving
  • Physics and collisions
  • Interaction with items, arrows, characters
  • Time limit behavior

Logging

The project uses java.util.logging for internal logging. A custom handler, LoggerStdOutErrHandler, redirects:

  • WARNING, SEVEREstderr
  • Others → stdout

Logging is configured in LoggerConfig and can be toggled using -logger or --logger arguments at startup.

Contribution

Want to contribute? Fork the project and create a pull request!

Adding New Objects

  • All renderable objects must inherit from GameObject (see IGameObject interface).

  • Pickable items must extend Item (IItem interface).

    • Also register them in ItemFactory and ItemEnum.
  • Characters should extend Character (ICharacter interface).

  • All objects must have textures managed in TextureEnum.

For saving/loading, update the Level class and associated JSON manager classes.

Custom gameplay logic should be added in the GamePhysics class.

Please use the same coding style, add Javadoc where appropriate, and read this documentation before contributing. It helps keep the project maintainable and fun to work on!