Secondary animations for triggered objects: Experimental version available

Started by namida, May 02, 2019, 09:24:27 PM

Previous topic - Next topic

0 Members and 1 Guest are viewing this topic.

Nepster

Quote from: namida on May 12, 2019, 08:45:07 PM
WRT complicating the code - most of the important stuff is contained in its own classes. Also, Simon suggested in Discord that the system should be future-proof and allow for multiple animations, in case future objects end up needing more. So I did exactly that. Lots of flexibility for content creators is a bonus that results from this. This system can easily be updated in the future to give extra conditions, if objects are added where they're relevant. It already accounts for objects that might end up needing multiple secondary animations - indeed, I've already found use cases for this:
- default:pickup - Absorbing masks into the secondary anims system, instead of having both of them in effect (which seems silly, as masks are basically just recolored but otherwise simpler secondary animations), means two secondaries are needed for the two different color regions. This would also hold true for any future objects that need multiple recoloring - and while this is currently only used by default:pickup and the various default:owa_xxx objects, it's supported for literally any object of any type in any style.
- namida_basement:exit - This one is purely aesthetic, and could be done without multiple (or even any) secondaries, had it been done up front. The only way in which it depends on the secondary anims system is that it allows the glows to be outside the original boundaries of the object.
- namida_horror:exit_02 - This one previously didn't have any secondary-animation-via-second-object (just the unlock animation, otherwise it was static), but had a (non-animated) red or green light depending on unlock status. Now, the light glows - this needs one secondary anim for the red glow, and one for the green glow. As well as multiple-secondary support in general, this also relies on the conditional logic type stuff - which really is quite simple.

And this is just with "what could it be useful for on existing objects". There's even more potential for it on new objects, which can be designed with these capabilities in mind.
I have nothing against future-proof extensible design. But it should be easy to understand and use. And given my problems to understand why we need nine-slicing, when to use it or even how to create a decent first frame for the editor, the current design is not easy to understand.
As soon as we have a system, where I don't have to look at the NL player rendering code to find out what the first (idling) frame should look like, but can determine this purely from the posts here and the .nxmo/.png files, I am happy. In that case I will gladly implement that rendering in the editor, to avoid the dependency to the NL player completely.

Quote from: namida on May 12, 2019, 08:45:07 PM
QuoteSo what is not needed?
- I really see no need for nine-slicing

Okay, I'm going to disagree with you really hard on this one. If anything, I would rather ditch the complex secondary anims and keep the nine-slicing feature. As a particularly strong case, take a look at the updrafts in namida_machine. To get the proper visual appearance, any updraft region using that style's updraft (assuming it doesn't intersect indestructible terrain or level boundaries on one or more sides) previously needed nine objects in the level, of six different object types (with the last three being the same object as another three, but flipped horizontally). With nine-slicing, this can now be done as a single object, and retain perfect visual appearance. The default updraft, and the fire objects in namida_machine, have also benefitted from this to a lesser extent. Further uses for this could allow for nice vertical resizing of water objects, too, rather than having to paste multiples on top of each other - I just haven't got around to actually doing this yet. Essentially - any resizable object that should have a clearly-defined edge, benefits hugely from this feature.
Ok, slowly but surely I start to understand what nine-slicing is about. But this seems to me like a completely different use-case than secondary animations, or am I mistaken there? Secondary animations are for switching animations depending on the object state, while nine-slicing is about nice looking edges of resizable objects.
If this take on secondary animations vs. nine-slicing is somewhat correct, could we please split the discussions (and feature branches)? I think I would have appreciated nine-slicing a lot more, if you had it introduced it in a thread "Nine-slicing: Resizable objects with nice edges" instead of here in "Secondary animations for triggered objects". 

Quote from: namida on May 12, 2019, 08:45:07 PM
I would perhaps agree that there's no real need for secondary animations to support it, but with the current implementation, it would be more code for secondaries to not support it.
I now wonder: Does NL first merge the secondary animations into the primary one and then apply the nine-slicing, or the other was around, i.e. first apply nine-slicing to both animations and then merge them?
I see reasons for both: The second one is more flexible, but it's easy to forget to add the nine-slicing parameters although one probably always wants to have them.

Quote from: namida on May 12, 2019, 08:45:07 PM
I would also agree that it should really have been implemented as a separate feature that in turn is supported in the animation system, instead of being implemented together with this animation system, but I approached this as a general "improve the object rendering" and thus did both at the same time.
Yeah, see my comments above. There has been quite a bit of confusion about this on my part.

Strato Incendus

Just a layman question: namida told me this is a feature NeoLemmix needs (in the sense of "can't live without it").

By that I understand "something that affects the rules / game mechanics" of Lemmings.

So far, most of the uses for this feature seem to serve aesthetic purposes. I do like the idea of being able to differentiate between armed and disarmed traps (without having to use clear physics, that is)! :thumbsup: Although I thought this could also be done in a similar way as with locked exits and their respective buttons, which simply have two different static graphics for each of their possible states.

Are there any other gameplay-relevant uses for this?

Not meaning to devalue your work here! :) It just seems curious to me that triggered animations were removed but secondary animations for triggered objects seem to be regarded as an actual requirement now.
My packs so far:
Lemmings World Tour (New & Old Formats), my music-themed flagship pack, 320 levels - Let's Played by Colorful Arty
Lemmings Open Air, my newest release and follow-up to World Tour, 120 levels
Paralems (Old Formats), a more flavour-driven one, 150 levels
Pit Lems (Old Formats), a more puzzly one, 100 levels - Let's Played by nin10doadict
Lemmicks, a pack for (very old) NeoLemmix 1.43 full of gimmicks, 170 levels

namida

QuoteI now wonder: Does NL first merge the secondary animations into the primary one and then apply the nine-slicing, or the other was around, i.e. first apply nine-slicing to both animations and then merge them?

NL never merges the animations at all. Instead, it essentially renders each one as if it were a separate object, offset by so-and-so from the primary animation's position. The end result, in terms of nine-slicing, is equivalent to your latter scenario.

QuoteAs soon as we have a system, where I don't have to look at the NL player rendering code to find out what the first (idling) frame should look like, but can determine this purely from the posts here and the .nxmo/.png files, I am happy. In that case I will gladly implement that rendering in the editor, to avoid the dependency to the NL player completely.

To reduce it to "only what the editor needs to know about". I am including here in some cases, details of how NeoLemmix would handle situations that should never actually occur, or how some common possible cases of invalid input might be handled.

