-
-
Notifications
You must be signed in to change notification settings - Fork 695
feat: TimerActionComponent to schedule actions per entity. #1469
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
michqql
wants to merge
2
commits into
AlmasB:dev
Choose a base branch
from
michqql:dev
base: dev
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
87 changes: 87 additions & 0 deletions
87
fxgl-entity/src/main/java/com/almasb/fxgl/entity/component/TimerActionComponent.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,87 @@ | ||
| /* | ||
| * FXGL - JavaFX Game Library. The MIT License (MIT). | ||
| * Copyright (c) AlmasB (almaslvl@gmail.com). | ||
| * See LICENSE for details. | ||
| */ | ||
|
|
||
| package com.almasb.fxgl.entity.component; | ||
|
|
||
| import com.almasb.fxgl.time.Timer; | ||
| import com.almasb.fxgl.time.TimerAction; | ||
| import javafx.beans.property.ReadOnlyBooleanProperty; | ||
| import javafx.util.Duration; | ||
|
|
||
| /** | ||
| * Component to schedule the execution of actions. | ||
| * The timer's time per frame (TPF) is tied to the entity's TPF. | ||
| * The entity's TPF can be modified by {@link com.almasb.fxgl.entity.components.TimeComponent}. | ||
| * | ||
| * @implNote - A wrapper around a Timer class, as a component. | ||
| * | ||
| * @author Michael Pearson (<a href="https://github.com/michqql/">https://github.com/michqql/</a>) | ||
| */ | ||
| public final class TimerActionComponent extends Component { | ||
|
|
||
| private final Timer timer = new Timer(); | ||
|
|
||
| @Override | ||
| public void onUpdate(double tpf) { | ||
| this.timer.update(tpf); | ||
| } | ||
|
|
||
| /** | ||
| * The Runnable [action] will be scheduled to start at given [interval]. | ||
| * The action will start for the first time after given interval. | ||
| * The action will be scheduled unlimited number of times unless user cancels it | ||
| * via the returned action object. | ||
| * | ||
| * @return timer action | ||
| */ | ||
| public TimerAction runAtInterval(Runnable action, Duration interval) { | ||
| return timer.runAtInterval(action, interval); | ||
| } | ||
|
|
||
| /** | ||
| * The Runnable [action] will be scheduled to start at given [interval]. | ||
| * The action will start for the first time after given interval. | ||
| * The action will be scheduled [limit] number of times unless user cancels it | ||
| * via the returned action object. | ||
| * | ||
| * @return timer action | ||
| */ | ||
| public TimerAction runAtInterval(Runnable action, Duration interval, int limit) { | ||
| return timer.runAtInterval(action, interval, limit); | ||
| } | ||
|
|
||
| /** | ||
| * The Runnable [action] will be scheduled to start at given [interval]. | ||
| * The Runnable action will be scheduled IFF | ||
| * [whileCondition] is initially true. | ||
| * The action will start for the first time after given interval. | ||
| * The action will be removed from schedule when [whileCondition] becomes "false". | ||
| * Note: you must retain the reference to the [whileCondition] property to avoid it being | ||
| * garbage collected, otherwise the [action] may never stop. | ||
| * | ||
| * @return timer action | ||
| */ | ||
| public TimerAction runAtIntervalWhile(Runnable action, Duration interval, ReadOnlyBooleanProperty whileCondition) { | ||
| return timer.runAtIntervalWhile(action, interval, whileCondition); | ||
| } | ||
|
|
||
| /** | ||
| * The Runnable [action] will be scheduled to run once after given [delay]. | ||
| * The action can be cancelled before it starts via the returned action object. | ||
| * | ||
| * @return timer action | ||
| */ | ||
| public TimerAction runOnceAfter(Runnable action, Duration delay) { | ||
| return timer.runOnceAfter(action, delay); | ||
| } | ||
|
|
||
| /** | ||
| * Remove all scheduled actions. | ||
| */ | ||
| public void clear() { | ||
| timer.clear(); | ||
| } | ||
| } | ||
267 changes: 267 additions & 0 deletions
267
fxgl-entity/src/test/java/com/almasb/fxgl/entity/component/TimerActionComponentTest.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,267 @@ | ||
| /* | ||
| * FXGL - JavaFX Game Library. The MIT License (MIT). | ||
| * Copyright (c) AlmasB (almaslvl@gmail.com). | ||
| * See LICENSE for details. | ||
| */ | ||
|
|
||
| package com.almasb.fxgl.entity.component; | ||
|
|
||
| import com.almasb.fxgl.time.TimerAction; | ||
| import javafx.beans.binding.BooleanBinding; | ||
| import javafx.beans.property.*; | ||
| import javafx.util.Duration; | ||
| import org.junit.jupiter.api.Test; | ||
|
|
||
| import static org.junit.jupiter.api.Assertions.*; | ||
|
|
||
| /** | ||
| * Tests for the {@link TimerActionComponent} class. | ||
| * | ||
| * @author Michael Pearson (<a href="https://github.com/michqql/">https://github.com/michqql/</a>) | ||
| */ | ||
| public class TimerActionComponentTest { | ||
|
|
||
| /** | ||
| * Tests that the action is ran multiple times as the interval elapses. | ||
| * Tests the method {@link TimerActionComponent#runAtInterval(Runnable, Duration, int) runAtInterval}. | ||
| */ | ||
| @Test | ||
| public void testRunAtInterval() { | ||
| final TimerActionComponent timerActionComponent = new TimerActionComponent(); | ||
| final IntegerProperty executionCounter = new SimpleIntegerProperty(); | ||
| final Runnable action = () -> executionCounter.set(executionCounter.get() + 1); | ||
|
|
||
| timerActionComponent.runAtInterval(action, Duration.seconds(0.5f)); | ||
|
|
||
| assertEquals(0, executionCounter.get()); | ||
| timerActionComponent.onUpdate(0.5f); | ||
| assertEquals(1, executionCounter.get()); | ||
| timerActionComponent.onUpdate(0.4f); | ||
| assertEquals(1, executionCounter.get()); | ||
| timerActionComponent.onUpdate(0.2f); | ||
| assertEquals(2, executionCounter.get()); | ||
| } | ||
|
|
||
| /** | ||
| * Tests that the repeating action will not be executed after being cancelled. | ||
| */ | ||
| @Test | ||
| public void testRunAtIntervalThenCancel() { | ||
| final TimerActionComponent timerActionComponent = new TimerActionComponent(); | ||
| final IntegerProperty executionCounter = new SimpleIntegerProperty(); | ||
| final Runnable action = () -> executionCounter.set(executionCounter.get() + 1); | ||
|
|
||
| TimerAction timerAction = timerActionComponent.runAtInterval(action, Duration.seconds(0.5f)); | ||
|
|
||
| assertEquals(0, executionCounter.get()); | ||
| timerActionComponent.onUpdate(0.5f); | ||
| assertEquals(1, executionCounter.get()); | ||
|
|
||
| timerAction.expire(); | ||
|
|
||
| timerActionComponent.onUpdate(0.5f); | ||
| assertEquals(1, executionCounter.get()); | ||
| } | ||
|
|
||
| /** | ||
| * Tests that the action can be executed multiple times. | ||
| */ | ||
| @Test | ||
| public void testRunAtIntervalInLoop() { | ||
| final TimerActionComponent timerActionComponent = new TimerActionComponent(); | ||
| final IntegerProperty executionCounter = new SimpleIntegerProperty(); | ||
| final Runnable action = () -> executionCounter.set(executionCounter.get() + 1); | ||
|
|
||
| timerActionComponent.runAtInterval(action, Duration.seconds(0.5f)); | ||
|
|
||
| for(int i = 0; i < 10; ++i) { | ||
| timerActionComponent.onUpdate(0.5f); | ||
| assertEquals(i + 1, executionCounter.get()); | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Tests that the action expires with the limit and will not execute again. | ||
| */ | ||
| @Test | ||
| public void testRunAtIntervalWithLimit() { | ||
| final TimerActionComponent timerActionComponent = new TimerActionComponent(); | ||
| final IntegerProperty executionCounter = new SimpleIntegerProperty(); | ||
| final Runnable action = () -> executionCounter.set(executionCounter.get() + 1); | ||
|
|
||
| timerActionComponent.runAtInterval(action, Duration.seconds(0.5f), 4); | ||
|
|
||
| for(int i = 0; i < 10; ++i) { | ||
| timerActionComponent.onUpdate(0.5f); | ||
| } | ||
|
|
||
| assertEquals(4, executionCounter.get()); | ||
| } | ||
|
|
||
| /** | ||
| * Tests that the action expires when the condition becomes false. | ||
| */ | ||
| @Test | ||
| public void testRunAtIntervalConditional() { | ||
| final TimerActionComponent timerActionComponent = new TimerActionComponent(); | ||
| final IntegerProperty executionCounter = new SimpleIntegerProperty(); | ||
| final Runnable action = () -> executionCounter.set(executionCounter.get() + 1); | ||
|
|
||
| IntegerProperty iterationCount = new SimpleIntegerProperty(); | ||
| BooleanBinding condition = iterationCount.lessThan(5); | ||
|
|
||
| BooleanProperty conditionProperty = new SimpleBooleanProperty(); | ||
| conditionProperty.bind(condition); | ||
|
|
||
| timerActionComponent.runAtIntervalWhile(action, Duration.seconds(0.25f), conditionProperty); | ||
|
|
||
| for(int i = 0; i < 10; ++i) { | ||
| timerActionComponent.onUpdate(0.5f); | ||
| iterationCount.set(iterationCount.get() + 1); | ||
| } | ||
|
|
||
| assertEquals(5, executionCounter.get()); | ||
| } | ||
|
|
||
| /** | ||
| * Tests that the conditional action can be cancelled by the user. | ||
| */ | ||
| @Test | ||
| public void testRunAtIntervalConditionalCancelledEarly() { | ||
| final TimerActionComponent timerActionComponent = new TimerActionComponent(); | ||
| final IntegerProperty executionCounter = new SimpleIntegerProperty(); | ||
| final Runnable action = () -> executionCounter.set(executionCounter.get() + 1); | ||
|
|
||
| IntegerProperty iterationCount = new SimpleIntegerProperty(); | ||
| BooleanBinding condition = iterationCount.lessThan(5); | ||
|
|
||
| BooleanProperty conditionProperty = new SimpleBooleanProperty(); | ||
| conditionProperty.bind(condition); | ||
|
|
||
| TimerAction timerAction = timerActionComponent.runAtIntervalWhile(action, Duration.seconds(0.25f), conditionProperty); | ||
|
|
||
| for(int i = 0; i < 10; ++i) { | ||
| timerActionComponent.onUpdate(0.5f); | ||
| iterationCount.set(iterationCount.get() + 1); | ||
|
|
||
| /* Cancel on the 3rd iteration */ | ||
| if(i == 2) | ||
| timerAction.expire(); | ||
| } | ||
|
|
||
| assertEquals(3, executionCounter.get()); | ||
| } | ||
|
|
||
| /** | ||
| * Tests that the action is ran exactly once after the delay has elapsed | ||
| * when using {@link TimerActionComponent#runOnceAfter(Runnable, Duration) runOnceAfter}. | ||
| */ | ||
| @Test | ||
| public void testRunOnceAfter() { | ||
| final TimerActionComponent timerActionComponent = new TimerActionComponent(); | ||
| final IntegerProperty executionCounter = new SimpleIntegerProperty(); | ||
| final Runnable action = () -> executionCounter.set(executionCounter.get() + 1); | ||
|
|
||
| timerActionComponent.runOnceAfter(action, Duration.seconds(1)); | ||
|
|
||
| assertEquals(0, executionCounter.get()); | ||
| timerActionComponent.onUpdate(1.0f); | ||
| assertEquals(1, executionCounter.get()); | ||
| timerActionComponent.onUpdate(1.0f); | ||
| assertEquals(1, executionCounter.get()); | ||
| } | ||
|
|
||
| /** | ||
| * Tests that the action is only ran after the delay has elapsed, | ||
| * when the component has already seen time elapse. | ||
| * Tests the method {@link TimerActionComponent#runOnceAfter(Runnable, Duration) runOnceAfter}. | ||
| */ | ||
| @Test | ||
| public void testRunOnceAfterWithStartingTime() { | ||
| final TimerActionComponent timerActionComponent = new TimerActionComponent(); | ||
| final IntegerProperty executionCounter = new SimpleIntegerProperty(); | ||
| final Runnable action = () -> executionCounter.set(executionCounter.get() + 1); | ||
|
|
||
| timerActionComponent.onUpdate(2.0f); | ||
| timerActionComponent.runOnceAfter(action, Duration.seconds(1)); | ||
|
|
||
| assertEquals(0, executionCounter.get()); | ||
| timerActionComponent.onUpdate(1.0f); | ||
| assertEquals(1, executionCounter.get()); | ||
| } | ||
|
|
||
| /** | ||
| * Tests that the action is not ran if not enough time elapses. | ||
| * Tests the method {@link TimerActionComponent#runOnceAfter(Runnable, Duration) runOnceAfter}. | ||
| */ | ||
| @Test | ||
| public void testRunOnceNotElapsed() { | ||
| final TimerActionComponent timerActionComponent = new TimerActionComponent(); | ||
| final IntegerProperty executionCounter = new SimpleIntegerProperty(); | ||
| final Runnable action = () -> executionCounter.set(executionCounter.get() + 1); | ||
|
|
||
| timerActionComponent.runOnceAfter(action, Duration.seconds(1)); | ||
|
|
||
| assertEquals(0, executionCounter.get()); | ||
| timerActionComponent.onUpdate(0.999f); | ||
| assertEquals(0, executionCounter.get()); | ||
| } | ||
|
|
||
| /** | ||
| * Tests that the action is not executed if cancelled before the delay elapses. | ||
| * Tests the method {@link TimerActionComponent#runOnceAfter(Runnable, Duration) runOnceAfter}. | ||
| */ | ||
| @Test | ||
| public void testRunOnceCancelled() { | ||
| final TimerActionComponent timerActionComponent = new TimerActionComponent(); | ||
| final IntegerProperty executionCounter = new SimpleIntegerProperty(); | ||
| final Runnable action = () -> executionCounter.set(executionCounter.get() + 1); | ||
|
|
||
| TimerAction timerAction = timerActionComponent.runOnceAfter(action, Duration.seconds(1)); | ||
| timerAction.expire(); | ||
|
|
||
| assertEquals(0, executionCounter.get()); | ||
| timerActionComponent.onUpdate(1.0f); | ||
| assertEquals(0, executionCounter.get()); | ||
| } | ||
|
|
||
| /** | ||
| * Tests that multiple actions can be scheduled. | ||
| */ | ||
| @Test | ||
| public void testRunOnceWithMultiple() { | ||
| final TimerActionComponent timerActionComponent = new TimerActionComponent(); | ||
| final IntegerProperty executionCounter = new SimpleIntegerProperty(); | ||
| final Runnable action = () -> executionCounter.set(executionCounter.get() + 1); | ||
|
|
||
| timerActionComponent.runOnceAfter(action, Duration.seconds(0.1f)); | ||
| timerActionComponent.runOnceAfter(action, Duration.seconds(0.2f)); | ||
| timerActionComponent.runOnceAfter(action, Duration.seconds(0.29f)); /* Floating point cannot represent 0.3 well */ | ||
| timerActionComponent.runOnceAfter(action, Duration.seconds(0.4f)); | ||
| timerActionComponent.runOnceAfter(action, Duration.seconds(0.5f)); | ||
|
|
||
|
|
||
| for(int i = 0; i < 5; ++i) { | ||
| timerActionComponent.onUpdate(0.1f); | ||
| assertEquals(i + 1, executionCounter.get()); | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Tests that clearing the component will remove all actions. | ||
| */ | ||
| @Test | ||
| public void testClear() { | ||
| final TimerActionComponent timerActionComponent = new TimerActionComponent(); | ||
| final IntegerProperty executionCounter = new SimpleIntegerProperty(); | ||
| final Runnable action = () -> executionCounter.set(executionCounter.get() + 1); | ||
|
|
||
| timerActionComponent.runAtInterval(action, Duration.seconds(1)); | ||
| timerActionComponent.runOnceAfter(action, Duration.seconds(0.1f)); | ||
|
|
||
| timerActionComponent.clear(); | ||
| timerActionComponent.onUpdate(1.0f); | ||
| assertEquals(0, executionCounter.get()); | ||
| } | ||
|
|
||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Worth noting that the timer's clock will be tied to the entity's clock. So 1 second in this TimerActionComponent will depend on how long 1 second is for the entity, e.g. TimeComponent may alter that behaviour (which is the correct desirable behaviour). Not quite sure exactly the wording for the doc, but something along these lines should do the trick.