Porting game to MonoAGS

Started by cat, Sun 13/05/2018 21:12:08

Previous topic - Next topic

cat

Hi!
I want to port Cornelius Cat from AGS to MonoAGS. It is a fairly short game and has neither save/load nor translations, which is not available in MonoAGS, yet.
I thought I'd create a new thread for this, now that there is a separate board, to keep things organised.

I'm currently trying to set up the basic game structure (using mostly the Demo game and a bit of CW's games) and already have the first questions:

Is a player character needed, because it is loaded before the splash screen?

The splash screen (loadSplashScreen) has nothing to do with the Splash asset, right? It is the AGSSplashScreen from the engine but there does not seem to be documentation for it?

Crimson Wizard

#1
Quote from: cat on Sun 13/05/2018 21:12:08
Is a player character needed, because it is loaded before the splash screen?

No, you may make a game without characters at all (none of my games have any).
Changing rooms is done using Game.State.ChangeRoomAsync(room), so it is not directly related to character's going there, unlike AGS.

I am not sure how player character's position is synchronized with changing the room, haven't investigated that yet. Probably you may set Game.State.Player, but idk if it changes room automatically after the character.

tzachs

Quote from: cat on Sun 13/05/2018 21:12:08
The splash screen (loadSplashScreen) has nothing to do with the Splash asset, right? It is the AGSSplashScreen from the engine but there does not seem to be documentation for it?
What do you mean by "splash asset"?
Yes, sorry, the splash screen is not currently documented, and is very basic for now. It's basically a blank room with a loading text that tweens its scaling until you tell it to move to another room.
You can change the text by setting the "LoadingText" property.
You can change the font/text color/outline/shadow/etc by setting the "TextConfig" property.
And when you call the load function on the splash screen you get back a reference to your room so you can set a background or add more stuff to the room (like any other room).
Here's an example which changes the text, changes it to a blue color and adds a background image:

Code: csharp

var mySplashScreen = new AGSSplashScreen();
mySplashScreen.LoadingText = "My Loading Text";
mySplashScreen.TextConfig = new AGSTextConfig(brush: game.Factory.Graphics.Brushes.LoadSolidBrush(Colors.Blue));
var room = mySplashScreen.Load(game);
room.Background = myBackgroundImage;
await game.State.ChangeRoomAsync(room);

...
//Load stuff here
...

await game.State.ChangeRoomAsync(firstGameRoom);


Quote
I am not sure how player character's position is synchronized with changing the room, haven't investigated that yet. Probably you may set Game.State.Player, but idk if it changes room automatically after the character.
If you change the player character's room it automatically changes the room in the game state as well, so the behavior matches what you expect when you change the player's position in AGS, but like CW said, you don't need to have a player character as you can just change the room in the state (which won't move the player character to that room). I think I set the player first because at the beginning I had the exact AGS behavior so the player was mandatory, but at some point I changed it.

cat

Thanks for the explanation
Quote from: tzachs on Mon 14/05/2018 00:44:36
What do you mean by "splash asset"?
In the assets folder->rooms there is a splash screen, but I don't think it is ever used.

One reason for this thread is to point out stuff that is either confusing in the demo game, needs more documentation or should be improved.



Monsieur OUXX

Itis ndeed not used, however that's not the important thing about the Splashscree room.
the important thing is that it has a different structure from the other rooms. It is designed to have an unpredictable framerate, which means that onRepeatelyExecute and the tweens update are called manually from this room's loading function itself.
If you don't feel too confortable with that weird room, I'd say that you may skip the splash screen altogether and just go directly to an actual room. It will take a few seconds to load, but for a small game it's no big deal.


 

Crimson Wizard

#5
Quote from: Monsieur OUXX on Mon 14/05/2018 15:08:23
If you don't feel too confortable with that weird room, I'd say that you may skip the splash screen altogether and just go directly to an actual room. It will take a few seconds to load, but for a small game it's no big deal.

Don't remember if I mentioned this before, but IMHO DemoGame is not the best example of resource management, because it loads everything at the start (last time I checked), hence the need for "loading" screen. You may write a trivial resource manager which loads stuff for each room only when game is going there, that would reduce waiting time significantly.

