Porting game to MonoAGS

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

Previous topic - Next topic

tzachs

Quote from: cat on Sun 11/11/2018 19:22:12
When the player performs a right click when an inventory is active, there is a null reference exception at AGS.Engine.TwoButtonsInputScheme.SetInventoryCursor(IInventoryItem inventoryItem) in line 37
onRightMouseDown sets the ActiveItem to null, which then triggers the OnPropertyChanged handler.
I'd change SetInventoryCursor to reset the default cursor when null is passed and the active inventory is null. This would help regarding the exception, but also allow the programmer to change back the active inventory programmatically. This is needed when I use the active inventory item on a hotspot and this results in me losing the inventory item.
Should be fixed now (still same branch).

Quote from: cat on Sun 11/11/2018 19:22:12
Another issue: You might want to check if the previous cursor should really be changed. Otherwise, there will be weird issues when setting two active items after each other without returning to the default cursor in between.
Consider renaming _previousCursor to _defaultCursor, then it will be more clear. You could also provide a method SetDefaultCursor. This way you can safely change the default cursor, no matter if there is currently an inventory active or not.
Right, I was doing copypasta from the rotating cursors scheme... :-[
Changed it now (there's a default cursor you can pass in the constructor, and also a DefaultCursor property you can set on the scheme afterwards).

Quote from: cat on Sun 11/11/2018 19:22:12
Works, but 80% seems a bit too much. Maybe 70% is enough? (But this might be because of my setup with the stage frame around it.)
Ok, I don't have any preference myself, so I changed it to 70%.

Quote from: cat on Sun 11/11/2018 19:22:12
Btw, is it on purpose that the text wraps left-aligned instead of centered? It doesn't look that nice, especially when there is only a word or two in the second line.
Not on purpose, but a bit more complicated to get right for all text rendering scenarios, I'm working on it and will update.

cat

It works except for one thing: I set the default cursor via the constructor, but it is only applied after changing to the inventory cursor and back. Of course I could set the cursor myself, but actually I'd expect this to happen automatically when TwoButtonsInputScheme.Start() is called. The same applies for the setter of the DefaultCursor property.

tzachs

Quote from: cat on Mon 12/11/2018 19:54:21
It works except for one thing: I set the default cursor via the constructor, but it is only applied after changing to the inventory cursor and back. Of course I could set the cursor myself, but actually I'd expect this to happen automatically when TwoButtonsInputScheme.Start() is called. The same applies for the setter of the DefaultCursor property.
Done.

Quote from: cat on Sun 11/11/2018 19:22:12
Btw, is it on purpose that the text wraps left-aligned instead of centered? It doesn't look that nice, especially when there is only a word or two in the second line.
I'm still not finished with this, but I pushed just enough so it should work for you as you expect, I hope.

cat

It works nicely! The cursor is now correct and the text centred nicely. Thanks :)

cat

#84
Next topic: following a character. I now realize that Cornelius Cat uses quite a lot of different features for such a short game :)

In the original AGS game I use the setting
Code: ags
cMouse.FollowCharacter(cCat, 150, 0);

This results in the mouse somewhat following the cat, but mostly running around, even when the cat is standing. How do I have to set the values in AGSFollowSettings to achieve the same?

Edit: Btw, there seems to be a problem when un-following a character with Follow(null):
System.NullReferenceException
  HResult=0x80004003
  Message=Object reference not set to an instance of an object.
  Source=AGS.Engine
  StackTrace:
   at AGS.Engine.FollowTag.AddTag(IObject target, IEntity follower) line 23

tzachs

Quote from: cat on Wed 14/11/2018 20:26:24
Next topic: following a character. I now realize that Cornelius Cat uses quite a lot of different features for such a short game :)

In the original AGS game I use the setting
Code: ags
cMouse.FollowCharacter(cCat, 150, 0);

This results in the mouse somewhat following the cat, but mostly running around, even when the cat is standing. How do I have to set the values in AGSFollowSettings to achieve the same?
So looking at the manual, the first parameter is the distance the follower will stand from its target, and 150 means "about" 150 pixels. I don't know what "about" means exactly, haven't looked at the code, but let's assume that it's give-or-take 10 pixels.
In MonoAGS there are four parameters to control the distance: MinXOffset, MaxXOffset, MinYOffset and MaxYOffset. This gives you better control over the entire square you want to allow the follower. So to kind of mimic what AGS does (assuming that "about" is 10 pixels), I would give something like MinXOffset = 140, MaxXOffset = 160, MinYOffset = 140, MaxYOffset = 160. It's not going to be exactly the same, as AGS gives you one value so I'm guessing it forms a circle, where in MonoAGS it's a square. But this shouldn't make that much of a difference in practice, I think.

