Design history of Lix

Started by Simon, December 04, 2018, 12:49:04 AM

Previous topic - Next topic

0 Members and 1 Guest are viewing this topic.

Simon

Hi,

Double resolution

Lemmings 1 and Lemmings 2 were written for 320x200 VGA mode. The playing characters are 8 or 9 pixels high, walk ahead by 1 per frame, and builder bricks are 1 pixel high.

Lix is double-resolution: Characters are about 17 pixels high, walk ahead by 2 hi-res pixels, and builder bricks are 2 pixels high. More precisely, physics work with 2x1-pixel-chunks. All lix' x-coordinate is always divisible by 2. Only the y-coordinate may be any integer. A 2x1-chunk of land counts as solid as long as at least one of its two pixels is solid.

Where does this concocted rule come from?

In early 2003, I scripted levels for the freeware game Gravity Strike, this ran with hardcoded 640x480 fullscreen. In the early 2000s, CRT monitors were widespread and could display any resolution sharply -- up to a point. But every monitor could display 640x480, thus Gravity Strike's author decided to hardcode this size.

I wrote GS Lemmings, a Lua script that ran in Gravity Strike, removed everything that the game normally provides (spaceship, terrain, enemies, status panel) and instead loaded my own graphics and ran my own logic. This is the birth of double resolution: It was easiest to work with what I had.

In Lua, apart from numbers, everything is a reference type, everything is garbage collected. I had no idea what was happening under the hood. Would arguments be passed by value or by reference? What is the difference between calling functions object.func() or object:func()? No idea. Programming was magic. If it failed, I would guess and re-try. I would never search the web for help because I found the technical explanations hard to understand. (Solution: object:func() is equivalent to object.func(object)).

In 2006, I finished my civil service -- back then, it was mandatory for every male German to either serve in the military or work civil service for 9 months. I had some time before university started in fall 2006. What to do?

I learned C++ and began the work on L++, a Lemmings variant with hardcoded 640x480 fullscreen resolution. Why would I do that? It worked in Gravity Strike, and I still had a CRT monitor that could display any resolution well. I kept my rule that the tiles could be anything and need not stick to 2x2-blocks. I would not upscale. I would load the tiles as they came from disk. That was easiest.

The 2x1 physics instead of 1x1 come from the desired walker speed. It was simpler to move always by 2 horizontal pixels than to write a basic walk action for 1 pixel, then call that twice per frame. The walk cycle would look at two pixels and deduce from them together whether the 2x1 chunk was air or solid. I didn't expect any odd cases from the rule.
Of course, there are odd cases (I should link the example gifs).

L++ became Lix in 2010 and got ported to D in 2015. Like Theseus's ship, all code, graphics, etc., got replaced over the years, but really, 2006 L++ was the first version of Lix.

With my own game project at 640x480 fullscreen hardcoded, I spent considerable time on choosing my first laptop in 2007. I wanted 4:3 aspect ratio for my laptop screen, but that was getting unpopular. Laptop computers were booming, and widescreen displays had gotton common. I loved 4:3, why would anybody want 16:10? Documents, websites, code, etc., everything is vertical, but most importantly, 16:10 TFT displays would blur at 640x480 hardware fullscreen.

Lix/L++ is now 12.5 years old and programming been my longest-standing hobby. Now, Lix supports any screen resolution, windowed or fullscreen. The user interface scales smoothly, a major design goal during the 2015 rewrite.

Even with the user interface resolution decoupled from the physics resolution, Lix still has 2x1-hi-res-physics. I don't like how confusing the 2x1-block rule can be. But it has been so deeply ingrained since the beginning. It allows for more graphical details on tiles. On the other hand, it forces tiles to have more details to look good.

tl;dr: It was easiest because of the hardcoded 640x480 resolution in the early versions. And now too much depends on it to get rid of double resolution.




Passage of time

More rambling, because I'm in the mood.

I feel like time is passing much quicker now. In 2003, GS Lemmings was released with 20 levels after maybe 2 months of work, with some bugs of course, some crashes, some reliance on 60 FPS -- Yes, the game would skip some physics logic, but not all physics logic, when the machine was slow! Horrendous bug! -- but it worked well enough to play. The 2-3 regulars on the Gravity Strike forums loved it.

