Author Topic: FaceObject() and FaceCharacter() and direction woes, and a fix  (Read 114 times)

Hi folks,
Meet Luke:
Luke is a bit of an autist, and so he doesn't know about his surroundings too well. When told to look at something on his right, he sometimes faces to the front — or back — instead. Sometimes an object is directly behind him, yet looking at it, he turns to the left as if he was only interested in its left edge for some strange reason. See my current game entry to watch him fumbling and bumbling, especially in the tree branch room.

What an awkward fellow this Luke guy is, and AGS makes him that way. Luke is using the standard functions Character.FaceLocation() and Character.FaceObject(), and those functions give results that are at the very least very counterintuitive, not to say plainly wrong.

How come? I've tried to research a bit of context for doing directions in the two-dimensional room, and this is what I've found:

To start off with, AGS doesn't natively know the concept of "looking downwards" as in "looking at your feet" or "looking upwards" as in "looking into the sky". You can code and draw it, of course, but it will be a special animation. Instead, when AGS talks about "down", it means "turned toward the human player (towards the fourth wall)", and when it talks about "up", it means "turned away from the human player (towards the back stage wall)".

Thus, "down" and "up" describe aspects of the depth dimension, not the height dimension.

This is relevant for FaceDirection() and such. No matter what direction you specify, the character will always look straight ahead, but it will be turned in different ways.
  • If a character does FaceDirection(eDirectionUp), it doesn't really look "upwards", it looks upstage instead and you'll see the back of the character's head. If it does "FaceDirection(eDirectionDown)" it looks downstage.
  • Similarly, if it does FaceDirection(eDirectionLeftUp) it looks upstage to the left, etc

Well, FaceObject() seems to often calculate this turn incorrectly.
How calculate a correct turn?

Details inside:
Spoiler: ShowHide
The character will always look straight ahead no matter how high the object is above the floor, so the object's height or elevation must be irrelevant information when calculating how the character should be turned. Instead, exactly two specifications should be relevant:
  • Whether the object is to the left or the right of the character.
  • Whether the object is upstage or downstage with respect to the character.

The easier part is calculating whether the object is to the left of the character and how far. You compare the character's and object's X dimensions. However, the relevant point for this comparison should  be the middle point of the object's width, and the scaling of the object should be taken into account when determining this middle.

It's harder to find out whether the object is in front of the character:
  • Usually, whenever AGS tries to find out whether something is "in front of" something else, it uses the baselines: The (geometrically) lower the baseline of a thing is, the more downstage it is considered to be.
  • If we use the lower edge of the sprite instead of the baseline, we will get into trouble in a lot of very common situations. Take for instance, a cup object that is lying on a table. The lower edge of the cup sprite will be near the table top, that is "high up". If we use this value as a depth information, we would conclude that the cup is "far away in the distance", which is wrong.
  • The only way to tell AGS how distant the object is is setting the baseline of the object. AGS doesn't provide any other way to tell this information. So conversely, AGS should actually consider the baseline for depth calculations whenever it is set; it should not deliberately play dumb and ignore a set baseline, taking the lower edge of the object against better knowledge.

