Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.
Copy link
Copy Markdown
Owner

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.

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

}
Loading
Loading