[SOLVED] Saving game before changing room

Started by Laura Hunt, Thu 28/05/2020 18:22:07

Previous topic - Next topic

Laura Hunt

Hey all,

My situation is the following:

- My game has its Main Menu in Room 1.

- I have a Pause Menu with, among others, a "Back to Main Menu" button.

- When you click on this button, you get a warning saying "Save progress?" and two buttons for Yes and No. Clicking on any of these buttons takes you to Room 1.

I have no idea, however, how to save the game before changing to room 1. Since SaveGameSlot is always executed at the end of the script, the room change will take place before that, and the Save command will never get executed.

Any hints? Is this even possible? ???




Snarky

#1
https://www.youtube.com/watch?v=C9JgZ45ENl4

Put the room change on a timer.


Laura Hunt

I got it! Here's the code for posterity :) (and in case anything can be improved)

Clicking on the "Yes, I wish to save my progress thank you very much" button:

Code: ags
function bYesSave_OnClick(GUIControl *control, MouseButton button)
{
  gPauseMenu.Visible = false;
  SetTimer(20, 3);  // maybe just 1 cycle is enough? Just being cautious here
  SaveGameSlot(23, "saved game");
}


In repeatedly_execute:

Code: ags
if (IsTimerExpired(20)) {
    Game.StopAudio();
    cInvisibleCharacter.ChangeRoom(1);
    cInvisibleCharacter.SetAsPlayer();
}


This causes a little problem, because as soon as you restore your saved game, the timer gets checked again, and you get sent to room 1 again and again. So we need one more check:

Code: ags
function on_event(EventType event, int data)
{
  if (event == eEventRestoreGame && data == 23) {
    SetTimer(20, 0);  // disarm timer
    gPauseMenu.Visible = false; // because there are other points in which you can save with the GUI still open, so just to be sure
  }
}


And everything works perfectly! Thank you so much, Snarky :)

Crimson Wizard

Quote from: Snarky on Thu 28/05/2020 18:26:56
Put the room change on a timer.

Depending on circumstances this may require to script a "disabled" state for your UI, during which player cannot press or click anything and no guis open no matter what.

Laura Hunt

Quote from: Crimson Wizard on Thu 28/05/2020 19:04:59
Quote from: Snarky on Thu 28/05/2020 18:26:56
Put the room change on a timer.

Depending on circumstances this may require to script a "disabled" state for your UI, during which player cannot press or click anything and no guis open no matter what.

What would you use instead of a timer? A boolean variable maybe? (set to true when you save, check if it is true in repeatedly_execute_always, change room and set to false if it is, set to false also on on_event?)

Crimson Wizard

Quote from: Laura Hunt on Thu 28/05/2020 19:47:08
What would you use instead of a timer? A boolean variable maybe? (set to true when you save, check if it is true in repeatedly_execute_always, change room and set to false if it is, set to false also on on_event?)

That is also an option that might work in most cases.

But personally I'd probably get paranoid that AGS can call on_mouse_click or key_pressed anyway before checking rep-exec, and implement disabled controls mode :).

Laura Hunt

Quote from: Crimson Wizard on Thu 28/05/2020 20:02:37
Quote from: Laura Hunt on Thu 28/05/2020 19:47:08
What would you use instead of a timer? A boolean variable maybe? (set to true when you save, check if it is true in repeatedly_execute_always, change room and set to false if it is, set to false also on on_event?)

That is also an option that might work in most cases.

But personally I'd probably get paranoid that AGS can call on_mouse_click or key_pressed anyway before checking rep-exec, and implement disabled controls mode :).

I can send you the game if you want and you can try clicking really fast and see if you manage to break it :D

Laura Hunt

An additional note (just in case this is useful for somebody in the future): I quickly realized that what I want to do is not simply go back to the main menu, because if a player chooses "New Game" from there, all the game states, variables, etc will be completely out of whack. What I actually need is to restart the game in order to revert everything to its initial state.

However, this would make the intro play again instead of going straight to the main menu, so what I've done is:

- Replaced the whole cInvisibleCharacter.ChangeRoom(1); shebang with RestartGame();
- Created a global bool, skipintro, and set it to false.
- Added the following condition to my on_event function:

