[DISCUSSION][PLAYER] Climber mechanics

Started by namida, February 13, 2016, 06:08:23 PM

Previous topic - Next topic

0 Members and 1 Guest are viewing this topic.

namida

So, as mentioned in this bug report, I've noticed in general a lot of inconsistencies in regards to how climbers behave.

Essentially, these all come from two simple facts:
1. The climber checks for the top during the first 4 frames of his animation, and moves upwards on the rest.
2. Both of these have a terrain check, but they get triggered at different times.

In general - what happens is that, when a climber meets an overhang at the same height, how many frames it takes before he falls depends on whether that overhang is at the top of the climb or not. I've attached a level to illustrate this. The level has climbs with overhangs - in both "overhang is at top" and "overhang is in middle" varieties - at various heights from 7 pixels to 22 pixels.

A summary of the current behaviour, in mostly-human-readable language, and with stuff relating to the implementation of gimmicks stripped out, is:

(Keep in mind - the climber's X position is one pixel inside the wall, similar to how when walking the lemming's Y position is one pixel inside the ground.)
If the Climber animation is on frame 0, 1, 2 or 3:
>> Check for no solid pixel at the climber's X position, and (7 pixels + current frame number) above the climber's Y position
>>>> If no solid pixel is found, check for solid pixels one pixel behind the climber, and (6 pixels + current frame number) above the climber's Y position; if this is not the first cycle through its animation, also check 5 pixels above*; just remember the result for now
>>>> Move the lemming to the top of the wall
>>>> If a solid pixel was found before, move the lemming down one pixel, turn around, change to a faller, and move forward one pixel (in new direction; ie: away from the wall)
>>>> Otherwise, change to a hoister
>> If there was a solid pixel there (remember - this is the wall itself, not an overhang), do nothing except continue the animation

* Skipping this check on the first cycle is to allow them to climb through an overhang at 6 pixels from the ground.

If the Climber animation is on frame 4, 5, 6, or 7:
>> Decrease the lemming's Y position by 1
>> Check for any of the following:
      - Solid pixel 1 pixel behind the lemming and 7 above it
      - Solid pixel 1 pixel behind the lemming and 6 above it, if this is frame 4 of the first cycle*
      - A blocker or one-way-field trigger area in the opposite of the lemming's current direction, at the exact X and Y coordinates of the lemming
>>>> If any of those were the case, move downwards 1 pixel, turn around, change to a faller, and move forward one pixel (in new direction; ie: away from the wall)

* Performing this check is to prevent climbing through an overhang at exactly 7 pixels when it isn't the top of the climb



Any change here has the potential to break a lot of replays; it isn't so likely to break too many levels (but we never know - those that rely on very accurate timing may be broken). But, unless we accept this inconsistency (which I am open to as an option, but feel it needs to be discussed), some change is needed - the question is what exactly that should be.

To those who want to look at the code itself - keep in mind it has some gimmick-related stuff in there too, as well as some dud stuff like conditionals that will always be false (which I've removed now in my copy), and does not yet have the fix for the 7-pixel-not-at-top overhang in the current latest code (V1.42n's) - look at the "TLemmingGame.HandleClimbing" function, beginning at (or near) line 7090 in LemGame.pas which I've copy-pasted an up-to-date version of in this post.


EDIT: I added a second test map, with the situations described in Nepster's post in this topic.
Both test maps require the respective VGASPEC (also attached), but this aside do not require any custom styles.
My projects
2D Lemmings: NeoLemmix (engine) | Lemmings Plus Series (level packs) | Doomsday Lemmings (level pack)
3D Lemmings: Loap (engine) | L3DEdit (level / graphics editor) | L3DUtils (replay / etc utility) | Lemmings Plus 3D (level pack)
Non-Lemmings: Commander Keen: Galaxy Reimagined (a Commander Keen fangame)

namida

Added a demonstration level, and an actual description of the inconsistency rather than just describing what causes it.
My projects
2D Lemmings: NeoLemmix (engine) | Lemmings Plus Series (level packs) | Doomsday Lemmings (level pack)
3D Lemmings: Loap (engine) | L3DEdit (level / graphics editor) | L3DUtils (replay / etc utility) | Lemmings Plus 3D (level pack)
Non-Lemmings: Commander Keen: Galaxy Reimagined (a Commander Keen fangame)

Simon

#2
Thanks for the description.

The first hunch, obviously, is that check-for-overhang should be completely independent of check-for-top. But I haven't dived into why it's not. So, don't want to advise anything yet.