Order to render, back to front, is lowest Z_INDEX to highest Z_INDEX. If Z_INDEX is not specified, it's assumed to be 1 for the primary animation, and 0 for any other animation. In the case of two animations with equal Z_INDEX value, primary animation is rendered first if it's one of them, after that (or if it isn't) the rendering order is determined by which one comes first in the file.

To load animation data - primary animation comes from a $PRIMARY_ANIMATION section or the main section, all others from a $ANIMATION section.

Primary animation in the main section (Ignore ALL of these if a $PRIMARY_ANIMATION segment exists)
FRAMES - frame count, integer, assume 1 if not present
HORIZONTAL_STRIP - sprite strip is horizontal if present, vertical if absent
INITIAL_FRAME - self-explanatory, -1 = random. Ignored if the current frame number is physics-significant (traps, entrances, etc).
CUT_LEFT - How many pixels on the left of the primary animation are considered to be part of the edge for nine-slicing
CUT_TOP, CUT_RIGHT, CUT_BOTTOM - Ditto for the respective sides
PREVIEW_FRAME - same as INITIAL_FRAME, treat as deprecated
RANDOM_START_FRAME - same as "INITIAL_FRAME -1", treat as deprecated


Primary animations in a $PRIMARY_ANIMATION segment
FRAMES - same as above
NAME - suffix on the piece name for the image file, eg. if this is "primary" on an object "bob", the primary animation comes from "bob_primary.png". If this is empty, no suffix (not even the underscore) is appended, so on a piece called "bob", empty (or no) NAME would load the image from "bob.png".
COLOR - name of a color (from the theme file) to recolor the primary animation with, see default:owa_left (or any other direction) for example of this on primary anim
HORIZONTAL_STRIP - same as above
Z_INDEX - determines Z index, as explained in rendering order (default = 1 for primary anim, 0 for any other anim. Negative values are valid but decimal values currently are not - though maybe they should be?)
INITIAL_FRAME - same as above (note, the deprecated PREVIEW_FRAME / RANDOM_START_FRAME do not work here)
OFFSET_X, OFFSET_Y - offset from physics position of the object to draw the primary anim at (in normal orientation)
CUT_LEFT / TOP / RIGHT /BOTTOM - same as above

Secondary animations ($ANIMATION segment) - Everything from the $PRIMARY_ANIMATION segment, plus:
HIDE - don't render this animation (unless overridden by a trigger, see below), unless ((initial frame != 0) and (state != stop or pause)) or (state = match-primary-animation-frame)

These next few are mutually exclusive, this is the order from highest to lowest priority if more than one exists:
> PAUSE - Editor does not need to do anything special here (beyond interaction with "HIDE" above)
> STOP - INITIAL_FRAME should be ignored, instead rendering as frame 0 (in NL, "STOP" has the effect of setting the current frame to 0, then immediately changing state to PAUSE)
> LOOP_TO_ZERO - for editor purposes this can be treated as equivalent to "PLAY" if (initial frame != 0), or "PAUSE" if (initial frame == 0)
> MATCH_PRIMARY_FRAME - ignore INITIAL_FRAME, render as same frame number as the primary animation is on
> PLAY - Editor does not need to do anything special here (beyond interaction with "HIDE" above)
> If none of the above are present - this is treated as "PLAY", except if the line "HIDE" is present, in which case it's treated as "STOP" (to avoid "Why is my animation not hiding? I put hide there!" "Because you need STOP too")

$TRIGGER subsections - these are used to define the different conditions for animations to appear or disappear. Just to be clear (in case the layout of this message is confusing) - $TRIGGER only works for secondary animations! These subsections can contain the following requirements, with either "true" or "false". If a requirement is not specified, then the trigger does not depend either way on that requirement. Remember - these explanations are just for editor purposes (and exclude a few edge cases), the game has more detail to it than this. If multiple $TRIGGER sections exist, the last one (in file order) where all conditions are met, is the one that takes effect.
FRAME_ZERO - "True" for infinite-use trap, teleporter, receiver, splitter pointing left, window*. Otherwise, "false".
FRAME_ONE - "True" for pickup skill, locked exit, unlock button, splitter pointing right, or single-use trap. Otherwise, "false".
BUSY, TRIGGERED, DISABLED - Always "false" for editor purposes.

* assuming the editor retains "render windows in open state". If it renders them in closed state, FRAME_ZERO is false and FRAME_ONE is true.

If you want to account for the edge cases (still just "what actually matters to the editor")
- FRAME_ZERO is true, and FRAME_ONE is false, for a pickup skill that doesn't have a skill type assigned to it
- FRAME_ZERO is true, and FRAME_ONE is false, for a locked exit in a level that doesn't contain any unlock buttons
- DISABLED is true for a teleporter in a level with no receivers, or vice versa

STATE - This can have a value of "STOP", "PAUSE", "PLAY", "LOOP_TO_ZERO" or "MATCH_PRIMARY_ANIMATION". These all work the same way as the equivalent lines in the main part of the $ANIMATION section, but note they're values in a line with a key of "STATE" here, rather than their own lines. (Yeah, this is weird. I should change this to be consistent - which way do you think is better?)
HIDE - Works the same way as in the main $ANIMATION section. Just like there, absence of "HIDE" is treated as "show", there's no explicit "SHOW" keyword.

As in the main part, too, the absence of a "STATE" line is treated as equivalent to "STATE PLAY", except if "HIDE" is present, in which case it's treated as "STATE STOP". However - "STATE" with an invalid parameter, regardless of the presence of "HIDE", is always treated as "STATE PLAY".

Any case-sensitivity in NeoLemmix in any of this, is a bug, so feel free to treat it all as case-insensitive. Behaviour of "MATCH_PRIMARY_FRAME" when primary anim's current frame number is higher than the amount of frames the secondary has, is (probably) that NeoLemmix crashes - up to you how accurately you want to replicate that, I suggest don't put too much effort into it as this behaviour may change in future commits to something more sane like "display the last frame" or "display nothing".

Some possible "gotchas":
- "COLOR" is valid in $PRIMARY_ANIMATION
- "OFFSET_X" and "OFFSET_Y" are valid in $PRIMARY_ANIMATION
- Empty / absent name is valid in $ANIMATION (the secondary animation would load from the non-suffixed PNG file in this case)
- Multiple animations with the same NAME is valid (see: namida_basement:exit for an example of this), both animations would just use the same graphic (they could still differ in any other params!)
- Again, watch out for "INITIAL_FRAME -1". Entirely up to you how you actually handle that - whether you want to render a random frame, or perhaps just render it as frame 0 or something.
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

Quote from: Strato Incendus on May 13, 2019, 09:08:40 AM
By that I understand "something that affects the rules / game mechanics" of Lemmings.
...
Are there any other gameplay-relevant uses for this?

This is mostly an aesthetic feature, with gameplay influence limited to that well-designed use of secondary animations can help give the player more information on the state of the level / the objects in it. Let's take for example, the weed trap in the Rock set - someone unfamiliar with the set could mistake it for terrain and not even check clear physics mode. That won't happen when it's got an animation while idle.

There's also potential future use for it. I'm currently working on a feature that uses a specially-made secondary animation to allow for custom digits for pickup skill counts - and the biggest reason for this, is that I intend to then also use this same digit animation feature for limited-lemming exits / entrances. As an actual animation, this would be hidden - and then the animation frames of it are rendered as the digits. (Sure, they don't need it, but they'll sure as hell be able to look a lot nicer with this.)

I was actually quite surprised to see triggered animations had been removed. Unlike radiation / slowfreeze, they don't complicate gameplay, and 99% of the code needed for them to function is needed anyway for other object types that still exist (most notably traps, which are essentially just a triggered animation that also removes a lemming). And especially with the secondary animations feature, there's probably a lot of artistic potential in them, too...
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

Quote from: namida on May 13, 2019, 10:05:34 AM
[explanations on animations]
Ok, that's a lot of information and I am pretty sure I am still misunderstanding half of it. I have a few questions, but almost certainly more will appear once I start implementing the editor-side rendering in earnest:
1) Why do we have to distinguish between primary animations and secondary ones? It seems that the secondary animations are even more flexible, and the only advantage of primary animations is, that they override certain attributes. But then the style designer should not set the attributes in the first place.
2) As the STATEs seem to be (and should be) mutually exclusive, they should be set as an attributes, i.e. like "STATE PLAY". If we have stuff like "LOOP_TO_ZERO" as keywords, that would indicate to me, that it can be combined with the "PLAY" or "PAUSE" state for example.
3) Why do we need "Z_INDEX"? The default specifies a certain order anyway, which is pretty natural, so I don't see any need for this attribute.
4) Why do we need "HIDE" as an option for secondary animations? If we are not displaying anything, then why have this secondary animation at all?
5) Regarding "INITIAL_FRAME": At what point did we deprecate "RANDOM_START_FRAME" and introduced the "-1" instead? I would have preferred to add "INITIAL_FRAME RANDOM" instead. (Might very well be, that it was me who introduced the "-1" and I am criticizing myself here...)
6) I am still unsure which of the STATEs and the TRIGGER types are actually necessary for the most common use-cases (i.e. idling traps and locked exits), and which are just for potential future use? Can we stick to the absolute essentials for now? This would help people like me to get used to this.

Quote from: namida on May 13, 2019, 10:14:25 AM
I was actually quite surprised to see triggered animations had been removed. Unlike radiation / slowfreeze, they don't complicate gameplay, and 99% of the code needed for them to function is needed anyway for other object types that still exist (most notably traps, which are essentially just a triggered animation that also removes a lemming). And especially with the secondary animations feature, there's probably a lot of artistic potential in them, too...
The main reason is player expectations: If something triggers, then this signals a game physics effect on a lemming. With the various gadget types it is hard enough to newer players to learn the various effects NL supports. No need to confuse them with no-effect effects ;). This all stems directly from my own experience as a player: Such objects have been very rare, so when I first encountered it, I wasn't even aware that NeoLemmix supported triggered animations. So I tried for half an hour to find out, what this gadget actually does and never found anything! :devil::devil::devil:
PS: I actually would prefer an even stronger rule, namely: If something moves, it either is currently interacting with a lemming or has at least the potential for it. Unfortunately moving background pieces break that rule (which is why I wanted to remove them at some point)...

