-
-
Notifications
You must be signed in to change notification settings - Fork 710
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
base: dev
Are you sure you want to change the base?
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,86 @@ | ||
| /* | ||
| * 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. | ||
| * | ||
| * @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 class TimerActionComponent extends Component { | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should be fine to make it |
||
|
|
||
| private final Timer timer = new Timer(); | ||
|
|
||
| @Override | ||
| public void onUpdate(double tpf) { | ||
| super.onUpdate(tpf); | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| 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(); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,240 @@ | ||
| /* | ||
| * 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.BeforeEach; | ||
| 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 { | ||
|
|
||
| /* Counter class to allow for atomic operations from Runnable action */ | ||
| private static class Counter { | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's fine to use such a helper class, but best to avoid using the same object from multiple tests in this way. For example, if the test environment runs the tests in parallel (I don't think it does, however we should try best practice where it's easy to do so), there may be an issue with multiple threads writing and reading the same data. This would mean simply using an |
||
| private int value; | ||
|
|
||
| int get () { return value ; } | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This formatting is largely inconsistent with the rest of the project's formatting. This will need to be updated to follow the standard Java guidelines. Though I think the fix for the above comment will remove the need for this altogether. |
||
| void reset () { value = 0; } | ||
| void increment() { value++ ; } | ||
| } | ||
|
|
||
| private final Counter executionCounter = new Counter(); | ||
| private final Runnable action = executionCounter::increment; | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same comment about formatting Line 34 and 35 as above. There is no need to align elements. |
||
|
|
||
| private TimerActionComponent timerActionComponent; | ||
|
|
||
| @BeforeEach | ||
| public void setUp() { | ||
| timerActionComponent = new TimerActionComponent(); | ||
| executionCounter.reset(); | ||
| } | ||
|
|
||
| /** | ||
| * 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() { | ||
| 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() { | ||
| 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() { | ||
| 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() { | ||
| 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() { | ||
| 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() { | ||
| 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() { | ||
| 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() { | ||
| 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() { | ||
| 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() { | ||
| 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() { | ||
| 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() { | ||
| timerActionComponent.runAtInterval(action, Duration.seconds(1)); | ||
| timerActionComponent.runOnceAfter(action, Duration.seconds(0.1f)); | ||
|
|
||
| timerActionComponent.clear(); | ||
| timerActionComponent.onUpdate(1.0f); | ||
| assertEquals(0, executionCounter.get()); | ||
| } | ||
|
|
||
| } | ||
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.