-- Simon

namida

Here's the current code, including the removal of do-nothing junk and tidying up the formatting a bit so it's more readable, as well as implementing the overhang-at-7-pixel fix:

function TLemmingGame.HandleClimbing(L: TLemming): Boolean;
var
  FoundClip: Boolean;
begin

  with L do
  begin

    if (LemFrame <= 3) then
    begin
      // check if we approached the top
      if (HasPixelAt_ClipY(LemX, LemY - 7 - LemFrame, 0) = FALSE) then
      begin
        FoundClip := HasPixelAt(LemX - LemDx, LemY - 6 - Lemframe)
                  or (HasPixelAt(LemX - LemDx, LemY - 5 - Lemframe) and (LemClimbed > 0));
        LemY := LemY - LemFrame + 2;

        if FoundClip then
        begin
          if CheckGimmick(GIM_BACKWARDS) then TurnAround(L);
          if not CheckGimmick(GIM_NOGRAVITY) then
          begin
            LemY := LemY + 1;
            Transition(L, baFalling, TRUE)
          end else
            Transition(L, baWalking, TRUE);

          if CheckGimmick(GIM_BACKWARDS) then
            Dec(LemX, LemDx)
          else
            Inc(LemX, LemDx);
          Result := True;
          Exit;
        end else
          LemClimbed := 0;
          Transition(L, baHoisting);

      end;
      Result := True;
      Exit;
    end
    else begin
      Dec(LemY);
      Inc(LemClimbed);
      // check for overhang or level top boundary
      if (HasPixelAt_ClipY(LemX - LemDx, LemY - 7, -8) = true)
      or ((LemClimbed = 1) and HasPixelAt_ClipY(LemX - LemDx, LemY - 6, -8))
      or ((ReadObjectMapType(LemX, LemY) = DOM_FORCELEFT) and (LemDx > 0)) or ((ReadObjectMapType(LemX, LemY) = DOM_FORCERIGHT) and (LemDx < 0))
      or ((ReadBlockerMap(LemX, LemY) = DOM_FORCELEFT) and (LemDx > 0)) or ((ReadBlockerMap(LemX, LemY) = DOM_FORCERIGHT) and (LemDx < 0))
      or ((CheckGimmick(GIM_EXHAUSTION)) and (LemClimbed >= fFallLimit)) then
      begin
        if CheckGimmick(GIM_BACKWARDS) then TurnAround(L);
        if not CheckGimmick(GIM_NOGRAVITY) then
          begin
          LemY := LemY + 1;
          Transition(L, baFalling, TRUE);
          end
        else
          Transition(L, baWalking, TRUE);
        if CheckGimmick(GIM_BACKWARDS) then
          Dec(LemX, LemDx)
          else
          Inc(LemX, LemDx);
      end;
      Result := True;
      Exit;
    end;

  end;
end;