namida

Quote1) Why do we have to distinguish between primary animations and secondary ones? It seems that the secondary animations are even more flexible, and the only advantage of primary animations is, that they override certain attributes. But then the style designer should not set the attributes in the first place.

Primary animation is tied to physics, same way that the lone animation up until now has been. Aside from that, they are indeed identical (outside of NL rejecting a few specific settings on the primary) - the primary animation is loaded by the same code as the secondaries just with a few properties being overridden. Rendering also uses the same code as primaries, though frame update does not[b/] - the primary is perpetually in a "paused" state, with the frame number being directly manipulated by game physics.

Quote2) As the STATEs seem to be (and should be) mutually exclusive, they should be set as an attributes, i.e. like "STATE PLAY". If we have stuff like "LOOP_TO_ZERO" as keywords, that would indicate to me, that it can be combined with the "PLAY" or "PAUSE" state for example.
I noted above that it seems weird to have them defined as single-line keywords in one place, and values of a "STATE" keyword in another. Good point here; let's make it "STATE _____" in both cases.

Quote3) Why do we need "Z_INDEX"? The default specifies a certain order anyway, which is pretty natural, so I don't see any need for this attribute.
Currently, the primary animation is treated as coming first in file order regardless of where it's actually placed. Z_INDEX allows placing in front of OR behind it. We would need to change how the primary animation is defined, to continue allowing for this without a Z_INDEX - relying on its order in the file, since it uses a $PRIMARY_ANIMATION tag instead of an $ANIMATION, would violate the "lines / sections are not order-sensitive in relation to lines / sections with different keywords" rule, and would also need a rule on where in the Z order it goes if specified in the main segment (for backwards compatibility, or just because it's an object with only one animation, or any other reason).

Quote4) Why do we need "HIDE" as an option for secondary animations? If we are not displaying anything, then why have this secondary animation at all?
Reason 1 - future animations that don't get used directly. For example, digits for a limited-count exit / entrance.
Reason 2 - animations that don't display in default state, but do when one of the triggers is fulfilled. For an example of this, see namida_horror:exit_02. Or the inverse - displaying usually, and hiding when a condition is fulfilled - which namida_horror:exit_02 also provides an example of.

Quote5) Regarding "INITIAL_FRAME": At what point did we deprecate "RANDOM_START_FRAME" and introduced the "-1" instead? I would have preferred to add "INITIAL_FRAME RANDOM" instead. (Might very well be, that it was me who introduced the "-1" and I am criticizing myself here...)
No, this was something I did during creating this system. Yeah, "INITIAL_FRAME RANDOM" could be a special case that's interpreted as equivalent to -1. I'd say similar logic to in your point #2 applies here - don't have it as an entirely separate command either way.

Quote6) I am still unsure which of the STATEs and the TRIGGER types are actually necessary for the most common use-cases (i.e. idling traps and locked exits), and which are just for potential future use? Can we stick to the absolute essentials for now? This would help people like me to get used to this.
I've already found use cases for all of these (except, IIRC, "TRIGGERED", but there are differences player-side between this and "BUSY"*), though some are definitely more common. But as a few examples:
- The common blinking-light makes use of the instant-stop (vs loop_to_zero), and the "BUSY" condition (light stops blinking while trap is killing) and "DISABLED" condition (light stops blinking when trap disarmed).
- The "bob up and down / side to side when idle" effect, eg. ohno_snow:trap, requires hiding secondary animations on a BUSY (or TRIGGERED) condition to avoid really weird visuals when triggered, and the "PAUSE" (without going back to frame zero) so the position won't suddenly jump by a couple of pixels (depending on current frame) when the trap gets disarmed.
- For an example of why the absence of these can be important, see ohno_brick:trap_02. The secondary animation on that one only pauses (remaining on its current frame) when the trap is disarmed, not in any other case.

A post earlier in the topic, which I've mostly been keeping updated with new exp's, details in full how this new system works.

* Differences in TRIGGERED vs BUSY
- For a teleporter (A) linked to a receiver (B), "TRIGGERED" is true for A while A is animating, and true for B while B is animating; at other times, it is "false". "BUSY" is true for both A and B while either of them is animating, and "false" at other times.
- For traps, including single-use traps, the two conditions are identical.
- For all other objects, "BUSY" is always false, but "TRIGGERED" will be true if the object (a) doesn't constantly animate, but (b) is currently animating.

Maybe this can be merged into a single condition (I suggest "BUSY" as the actual name in this case, as that's what's been used in 95% of cases so far), which has the behaviour of "BUSY" for teleporters / receivers and the behaviour of "TRIGGERED" for all other object types.




QuoteThe main reason is player expectations: If something triggers, then this signals a game physics effect on a lemming.
Counter-examples: Unlock buttons, pickup skills, locked exits. None of their animations directly affect a lemming physics-wise. Counter-counter-point: ...although they do all indicate something physics-related at least.

QuoteWith the various gadget types it is hard enough to newer players to learn the various effects NL supports. No need to confuse them with no-effect effects ;). This all stems directly from my own experience as a player: Such objects have been very rare, so when I first encountered it, I wasn't even aware that NeoLemmix supported triggered animations. So I tried for half an hour to find out, what this gadget actually does and never found anything! :devil::devil::devil:
We have clear physics mode nowdays; and an up-to-date NeoLemmix Introduction Pack should help here too. I did start working on something, but then got bored with it for now.

QuotePS: I actually would prefer an even stronger rule, namely: If something moves, it either is currently interacting with a lemming or has at least the potential for it. Unfortunately moving background pieces break that rule (which is why I wanted to remove them at some point)...
Clear physics mode, and (maybe?) the "hide background" options deal with those situations. (Triggered animations could perhaps be included in that option too, maybe a general "remove visual fluff" option? I suspect few, if any, users will feel the need to be able to pick-and-choose on a finer level here.). NeoLemmix has always tried to account for both the puzzle side and the artistic side of creativity; triggered animations and moving backgrounds are both beneficial to this, and the full extent of secondary animations will be helpful to that end too.




Perhaps we should approach this as a joint effort - you know how you like things to be loaded, etc; while I understand in full how the system works, down to the technicalities / etc. Perhaps you can write the code as you'd like it to be laid out / etc, with a call to a dummy "GetThisAnimFrameNumber" function for any secondary animations (for the primary animation, continue to handle it the way you do now, aside from paying attention to INITIAL_FRAME on any object where current frame is not physics-significant). Then, I'll actually write the GetThisAnimFrameNumber function. "GetThisAnimFrameNumber" should be passed the following params:
- Gadget type
- Current frame number of primary animation
- Value of INITIAL_FRAME
- The STATE (out of "PLAY", "PAUSE", "LOOP_TO_ZERO", "STOP", "MATCH_PRIMARY_FRAME", <other> or <absent>) and HIDE (out of <present> or <absent>) in the secondary anim's main section
- Any $TRIGGER sections, in file order, the conditions (keys {"FRAME_ZERO", "FRAME_ONE", "BUSY", "TRIGGERED", "DISABLED"} with values "TRUE", "FALSE", or <absent>), and STATE and HIDE (same as for main section)

If you'd rather write this yourself, the following C#-ish pseudocode will produce correct results for the frame to display. As before, this only operates to the extent of "what the editor needs to know about".