As for the second parameter, the eagerness. From looking in the manual it says that setting it to 0 will make the follower to always be on the move and that it will wander around the target like an energetic dog. It doesn't sound like that is what it actually does, from what you're describing, though.
Anyway, the first part, how often it moves, is controlled from MonoAGS by MinWaitBetweenWalks and MaxWaitBetweenWalks. To make it always on the move, set both to 0. The second part per your description, that it will be mostly running around and not actually following the cat, is something that you can control with WanderOffPercentage. If for example you set it to 50, there's a 50% chance for each walk to be to a completely random place, as opposed to the configured square. So for mostly running around to unrelated places, I guess you can set it to 80?
Let me know if it doesn't work out, maybe we need more parameters for better control.

Btw, I plan at some point to add presets to the follow settings with defaults to match common scenarios, so you'll be able to write something like:
Code: csharp

mouse.Follow(cornelius, AGSFollowSettings.Companion());

//It will also allow you to override specific settings, for example:
mouse.Follow(cornelius, AGSFollowSettings.Companion(wanderOffPercentage: 0));


Quote from: cat on Wed 14/11/2018 20:26:24
Edit: Btw, there seems to be a problem when un-following a character with Follow(null):
System.NullReferenceException
  HResult=0x80004003
  Message=Object reference not set to an instance of an object.
  Source=AGS.Engine
  StackTrace:
   at AGS.Engine.FollowTag.AddTag(IObject target, IEntity follower) line 23

Should be fixed now (in your branch).

cat

#86
There still seems to be an issue with stopping the follow.
After fade in, I do
Characters.Mortimer.Follow(Characters.Cornelius);

Later, when using the mouse item on the gate, I have the following cutscene (not finished yet):
Code: ags

_game.State.Cutscene.Start();

Characters.Cornelius.Inventory.ActiveItem = null;
await Characters.Cornelius.WalkAsync((2479f, 0f));
await Characters.Cornelius.SayAsync("Mortimer, come here!");
Characters.Mortimer.Follow(null);
await Characters.Mortimer.WalkAsync((2035, 0f));
Characters.Mortimer.FaceDirection(Characters.Cornelius);
await Characters.Cornelius.SayAsync("I need you to help me with the gate.");
await Characters.Cornelius.SayAsync("You are small. Try to squeeze through it to the other side and open it for me.");
await Characters.Mortimer.SayAsync("* squeak *");

// TODO follow cat again

_game.State.Cutscene.End();

However, when it comes to the line Characters.Mortimer.Follow(null); the game freezes. I cannot even move the mouse cursor anymore.


On a different note: I tried the setting
Characters.Mortimer.Follow(Characters.Cornelius, new AGSFollowSettings(true, 80, 0, 10, 140, 160, 0, 10));
This works almost as desired, but the character will wander off till the end of the walkable area (much farer away than 160 pixel), is it possible to limit this somehow?

tzachs

Quote from: cat on Thu 15/11/2018 19:05:29
However, when it comes to the line Characters.Mortimer.Follow(null); the game freezes. I cannot even move the mouse cursor anymore.
Should be fixed now, and also:
1. Follow(null) followed by walk is problematic, as when you stop following the stop walking action will only be called on the next tick after you fired your walk, so it will stop your walk.
So I added a StopFollowingAsync to be used instead, so await it before walking.
2. I removed the blocking versions of FaceDirection because I'm concerned about their safety, so also replace it with await FaceDirectionAsync.

Quote from: cat on Thu 15/11/2018 19:05:29
On a different note: I tried the setting
Characters.Mortimer.Follow(Characters.Cornelius, new AGSFollowSettings(true, 80, 0, 10, 140, 160, 0, 10));
This works almost as desired, but the character will wander off till the end of the walkable area (much farer away than 160 pixel), is it possible to limit this somehow?
Ah, so if you want to stay in the vicinity of the cat, then you don't want to wander off at all, so I'll put it at 0. Wandering off is basically going wherever in the room without care for where the target is.
And btw, you can use named parameters which will make the constructor more readable:
Code: csharp