For the record, a "true" for the last parameter of L.Transition causes the lemming to turn around in addition to changing state.
The function as a whole returning true (which if I'm not mistaken, will always be the case for the Climber, but not nessecerially so for other actions) enables the full check for objects afterwards; otherwise, only certain types are checked for.
HasPixelAt_ClipY no longer does anything different from a regular "check if there's a pixel here" in NeoLemmix; meaning the last parameter of it is meaningless. It did special stuff compared to a regular check in vanilla Lemmix / early versions of NeoLemmix, and like many things, I haven't completely stripped it out yet.
My projects
2D Lemmings: NeoLemmix (engine) | Lemmings Plus Series (level packs) | Doomsday Lemmings (level pack)
3D Lemmings: Loap (engine) | L3DEdit (level / graphics editor) | L3DUtils (replay / etc utility) | Lemmings Plus 3D (level pack)
Non-Lemmings: Commander Keen: Galaxy Reimagined (a Commander Keen fangame)

Nepster

Will look at the code soon, but here is something else (that I wanted to post in the bug report thread, but it's closed again!):
If the wall is precisely 9 pixels high with an overhang at 8 pixels, the climber may climb this as well (at least in V1.42).

namida

You should be able to re-open your own topics, but anyway, that's something we can look at here. I'm thinking the entire Climber should be overhauled at this point; between the inconsistencies and the bugs.
My projects
2D Lemmings: NeoLemmix (engine) | Lemmings Plus Series (level packs) | Doomsday Lemmings (level pack)
3D Lemmings: Loap (engine) | L3DEdit (level / graphics editor) | L3DUtils (replay / etc utility) | Lemmings Plus 3D (level pack)
Non-Lemmings: Commander Keen: Galaxy Reimagined (a Commander Keen fangame)

IchoTolot

So as I see the example level: It would affect short height climbs with an ongoing wall up to 9 pixels. Prob short height climbers with this ongoing wall will behave as the normal climbers then (stop climbing a few frames earlier).

First things first: No player will notice this at all, but this could be worth fixing and if I would combine this with the 7 pixel high climbs fix (to minimize checking everything again to only 1 more time).

It will most certainly not break 99,9% of the levels only the ones with this exactly short climbs + pixel perfection.
But a big bunch of replays will break for sure!

I am not 100% sure: It is an inconsistancy that should be fixed, but it is also really unimportant to the overall gameplay so dont bother to fix. hmm



Nepster

I am trying to understand the code first, before forming an opinion on possible changes:
- During frames 0-3 the climber does not change position, but the checks to transition to a hoister are made higher and higher, i.e. for hoister purposes, the lemming climbs on frames 0-3.
- During frames 4-7, the position of the lemming changes. However during frames 4-6 he cannot transition to hoister (assuming the terrain stays constant), because the terrain checks are actually made for lower pixels than during frame 3.
Is this correct?

namida

Correct; he does not change position during frames 0-3, but does indeed check higher and higher.
During frames 4-7, he cannot become a hoister ever - there is no Transition(L, baHoisting) in there. If the terrain is removed, he will continue to move upwards until he reaches frame 0 again, at which point (assuming he hasn't moved beyond the gap) he will become a hoister.

If I remember correctly, the underlying movement and check positions are unaltered from vanilla Lemmix. The changes (apart from those relating to gimmicks) are related to preventing him from ignoring overhangs in some cases.
My projects
2D Lemmings: NeoLemmix (engine) | Lemmings Plus Series (level packs) | Doomsday Lemmings (level pack)
3D Lemmings: Loap (engine) | L3DEdit (level / graphics editor) | L3DUtils (replay / etc utility) | Lemmings Plus 3D (level pack)
Non-Lemmings: Commander Keen: Galaxy Reimagined (a Commander Keen fangame)

Nepster

#9
Here a list of the frames, positions and check-locations (as I understand it - so no guarantee). If a Hoist-check is made, the Terrain-check only occurs if the Hoist-check was successful.


Frame    Y-Pos    Hoist-Check    Terrain-Check
00-7never occurs
10-8-7
20-9-8
30-10-9
4-1none-7, -8
5-2none-9
6-3none-10
7-4none-11
8-4-11-9, -10
9-4-12-10, -11
10-4-13-11, -12
11-4-14-12, -13
12-5none-12
13-6none-13
14-7none-14
15-8none-15

This tells me to expect the following irregular behaviors in addition to the one described by namida:
1) If the wall is 8 pixels high, the code does nowhere check for terrain at height 7.
2) If the wall is 9 pixels high, the code does nowhere check for terrain at heights 7 or 8.
3) If the wall is (10+4k) pixels high, but there is an overhang at height (11+4k), the terrain check at frame (7+8k) detects this overhang and the lemming falls down. If this setup appears for any other height of the wall, the lemming may climb it.

Finally in your code snippet for frames 4-7 you have the line:
or ((LemClimbed = 1) and HasPixelAt_ClipY(LemX - LemDx, LemY - 6, -8))
Why is it LemClimbed = 1 and not LemClimbed = 0, if you want to check for the first cycle?


namida

Notice just above that:

Inc(LemClimbed);

Because of this, LemClimbed will never be 0 at the time it reaches that line - it will be 1 the first time that line is reached.
My projects
2D Lemmings: NeoLemmix (engine) | Lemmings Plus Series (level packs) | Doomsday Lemmings (level pack)
3D Lemmings: Loap (engine) | L3DEdit (level / graphics editor) | L3DUtils (replay / etc utility) | Lemmings Plus 3D (level pack)
Non-Lemmings: Commander Keen: Galaxy Reimagined (a Commander Keen fangame)

Nepster

Thanks. I corrected my table above accordingly.

Nepster

Please chack the following suggestions, whether they are practicable and don't introduce new glitches.