- If result is "RESULT_HIDE", don't display the frame at all.
- If result is "RESULT_RANDOM", display a random frame.
- If result is "RESULT_HIDE_OR_RANDOM", either hide or display a random frame other than frame 0. This result only comes up in a single edge case that shouldn't actually happen in real-world situations.
** This pseudo-code is based on the assumption we change from different keywords, to key "STATE" + value ["STOP", "PAUSE", etc] in the base $ANIMATION section. It also includes an oddity fix that I plan to (but haven't yet) implemented, namely for that "STATE <invalid input>" is different from no "STATE" line at all.
** Commented-out lines in the conditionals, are only needed to account for edge cases.

bool DoesConditionPass(bool conditionResult, enum conditionRequirement)
// enum values: "TRUE", "FALSE", <not present>
{
  switch (conditionRequirement)
  {
    case "TRUE": return conditionResult;
    case "FALSE": return !conditionResult;
    case <not present>: return true;
  }
}

int GetAnimationDisplayFrame(params)
{
  const int RESULT_HIDE = -1; // actual value doesn't matter, just needs to be a unique negative number
  const int RESULT_RANDOM = -2;
  const int RESULT_HIDE_OR_RANDOM = -3;
 
  bool hideIfAllowed;
  enum state; // enum values: "PLAY", "PAUSE", "STOP", "LOOP_TO_ZERO", "MATCH_PRIMARY_FRAME", <other / not present>

  foreach ($ANIMATION + all "$TRIGGER"s inside it) // $ANIMATION first, then each $TRIGGER in file order
  { 
    if (this is a $TRIGGER, not the base $ANIMATION)
    {
      bool conditionsPassed = true;
     
      // FRAME_ZERO
      bool thisConditionState = true;

      if (<gadget type> is not one of [infinite-trap, /*pickup, locked-exit,*/ teleporter, receiver, splitter, hatch])
      or (<gagdet type> is splitter && gadget is facing right (ie: frame 1))
  //  or (<gadget type> is pickup && gadget has a skill assigned (ie: not frame 0))
  //  or (<gadget type> is locked exit && level contains unlock buttons (ie: exit should display as frame 1))
        thisConditionState = false;
     
      if (!DoesConditionPass(thisConditionState, $TRIGGER."FRAME_ZERO"))
        Continue; // the forEach loop
     
      // FRAME_ONE
      bool thisConditionState = true;

      if (<gadget type> is not one of [pickup, locked-exit, button, splitter, single-trap])
      or (<gagdet type> is splitter && gadget is facing left (ie: frame 0))
  //  or (<gadget type> is pickup && gadget has no skill assigned (ie: frame 0))
  //  or (<gadget type> is locked exit && level lacks unlock buttons (ie: exit should display as frame 0))
        thisConditionState = false;
     
      if (!DoesConditionPass(thisConditionState, $TRIGGER."FRAME_ONE"))
        Continue; // the forEach loop
       
      // TRIGGERED
      if (!DoesConditionPass(false, $TRIGGER."TRIGGERED"))
        Continue;
       
      // BUSY
      if (!DoesConditionPass(false, $TRIGGER."BUSY"))
        Continue;
       
      // DISABLED
      bool thisConditionState = false;
     
  //  if (<gadget type> is teleporter && <no receivers exist in level>)
  //  or (<gadget type> is receiver && <no teleporters exist in level)
  //    thisConditionState = true;

      if (!DoesConditionPass(thisConditionState, $TRIGGER."DISABLED"))
        Continue;
      }
    }
   
    hideIfAllowed = $TRIGGER."HIDE".IsPresent?; // or $ANIMATION, as applicable
    state = $TRIGGER."STATE"; // or $ANIMATION, as applicable.

    if (state = <other / not present>)
    {
      if (hideIfAllowed)
        state = "STOP";
      else
        state = "PLAY";
    }
  }
 
  if (hideIfAllowed)
  {
    if (state is one of [STOP, PAUSE])
      return RESULT_HIDE;
     
    if (state is LOOP_TO_ZERO && $ANIMATION."INITIAL_FRAME" = 0) // INITIAL_FRAME being absent counts as zero here
      return RESULT_HIDE;
     
    if (state is LOOP_TO_ZERO && $ANIMATION."INITIAL_FRAME" < 0)
      return RESULT_HIDE_OR_RANDOM; // edge case.
  }
 
  switch (state)
  {
    case "PAUSE", "PLAY", "LOOP_TO_ZERO": // note that <other / not present> is already changed
                                          // to either "PLAY" or "STOP" by this point
      if ($ANIMATION."INITIAL_FRAME" < 0)
        return RESULT_RANDOM;
      else
        return $ANIMATION."INITIAL_FRAME";
       
    case "STOP":
      return 0;
     
    case "MATCH_PRIMARY_FRAME":
      return <primary anim's frame>;
  }
}


I've generally found, so far, that - for the most common cases of a single secondary anim - single-use traps generally need three conditions (one for BUSY, one for DISABLED, one for FRAME_ZERO (ie: used up)), while infinite traps usually need one or two depending on the exact nature of the animation, and locked exits usually don't need any (though namida_horror:exit_02, which has a somewhat different from usual animation setup, uses two).
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

After further thought (but no implementation, yet), perhaps we can simplify the conditions even further - "IDLE", "BUSY" and "DISABLED". All cases of FRAME_ZERO or FRAME_ONE can be folded into either "IDLE" or "DISABLED" (whichever is more applicable), while "TRIGGERED" would be folded into "BUSY" (with teleporters / receivers retaining the current behaviour of Busy, and everything else having the behaviour of Triggered). I suspect that the few cases this would directly break, could be re-implemented with careful ordering.

These conditions would be mutually exclusive, so in turn, triggers could contain a "CONDITION _____" line, instead of the current "<condition> <TRUE/FALSE>" line. Priority would remain indicated by the order in file.

I really do feel all the current values for STATE are necessary, though.
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

Quote from: NepsterIf something moves, it either is currently interacting with a lemming or has at least the potential for it.

:thumbsup:

I have pushed for more than 2 animations per gadget because of an even stronger variant of this guideline: What moves, can interact, and what does not move, is terrain.

Not all animations classify into idle or busy. I'd rather not associate "primary" with busy and "secondary" with idle (exits feel more idle than busy, yet have only a primary animation).

Quote from: namida- For a teleporter (A) linked to a receiver (B), "TRIGGERED" is true for A while A is animating, and true for B while B is animating; at other times, it is "false". "BUSY" is true for both A and B while either of them is animating, and "false" at other times.

Hmm, I haven't thought about such a very strong link between two gadgets' states/animations. This breaks a 1:1-association between states and animations. I'll read everything again before advising further; I'd really like to keep all physics intact.

-- Simon

namida

QuoteHmm, I haven't thought about such a very strong link between two gadgets' states/animations. This breaks a 1:1-association between states and animations. I'll read everything again before advising further; I'd really like to keep all physics intact.

Teleporters and receivers are already co-dependent. If a receiver is busy, the corresponding teleporter cannot be used. Prior to a change requiring one teleporter = one receiver (before this, it was possible for multiple teleporters to link to the same receiver, though not the reverse), the inverse also applied - if one teleporter is busy, the receiver (and thus, any other teleporters linked to it) cannot be used. Thus, to me, it makes sense for any animation change while the teleporter / receiver is busy, to extend to when the paired device is busy too.

QuoteNot all animations classify into idle or busy. I'd rather not associate "primary" with busy and "secondary" with idle (exits feel more idle than busy, yet have only a primary animation).

