-
Notifications
You must be signed in to change notification settings - Fork 0
Day and Night Cycle Service
Jump to a section or return to Day and Night Cycle Summary here!
DayNightCycleService
was created to be the foundation of the day and night cycle. The service is called when the game is first started started by the player and runs in the background in its own thread for the duration of the game -- keeping track of how many milliseconds have elapsed in the current day, what day number it is and what phase (DAWN
, DAY
, DUSK
, NIGHT
) of the cycle it currently is. The service also handles triggering of the events that inform other features of the current day phase so they are able to dynamically update their status based on the current day state (e.g. trigger the enemies to spawn at night).
The main part of implementing the cycle, DayNightCycleService
controls the running of the day/night service.
public CompletableFuture<Object> start() {
if (this.isStarted && !this.isPaused) {
throw new IllegalStateException("The timer has already been started");
}
if (this.isPaused) {
// Resuming a timer
logger.info("Day/night cycle resumed");
this.isPaused = false;
return null; // Avoid running another async job
}
this.isStarted = true;
this.setPartOfDayTo(DayNightCycleStatus.DAWN);
return JobSystem.launch(() -> {
try {
this.run();
} catch (InterruptedException e) {
logger.error(e.getMessage());
}
return null;
});
}
public void pause() {
this.isPaused = true;
this.timePaused = this.currentDayMillis;
logger.info("Day/night cycle paused");
}
The run method handles updating the current day, the milliseconds passed, and triggering the events for the different phases of the day/night cycle. The method sleeps for 100 milliseconds after each loop to reduce the memory load on the system running it.
When the game is not paused, the current day time is updated and then compared to the different thresholds for DAWN
, DAY
, DUSK
and NIGHT
. If the current time falls into a new threshold and is currently in the correct phase of the day, the day cycle will be progressed to the next phase and the EVENT_PART_OF_DAY_PASSED
event is triggered.
this.currentDayMillis = this.timer.getTime() - (this.currentDayNumber * (config.nightLength +
config.duskLength + config.dayLength + config.dawnLength)) - this.totalDurationPaused + this.loadedTimeOffset;
For more information on the loadedTimeOffset, see Save Game
After checking all thresholds, if the day would tick over from NIGHT
to DAWN
, the method will check if all days have been run through. If they have then the service will stop before informing listeners that the a day has passed through the EVENT_DAY_PASSED
event.
if (this.currentDayNumber == config.maxDays - 1) {
// End the game
this.stop();
Gdx.app.postRunnable(() -> {
events.trigger(EVENT_DAY_PASSED, this.currentDayNumber + 1);
});
return;
}
If the game is paused then the run method will keep track of how many milliseconds it has been since the service was paused.
durationPaused = this.timer.getTimeSince(this.timePaused);
The DayNightCycleConfig
class holds the default values for the different day/night phase lengths and the max number of game days. This can be populated by a config file with the desired durations and game days, or manually set whenever a new instance of the service is created.
public class DayNightCycleConfig {
public long dawnLength = 1;
public long dayLength = 1;
public long duskLength = 1;
public long nightLength = 1;
public int maxDays = 1;
}
DayNightCycleStatus
holds an enum for the different day statuses to reduce errors when implementing the day/night cycle in the game from misspellings.
public enum DayNightCycleStatus {
NONE,
DAWN,
DAY,
DUSK,
NIGHT;
}
The below UML diagram outlines the structure of the service and its dependencies, with the classes above highlighted in green.
This type of representation makes it easier to see how DayNightCycleService
works within the context of the game.
The functionality of DayNightCycleService
was tested using JUnit and Mockito in DayNightServiceTest
. These tests check that days are updating as they should, the timer is pausing and resuming correctly, and that events are being sent correctly from by the event handler.
These tests were written to check that the service was correctly cycling through the different stages of the day, and that the timer was correctly stopping after the maximum number of days had passed.
shouldAdvanceToDayWhenAtDawn()
shouldCompleteFullCyclePassingThroughAllPartsOfDay()
shouldCompleteFullDayRestartingAtDawn()
shouldStopWhenDaysAreCompleted()
shouldGoThroughAllNumberOfDays()
These tests are important for checking the core functionality of the day/night cycle service. If they were failing then it would mean there is a flaw in the implementation of the service and it likely will not function as intended at all.
Pausing and resuming the timer is also an important part of the service. The timer for the service is GameTime
object that is passed through the declaration method, and does not have any pause functionality by itself.
shouldStopDayTimeWhenPaused()
shouldResumeDayTimeWhenResumed()
shouldResumeDayTimeAfterBeingPausedAndResumedMultipleTimes()
If these tests were not passing, it would mean that either the service was not stopping running when paused or that the amount of time the timer had been paused for was not being accounted for correctly.
The final batch of tests for the DayNightCycleService
were intended to check the event trigger methods.
whenSendingFirstDayPassedEventDayShouldBeginAtOne()
shouldGoThroughCorrectSequenceOfDays()
shouldCycleThroughAllWedgesOfDay()
shouldCycleThroughAllWedgesOfNight()
shouldGoThroughAllEightWedges()
shouldHaveDelaysBetweenAllWedges()
These events allow other services to be informed of the current state of the game - including what day number it is and what the current phase of the day/night cycle the game is currently in. This is important for the game as the spawning of enemy entities is tied to the night phase of the cycle, so if the service was not able to correctly send the event triggers it would negatively affect the development of the rest of the game.
The main challenges faced in development of the day/night service in handling pausing and resuming the timer. The method used is likely not the most optimal as it requires the service to keep track of multiple different millisecond values; however, by subtracting the total duration paused when calculating the current milliseconds that have passed for the current day it is possible to keep the timer consistent.