Code: ags
  if (event == eEventRestoreGame && data == 999) {
    skipintro = true;
    game_start();
  }


- Wrapped my intro in the conditional if (!skipintro)

Et voilà! Now the intro only plays when you launch the game, but not when you restart.

Now my next challenge is figuring out how to store and apply the player's current volume level on restart. It would be very unpleasant for somebody who has set the volume slider really low to suddenly get blasted in the face with the main menu music at its default game_start value, so it would be good to figure out if there's a way to pull this off *puts thinking hat on but also accepts suggestions*

Crimson Wizard

Quote from: Laura Hunt on Thu 28/05/2020 23:14:43
Now my next challenge is figuring out how to store and apply the player's current volume level on restart. It would be very unpleasant for somebody who has set the volume slider really low to suddenly get blasted in the face with the main menu music at its default game_start value, so it would be good to figure out if there's a way to pull this off *puts thinking hat on but also accepts suggestions*

That should be easy with custom file in savegame directory. Files are natural solution for storing state independent of what happens in program.

Laura Hunt

Quote from: Crimson Wizard on Thu 28/05/2020 23:17:04
Quote from: Laura Hunt on Thu 28/05/2020 23:14:43
Now my next challenge is figuring out how to store and apply the player's current volume level on restart. It would be very unpleasant for somebody who has set the volume slider really low to suddenly get blasted in the face with the main menu music at its default game_start value, so it would be good to figure out if there's a way to pull this off *puts thinking hat on but also accepts suggestions*

That should be easy with custom file in savegame directory. Files are natural solution for storing state independent of what happens in program.

Amazing! I'll look into it and see what I can do with this. Thanks! ;-D

Laura Hunt

Code: ags
function bNoSave_OnClick(GUIControl *control, MouseButton button)
{
  gPauseMenu.Visible = false;
  
  File *customvolume = File.Open("$SAVEGAMEDIR$/customdata.dat", eFileWrite);
  if (customvolume == null) return;
  else {
    customvolume.WriteInt(System.Volume);
    customvolume.Close();
  }
  
  RestartGame();
  
}



Code: ags
function game_start() 
{
  File *customvolume = File.Open("$SAVEGAMEDIR$/customdata.dat", eFileRead);
  if (customvolume == null) System.Volume = 70;
  else {
    System.Volume = customvolume.ReadInt();
    sAudioVolume.Value = System.Volume;
    customvolume.Close();
  }
...
...
...
}



Works beautifully! And as a bonus, this also allows me to keep my previous volume level even when I close and re-launch the game. Thanks again for the tip, CW ;-D

fernewelten

#12
Quote from: Laura Hunt on Thu 28/05/2020 19:47:08
What would you use instead of a timer? A boolean variable maybe? (set to true when you save, check if it is true in repeatedly_execute_always, change room and set to false if it is, set to false also on on_event?)

The mechanism that AGS provides for that is CallRoomScript().

The button would call "SaveGameSlot(99);" and then  "CallRoomScript(RESTART_GAME);". Each room would have "function on_call(int script) { if (RESTART_GAME == script) do_your_thing();". Then the function do_your_thing() would be implemented in GlobalScript so that all the rooms have identical handling code. This handling code would run after "SaveGameSlot();" AFAIK. I hope.

CallRoomScript() is my go-to method whenever I need to start some code after the current script has finished. What a pity that this approach entails putting a room event handler into _every_ room, which is easy to forget and error prone.

Now if AGS had a similar "CallGlobalScript()" function that would trigger a "on_global_call(int script)" function in GlobalScript.asc … but alas.

Khris

That would be a huge pity, but you can just call the function directly as long as it's above the current one in the global script?

Crimson Wizard

Perhaps for this case, it would be more useful to have something like "after save game" event that you could get in on_event callback, similar to eEventRestoreGame?

But AGS events and callbacks would benefit from thoughtful redesign anyway.

Laura Hunt

#15
Quote from: fernewelten on Fri 29/05/2020 00:14:23
Quote from: Laura Hunt on Thu 28/05/2020 19:47:08
What would you use instead of a timer? A boolean variable maybe? (set to true when you save, check if it is true in repeatedly_execute_always, change room and set to false if it is, set to false also on on_event?)