Currently, the rule here is: If there is an animation which is directly tied to physics in some way (such as a trap's killing animation), it is always the primary animation. I would expand this rule a bit for futureproofing - "If there are any animations which are directly tied to physics in some way, whichever one of these is relevant first in a typical use case, is the primary animation." I already have a WIP case where this distinction would matter - the limited-lemming-count exits, a locked exit could use a second physics-relevant animation for closing again. (This can be optional for locked exits - if no closing anim is present, play the opening anim in reverse. But I would very much like the support to be there.)

However - the "IDLE", "BUSY" and "DISABLED" flags (using the simpler proposal above) are not related to directly specifying the animations, but rather, for some basic conditional logic on an animation. You can pause or hide (or conversely, play or show) an animation based on these flags, but you could also have multiple animations that aren't tied to any of them - perhaps for parts that don't loop with the same frequency, or just to simplify graphic creation by splitting it into a couple of parts, or perhaps even just to expand an object's graphic outside the original boundaries of the object.

I have been wondering if having a small number of pre-set selections (that automatically set up triggers based on a few common use cases) might not be a bad idea, though. I would still like to support the full system - I suspect the back-end code is better off keeping most of the flexibility, so any change would really just be "we're intentionally withholding some of the capability from users". And I think the real potential of this system will only become apparent when new content, created with it in mind, is released...
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

Okay, I've reviewed (but not yet implemented) some possible changes to simplify this. With this system, it should be possible for every trigger to have just a single "is this TRUE?" condition, as for the most part these are mutually exclusive. The few cases this does rule out that were possible before, should still be doable by use of multiple secondary animations. I'll see if I can indeed replicate all secondary-anim effects I've made so far using this system, after implementing the new conditions but before the change to one-condition-per-trigger. Even without that change, in practice most triggers would still only ever use a single condition.

Almost all cases are covered by the first three conditions - "READY", "BUSY" and "DISABLED". The last three are only for a few specific objects to use, that may have states that we want to be able to distinguish but aren't completely covered by the main conditions. Many object types, that would in practice have no reason to ever utilize these conditions in the first place, have sensible fallback values (basically, whatever result would come from applying the general rule) set so there's a defined behaviour if a condition is specified.

Those with a (?), I'm not 100% sure about. And the "gatc" prefix is just an internal-use thing in the code, it has nothing to do with how any of this would be represented in data files.

Spoiler
  OBJECT TYPE     | gatcReady
  ----------------|-----------------------------------
  GENERAL RULE    | The condition will be true if the object would able to interact with a lemming at this moment
  DOM_TRAP        | True when the trap is idle (but not disabled)
  DOM_TELEPORT    | True when the teleporter and its paired receiver (if any) are idle
  DOM_RECEIVER    | True when the receiver and its paired teleporter (if any) are idle
  DOM_PICKUP      | True when the skill has not been picked up
  DOM_LOCKEXIT    | True when the exit is open (not just opening - must be fully open)
  DOM_BUTTON      | True when the button has not been pressed
  DOM_WINDOW      | True when the window is open (not just opening - must be fully open)
  DOM_TRAPONCE    | True when the trap has not yet been triggered (or disabled)
  All others      | Always true


  OBJECT TYPE     | gatcBusy
  ----------------|-----------------------------------
  GENERAL RULE    | The condition will be true when the object is transitioning between states, or currently in use
  DOM_TRAP        | True when the trap is mid-kill
  DOM_TELEPORT    | True when the teleporter, or its paired receiver, are mid-operation
  DOM_RECEIVER    | True when the receiver, or its paired teleporter, are mid-operation
  DOM_LOCKEXIT    | True when the exit is in the process of opening
  DOM_WINDOW      | True when the window is in the process of opening
  DOM_TRAPONCE    | True when the trap is mid-kill
  All others      | Always false


  OBJECT TYPE     | gatcDisabled
  ----------------|-----------------------------------
  GENERAL RULE    | The condition will be true when the object is unable to interact with a lemming, either permanently or
                  | until some external condition is fulfilled.
  DOM_TRAP        | True if the trap has been disabled (most likely by a disarmer)
  DOM_TELEPORT    | True if no receiver exists on the level (edge case - might not implement)
  DOM_RECEIVER    | True if no teleporter exists on the level (edge case-  might not implement)
  DOM_PICKUP      | True if the skill has been picked up
  DOM_LOCKEXIT    | True while the exit is in a locked state
  DOM_BUTTON      | True when the button has been pressed
  DOM_WINDOW      | Always false (? - maybe, "true when no more lemmings are to be released")
  DOM_TRAPONCE    | True when the trap has been disabled (most likely by a disarmer) or used
  All others      | Always false


  OBJECT TYPE     | gatcDisarmed
  ----------------|-----------------------------------
  GENERAL RULE    | The condition will be true if a Disarmer has deactivated the object. Exists as a separate condition
                  | from Disabled for the purpose of single-use traps, which may want to differentiate between disarmed
                  | and used.
  DOM_TRAP        | True if the trap has been disarmed
  DOM_TRAPONCE    | True if the trap has been disarmed
  All others      | Always false


  OBJECT TYPE     | gatcLeft
  ----------------|-----------------------------------
  GENERAL RULE    | True if a direction-sensitive object is currently facing left.
  DOM_FLIPPER     | True if the splitter will turn the next lemming to the left
  DOM_WINDOW      | True if the window releases lemmings facing left
  All others      | Always false


  OBJECT TYPE     | gatcRight
  ----------------|-----------------------------------
  GENERAL RULE    | True if a direction-sensitive object is currently facing right.
  DOM_FLIPPER     | True if the splitter will turn the next lemming to the right
  DOM_WINDOW      | True if the window releases lemmings facing right
  All others      | Always false

EDIT: Working on implementing this, in a separate branch in case I feel I'd rather stick to the original system (or a hybrid - possibly new conditions, but old support for condition mixing; but I only want to go back to this if it's really necessary). In regards to the edge cases in the above - I've decided to keep DOM_NONE and DOM_BACKGROUND consistent with the default for their group; but I did implement the unpaired teleporter / receiver edge case simply because the code is simpler to implement this than it is to not implement it (it gets picked up by the same test that checks for disarming on traps).

EDIT: I can now confirm, everything I did in the official and my styles combined under the previous system, remains 100% possible under this system. Single-use traps needed a bit of reordering of the trigger conditions, but everything else just needed updating to the new equivalent conditions. No images needed modification.




Under the new code, the editor needs to be aware of the following trigger conditions (all specified in the format of a single line "CONDITION ______" inside the trigger - multiple conditions or testing for "false" is no longer supported). As always, these values are "only as far as the editor needs to know":

"READY" - False for locked exits (edge case: unless no buttons exist in the level), otherwise true (edge case: false for teleporters and receivers if no object of the other type exists in the level)
"BUSY" - Always false
"DISABLED" - True for locked exits (edge case: unless no buttons exist in the level), otherwise false (edge cases: true for pickup skills if no skill has been assigned to it; true for teleporters and receivers if no object of the other type exists in the level)
"DISARMED" - Always false
"LEFT" - True for a window or splitter (currently) facing left, otherwise false
"RIGHT" - True for a window or splitter (currently) facing right, otherwise false
Any other value, including blank - Always true

(Futureproofing: "EXHAUSTED", always false. This is for the limited-count exits / entrances feature, and in-game would be "true" when their count has been used up.)
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

Quote from: namida on May 13, 2019, 10:32:27 PM
Quote1) Why do we have to distinguish between primary animations and secondary ones? It seems that the secondary animations are even more flexible, and the only advantage of primary animations is, that they override certain attributes. But then the style designer should not set the attributes in the first place.

Primary animation is tied to physics, same way that the lone animation up until now has been. Aside from that, they are indeed identical (outside of NL rejecting a few specific settings on the primary) - the primary animation is loaded by the same code as the secondaries just with a few properties being overridden. Rendering also uses the same code as primaries, though frame update does not - the primary is perpetually in a "paused" state, with the frame number being directly manipulated by game physics.
Sorry, but if they are so similar, then why do we need to differentiate them? I still see no reason for this. Wouldn't it be easier for both player and editor to have just one $ANIMATION keyword? Then all the issues with the Z_INDEX would disappear as well.

Quote from: namida on May 13, 2019, 10:32:27 PM
Quote4) Why do we need "HIDE" as an option for secondary animations? If we are not displaying anything, then why have this secondary animation at all?
Reason 1 - future animations that don't get used directly. For example, digits for a limited-count exit / entrance.
Reason 2 - animations that don't display in default state, but do when one of the triggers is fulfilled. For an example of this, see namida_horror:exit_02. Or the inverse - displaying usually, and hiding when a condition is fulfilled - which namida_horror:exit_02 also provides an example of.
Future animations should not be part of the style (or at least not be referenced in the nxmo file). I don't understand your reason 2: Is it not possible to describe within the $TRIGGER part, that the animation should only start under some condition (potentially in combination with an empty first frame)?

Quote from: namida on May 13, 2019, 10:32:27 PM
Quote6) I am still unsure which of the STATEs and the TRIGGER types are actually necessary for the most common use-cases (i.e. idling traps and locked exits), and which are just for potential future use? Can we stick to the absolute essentials for now? This would help people like me to get used to this.
I've already found use cases for all of these (except, IIRC, "TRIGGERED", but there are differences player-side between this and "BUSY"*), though some are definitely more common. But as a few examples:
- The common blinking-light makes use of the instant-stop (vs loop_to_zero), and the "BUSY" condition (light stops blinking while trap is killing) and "DISABLED" condition (light stops blinking when trap disarmed).
- The "bob up and down / side to side when idle" effect, eg. ohno_snow:trap, requires hiding secondary animations on a BUSY (or TRIGGERED) condition to avoid really weird visuals when triggered, and the "PAUSE" (without going back to frame zero) so the position won't suddenly jump by a couple of pixels (depending on current frame) when the trap gets disarmed.
- For an example of why the absence of these can be important, see ohno_brick:trap_02. The secondary animation on that one only pauses (remaining on its current frame) when the trap is disarmed, not in any other case.
Well, yes, one can always come up with some use-cases for anything. But a lot of your examples above don't sound like they occur often. As such I really don't know whether it is useful to have them: They add complexity and are potential sources for bugs. Is usage in one or two pieces really enough to compensate for this? My idea here is to implement only the most common features at first and then see what further requests are supported by multiple users here. After all - if I learned one thing from the change to new-formats, it is: Adding new features is far easier than removing existing ones.
As such I really approve your most recent changes. But I would go even farther:
- Remove gatcDisarmed and use gatcDisabled instead: The only difference is for one-use traps, but as the game basically treats used-up such traps in the same way as disarmed ones anyway (for all other purposes like display in clear-physics), I don't see a compelling reason to have this distinction here.
- Remove gatcLeft and gatcRight: For hatches, left-facing only occurs when flipping the hatch, which (hopefully) flips the secondary animations as well. So the only use-case would be splitters with a direction-dependant animation. But then I really wonder whether we need to introduce two keywords just for this use-case? I would prefer omitting them for the first version with secondary animations and see whether there is actual demand for it.