Since we are at that, a small warning: MonoAGS currently does not support audio streaming (playing & loading at the same time), for that reason loading audio clips takes longer than in AGS.

cat

Quote from: Monsieur OUXX on Mon 14/05/2018 15:08:23
If you don't feel too confortable with that weird room, I'd say that you may skip the splash screen altogether and just go directly to an actual room. It will take a few seconds to load, but for a small game it's no big deal.
I'm comfortable enough to use it, I just want to point out issues that may confuse other game developers. Like the missing documentation or the unused graphic in the assets folder.

tzachs

Added a doc for the splash screen and added the splash background asset to the splash screen in the demo game so it's being used now.

cat

I pulled the latest version but I get an exception when starting DemoQuest.Desktop at AGS.Engine.FileSystemResourcePack.autoDetectAssetsFolder line 104

System.ArgumentException: 'URI formats are not supported.'

tzachs

I have a fix for this which I'll hopefully push tonight.
Meanwhile you can work around it by passing the path to your assets folder to the constructor of FileSystemResourcePack which bypasses the auto detection code.

I.e change the line here: https://github.com/tzachshabtay/MonoAGS/blob/f7be8974648591740752d2c3be23ac2ea7990a4a/Source/Demo/DemoQuest/Program.cs#L23

to:

Code: csharp

string pathToMyAssetsFolder = "c:\cat\game\Assets";
game.Factory.Resources.ResourcePacks.Add(new ResourcePack(new FileSystemResourcePack(AGSGame.Device.FileSystem, AGSGame.Device.Assemblies.EntryAssembly, pathToMyAssetsFolder), 0));

cat

