Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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,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.

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.

*
* @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 {

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.

Should be fine to make it final by default. It can be opened in the future if required


private final Timer timer = new Timer();

@Override
public void onUpdate(double tpf) {
super.onUpdate(tpf);

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.

super.onUpdate(tpf); calls Component onUpdate(), which I think may be empty. If it is, then we can safely remove this line.

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 {

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.

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 int count = 0. If this doesn't work because of lambda functions, you can use var count = new SimpleIntegerProperty(), in each test (like you have done in public void testRunAtIntervalConditionalCancelledEarly()). This should give you more or less the same functionality, but in a relatively safer way.

private int value;

int get () { return value ; }

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.

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;

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.

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

}
Loading
Loading