Quote from: namida on May 13, 2019, 10:32:27 PM
A post earlier in the topic, which I've mostly been keeping updated with new exp's, details in full how this new system works.
Ok, will probably have to read through all of this. Thanks for linking it.

Quote from: namida on May 13, 2019, 10:32:27 PM
QuoteThe main reason is player expectations: If something triggers, then this signals a game physics effect on a lemming.
Counter-examples: Unlock buttons, pickup skills, locked exits. None of their animations directly affect a lemming physics-wise. Counter-counter-point: ...although they do all indicate something physics-related at least.
Yeah, perhaps a more precise way of stating my point would have been: If something animates, this signals a game physics effect either on a single level or the level state as a whole.

Quote from: namida on May 13, 2019, 10:32:27 PM
QuoteWith the various gadget types it is hard enough to newer players to learn the various effects NL supports. No need to confuse them with no-effect effects ;). This all stems directly from my own experience as a player: Such objects have been very rare, so when I first encountered it, I wasn't even aware that NeoLemmix supported triggered animations. So I tried for half an hour to find out, what this gadget actually does and never found anything! :devil::devil::devil:
We have clear physics mode nowdays; and an up-to-date NeoLemmix Introduction Pack should help here too. I did start working on something, but then got bored with it for now.
But I don't want to play Lemmings in the future only in clear physics mode! My goal is still that the usual level image gives the player all the necessary information, with clear-physics being used only as a fallback.

Quote from: namida on May 13, 2019, 10:32:27 PM
NeoLemmix has always tried to account for both the puzzle side and the artistic side of creativity;
... but it's still mainly a computer game, not some art show. As such NeoLemmix has to strike a balance between these two, and triggered animations are far more confusing for game-play compared to the artistic advantages. At least their removal was one of the least controversial cullings, with only one or two triggered animations ever made for the old-formats styles, which tells me that there is no real need for them even from an artistic point of view.

Quote from: namida on May 13, 2019, 10:32:27 PM
Perhaps we should approach this as a joint effort - you know how you like things to be loaded, etc; while I understand in full how the system works, down to the technicalities / etc.
Basically I am currently using the editor support also as a check: Is this feature simple enough that other users, except you yourself, can understand and use this feature? So I would like to continue trying to implement the frame-finding in the editor, but you are more than welcome to check the implementation, once I consider my implementation finished. Nevertheless thanks for the offer! :thumbsup:
And with your latest changes we go a good way in the direction towards "simple enough for me to understand".

Quote from: Simon on May 14, 2019, 11:08:12 AM
Quote from: NepsterIf something moves, it either is currently interacting with a lemming or has at least the potential for it.
I have pushed for more than 2 animations per gadget because of an even stronger variant of this guideline: What moves, can interact, and what does not move, is terrain.
Yeah, and I totally agree with you that we need idling animations both from an artistic and from a game-play point of view. I am just criticizing the (remaining) complexity of the current solution.




Finally one note regarding nine-slicing: I tried today to implement native editor-support for it and it worked pretty smoothly. For this feature I am happy to say: It is so simple, that even I can understand it! :thumbsup: I just have one minor suggestion: Rename the keywords from "CUT_LEFT" to "NINE_SLICE_LEFT" (and similar for the other directions). The current naming suggests (at least to me) some kind of cropping, i.e. we cut away some part of the piece.

namida

QuoteFuture animations should not be part of the style (or at least not be referenced in the nxmo file). I don't understand your reason 2: Is it not possible to describe within the $TRIGGER part, that the animation should only start under some condition (potentially in combination with an empty first frame)?

I mean "future animations" in the sense of, features in the future that may make use of an animation, but don't want to display it the normal way. Not as in "an animation someone's creating but isn't using yet".
For #2, animations work by first defining a default STATE and (Visible / Invisible). Then, triggers can override this with a different one. For the case you describe - an animation that's visible by default, but only animates when a condition is fulfilled; you'd want the base state to be STOP or PAUSE (depending on exactly what you're trying to achieve), and then a trigger that changes this to PLAY (or possibly, LOOP_TO_ZERO) when a certain condition is fulfilled.

Quote- Remove gatcDisarmed and use gatcDisabled instead: The only difference is for one-use traps, but as the game basically treats used-up such traps in the same way as disarmed ones anyway (for all other purposes like display in clear-physics), I don't see a compelling reason to have this distinction here.

These are different states visually, even in the current version. The most noticable difference: A single-use trap's graphic, after being used-up, may show a dead lemming. This should never happen, of course, for one that was disarmed. Even when no dead lemming is visible, the appearance of the trap in the two cases is generally radically different - if it goes back to close to its original appearance, this is quite misleading for a single-use trap - and thus I don't feel we can assume the same animation would be fit for both.

However - if the limited-count exits / entrances are going to be used, then there's another option here: We remove gatcDisarmed. gatcDisabled remains valid for either "disarmed" or "used up". Then, gatcExhausted - which currently only exists in the limited-count entrances / exits branch - could also cover single-use traps that have been used up. (Counter-argument: In this case, shouldn't they apply to other one-shot things, like buttons and pickups skills? Counter-counter-argument: Sure, that's easy enough to implement player-side, while the editor side can generally disregard it.)

PS: Any reason you reimplemented nineslicing? If you just didn't see I had implemented it, no worries. But if you did, but chose to use your own implementation - mind if I ask what in particular you weren't keen on from mine, just so I get an idea of how you want things done for future commits?
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

Let's have an up-to-date summary of how things work now, in the latest commits in my repo (which are much newer than the latest experimental posted here) (EDIT: This is no longer true; I updated the experimental). For this, I will be describing the features in full, not just "what the editor needs to know".

Anything struck-out here is valid in the experimental, but has since been removed in the source code, so don't use it (or if you have a really good use case for it, explain that, so we can see why we should re-add it).

Basic setup
An object must have a primary animation, defined either in the base section of the object (I encourage doing it this way where possible, for backwards-compatibility) (this method will be considered deprecated and exists for backwards-compatibility purposes) or in a $PRIMARY_ANIMATION section. I will explain how these work later in the post. The primary animation is given special handling to allow defining it in the main section, which means old pieces are forwards-compatible and new pieces can be made backwards-compatible.

An object may (but does not have to) have one or more secondary animations. These are defined in $ANIMATION sections.

