Paper Mario-style diagonal movement

Started by celadonk, Sat 27/04/2024 20:36:00

Previous topic - Next topic

celadonk

Hello! I'm new to AGS and I'm excited to start making a game.

One of the things I'd love to do is Paper Mario-type diagonal movement. I've searched around here for a method of accomplishing this, and although I've found a couple of things that are close (isometric stuff), nothing is exactly right.

What I'd like is for characters to have free movement-- being able to walk to any point in any direction-- but for their animations to only be diagonal. In other words, instead of characters having four orthogonal facings (up, down, left, and right), they each have four DIAGONAL facings (ul/ur/dl/dr).

I was able to accomplish this in Godot before I decided that AGS may be better for what I want to do. Below is a GIF of how that looked.



I'd love to extend this behavior to all characters, too, which leads into a broader question of how does one do that? I know how to (generally) edit the behavior of one character but I'd like to make all characters move a certain way without having to manually do each one.

Thanks!

celadonk

Snarky

#1
This is pretty easy to do as long as the character always walks in a straight line towards where you click, but if you want pathfinding (where it figures out detours around obstacles), I don't think AGS really supports this as of now.

The straightforward approach is to calculate which direction your character is walking (using the origin and destination coordinates and calculating the angle by taking arctan((y1-y0)/(x1-x0))), and setting the correct loop based on that.

celadonk

Sorry, where/in what file do I do that calculation? Apologies, I just started with AGS yesterday. I've read through all the tutorials but am struggling a bit with going past default capabilities.

Snarky

I would create a helper function in GlobalScript that selects the character angle and walks to a given set of coordinates. (There are a couple of wrinkles in the calculation that also need to be taken care of—avoiding a division by zero if you try to walk straight up or down, for example.)

Then you would call that function when you make a character walk. For example, if you walk by clicking, you would probably call it in on_mouse_click(). Without more details about the game I can't really specify it further.

eri0o

What is the intended movement control scheme? Is it by mouse or keyboard button?

celadonk

Quote from: eri0o on Sun 28/04/2024 03:06:43What is the intended movement control scheme? Is it by mouse or keyboard button?
Both, which I've got set up. I'd also like characters that the player does not control to animate this way, too.

Quote from: Snarky on Sat 27/04/2024 23:10:18Then you would call that function when you make a character walk. For example, if you walk by clicking, you would probably call it in on_mouse_click().
I've put together a block of code, but it seems like putting anything in on_mouse_click() nullifies the movement-- I'm only getting the animation.

Is it possible to set this animation scheme up without connecting it to an input?

Snarky

#6
Quote from: celadonk on Sun 28/04/2024 16:15:05I've put together a block of code, but it seems like putting anything in on_mouse_click() nullifies the movement-- I'm only getting the animation.

You'll need to also make an explicit call to the Character.Walk() function, if you aren't already.

Quote from: celadonk on Sun 28/04/2024 16:15:05Is it possible to set this animation scheme up without connecting it to an input?

