Okay, just to give a bit of background, now that I've got a basic idea of my overall game loop I've been experimenting with making sure that blocks of code I execute only execute once. I looked up the idea of booleans and integers being used as stoppers to prevent code from executing multiple times which I have seen happen thanks to some debugging with print but also when switching up my environments they would add 16 times or so which I think is due to the code executing over multiple frames.

To confirm this I put in an execution stopper using a simple integer like how some people do with booleans. This works really well considering and I'm pretty pleased with the results as when the in-game clock ticks over to the correct time everything executes once and only once and this has happened each time I've run the game which is going to help optimise my AI and in-game day/night cycle a lot. However, one thing I am concerned about is the idea of using an if not statement to reset the stopper once the code has executed. Is there a better way of optimising this code? Or is the resources usage from the integer being reset to 0 constantly when the if statements aren't being executed so negligible it's not worth worrying about?

	if hours == 7 && minutes == 0 && seconds == 0 && executionOrderStopper == 0:
		SetDay()
		godHandNightLight.visible = false
		isNight = false
		isDay = true
		executionOrderStopper = 1
		
	if not hours == 8 && minutes == 0 && seconds == 0 && executionOrderStopper == 1:
		executionOrderStopper = 0
	
	if hours == 8 && minutes == 0 && seconds == 0 && executionOrderStopper == 0:
		SetNight()
		godHandNightLight.visible = true
		isNight = true
		isDay = false
		executionOrderStopper = 1
		
	if not hours == 8 && minutes == 0 && seconds == 0 && executionOrderStopper == 1:
		executionOrderStopper = 0
	if dayNightTimer.hours == 8 && dayNightTimer.minutes == 0 && dayNightTimer.seconds == 0 && executionOrderStopper == 0:
		isSleeping = true
		isWondering = false
		isWalking = false
		speed = 0
		villagerStatusText.text = "Sleeping"
		MaleVillagerSleepingAnimation()
		print ("Sleep Executed")
		executionOrderStopper = 1
		
	if not dayNightTimer.hours == 8 && dayNightTimer.minutes == 0 && dayNightTimer.seconds == 0 && executionOrderStopper == 1:
		executionOrderStopper = 0
		print ("Sleep Code Stopped")
		
	if dayNightTimer.hours == 7 && dayNightTimer.minutes == 0 && dayNightTimer.seconds == 0 && executionOrderStopper == 0:
		MaleVillagerGettingUpFromSleepingAnimation()
		isSleeping = false
		isWondering = true
		isWalking = true
		speed = 500
		villagerStatusText.text = "Wondering"
		hasAnimationPlayed = false
		executionOrderStopper = 1
		
	if not dayNightTimer.hours == 7 && dayNightTimer.minutes == 0 && dayNightTimer.seconds == 0 && executionOrderStopper == 1:
		executionOrderStopper = 0

Edit: Although now I think about it I should just put a print to debug the if not statement and double check if it is indeed doing an infinite loop.