new AGSFollowSettings(wanderOffPercentage: 0, 
	minWaitBetweenWalks: 0, maxWaitBetweenWalks: 10, 
	minXOffset: 140, maxXOffset: 160, 
	minYOffset: 0);


I also added more parameters for tuning the follow behaviour (I don't think you care too much about those parameters for your scenario, but just in case):
1. Be able to have more control over the "wander off" if you want, so it's MinXOffsetForWanderOff etc, same as MinXOffset only just for the wander off phase (if you do want it).
2. Stay put percentage- if the follower is already in valid range of the target, what's the probability to keep moving to a new point anyway? It sounds like you want to always be on the move, so you can leave this at 0 which is the default.
3. Minimum walking distance- I noticed that it looks a little funny when the characters are moving just a few pixels, so this is to enforce a minimum distance for consecutive movements. This will be 10 pixels by default.
4. Stay on the same side percentage (one for x and one for y)- i.e if the follower is on the right of the target, what's the probability for it to stay to the right of the target on the next walk. This is 70% by default, so leaning a bit towards staying in the same side.

cat

Thanks! I changed the values and it looks quite good already. I might fiddle with it more a bit later, when I have the final walking speed etc.
Code: ags
Characters.Mortimer.Follow(Characters.Cornelius, new AGSFollowSettings(wanderOffPercentage: 0,
minWaitBetweenWalks: 0, maxWaitBetweenWalks: 100,
minXOffset: 10, maxXOffset: 260,
minYOffset: 0, maxYOffset: 10));


However, stopping still freezes the application (during await Characters.Mortimer.StopFollowingAsync();)

This is what my code looks like now:
Code: ags

_game.State.Cutscene.Start();

Characters.Cornelius.Inventory.ActiveItem = null;
await Characters.Cornelius.WalkAsync((2479f, 0f));
await Characters.Cornelius.SayAsync("Mortimer, come here!");
await Characters.Mortimer.StopFollowingAsync();
await Characters.Mortimer.WalkAsync((2035, 0f));
await Characters.Mortimer.FaceDirectionAsync(Characters.Cornelius);
await Characters.Cornelius.SayAsync("I need you to help me with the gate.");
await Characters.Cornelius.SayAsync("You are small. Try to squeeze through it to the other side and open it for me.");
await Characters.Mortimer.SayAsync("* squeak *");

// TODO follow cat again

_game.State.Cutscene.End();

When I click the pause button in VS while the game is stuck, it seems to be stuck in AGSGameWindow line 74 with call stack (not very helpful, I guess)
Spoiler

>   AGS.Engine.Desktop.dll!AGS.Engine.Desktop.AGSGameWindow.SwapBuffers() Line 74   C#
   AGS.Engine.dll!AGS.Engine.AGSGame.onRenderFrame(object sender, AGS.Engine.FrameEventArgs e) Line 225   C#
   AGS.Engine.Desktop.dll!AGS.Engine.Desktop.AGSGameWindow.onRenderFrame(object sender, OpenTK.FrameEventArgs args) Line 87   C#
   [External Code]   
   AGS.Engine.Desktop.dll!AGS.Engine.Desktop.AGSGameWindow.Run(double updateRate) Line 73   C#
   AGS.Engine.dll!AGS.Engine.AGSGame.Start() Line 150   C#
   CorneliusCat.Desktop.exe!CorneliusCat.GameStarter.Run() Line 19   C#
   CorneliusCat.Desktop.exe!CorneliusCat.Desktop.Program.Main(string[] args) Line 10   C#
[close]

Another question: if follow(null) can cause problems, wouldn't it be better to remove this synchronous call completely?

Crimson Wizard

Quote from: cat on Sat 17/11/2018 16:18:14
When I click the pause button in VS while the game is stuck, it seems to be stuck in AGSGameWindow line 74 with call stack (not very helpful, I guess)

Not following all the story, but sometimes you may try switching to other threads in "Debug->Windows->Threads" and see if any of available thread stacks has any visible relation to the recent command you are stuck at.

cat

Good idea. Sadly, I didn't find anything that seems more helpful.

tzachs

Ok, I think I really fixed it this time, can you try again?
Sorry.. :-[

Quote from: cat on Sat 17/11/2018 16:18:14
Another question: if follow(null) can cause problems, wouldn't it be better to remove this synchronous call completely?
Yeah, I'm not really sure what's the best way to handle it yet.
Here's the thing, the follow method just sets the follow target, and then on each game tick it manages its "state machine" to see what's the next move to perform (and if it's stop following, then it will want to stop the existing walk).
So it kind-of follows the same behavior of ChangeRoom in AGS where the script you write after ChangeRoom will actually happen before the room is changed (not a problem in MonoAGS, btw, that's why it's ChangeRoomAsync). And StopFollowingAsync basically calls Follow(null) with an additional task that you can hang onto to wait until the StopWalking happens in the next tick.

Now, I can't really disallow Follow(null) in compile-time yet (though it will be possible when c# 8 is released). I can make it throw an exception in run-time, though the only problem with Follow(null) is the scenario in which you follow(null) and then walk immediately after it (it might ignore that walk) -> in all other scenarios it will work fine, so I'm not too keen on throwing an exception if you pass null to it.
So here's the possible directions I thought I can take this (let me know what you think):
1. I can make Follow into FollowAsync (and remove StopFollowingAsync) and always return a task, even though it's completely not needed if you don't pass null. And also, it might be confusing for the user, as making it async hints that it will await the entire follow until done following, which is not correct. So I'm not in favor of this approach.
2. I can keep things the way they are for now, and when c# 8 is released to disallow nulls to Follow in compile time.
3. I can change it so it won't stop the current walk when you stop following with Follow(null). I'm not sure what AGS does in that scenario, actually (does anybody know?). But I could just let the character complete its last walk as a follower and then just not do any more walks. And I can make StopFollowingAsync to call Follow(null) and then call StopWalkingAsync, so if you do want to stop walking you'll call StopFollowingAsync and if you don't care you can call Follow(null).
4. I can add an option for StopWalking to accept a specific walking task. Then in the follow component I'll call stop walking on "my" specific walk, so there can't be any confusion, the follow component will never stop a walk issued by somebody else (and I can delete StopFollowingAsync completely). I'm not sure how complicated this is to pull off, but this option might be useful in other future scenarios as well.

cat

It works now :)

Ah, I am so used to Javascript with Closure compiler type annotations, I completely forgot it is not possible to have non-nullable types in C#.

I prefer option 3. I tried it in AGS and there the current walk is not stopped. The mouse will continue walking until it reaches the cat and then stop. StopFollowingAsync sounds like a good add-on. I think it's much easier to explain this way.

tzachs

Ok, done in your branch, so you can choose if you want Follow(null) or StopFollowingAsync depending if you want to stop the current walk or not.

cat

Great! I stick to StopFollowingAsync, since I want to walk directly afterwards.

cat

Today I started to work again on the port.

Until now, I was only using the walk-left animation. When I added the walk-right animation, it looks weird. I see the correct sprites (i.e. flipped) but the starting point is off. It is hard to explain, but it almost looks as if the mouse is rotated around its nose. It looks like it is not flipped inside the sprite bounds, but mirrored along the left border, i.e. the whole character seems to jump.

Here is how I init the ani:
Code: ags

            var walkAni = game.Resolver.Resolve<IAnimation>();
            walkAni.Frames.Add(new AGSAnimationFrame(walkSprite1));
            walkAni.Frames.Add(new AGSAnimationFrame(walkSprite2));
            var walkRightAni = walkAni.Clone();
            walkRightAni.FlipHorizontally();
            var walk = new AGSDirectionalAnimation { Left = walkAni, Right = walkRightAni };

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

tzachs

You need to center your sprites (they're using lower-left by default):
Code: csharp

walkSprite1.Pivot = (0.5f, 0f);
walkSprite2.Pivot = (0.5f, 0f);


Note that if you load an outfit or directional animation from the factory, it does it for you automatically, i.e if you only supply the left animation it will flip and center it to create the right animation (or vice-versa).

cat

Now I am a bit confused  ???

Previously I had problems putting an object in the right place. There it turned out that the pivot was initially at 0.5 and I had to reset it to 0.0 (https://www.adventuregamestudio.co.uk/forums/index.php?topic=56055.msg636595291#msg636595291 ff). But now the pivot suddenly is at 0.0 and I have to set it to 0.5? And why does flipping the animation also flip the pivot?

How do I load the outfit/animation from the factory? Here is the full init code:
Spoiler
Code: c#

        public async Task<ICharacter> LoadAsync(IGame game)
        {
            _game = game;

            AGSLoadImageConfig loadConfig = new AGSLoadImageConfig();

            var factory = game.Factory.Graphics;

            var walkSprite1 = await factory.LoadSpriteAsync(_baseFolder + "mouse_walk1.png");
            var walkSprite2 = await factory.LoadSpriteAsync(_baseFolder + "mouse_walk2.png");
            var speakSprite = await factory.LoadSpriteAsync(_baseFolder + "mouse_speak.png");

            var idleAni = game.Resolver.Resolve<IAnimation>();
            idleAni.Frames.Add(new AGSAnimationFrame(walkSprite1));
            var idle = new AGSDirectionalAnimation { Left = idleAni };

            var walkAni = game.Resolver.Resolve<IAnimation>();
            walkAni.Frames.Add(new AGSAnimationFrame(walkSprite1));
            walkAni.Frames.Add(new AGSAnimationFrame(walkSprite2));
            var walkRightAni = walkAni.Clone();
            walkRightAni.FlipHorizontally();
            var walk = new AGSDirectionalAnimation { Left = walkAni, Right = walkRightAni };

            var speakAni = game.Resolver.Resolve<IAnimation>();
            speakAni.Frames.Add(new AGSAnimationFrame(walkSprite1));
            speakAni.Frames.Add(new AGSAnimationFrame(speakSprite));
            var speak = new AGSDirectionalAnimation { Left = speakAni };

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

            _character = game.Factory.Object.GetCharacter("Mortimer", outfit).Remember(game, character =>
            {
                _character = character;
                subscribeEvents();
            });

            _character.SpeechConfig.TextConfig = AGSTextConfig.ChangeColor(_character.SpeechConfig.TextConfig, Colors.Gray, Colors.Black, 1f);
            var approach = _character.AddComponent<IApproachComponent>();
            approach.ApproachStyle.ApproachWhenVerb["Talk"] = ApproachHotspots.WalkIfHaveWalkPoint;

            _character.DisplayName = "Mortimer Mouse";
            _character.IsPixelPerfect = true;
            _character.StartAnimation(_character.Outfit[AGSOutfit.Idle].Left);

            return _character;
        }
[close]

tzachs

Quote from: cat on Sun 27/01/2019 19:09:15
And why does flipping the animation also flip the pivot?
It doesn't. The problem is that the FlipHorizontally function, instead of flipping the texture, it flips the scale on the horizontal axis.
Scaling is done from the pivot point. So a -1 scale (on x) when the pivot is 0 will shift the entire sprite to the other side.

There is an item in the backlog for changing this behavior and just flipping the texture, which should make flipping work regardless of the sprite's pivot.

For your previous issue, I believe you changed the character's pivot point. Note that you can change the pivot on the character but also on each individual animation frame (sprite) as well. So not sure, but it might work having the character's x pivot be 0 (for your previous issue: keeping the same co-ordinates as AGS) and your sprite's x pivot to be 0.5 (for flipping to work in the current scheme).
If it doesn't work, let me know, and I'll work on the texture flip item now.

Quote from: cat on Sun 27/01/2019 19:09:15
How do I load the outfit/animation from the factory?
The factory methods assume that each animation loop is in its own folder.
So for example, you can setup your files in the following structure:
mortimer/
    -> walk/
         -> left/
             -> mouse_walk1.png, mouse_walk2.png
    -> idle/
         -> down/
             -> mouse_walk1.png
    -> speak/
         -> down/
             -> mouse_speak.png

And then you can load the entire outfit with:
Code: csharp

var baseFolder = "mortimer";
var outfit = await game.Factory.Outfit.LoadOutfitFromFoldersAsync(baseFolder, walkLeftFolder: "walk/left",
				idleDownFolder: "idle/down", speakDownFolder: "speak/down");


Or alternatively you can load directional animations with "game.Factory.Graphics.LoadDirectionalAnimationFromFoldersAsync" and construct your outfit from the directional animations like you did in your code.

cat

Okay, setting the pivot of each sprite to 0.5 did it.

I reuse some sprites for various animations and making separate folders for it would be unnecessary overhead. I'll stick to manual sprite creation for now.

SMF spam blocked by CleanTalk