The mechanism that AGS provides for that is CallRoomScript().

The button would call "SaveGameSlot(99);" and then  "CallRoomScript(RESTART_GAME);". Each room would have "function on_call(int script) { if (RESTART_GAME == script) do_your_thing();". Then the function do_your_thing() would be implemented in GlobalScript so that all the rooms have identical handling code. This handling code would run after "SaveGameSlot();" AFAIK. I hope.

CallRoomScript() is my go-to method whenever I need to start some code after the current script has finished. What a pity that this approach entails putting a room event handler into _every_ room, which is easy to forget and error prone.

Now if AGS had a similar "CallGlobalScript()" function that would trigger a "on_global_call(int script)" function in GlobalScript.asc … but alas.

I did not understand a single word of that :)

Is "RESTART_GAME" an... int? Where do you get/set the value of "script"? What should do_your_thing() do? This is all a bit too much for me, and the manual isn't helping (can't find on_call on there, for example). Would you be so kind as to give me a breakdown of what exactly is happening here to see if I can grasp it?

Snarky

#16
Check out this: https://www.adventuregamestudio.co.uk/manual/ags53.htm#CallRoomScript

Yes, RESTART_GAME is an int (preferably a constant; you could also use an enum).

Laura Hunt

Quote from: Snarky on Fri 29/05/2020 08:20:05
Check out this: https://www.adventuregamestudio.co.uk/manual/ags53.htm#CallRoomScript

Yes, RESTART_GAME is an int (preferably a constant; you could also use an enum).

I read that yesterday trying to grasp what fernewelten was talking about, but I still can't make heads or tails of it. The first thing that this entry says is "Calls the on_call function in the current room script" but there seems to be no entry or explanation in the manual for the "on_call" function and how it works.

If RESTART_GAME is an int, where do you define it? What value do you assign to it? 0, 1, 5, 23675? What exactly are you comparing when you do if (RESTART_GAME == script)? I seriously feel like I'm missing something super basic here that is keeping me from getting the big picture because right now this is like trying to read Japanese to me... Maybe a super super basic example would help me out?

Laura Hunt

#18
ok waitwaitwait I think I got it.

I assume You use RESTART_GAME instead of simply passing an int value directly, in order to make things more readable and not have to memorize what custom event each value refers to. If you want to have different stuff happening, you can create an enum assigning int values to names that make it easier to remember what you're doing (e.g., instead of having to remember that 0 in this context means "restart game", I assign the value 0 to RESTART_GAME). (Edit: I was confusing enums with arrays here; it's all clear now.)

Another thing that was confusing me a bunch was if (RESTART_GAME == script), but this is just the same as if (script == RESTART_GAME), right? You're just comparing both sides of an equation. It felt super counterintuitive to see it written "the other way around".

And then do_your_thing() would actually be RestartGame();

For this method to work though, this has to be true:

Quote from: fernewelten on Fri 29/05/2020 00:14:23
This handling code would run after "SaveGameSlot();" AFAIK. I hope.

Is this the case, then? Can anybody confirm that the handler would indeed be called after SaveGameSlot?

Let me know if I got everything right!

Khris

#19
Here's a minimal example:

Code: ags
// global script header

enum eRoomScriptParam { RESTART_GAME };

import function do_your_thing();

// global script
function do_your_thing() {
  RestartGame();
}

function btnRestart_OnClick(GUIControl* control, MouseButton button) {
  CallRoomScript(RESTART_GAME);
}


And this in *every* room script:
Code: ags
function on_call(eRoomScriptParam p) {
  if (p == RESTART_GAME) do_your_thing();
}


The manual says:
QuoteThe function doesn't get called immediately; instead, the engine will run it in due course, probably during the next game loop, so you can't use any values set by it immediately.

However, to reiterate: the same thing can be achieved by simply using SetTimer() with a delay of 1.

CallRoomScript() is not a mechanism to schedule code for the next engine loop, it's a mechanism to run room code from the global context.
Also: definitely seconding that  if (RESTART_GAME == script)  is counterintuitive and weird to read; the constant is supposed to be on the right of the operator. You don't say "if 5 equals x", you say "if x equals 5".

SMF spam blocked by CleanTalk