The primary animation's frame updating is not controlled by anything new. Just like the sole animation was in stable-NL, the primary animation is directly controlled by game physics, even when it's as simple as "a looping animation that never does anything else". This is to minimize the risk of breaking anything physics-wise. (Code exists, for future use, to allow an object's primary animation to be replaced at runtime with one of the secondaries; but this is currently not used by anything. This should not be something the user can do arbitrarily, but rather, if a future object type needs this functionality, it exists.)

Secondary animations are controlled entirely by the new animation system, which I will explain the workings of after the explanation of the data.

Defining an animation
A $PRIMARY_ANIMATION or $ANIMATION segment can contain the following data. Most of these are valid for either, but some are only valid for secondary animations.

NAME - This defines the suffix of the filename to load the animation from. For example, if the name is "example", on an object called "bob" (ie: the NXMO file is "bob.nxmo"), the animation would be loaded from "bob_example.png". If NAME is blank or absent, no suffix is applied - it would be loaded from "bob.png" (not "bob_.png"). This works the same way, regardless of whether the animation is primary or secondary - the primary can use a suffix, a secondary can use the non-suffixed file!
FRAMES - Frame count of the animation. If absent, this is treated as 1.
COLOR - Defines the color name (from the currently-active theme) to recolor the animation with. Works exactly the same way as, in the old system, a mask with a target of *SELF sould have. If absent (which it will be in 99% of cases - exceptions being the default style one-ways and pickups), no recoloring occurs.
HORIZONTAL_STRIP - Only the presence / absence (not any value) of this keyword is tested. If it's present, the frames are loaded from the PNG file horizontally rather than vertically.
Z_INDEX - Defines the order in which animations are rendered. If absent, it defaults to 1 for the primary animation, and 0 for any other animation.
INITIAL_FRAME - Sets the initial frame of the animation. This is ignored on the primary animation if the object is of a type where the frame is physics-significant (but not if it isn't). If absent, defaults to 0. "RANDOM" can be used as the value to specify a random initial frame.
OFFSET_X and OFFSET_Y - Sets the position, relative to the object's in-data-file coordinates, that the animation is rendered at. Default to zero. To be clear: These are valid for the primary animation!
NINE_SLICE_LEFT, NINE_SLICE_TOP, NINE_SLICE_RIGHT and NINE_SLICE_BOTTOM - Sets the number of pixels on each side to treat as the edge when nine-slicing.
HIDE - Only the presence / absence (not any value) of this keyword is tested for. If it's present, the default visibility of the animation is "hidden". Ignored on the primary animation.
STATE - Sets the default animation state of the animation. Ignored on the primary animation. This can be one of: "PLAY", "PAUSE", "STOP", "LOOP_TO_ZERO", "MATCH_PRIMARY_FRAME". If absent (or invalid), the default value is either "STOP" (if "HIDE" is present - but I am very strongly considering changing this to "PAUSE") "PAUSE" (if "HIDE" is present) or "PLAY" (if "HIDE" is absent).
$TRIGGER - Explained below.

Defining the primary animation in the main body of the NXMO file
Internally, NeoLemmix - if it detects that no $PRIMARY_ANIMATION section exists - it creates one based on these lines in the main segment, then parses it the same way it usually would. Any line that doesn't get created by this translation, just uses its default value. Note that not all features supported in a $PRIMARY_ANIMATION segment, are supported here. Any I list here with no comments, just get copied verbatim to the generated $PRIMARY_ANIMATION segment.
FRAMES
HORIZONTAL_STRIP
NINE_SLICE_LEFT, NINE_SLICE_TOP, NINE_SLICE_RIGHT and NINE_SLICE_BOTTOM
INITIAL_FRAME
PREVIEW_FRAME - Copied to the generated segment, with the keyword changed to "INITIAL_FRAME".
RANDOM_START_FRAME - If this is present, "INITIAL_FRAME -1" is added to the generated segment.


Defining a trigger condition on an animation
Any animation except the primary can contain any number of trigger conditions. These are order-sensitive - the later in the file, the higher the priority of it. Trigger conditions can contain the following lines:

CONDITION - The condition on which the trigger is applied. Valid values are "READY", "BUSY", "DISABLED", "DISARMED", "LEFT", "RIGHT", (in limited-exits/entrances branch only) "EXHAUSTED". Absent or invalid gets treated as an unconditional trigger - ie: it will always pass.
HIDE and STATE - These work the same way as in the base $ANIMATION segment.




Processing / displaying animations

There are three steps in updating and displaying the object, in this order - testing the trigger conditions, updating the frame number, and actually rendering the animation. The first two happen one after another during the game's physics update loop. The third one happens, of course, during object rendering. During each one of these steps, each animation is handled one-by-one.

Special case: The primary animation does not get any trigger tests or frame number updates applied (the frame number is updated by game physics). However, the actual rendering works exactly the same way as any other animation.

Testing trigger conditions

The trigger condition test starts at the last trigger in the file, and works backwards. When it finds a trigger where the condition is fulfilled, it sets the animation's current STATE and VISIBLE to whatever is defined in that trigger - and then stops testing. Should it get through all the triggers without any of the conditions being fulfilled (for example, an object with "BUSY" and "DISABLED" conditions, but it's currently idle), the animation's current STATE and VISIBLE are set to whatever is defined in the base $ANIMATION segment.

Here's a reminder on how the conditions work. This is mostly a copy-paste from the above post.

Spoiler
  OBJECT TYPE     | "READY"
  ----------------|-----------------------------------
  GENERAL RULE    | The condition will be true if the object would able to interact with a lemming at this moment
  DOM_TRAP        | True when the trap is idle (but not disabled)
  DOM_TELEPORT    | True when the teleporter and its paired receiver (if any) are idle
  DOM_RECEIVER    | True when the receiver and its paired teleporter (if any) are idle
  DOM_PICKUP      | True when the skill has not been picked up
  DOM_LOCKEXIT    | True when the exit is open (not just opening - must be fully open)
  DOM_BUTTON      | True when the button has not been pressed
  DOM_WINDOW      | True when the window is open (not just opening - must be fully open)
  DOM_TRAPONCE    | True when the trap has not yet been triggered (or disabled)
  All others      | Always true


  OBJECT TYPE     | "BUSY"
  ----------------|-----------------------------------
  GENERAL RULE    | The condition will be true when the object is transitioning between states, or currently in use
  DOM_TRAP        | True when the trap is mid-kill
  DOM_TELEPORT    | True when the teleporter, or its paired receiver, are mid-operation
  DOM_RECEIVER    | True when the receiver, or its paired teleporter, are mid-operation
  DOM_LOCKEXIT    | True when the exit is in the process of opening
  DOM_WINDOW      | True when the window is in the process of opening
  DOM_TRAPONCE    | True when the trap is mid-kill
  All others      | Always false


  OBJECT TYPE     | "DISABLED"
  ----------------|-----------------------------------
  GENERAL RULE    | The condition will be true when the object is unable to interact with a lemming, either permanently or
                  | until some external condition is fulfilled.
  DOM_TRAP        | True if the trap has been disabled (most likely by a disarmer)
  DOM_TELEPORT    | True if no receiver exists on the level (edge case - might not implement)
  DOM_RECEIVER    | True if no teleporter exists on the level (edge case-  might not implement)
  DOM_PICKUP      | True if the skill has been picked up
  DOM_LOCKEXIT    | True while the exit is in a locked state
  DOM_BUTTON      | True when the button has been pressed
  DOM_WINDOW      | Always false (? - maybe, "true when no more lemmings are to be released")
  DOM_TRAPONCE    | True when the trap has been disabled (most likely by a disarmer) or used
  All others      | Always false


  OBJECT TYPE     | "DISARMED"
  ----------------|-----------------------------------
  GENERAL RULE    | The condition will be true if a Disarmer has deactivated the object. Exists as a separate condition
                  | from Disabled for the purpose of single-use traps, which may want to differentiate between disarmed
                  | and used.
  DOM_TRAP        | True if the trap has been disarmed
  DOM_TRAPONCE    | True if the trap has been disarmed
  All others      | Always false


  OBJECT TYPE     | "LEFT"
  ----------------|-----------------------------------
  GENERAL RULE    | True if a direction-sensitive object is currently facing left.
  DOM_FLIPPER     | True if the splitter will turn the next lemming to the left
  DOM_WINDOW      | True if the window releases lemmings facing left
  All others      | Always false



  OBJECT TYPE     | "RIGHT"
  ----------------|-----------------------------------
  GENERAL RULE    | True if a direction-sensitive object is currently facing right.
  DOM_FLIPPER     | True if the splitter will turn the next lemming to the right
  DOM_WINDOW      | True if the window releases lemmings facing right
  All others      | Always false

 
  OBJECT TYPE     | "EXHAUSTED" (!!! This only exists in the limited-count-entrances-exits branch !!!)
  ----------------|-----------------------------------
  GENERAL RULE    | True if a lemming-limited object is used up.
  DOM_EXIT        | True if the exit has a capacity, and the capacity is used up
  DOM_LOCKEXIT    | True if the exit has a capacity, and the capacity is used up
  DOM_WINDOW      | True if the window has a lemming limit (outside of the total number of lemmings in the level overall),
                  |   and it has released all its lemmings.
  All others      | Always false

How the animation's frame is updated

Frame update depends on the current STATE (as set by the trigger test):

PLAY - The animation will advance by one frame every update cycle. If it reaches the end of the animation, it loops back to the beginning, and continues from there.
PAUSE - The animation remains on the current frame.
STOP - The animation jumps to frame 0. After this, STATE is changed to "PAUSED".
LOOP_TO_ZERO - This works the same way as "PLAY", except that when the animation reaches frame 0, STATE is changed to "PAUSED".
MATCH_PRIMARY_FRAME - The frame number will be set to whatever the primary animation's current frame number is. What will happen if the primary animation has more frames than this animatino, is undefined!


How the animations are rendered

The order in which animations are rendered, is determined firstly by their Z_INDEX value (lower number = rendered first), and where these are equal, by their order in the file. The primary animation is always treated as coming first in the file (regardless of where it actually is in the order), but its placement in the rendering can still be altered via Z_INDEX.

If an object is resizable, all animations are resized. However, each animation can have independent nine-slicing values. (Some interesting effects can probably be created here, by having one of the edge sizes, equal to the overall size of the animation graphic - then you can create an animation that only appears on one corner, for example. Though I need to test this, to ensure there's no infinite-loops or divide-by-zero errors that result from trying this...)

An animation will be displayed if either:
a) Its visibility is TRUE
or b) Its visibility is FALSE, and its current state isn't "PAUSE" (remember: its current state will NEVER be "STOP" at this point, due to the actions during frame number updating). Why is it this way? Because of the useful combination of visibility=false and state=loop_to_zero.