Where GS Lemmings took 2 months, L++ required 3 years before I felt ready to post it on Lemmings Forums including networking mode. A more complicated feature, sure, and I wrote the game engine from scratch instead of relying on a host game.

It's been nearly 4 years since I started the rewrite to D in early Feburary 2015. I'm very very happy with the rewrite, I've pushed the game further, and I'm supporting different operating systems better than ever before. But are there killer features in D Lix that weren't already there in C++ Lix? Did the fundamental game take 2-3 years to implement in 2006 to 2008, and the 10 years after it has been only polishing?

Sometimes, I miss the days where I would implement buggy crap with trial-and-error, but have something cool to show after minimal development time. The 15-year-old kid inside me wants to feature-bloat on some side project. And the 5-year-old inside me wants to play that and get even more wild ideas.

When time passes linearly, memories increase only logarithmically. You remember so much from when you start a new activity. But then you get proficient, and you don't remember as much when you're performing by routine.

Consider the time after my civil service. I learned a good chunk of C++ and wrote a prototype-ish game, L++ with some singleplayer, in half a year. Then, I had nearly no other hobbies, obligations, or girlfriends, and could focus fulltime on L++. Now, 12 years after, I had comparable time after finishing the PhD, but I haven't started a new hobby in that time. But I already have so many hobbies -- games, Lemmings Forums, speedruns, tech projects -- that I've enjoyed in that time. They merely generated less impactful memories than starting a new hobby typically generates. That's why time seems to fly quicker.

At least I've finished xmas2018 on time. :lix-grin:

-- Simon

Simon

#1
I had a discussion about what the birthday of Lix would be.

The first C++ code to display graphics with Allegro 4 was from the beginning of March 2006. But I didn't plan yet to write another Lemmings-like game here.

The first C++ code that animated walking lemmings on an otherwise black screen was from the beginning of April 2006. Thus maybe 2006-04-01 is the birthday? That feels more appropriate than March. I didn't use version control until 2011 and can't tell when exactly I wrote that code.

Mindless announced L++ on Lemmings Forums in 2008-08, but that didn't catch on: The only terrain was my programmer art, many bugs in the networking, and no compelling singleplayer because Lemmix and Lemmini existed already.

I announced L++ on Lemmings Forums in 2009-10 and we had a ton of great networking games. I had not rented a server yet; instead, I ran the game server on my home machine and shared the IP address before each session.

The first commit with D Lix code is from 2015-02-25.

The first announcement with downloads of D Lix 0.2 was 2016-02.

-- Simon

Simon

#2
Replay Verification

You can run Lix noninteractively on a directory full of replays:
  • lix --verify mydir will tell you, for each replay, whether it passes its pointed-to level. Finally, Lix tallies passes and fails.
  • lix --coverage mydir does everything from --verify mydir, and will also list the uncovered levels, i.e., levels in a level directory into which at least one replay pointed, but that themselves had no passing replay.
It's really a single feature with two forms of invocation. You want --coverage when you expect most levels to be covered by your replay collection and want to find omissions. Otherwise, you want --verify, which also makes sense for individual replay files.

The origin of replay verification: Around 2010-2013, geoo wanted to maintain online highscores for Lix. He considered to write a server based on Django, let people upload replays, then have the server run Lix in the background to verify the replay.

Thus, from day one, I saw a clear need to make replay verification run with as little of Lix as possible. I would still have to initialize graphics to load tiles, but I wouldn't be able to run the graphical user interface on the server. Everything had to work from the command line and print results as text output.

In the end, we never had online highscores. Nowadays, I don't even want them, they're extra stuff to maintain. There are enough other juicy objectives for Lix development in the next few years. But even without online highscores, the replay verification enjoys fast runtimes. It doesn't have to maintain a graphical land, only a physical land.

The biggest use case of replay verification is not competition between level players; it's tooling for level authors to verify pack solvability. I don't remember when exactly we realized this use case. Maybe we always had it in mind and it merely became the main use case over time.

Fully covered packs serve me as physics test cases: Whenever I touch physics code, if all replays pass, then most likely, I haven't changed physics.

I believe we had cross-pollination between Lix and NeoLemmix about replay verification, but I forgot the details. Most directly comes to mind: I'm still worrying how to associate replays to levels. Neither (level path only) nor (NL's level ID only) is perfect. And we eventually must start classifying replays in the proof collections: Intended solutions, accepted alternatives, backroutes, ...

-- Simon