No worries, I spent the evening learning about async/await (https://docs.microsoft.com/en-us/dotnet/csharp/async). It a bit mind-bending because I'm used to JS promises, but I think I start to understand it.

tzachs

#11
Cool, javascript has async/await too now, btw (they copied from c# and f#): https://javascript.info/async-await

Anyway, in case it's helpful, here's a really quick primer for AGSers on async/await:

AGS:     cEgo.Walk(eNoBlock)
MonoAGS: cEgo.WalkAsync()

AGS:     cEgo.Walk(eBlock)
MonoAGS: await cEgo.WalkAsync()

AGS:     cEgo.Walk(eNoBlock); /* Do stuff */ while (cEgo.IsMoving) Sleep(1);
MonoAGS: var walkTask = cEgo.WalkAsync(); /* Do stuff */ await walkTask;

This 3rd scenario is where async/await starts to add value. For walk you can check IsMoving on AGS, but for "say background" you'll need IsSpeaking, and you'll need more APIs for each async (non blocking) operation, whereas the API is always going to be the same with async/await. And it's not even going to be perfectly accurate in the AGS way of doing things, as while you were sleeping it might be possible (in case you have multiple threads) for the character to walk again because another command told the character to walk so you'll keep waiting. With the async/await scenario you know you're waiting for YOUR walk command, so you can't have this problem.

And where it really starts to shine is when you chain multiple async/await in whatever way you want.
For example, imagine a guard walking in the background between 4 different points in an endless loop.
In AGS, I would write something like this:

Code: ags

onRepeatedlyExecute()
{
    if (cGuard.X == 0 && cGuard.Y == 0 && !cGuard.IsMoving) cGuard.Walk(50, 0, eNoBlock);
    else if (cGuard.X == 50 && cGuard.Y == 0 && !cGuard.IsMoving) cGuard.Walk(50, 50, eNoBlock);
    else if (cGuard.X == 50 && cGuard.Y == 50 && !cGuard.IsMoving) cGuard.Walk(0, 50, eNoBlock);
    else if (!cGuard.IsMoving) cGuard.Walk(0, 0, eNoBlock);
}


In MonoAGS:

Code: csharp

async void guardWalkLoop() //Using "async void" to let people know I'm a "fire and forget" method. If I want whoever calls me to wait for me to finish, I'd change the signature to "async Task" (but then I can have a "while true" in the method)
{
    while (true)
    {
        await cGuard.WalkAsync(50, 0);
        await cGuard.WalkAsync(50, 50);
        await cGuard.WalkAsync(0, 50);
        await cGuard.WalkAsync(0, 0);
    }
}


Now imagine you want the guard to say something between a few of the points, or if you want to change routes, of have multiple guards walking in parallel but occasionally need to sync with each other, it gets really hard in AGS and is trivial with async/await.

cat

Nice! I haven't been coding for a year now (and at work I was limited to older script versions) so it's about time for me to get up-to-date :)

tzachs

Quote from: cat on Tue 15/05/2018 20:12:05
I pulled the latest version but I get an exception when starting DemoQuest.Desktop at AGS.Engine.FileSystemResourcePack.autoDetectAssetsFolder line 104

System.ArgumentException: 'URI formats are not supported.'

I pushed a fix for this, can you pull again and retry?
Thanks.

cat

#14
Yes, it's working now.

Edit: And another question about LoadOutfitFromFolders:
I see in the demo game that only speakLeftFolder for talk and walkLeftFolder and walkDownFolder for walk are used. How does the engine decide which one to use for what, e.g do I always have to provide speakLeftFolder when I want to have the same animation for all talk animations or could I also use speakRightFolder? This is missing in the code comments/documentation (or did I just not find it?)

tzachs

Quote from: cat on Wed 16/05/2018 20:01:52
Edit: And another question about LoadOutfitFromFolders:
I see in the demo game that only speakLeftFolder for talk and walkLeftFolder and walkDownFolder for walk are used. How does the engine decide which one to use for what, e.g do I always have to provide speakLeftFolder when I want to have the same animation for all talk animations or could I also use speakRightFolder? This is missing in the code comments/documentation (or did I just not find it?)

Yes, you can use speakRightFolder instead of speakLeftFolder (in general, the engine will try to use the closest directional animation it has). Documentation mentions it here.

And predicting a potential next question, yes, it currently does support only 8 directions, should add support to 360 (or rather, infinite because it will probably be a float) possible directions at some point (and we'll still have shortcuts to the 8 directions -> up will point to 0, down to 180, etc) and we'll probably also want explicit support for top-down views which will just rotate the sprite.

cat

Ok, I finally got the first background shown. I'm still not done with setup, but I want to find a way to make it cleaner than in the demo game.

I have basically just one huge scrolling background for the game and a few questions:

  • It's just one background, no objects, music and stuff - yet, the loading screen is shown for a full second or so. Why does it take so long? Is it because of the big background?
  • When the background is shown, after fade in, it quickly pans to the location which is undesired. Is this done automatically? I think it should fade directly to the position where I call change room. Or is it something different here?
  • I couldn't find documentation for Character.ChangeRoomAsync, especially how the coordinates work (i.e. where is 0,0)
  • It's hard to find/understand errors that occur. When resources for the walkcycle were not there, loading wouldn't work and I was stuck on splash screen, I only found the cause of the error when stepping through initialization with the debugger.
  • My character is not visible yet. Do I have to do something special to show it? I changed the room and the screen is on the correct position.
  • I tried to say something after the room change, but the game crashes on me:
    System.AccessViolationException: 'Attempted to read or write protected memory. This is often an indication that other memory is corrupt.' at AGS.Engine.OpenGLBackend line 30.

Here is my init code:
Code: ags

private async Task loadGameData(IGame game)
        {
            Debug.WriteLine("Startup: Initializing Splash Screen");
            IRoom splashScreen = new AGSSplashScreen().Load(game);
            game.State.Rooms.Add(splashScreen);
            splashScreen.Events.OnAfterFadeIn.SubscribeToAsync(async () =>
            {
                await Rooms.LoadAsync(game);
                await Characters.LoadAsync(game);
                game.State.Player = Characters.Cornelius;
                await game.State.Player.ChangeRoomAsync(Rooms.Stage, 2000, 500);
                Characters.Cornelius.Say("Hello, here I am.");
            });
            await game.State.ChangeRoomAsync(splashScreen);
        }




tzachs

Would it be possible for you to share a copy of your project with me?
Without it it's going to be much harder to help.

Quote from: cat on Sat 19/05/2018 21:55:51
It's just one background, no objects, music and stuff - yet, the loading screen is shown for a full second or so. Why does it take so long? Is it because of the big background?
I don't know. How big is the background (file size)?
You can try commenting out the line that loads the background and comparing the time differences.
Also, if you feel up to the challenge, you can profile the loading time to see what's taking up time. There's a built in profiler when you debug in visual studio: https://docs.microsoft.com/en-us/visualstudio/profiling/beginners-guide-to-performance-profiling
(If you share your project, I'll profile it).

Quote from: cat on Sat 19/05/2018 21:55:51
When the background is shown, after fade in, it quickly pans to the location which is undesired. Is this done automatically? I think it should fade directly to the position where I call change room. Or is it something different here?
It should fade directly to the position when you change room. Are you changing the viewport positions manually? Or are you moving the character in "after fade in"? Because if you move the character after the room fades in then the camera will not change at once but will do a smooth movement so maybe that's what happenning?

Quote from: cat on Sat 19/05/2018 21:55:51
I couldn't find documentation for Character.ChangeRoomAsync, especially how the coordinates work (i.e. where is 0,0)
Yes, I need to add a doc page on all the co-ordinates systems in the engine.
(0,0) is the bottom left.
Docs for Character.ChangeRoomAsync is here (though there's not much there).
And you can access it directly from the ICharacter page.
A tip if you tried using the search: there's currently a bug in DocFX that the search misses things. You can work around it by searching for "ChangeRoomAsync*" instead of "ChangeRoomAsync".
Here's a link to the bug. If you (or anybody) wants to comment on it and maybe put some more pressure on them to fix it already it would be nice.

Quote from: cat on Sat 19/05/2018 21:55:51
It's hard to find/understand errors that occur. When resources for the walkcycle were not there, loading wouldn't work and I was stuck on splash screen, I only found the cause of the error when stepping through initialization with the debugger.
If you're running in "Debug" from visual studio there's printouts to the output tab. Did you look there for errors?
Otherwise, not sure, I'll need to reproduce and see what went on there.

Quote from: cat on Sat 19/05/2018 21:55:51
My character is not visible yet. Do I have to do something special to show it? I changed the room and the screen is on the correct position.
Besides having the character in the same room, you also need to start the idle animation (or any animation, or set an image). You just need to do it the first time, it will then transition back to idle automatically after walk animations.

Quote from: cat on Sat 19/05/2018 21:55:51
I tried to say something after the room change, but the game crashes on me:
System.AccessViolationException: 'Attempted to read or write protected memory. This is often an indication that other memory is corrupt.' at AGS.Engine.OpenGLBackend line 30.
Can you share the complete stacktrace of the exception?
Also, can you try changing from "Characters.Cornelius.Say("Hello, here I am.");" to "await Characters.Cornelius.SayAsync("Hello, here I am.");" and see if it helps?
If it doesn't help, you can also try doing the say (or say async) in the "after fade in" event of the "stage" room instead of the splash screen room and see if that works.

cat

#18
Quote from: tzachs on Sun 20/05/2018 06:06:37
Would it be possible for you to share a copy of your project with me?
Without it it's going to be much harder to help.
Sure, I'll send you a link. I know it would be easier to make a public github repo for it, I'm just not sure I really want to make it all public (not because of the code but the general copyright to the game and assets).

Quote
I don't know. How big is the background (file size)?
Actually, it's pretty big, ~3MB png. It's a high-res photo.
Quote
You can try commenting out the line that loads the background and comparing the time differences.
A bit faster, but not much. But today it seems to start quicker than yesterday. Who knows what Windows was doing yesterday (mining bitcoins? :P)
It's not a big issue anyway, I was just curious.

Quote
It should fade directly to the position when you change room. Are you changing the viewport positions manually? Or are you moving the character in "after fade in"? Because if you move the character after the room fades in then the camera will not change at once but will do a smooth movement so maybe that's what happenning?
I just did what is shown in the code I posted, i.e. set the character as player and then call ChangeRoomAsync

Quote
Docs for Character.ChangeRoomAsync is here (though there's not much there).
And you can access it directly from the ICharacter page.
A tip if you tried using the search: there's currently a bug in DocFX that the search misses things. You can work around it by searching for "ChangeRoomAsync*" instead of "ChangeRoomAsync".
Here's a link to the bug. If you (or anybody) wants to comment on it and maybe put some more pressure on them to fix it already it would be nice.
Ah, that's why I didn't find it in the doc. Btw, when searching for ChangeRoomAsync* only IHasRoomComponent is in the result, not ICharacter.

Quote
If you're running in "Debug" from visual studio there's printouts to the output tab. Did you look there for errors?
Otherwise, not sure, I'll need to reproduce and see what went on there.
Right, I checked again and I see a null reference exception in output. However, the program doesn't crash but nothing else happens and the game is stuck. When I press the pause button, execution seems to be in AGSGamesWindow at line 65 public void SwapBuffers() => _gameWindow.SwapBuffers(); I think it might be better to let the game crash (at least in debug mode) instead of being just stuck.

Quote
Besides having the character in the same room, you also need to start the idle animation (or any animation, or set an image). You just need to do it the first time, it will then transition back to idle automatically after walk animations.
Thanks, that did it.

Quote
Quote from: cat on Sat 19/05/2018 21:55:51
I tried to say something after the room change, but the game crashes on me:
System.AccessViolationException: 'Attempted to read or write protected memory. This is often an indication that other memory is corrupt.' at AGS.Engine.OpenGLBackend line 30.
Can you share the complete stacktrace of the exception?
Also, can you try changing from "Characters.Cornelius.Say("Hello, here I am.");" to "await Characters.Cornelius.SayAsync("Hello, here I am.");" and see if it helps?
If it doesn't help, you can also try doing the say (or say async) in the "after fade in" event of the "stage" room instead of the splash screen room and see if that works.
With SayAsync it does not crash but I also don't see text.
Spoiler

Stacktrace (local paths obscured)

at OpenTK.Graphics.OpenGL.GL.Clear(ClearBufferMask mask)
   at AGS.Engine.OpenGLBackend.ClearScreen()
   at AGS.Engine.AGSGame.onRenderFrame(Object sender, FrameEventArgs e) in <local>\CorneliusCat\MonoAGS\Source\Engine\AGS.Engine\Game\AGSGame.cs:line 203
   at AGS.Engine.Desktop.AGSGameWindow.onRenderFrame(Object sender, FrameEventArgs args)
   at System.EventHandler`1.Invoke(Object sender, TEventArgs e)
   at OpenTK.GameWindow.OnRenderFrame(FrameEventArgs e)
   at OpenTK.GameWindow.OnRenderFrameInternal(FrameEventArgs e)
   at OpenTK.GameWindow.RaiseRenderFrame(Double elapsed, Double& timestamp)
   at OpenTK.GameWindow.DispatchUpdateAndRenderFrame(Object sender, EventArgs e)
   at OpenTK.GameWindow.Run(Double updates_per_second, Double frames_per_second)
   at OpenTK.GameWindow.Run(Double updateRate)
   at AGS.Engine.Desktop.AGSGameWindow.Run(Double updateRate)
   at AGS.Engine.AGSGame.Start(IGameSettings settings) in <local>\CorneliusCat\MonoAGS\Source\Engine\AGS.Engine\Game\AGSGame.cs:line 142
   at CorneliusCat.GameStarter.Run() in <local>\CorneliusCat\CorneliusCat\GameStarter.cs:line 16
   at CorneliusCat.Desktop.Program.Main(String[] args) in <local>\CorneliusCat\CorneliusCat.Desktop\Program.cs:line 10
[close]

Btw, is there another way of setting the character outfit? I want to reuse some frames for an animation and not copy them several times in resources. I'd like to build the animation manually.

tzachs

I pushed fixes to master for both the camera panning issue and the character speaking.
Note that I removed Character.Say, SayAsync should be used instead, that's because the blocking say can't work in all scenarios (like in after fade in, because it's blocking it also blocks the completion of the event so you will not see it happening). If I return it (technically it's not needed, SayAsync can do everything Say can do) I'll need to think of a more sophisticated way for it.

Also, I saw that there's a bug with how the fade transition works in your loading screen (instead of fading in the new room, it's fading in the splash screen). Not yet sure what's going on there, it seems more complicated, I will look at it after I finish my current work.
For the mean time, if it bothers you as it bothers me, you can remove the fade transition from the game load event, and move it to after your first (non splash screen) room fades in. Note that this will also reduce the time of the splash screen (by default the fade transition is 1 second fade in and 1 second fade out).

Quote from: cat on Sun 20/05/2018 09:27:57
I know it would be easier to make a public github repo for it, I'm just not sure I really want to make it all public (not because of the code but the general copyright to the game and assets).
You can also create a private repository. It costs money on github, but it's free on both VSTS and BitBucket.

Quote
Btw, when searching for ChangeRoomAsync* only IHasRoomComponent is in the result, not ICharacter.
This is because ICharacter doesn't really contain ChangeRoomAsync, it's composed out of components (like IHasRoomComponent) which contain the actual behaviors.

Quote
Right, I checked again and I see a null reference exception in output. However, the program doesn't crash but nothing else happens and the game is stuck.
Can you describe what changes I need to make to reproduce this? Also, if possible can you share the stacktrace of the null reference exception?
Thanks.

Quote
Btw, is there another way of setting the character outfit? I want to reuse some frames for an animation and not copy them several times in resources. I'd like to build the animation manually.
Yes, you can.
An outfit is a collection of directional animations (idle/walk/etc), a directional animation is a collection of animations (left/right/etc), an animation is a collection of animation frames, each animation frame is sprite + more configurations for the frame (sound and delays), and a sprite is an image + more configurations for the sprite (rotation, scale).
So you can pick and choose at what level you want to share your resources, which dictates which factory method(s) to use: the graphics factory has methods to load directional animations, to load animations, to load images, and also to get empty sprites.

So here's an untested example that combines some of those together to create an outfit.
We'll load the idle directional animation directly from folders, we'll create the walking directional animation by loading a few animation folders ourselves and create the directional animation from it, we'll create the speaking directional animations by loading images, creating animations from them and add to a directional animation, and finally, we'll create a jump directional animation by taking the images from the idle directional animations and just shift their offsets a bit to do a (really silly) "programmatic" jump.

Code: csharp

var factory = game.Factory.Graphics;

var idle = await factory.LoadDirectionalAnimationFromFoldersAsync("Animations/Idle", "Left", "Down", "Right", "Up");

var walkLeft = await factory.LoadAnimationFromFolderAsync("Animations/Walk/Left");
var walkDown = await factory.LoadAnimationFromFolderAsync("Animations/Walk/Down");
var walk = new AGSDirectionalAnimation { Left = walkLeft, Down = walkDown };

var speakLeftImage1 = await factory.LoadImageAsync("Animations/Speak/Left/1.png");
var speakLeftImage2 = await factory.LoadImageAsync("Animations/Speak/Left/2.png");
var speakLeftSprite1 = factory.GetSprite();
var speakLeftSprite2 = factory.GetSprite();
speakLeftSprite1.Image = speakLeftImage1;
speakLeftSprite2.Image = speakLeftImage2;

//I'm missing a factory method to create an empty animation, so using the resolver for this (for now):
var speakLeft = AGSGame.Resolver.Container.Resolve<IAnimation>();
speakLeft.Frames.Add(new AGSAnimationFrame(speakLeftSprite1) { Delay = 3});
speakLeft.Frames.Add(new AGSAnimationFrame(speakLeftSprite2) { MinDelay = 4, MaxDelay = 8});
var speak = new AGSDirectionalAnimation { Left = speakLeft };

var jumpLeft = AGSGame.Resolver.Container.Resolve<IAnimation>();
float yOffset = 0f;
foreach (var idleFrame in idle.Left)
{
    var sprite = factory.GetSprite();
    sprite.Image = idleFrame.Sprite.Image;
    sprite.Y = yOffset;
    yOffset += 0.5f;
    var jumpFrame = new AGSAnimationFrame(sprite) { Delay = idleFrame.Delay };
    jumpLeft.Frames.Add(jumpFrame);
}
var jump = new AGSDirectionalAnimation { Left = jumpLeft };

var outfit = new AGSOutfit();
outfit[AGSOutfit.Idle] = idle;
outfit[AGSOutfit.Walk] = walk;
outfit[AGSOutfit.Speak] = speak;
outfit["Jump"] = jump;


SMF spam blocked by CleanTalk