No, I'm afraid I can't see any way of setting it up statically. The basic walkcycle directions are hardcoded into AGS, and in order to use a different setup, you have to actively calculate the angle and override it each time. (But since you can put it into a helper function, it shouldn't be a big inconvenience, either. You'll just have to call the helper function instead of the default walk function—as long as you're not relying on any pathfinding around obstacles.)

It probably wouldn't be too hard to hack a custom build of the engine to use diagonal walk angles by default, but of course it would only work with that particular custom engine build.


celadonk

Interesting! So it basically gives more weight to vertical or horizontal animations, as demonstrated in this diagram on the GitHub you linked:



Is it possible to rotate this diagram entirely? Like so:


If not, then no worries. (I think) I'm making good progress getting this to work in 3.6.1.

Crimson Wizard

Quote from: celadonk on Sun 28/04/2024 21:46:03Is it possible to rotate this diagram entirely? Like so:


Hmm, no, it does not "rotate", only "scales" towards height or width.
At the theoretical maximums the 4 lines would merge into 2 lines, either left-right or up-down.

I keep forgetting how diagonal loop selection is calculated.

If it's not possible to do the wanted effect with the above factor, then perhaps this feature may be amended.
I am just not sure how. If someone can suggest a good way to configure this (from design perspective), we would try implementing that.

Khris

Like @Snarky said, arctan2(dy, dx) of the current movement returns an angle in the range of -Pi to Pi, which one can turn into a loop number relatively easily.
To shift the angle away from 45°, one only needs to multiply dy by a factor of 0.9 or 1.1 for instance before calculating the arctan.

Customizing this behavior means we can provide the engine with a custom function that accepts dx and dy and returns the loop number.
Alternatively, let the user do it via a command like Character.SetLoop(int loop_number, int from_angle, int to_angle).


Without a change to the engine, overriding the loop is possible by using an invisible player character and having a second character mirror its coordinates, view, loop and frame. Now you can freely set the loop to whatever you want.
The downside is that there's no way to reliably access the character's current movement vector, so we'd have to track their position every few frames to infer it.

celadonk

Hey everyone, I was able to find a solution! It's pretty clunky but it works and it seems like it'll be easily transferable to all characters.

What I ended up doing was setting up a helper script, as suggested, that determines the correct animation based on the angle, and then applied it in a function that handles the movement.

Thank you so much to all who offered solutions! Excited to have worked through my first AGS roadblock.

Snarky

#12
Quote from: Crimson Wizard on Sun 28/04/2024 22:46:30If it's not possible to do the wanted effect with the above factor, then perhaps this feature may be amended.
I am just not sure how. If someone can suggest a good way to configure this (from design perspective), we would try implementing that.

I see two possible approaches:

1) Allow users to completely customize what sort of directional loops a character has and the range of angles each should be applied to, overriding the AGS defaults. (So, a user could for example define that loop 0 is left and loop 1 is right, and the others are unused, or set it up for movement on a hex grid.) There'd be some way to set this up in the IDE, for example as a property pane, or even a dedicated node with its own GUI to edit it visually. Ideally you would probably want to do this on a per-character basis (or maybe not?), but the complication is that views are independent of character, and the default directions are listed for each loop in the view.

Possibly this implies a complete rework of the way you set up walk animations in AGS, which might in any case be necessary if we want to enable other effects (like transitions in and out of walks, as people sometimes request). Maybe instead of numbered loops in a view, we might move towards a "tagged loop" system, where each loop is given a semantic label? Perhaps different view types/templates? It has always kind of annoyed me that all views have those 8 directional loops, even if the animations they store are not directional.

2) Remove the requirement for the left/right loops (or any particular directions) to be set for a character view. That way, to enable diagonal-only animations like @celadonk wants, you'd simply leave loops 0-3 empty and only set up loops 4-7. (And similarly you could have exclusive up/down animations by only using loops 0-1.) The internal GetDirectionalLoop() function in the engine would need to check which directions have valid loops, work out the range of angles each should cover, and set it appropriately.

Option 2 is somewhat less flexible, but seems a good deal easier, both for users and in terms of implementation. Though there might be some unexpected side-effects of removing the left/right loop requirement (why was it added in 3.0 in the first place?), and changing the way directions are selected would definitely break backwards compatibility for some games. (Which I think is good, since we want to move decisively to AGS 4!)

Edit:

Also, the MVP for the issue mentioned in the github thread, to return the list of waypoints (and the current segment) from the built-in pathfinder, would make scripting custom-made solutions much more viable.

Quote from: Crimson Wizard on Sun 28/04/2024 21:23:00Explanation of use here:
https://github.com/adventuregamestudio/ags/pull/2304

Khris

@celadonk Could you post the solution / relevant code so others who find this thread in the future can implement it, too?

celadonk

Absolutely! I'm going to be away from my computer for a couple days but when I get back, I'd love to post my code and help out others who need it.

celadonk

Alright... here's what I came up with. It's probably not a super efficient way of going about things. I have very very little programming background, so hopefully this isn't TOO scary to all the coders out there :D