Edit 2: Hmm, yep resetting the executionOrderStopper is an obstacle for me right now, the code works best without it being reset so that's frustrating, any if statements I put in instantly switch the integer which then executes the code again for those small amounts of frames the time is equal to what I've written. I'm probably going to have to put something extra in to make sure the clock has gone past but if not isn't really doing it.

  • xyz replied to this.

    Lethn It's a very spaghettified way of doing it 🙂 with too much repetition and poorly readable code.
    You know that famous quote from Fred Brooks:
    Show me your flowcharts and conceal your tables, and I shall continue to be mystified. Show me your tables, and I won't usually need your flowcharts; they'll be obvious.

    What he meant is that code based on well organized data structures is easy to read and write. So you should always strive to solve a problem by figuring out best data structures to represent it. With well chosen and well named data structures, your code can become short, expressive and self-explanatory.

    In this case, you're basically making a scheduler so it'd be a good idea to make each task a separate function and keep them in a dictionary together with times they need to be executed at. Now simply go through this dictionary, check if time has come to execute a task. If yes, call the task's function. To simplify time comparison, your little scheduler's internal time keeping could be in seconds, but you'll perhaps want to input time in nice HH:MM:SS format So you'll need a helper function that converts from human friendly format to seconds.
    To put it all together:

    extends Node
    
    # helper function to convert HH:MM:SS strings to seconds
    func to_seconds(time_string:String):
    	var tokens = time_string.split(":")
    	return int(tokens[0]) * 60 * 60 + int(tokens[1]) * 60 + int(tokens[2])
    
    # Scheduled tasks
    func eat():
    	print("eating")
    
    func sleep():
    	print("sleeping")
    	
    # Schedule dictionary with task functions as values and task times (in seconds) as keys
    var schedule = { 
    	to_seconds("10:22:12"): eat,
    	to_seconds("10:22:14"): sleep,
    }
    
    # helper function to determine if time has passed
    func time_mark_passed(time_mark, time_last_frame, time_now):
    	return sign(time_last_frame - time_mark) <= 0 and sign(time_now - time_mark) > 0
    
    # current time and previous frame time
    var time_now = to_seconds("10:22:10") 
    var time_last_frame = time_now
    				
    # do the scheduling in real time
    func _process(delta):
    	for task_time in schedule.keys():
    		if time_mark_passed(task_time, time_last_frame, time_now):
    			schedule[task_time].call()
    	
    	time_last_frame = time_now
    	time_now += delta

    Note that I used real time here but it'd be exactly the same with some virtual ingame time.

      xyz It looks like I was right to focus on only one particular function then before everything got too complicated early on, it also looks like I need to learn properly about dictionaries and organising my code better, my only concern with the dictionary method is potentially won't I run into the same problem of an infinite loop? Or do dictionaries work differently compared to the if statements within if statements and the process function?

      • xyz replied to this.

        Lethn You should definitely make friends with Dictionaries 🙂 Associative arrays (aka dictionaries) are one of the basic concepts in programming in general. They are immensely useful for keeping all sorts of data.

        Using a dictionary isn't the main thing here though.

        So how to ensure that each task from the schedule is called only once?

        You have to periodically iterate through all tasks in the schedule and determine if particular task's time has come. In order to do this you'll need to maintain two time stamps: current time at which you're looking at the schedule, and time you last looked at the schedule. If task's scheduled time is between these two - run it. This makes impossible for a task to be called more than once in a day. Note that the interval duration between consecutive schedule checks is not really important. Although in the example I do it every frame, it will work properly with any interval. You can do it every second, or every 5 minutes...

        I suspect something like this may be good to learn though because I do plan on having quite a lot of villagers on screen at once for my game and things going on so the last thing I want is wasteful executions happening which is why I'm exploring it now before it gets out of hand. I've been experimenting with Godot 4's stuff though and I've been very impressed with the performance so far, massive difference compared to Godot 3. I mean I could use a general timer I guess and still use the if statements? I'll have to experiment, I need to get comfortable with dictionaries and see what I can do.

        You could use signals for this kind of thing.

        extends Node2D
        
        # Declare signals
        signal next_phase(day: bool)
        signal next_hr(hr: int, day: bool)
        
        # Declare vars to save day/night cycle state
        var hour := 0
        var max_hrs := 12
        var day := false
        
        
        # Called when the node enters the scene tree for the first time.
        func _ready():
        	# 1 hr in s. THIS IS IN REAL TIME
        	$DayNightCycle.start(3600)
        	# Every time the timer reaches 0,
        	# Advance the current hour
        	$DayNightCycle.timeout.connect(advance_hr)
        	# Toggle day/night if necessary
        	$DayNightCycle.timeout.connect(reset_hr)
        	# Check if it's time to sleep
        	next_hr.connect(sleep)
        
        
        func advance_hr() -> void:
        	hour += 1 # Advance hour
        	next_hr.emit(hour, day) # Notify all connected functions
        
        
        func reset_hr() -> void:
        	if hour == max_hrs: # Check if the day/night is over
        		hour = 0
        		day = not day
        		next_phase.emit(day) # Notify all connected functions
        
        
        func sleep(time: int, time_of_day: bool) -> void:
        	if time == 8 and not time_of_day: # Check if it's time to sleep
        		print("sleep")

        advance_hr() and reset_hr() will be called every time the timer reaches 0, and sleep() will be called every time that advance_hr() runs; however, the if-statements will make sure that it only runs when it's supposed to (i.e. at the 8th hr of the night). This example uses a Timer node, which counts real-world time, but it's trivial to adapt it to your own system: just declare the signals (along with any relevant state info) and emit them when you want things to happen. Then all you have to do is connect the functions from your other scripts.

        If you have a ton of different villagers, you might consider making an array of Callables and using the hour as an index, e.g.

        var night_tasks = [rest, rest, rest, sleep, rest, rest]
        
        
        func rest():
        	pass
        
        
        func do_night_tasks(hr: int):
        	var fn: Callable = night_tasks[hr]
        	if fn:
        		fn.call()
        
        
        next_hr.connect(do_night_tasks)

        This is easier to manipulate programmatically.

        These are some interesting solutions, hadn't thought of signals being used that way, I'll have a poke around at both because I do need to learn dictionaries regardless, thanks a lot for the suggestions guys.