I think that covers everything. In terms of what could maybe be changed:

"LEFT", "RIGHT": Maybe these are unnessecary. Let's remove them for now, maybe? Adding them back later would be simple enough.
"DISARMED" vs "DISABLED": As mentioned above, I do feel it's important to have some way to distinguish between "disarmed" and "used up" on a single-use trap, which is why I separated these in the first place. But I'm open to acheiving this a different way - as long as distinguishing remains possible.

This does leave a lot open to designers, but (a) I notice that more and more content designers are on board with the idea of "don't try to be deceptive", than ever. These days, deceptive content mostly seems to arise from the occasional creator outside our community, or as technical demonstrations / April Fools pranks, unlike in the past where some creators went out of their way to be deceptive; and (b) I feel the flexibility this allows for in object graphical design is worth it.

I am open to perhaps having a few presets that can be set as one liners, eg "PRESET TRAP_ANIMATE_UNITL_DISABLED", "PRESET TRAP_ANIMATE_IDLE_ONLY", etc, that will automatically set STATE, HIDE and $TRIGGERs as needed for some of the most common use cases. Then, only people who want to produce less-common effects need to worry about the full intracicies of the system.

Regarding less differentiation for the primary animation and getting rid of Z_INDEX (the former of which I feel is necessary if we're going to do the latter) - yeah, we could make this an $ANIMATION section that includes the "PRIMARY" keyword. The only thing here is we need a rule here - if the primary is instead defined in the main section (and I absolutely want to keep this, for many reasons), does it get treated as being first or last in the animation list? I would lean towards "last" here, for the same reason I defaulted it's Z-index to 1 - although not always, we'll usually want to draw the secondary behind the primary in case of overlap.

And to clarify another point I just spotted:

QuoteFor hatches, left-facing only occurs when flipping the hatch, which (hopefully) flips the secondary animations as well.

Yes - to confirm, secondary anims get flipped / inverted / rotated along with the main one, and yes, their offsets also get moved accordingly. To the user, it would be indistinguishable from rotating / flipping / inverting a single large composite image (even though internally, each animation is reoriented and positioned separately).
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 for the concise summary of the current state :thumbsup::thumbsup:

QuoteRegarding less differentiation for the primary animation and getting rid of Z_INDEX (the former of which I feel is necessary if we're going to do the latter) - yeah, we could make this an $ANIMATION section that includes the "PRIMARY" keyword. The only thing here is we need a rule here - if the primary is instead defined in the main section (and I absolutely want to keep this, for many reasons), does it get treated as being first or last in the animation list? I would lean towards "last" here, for the same reason I defaulted it's Z-index to 1 - although not always, we'll usually want to draw the secondary behind the primary in case of overlap.
Sorry for being obnoxious here, but I still don't understand why we need either a $PRIMARY-ANIMATION or a PRIMARY keyword? What would turn impossible if we just had the usual .png, that we already had in all previous versions, and then only the secondary animations? In other word: What would be impossible to do, if I never used either $PRIMARY-ANIMATION or the PRIMARY keyword?

Quote"LEFT", "RIGHT": Maybe these are unnessecary. Let's remove them for now, maybe? Adding them back later would be simple enough.
"DISARMED" vs "DISABLED": As mentioned above, I do feel it's important to have some way to distinguish between "disarmed" and "used up" on a single-use trap, which is why I separated these in the first place. But I'm open to acheiving this a different way - as long as distinguishing remains possible.
You convinced me, that having "DISARMED" as an extra keyword is worth having. But I would still remove the "LEFT" and "RIGHT" keywords for now (though keep the code in an extra branch, so that we can easily merge it back into the main one if desired).

QuotePS: Any reason you reimplemented nineslicing? If you just didn't see I had implemented it, no worries. But if you did, but chose to use your own implementation - mind if I ask what in particular you weren't keen on from mine, just so I get an idea of how you want things done for future commits?
Yeah, I simply didn't see your implementation, mainly because I didn't think of checking and was on a train ride when I coded my implementation. See your PMs for a code-review of your implementation.

namida

QuoteSorry for being obnoxious here, but I still don't understand why we need either a $PRIMARY-ANIMATION or a PRIMARY keyword? What would turn impossible if we just had the usual .png, that we already had in all previous versions, and then only the secondary animations? In other word: What would be impossible to do, if I never used either $PRIMARY-ANIMATION or the PRIMARY keyword?

I'm not quite getting what part of this doesn't make sense, so I'll try to cover everything. Also because this way, I'm thinking through it again myself.

The primary animation, in this system, is equivalent to the only animation under the old system. As such - it is tied to physics. Physics control which frame it's on, and which frame it's on can affect physics.
Could this be changed, so that the primary animation is just like any other? It's not impossible. But let's take for example, a DOM_TRAP. The frame count of the primary animation affects physics - if it has 20 frames, it can't kill another lemming for 20 physics updates. But if it has 10, it can kill another lemming 10 physics updates later. Also, the primary animation remains stopped until the trap is triggered, at which point it animates once. We do have "LOOP_TO_ZERO", but this will do nothing if set while the animation is already on frame zero; its purpose is to allow the animation to complete gracefully (then not repeat), rather than abruptly terminate, when we want the animation to stop. Thus, some kind of "RUN_ONCE" state (which presumably, advances the frame to 1 then changes to LOOP_TO_ZERO) - which would also need extra code to differentiate between eg. "the trap is still triggered" vs "the trap has been triggered again" - would have to be introduced. The object would still need to define how many physics updates the object is triggered for anyway. To me, it sounds like this would just complicate things.

The primary animation can be defined in either a $PRIMARY_ANIMATION segment (or as an alternate proposal: an $ANIMATION segment with a "PRIMARY" keyword), or in the main segment of the NXMO file.
Could this be changed, to only one or the other? Changing it to the sub-segment only would require modifying all existing NXMO files, and break forwards / backwards compatibility completely (by comparison, under the current experimental, new objects using the main segment are backwards compatible physics-wise though might look weird, while old objects are 100% forwards-compatible). On the other hand, changing it to the main segment only - that's feasible, yeah. Maybe that makes more sense than having the A or B options. The downside here is - either loading code needs to be duplicated, or the main segment has to be parsed a second time (as an $ANIMATION segment), or it just ends up getting translated internally to an $ANIMATION segment with special handling (which is pretty much what already happens). Also, I don't really know how I'd feel about having a "NAME" parameter for an animation, in the main segment - though perhaps NAME could just be disallowed altogether for the primary.

Or is it, the primary animation shouldn't need to be explicitly defined?
For this, I'd assume the rule is that either the first or last $ANIMATION, becomes the primary one. If so - this works only if we keep Z_INDEX, otherwise it's not possible to choose whether secondaries are drawn above or below the primary. This is a feasible - if maybe a bit counter-intuitive - approach if Z_INDEX is kept.
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)