Code: ags
function DetermineCharacterAngle(Character *c)
{
    if (c.GetProperty("TargetX") < c.x && c.GetProperty("TargetY") > c.y) { //down left
        c.SetProperty("CharacterAnim", 1);
  }
    else if (c.GetProperty("TargetX") > c.x && c.GetProperty("TargetY") > c.y) { //down right
        c.SetProperty("CharacterAnim", 0);
  }
    else if (c.GetProperty("TargetX") < c.x && c.GetProperty("TargetY") < c.y) { //up left
        c.SetProperty("CharacterAnim", 3);
  }
    else if (c.GetProperty("TargetX") > c.x && c.GetProperty("TargetY") < c.y) { //up right
        c.SetProperty("CharacterAnim", 2);
  }
  //orthogonal direction handling
  else if (c.GetProperty("TargetX") == c.x && c.GetProperty("TargetY") > c.y) { //down
    if (c.GetProperty("CharacterAnim") == 0 || c.GetProperty("CharacterAnim") == 2) {
      c.SetProperty("CharacterAnim", 0);
    }
    else {
      c.SetProperty("CharacterAnim", 1);
    }
  }
  else if (c.GetProperty("TargetX") == c.x && c.GetProperty("TargetY") < c.y) { //up
    if (c.GetProperty("CharacterAnim") == 0 || c.GetProperty("CharacterAnim") == 2) {
      c.SetProperty("CharacterAnim", 2);
    }
    else {
      c.SetProperty("CharacterAnim", 3);
    }
  }
  else if (c.GetProperty("TargetX") < c.x && c.GetProperty("TargetY") == c.y) { //left
    if (c.GetProperty("CharacterAnim") == 0 || c.GetProperty("CharacterAnim") == 1) {
      c.SetProperty("CharacterAnim", 1);
    }
    else {
      c.SetProperty("CharacterAnim", 3);
    }
  }
  else if (c.GetProperty("TargetX") > c.x && c.GetProperty("TargetY") == c.y) { //right
    if (c.GetProperty("CharacterAnim") == 0 || c.GetProperty("CharacterAnim") == 1) {
      c.SetProperty("CharacterAnim", 0);
    }
    else {
      c.SetProperty("CharacterAnim", 2);
    }
  }
}

function MovePlayerToMouse()
{
  cRoger.SetProperty("TargetX", mouse.x);
  cRoger.SetProperty("TargetY", mouse.y);
  DetermineCharacterAngle(cRoger);
  cRoger.Animate(cRoger.GetProperty("CharacterAnim"), 1, eRepeat, eNoBlock);
  cRoger.Move(cRoger.GetProperty("TargetX"), cRoger.GetProperty("TargetY"));
  while (cRoger.Moving) {
    Wait(1);
  }
  cRoger.ChangeView(cRoger.View);
}

DetermineCharacterAngle() uses the character's current coordinates and a set of target coordinates (two custom properties-- just in case two or more characters are moving different directions at once) to determine which animation to use. (I wasn't able to figure out the arctan thing, but I appreciated the suggestion!) The orthogonal bits have an extra thing to preserve the character's facing on the other axis (for example, a character who moves up and then straight left will use the up/left animation).

MovePlayerToMouse() is called in the BASS template's TwoClickHandler script. It just overwrites the character's normal movement animation with the one determined by DetermineCharacterAngle(). I haven't tested NPC movement or keyboard movement yet (though I do have a script for the former), but I'm hopeful that it'll translate easy.

Hope this helps anyone who needs it!

eri0o

Hey, for things like your TargetX and Y which is just a variable, you can just write like

Code: ags
int target_x;
int target_y;

And pass it as a parameter in your function DetermineCharacterAngle

Code: ags
void DetermineCharacterAngle(Character *c, int target_x, int target_y)
{
  // rest of the function here
}

celadonk

Ah! Smart! Guess I missed that. Thank you so much for the suggestion!

Khris

Disregarding that this code can be shortened greatly, this creates a blocking walk and doesn't support changing the loop mid-walk in case the character has to round a corner.

This should achieve the same thing in theory, I didn't test it:
Code: ags
function MovePlayerToMouse()
{
  int targetx = Game.Camera.X + mouse.x; // in case of scrolling room, add camera offset
  int targety = Game.Camera.Y + mouse.y;
  int dx = targetx - cRoger.x;
  int dy = targety - cRoger.y;
  int loop = (dy < 0) * 2 + (dx < 0); // true/false is forced to 1/0 in integer expression
  cRoger.LockView(cRoger.NormalView);
  cRoger.Animate(loop, 1, eRepeat, eNoBlock);
  cRoger.Move(targetx, targety, eBlock);
  cRoger.UnlockView();
}
I skipped over orthogonal keeping of the loop because it not really noticeable in-game anyway.
The blocking loop is gone because you can make the movement blocking.

celadonk

Interesting! I would have never thought of using greater than/less than as a math expression to determine the animation. Thank you very much for the suggestion! It does seem like this still creates a blocking walk, so you can't turn mid movement.

SMF spam blocked by CleanTalk