So to recap, a preliminary outline of a correct algorithm for ch.FaceDirection(o) would be:
Turn to eDirectionAB, where
  • A is "Up" when the baseline of o is geometrically higher than the baseline of ch, otherwise "Down" (use the lower edge of the sprite iff the baseline isn't set);
  • B is "Left" when the middle of o is geometrically to the left of the middle of ch, otherwise "Right" (and consider the scaling when determining the middle).

This preliminary outline must be adapted because the character can turn straight to the right or left and straight upstage or downstage, too. A good predictable calculation might be: Draw a vector in the 2d space from the middle of the baseline of the character to the middle of the baseline of the object. Then turn the vector to the nearest multiple of 45°. Then convert this angle into the direction we are looking for.

Also, some characters can only turn to the straight orthogonal directions, they don't have views for eDirectionLeftUp etc. — the calculation above should then use multiples of 90° in these cases.


Does AGS do this? No, and as far as I can determine it hasn't done this for decades:
  • When dealing with objects, it uses their left edge, so FaceObject() makes characters strangely prefer their left-hand side.
  • When the objects have a baseline set, it ignores the baseline, so FaceObject() wrongly assumes for a lot of objects that they are in the background when they are on a table etc. instead.
  • I haven't analysed the code that far, but I think that the function doesn't cleanly round to nearest multiples of 45° (90°) either; for instance, it seems to prefer the "DownRight" direction unless the lower left corner of the object is nearly directly under the character and only then switches to the "Down" direction.

Whole generations of adventure coders must have struggled with those functions and come to the conclusion that they must be broken in some elusive way. Oodles of adventures must contain kludgy workarounds. So there must be a very tenacious reason to keep those broken functions broken or else they would have been fixed long ago. And based on these experiences, I doubt that they can ever be fixed in future, either.

In the light of this it might be better to fix this on the AGS program side and provide drop-in replacements for FaceLocation(), FaceObject(), and FaceCharacter(). This is what I try to offer below:
  • FaceLocationBetter() offers directions in clean 45° multiples and so short-circuits any angle rounding operations that FaceLocation() might do.
  • FaceObjectBetter() and FaceCharacterBetter() use the baseline when set.
  • FaceObjectBetter() determines the object width whilst considering the scaling and uses the middle of this width. It uses an auxiliary function CurrentSprite() that gets the sprite that the object currently displays.

Perhaps the code might be useful in some way. It should go into GlobalScript.asc, and GlobalScript.h should get some suitable definitions. Or if you keep a module with often-used functions, you could stick it there instead.

The code might still have some bugs.

Code below:
Spoiler: ShowHide

Code: Adventure Game Studio
  1. int CurrentSprite(this Object *)
  2. {
  3.     readonly int view = this.View;
  4.     if (0 == view)
  5.         return this.Graphic;
  6.     readonly ViewFrame *frame =
  7.         Game.GetViewFrame(view, this.Loop, this.Frame);
  8.     return frame.Graphic;
  9. }
  10.  
  11. function FaceLocationBetter(this Character *, int dest_x, int dest_y,  BlockingStyle bs)
  12. {
  13.     readonly int diff_x = dest_x - this.x;
  14.     readonly int diff_y = dest_y - this.y;
  15.     int sign_of_diff_x = 2 * (diff_x >= 0) - 1;
  16.     int sign_of_diff_y = 2 * (diff_y >= 0) - 1;
  17.     readonly int abs_x = diff_x * sign_of_diff_x;
  18.     readonly int abs_y = diff_y * sign_of_diff_y;
  19.    
  20.     if (abs_x < abs_y)
  21.     {
  22.         readonly int tan_val = diff_x * 29 / diff_y; // 12/29 is an approximation for tan 22.5°
  23.         if (tan_val >= -12 && tan_val <= 12)
  24.             sign_of_diff_x = 0;
  25.     }
  26.     else
  27.     {
  28.         readonly int tan_val = diff_y * 29 / diff_x; // 12/29 is an approximation for tan 22.5°
  29.         if (tan_val >= -12 && tan_val <= 12)
  30.             sign_of_diff_y = 0;
  31.     }
  32.     return this.FaceLocation(
  33.         this.x + 10 * sign_of_diff_x, this.y + 10 * sign_of_diff_y, bs);
  34. }
  35.  
  36. function FaceObjectBetter(this Character *, Object *o, BlockingStyle bs)
  37. {
  38.     int y = o.Y;
  39.     if (o.Baseline != 0)
  40.         y = o.Baseline;
  41.     int x = o.X;
  42.     int width = Game.SpriteWidth[o.CurrentSprite()];
  43.     if (!o.IgnoreScaling)
  44.         width *= GetScalingAt(o.X, o.Y) / 100;
  45.     x += width / 2;
  46.     this.FaceLocationBetter(x, y, bs);
  47. }
  48.  
  49. function FaceCharacterBetter(this Character *, Character *ch, BlockingStyle bs)
  50. {
  51.     int y = ch.y;
  52.     if (ch.Baseline != 0)
  53.         y = ch.Baseline;
  54.     this.FaceLocationBetter(this.x, y, bs);
  55. }
  56.  
« Last Edit: 23 May 2020, 05:53 by fernewelten »

Snarky

  • Global Moderator
  • Global Moderator
  • Mittens Lord
  • Private Insultant
    • Best Innovation Award Winner 2018, for his numerous additions to the AGS open source ecosystem including the new Awards Ceremony client and modules
    • Snarky worked on one or more games that won an AGS Award!
    •  
    • Snarky worked on one or more games that was nominated for an AGS Award!
I'm pretty sure FaceCharacter() and FaceObject() simply compare the x,y coordinates of the character/object with the coordinates of the character doing the facing (basically, they get the coordinates and then run FaceLocation()). Since objects' x-coordinates represent their left edge, that accounts for that. As for whether it should take into account overridden baselines… yeah, possibly. At least for objects. With characters I think it's a little more debatable.

Whole generations of adventure coders must have struggled with those functions and come to the conclusion that they must be broken in some elusive way. Oodles of adventures must contain kludgy workarounds. So there must be a very tenacious reason to keep those broken functions broken or else they would have been fixed long ago. And based on these experiences, I doubt that they can ever be fixed in future, either.

:-D
Much more likely, this comes up very rarely, and hasn't been fixed because nobody has ever reported it. A fix would almost certainly be trivial, though I would argue that using the actual object x-coordinate is safer and more consistent than calculating the mid-point.

As for the angles, this has been discussed a number of times. Because properly, it shouldn't be 45° or 90° wide for each direction, due to perspective. (Think of a circle, divided into 4 or 8 equal slices, and then laid down on the floor: it gets squashed because you're looking at it from an angle, and some slices — the ones you view straight on: up/down — get wider while others — the ones that point to the sides: left/right — get thinner.) However, the extent of this effect will vary depending on the room perspective, so if we are to do it properly, it should be a setting for each walkable area (since different walkable areas can have different perspectives, e.g. a floor, a staircase and a platform).