Overhang middle/top make difference in trigger falling:
I can see only rather elaborate fixes to this. The first idea would be to keep climbing instead of falling, when on frames 0-3 the hoister-check was successful, but terrain was encountered. Then ideally the terrain checks for frames 4-7 would see the overhang and trigger the transition to faller.
Problems appear when the overhang gets removed in the meantime, but the gap remains: Then the climber would simply continue climbing over the gap in the wall without transitioning to faller or hoister at any time. So one would have to include hoister checks for frames 4-7 as well...
Thus I vote for keeping this inconsistency.

Walls of heights 8 or 9:
I cannot think of anything that is simpler than adding special terrain checks for frames 1, 2 and 3 on the first cycle. To keep the code readable, perhaps one could deal with the first 4 frames separately, e.g. in the form

if (LemClimbed = 0) then
  // Check for hoister transition on first cycle
  ...
else if (LemFrame <= 3) then
  // Check for hoister transition on all other cycles
  ...
else
  // Move lemming and check for overhangs
  ...
end


Wall is (10+4k) pixels high, but overhang at height (11+4k):
This seems surprisingly difficult to fix. The best I could come up with, is the following:
Change the terrain check on frame 7 to
(HasPixelAt_ClipY(LemX - LemDx, LemY - 7, -8) = TRUE) and (HasPixelAt_ClipY(LemX, LemY - 7, 0) = FALSE)
i.e. only transition to faller if the lemming will not become a hoister in the next frame.

Again we have a problem, if the terrain is modified between frame 7 and the following frame 0: If the gap in the wall (which should trigger the hoister transition) is filled during this one frame, the lemming continues climbing and thus passes through the overhang.
So on frame 0, one needs an additional check for
(LemClimbed > 0) and (HasPixelAt_ClipY(LemX - LemDx, LemY - 7, -8) = TRUE)
which triggers falling, regardless what the hoister-check does.

At first glance a stricter terrain check on frame 4 seems to work as well, similarly to the one done in the first cycle. So instead of
or ((LemClimbed = 1) and HasPixelAt_ClipY(LemX - LemDx, LemY - 6, -8))
we would have:
or ((LemFrame = 4) and HasPixelAt_ClipY(LemX - LemDx, LemY - 6, -8))
However then the following weird glitch appears: If there are gaps in the wall at heights 11+4k and 14+4k and overhang at height 11+4k. If the gap at height 11+4k is filled in frame 7+8k, the lemming continues in the climbing animation. However on frame 11+8k, i.e. one frame before the additional terrain check would trigger the falling animation, the hoister-check finds the gap at height 14+4k. The following terrain checks check only at heights 12+4k and 13+4k, so succeeds and the lemming will transition to a hoister.


An alternative would be to disallow hoisting in such cases in general by adding a terrain check at the same height as the hoist-check. However this might break much more replays or even levels than always allowing hoister-transitions.


Btw: What is the the third argument in HasPixelAt_ClipY?

namida

In vanilla Lemmix, HasPixelAt_ClipY was used to limit the region that would be checked - I'm not sure exactly what it was relative to (the top of the level, I think). However, in NeoLemmix, HasPixelAt_ClipY is much simpler, and the last parameter does nothing - I just haven't got around to stripping that out yet.

function TLemmingGame.HasPixelAt_ClipY(X, Y, minY: Integer; SwimTest: Boolean = false): Boolean;
begin
  Result := HasPixelAt(X, Y, SwimTest);
  // need to remove this function; NeoLemmix doesn't ever clip Y in solidity tests
end;
My projects
2D Lemmings: NeoLemmix (engine) | Lemmings Plus Series (level packs) | Doomsday Lemmings (level pack)
3D Lemmings: Loap (engine) | L3DEdit (level / graphics editor) | L3DUtils (replay / etc utility) | Lemmings Plus 3D (level pack)
Non-Lemmings: Commander Keen: Galaxy Reimagined (a Commander Keen fangame)

namida

I put up a better climber test map, that rather than being kludgily thrown together out of pieces and erasers, uses a VGASPEC so it can be more precisely designed (and labelled).

I'll make one soon that includes the cases Nepster mentions, and perhaps some other similar cases. This way we can better discuss what we think should happen in each case shown in such a map.
My projects
2D Lemmings: NeoLemmix (engine) | Lemmings Plus Series (level packs) | Doomsday Lemmings (level pack)
3D Lemmings: Loap (engine) | L3DEdit (level / graphics editor) | L3DUtils (replay / etc utility) | Lemmings Plus 3D (level pack)
Non-Lemmings: Commander Keen: Galaxy Reimagined (a Commander Keen fangame)