Welcome, Guest. Please login or register.

Login with username, password and session length

 
Advanced search

1411423 Posts in 69363 Topics- by 58416 Members - Latest Member: JamesAGreen

April 19, 2024, 12:01:16 AM

Need hosting? Check out Digital Ocean
(more details in this thread)
TIGSource ForumsCommunityDevLogsSpider Game - no-gravity puzzle platformer, explore geometrical spaces
Pages: [1] 2
Print
Author Topic: Spider Game - no-gravity puzzle platformer, explore geometrical spaces  (Read 3794 times)
bayersglassey
Level 0
***



View Profile
« on: December 13, 2020, 11:58:20 PM »

Posts in this devlog:

Hey hey!
I've been making games since I was a kid, originally in QBASIC on DOS, these days in C on Linux.

In university we didn't have a CS department but we just barely had a math department, so I did that.
I was terrible at university, but great at obsessively studying random stuff on my own time.
Among other things, I got into algebraic geometry, and did a lot of "research" (sketching in notebooks) on tilings of the plane using squares and triangles.



Ummm, I recommend this book? Your mileage may vary.
Tilings and Patterns, by Grünbaum and Shephard.

Eventually I thought to myself "what if you had like, a screen, right?.. but where the pixels were squares and triangles".
And so I wrote various demos to that effect.


Unrelatedly, over the years I occasionally sketched out ideas for a game taking place on a grid of triangles, where your character's movement uses a finite state machine.
(You can also say "your character's movement is like the original Prince of Persia games", although I never played those... the characters in Another World / Out of This World moved this way as well.)



Eventually, the squares-and-triangles geometry stuff and the triangular-grid-game idea came together into a game demo.
And the main character looks like a spider robot thing, because it's super hard to draw anything when your "pixels" are squares and triangles.
So in casual conversation the demo always ended up being called "the spider game", and the name has become official.








It has a litle website where you can play it online!
You will likely want to have your browser in fullscreen mode (F11 in Chrome) so the up/down arrow keys don't scroll.
I... should probably look into improving this situation somehow. Tongue

The source is up on Github.

It's written in C + SDL2 with basically no other dependencies. Emscripten is used to compile it for the browser.
I'm on Linux, so I don't have a Windows build, unfortunately. :/ If there is any interest in this, perhaps I'll try to figure out how to do that. I would be happy to know if anyone else is able to build it. Smiley

There is a fair amount of stuff to find in the game, although there's no particular goal.
It certainly doesn't hold your hand... although I've tried to give you a safe place to start from, and exciting places to go as you master the controls and solve puzzles.
There is now a tutorial which you can complete, and I'm now working on tying the rest of the game map together with puzzles and NPCs and things, and an endgame. But for now, after the tutorial you're just basically free to explore.
« Last Edit: January 17, 2022, 11:00:48 PM by bayersglassey » Logged
Rogod
Level 3
***



View Profile WWW
« Reply #1 on: December 14, 2020, 01:51:50 AM »

Quote
because it's super hard to draw anything when your "pixels" are squares and triangles.
Never stopped me Wink

No but seriously, this does look like a unique concept.
Put some music to it and it could be the next geometry dash :D

I am always interested to see where other people take these sorts of tech-demo concepts and turn them into a game somehow (the more involved and imaginative, the better; I'm anticipating a full open world, multiplayer co-op, randomly generated dungeons, a full inventory system for powerups and cosmetics, other creatures that live in the tessellation world and a Flatland-esque backstory Grin )
Logged

marcgfx
Level 8
***


if you don't comment, who will?


View Profile WWW
« Reply #2 on: December 14, 2020, 03:22:40 PM »

This looks pretty insane/cool! It was kind of fun to move around, but also pretty confusing. I think you might need to add the hand holding you mentioned was missing and build up the complexity slowly.
Logged

bayersglassey
Level 0
***



View Profile
« Reply #3 on: December 14, 2020, 04:23:28 PM »

Hmm, confusing in what way? Just the flipping upside down and running on the ceiling, or something else?
Or to put it another way, would you have kept playing a bit longer if you hadn't gotten flipped upside down on the very first screen?
Logged
michaelplzno
Level 10
*****



View Profile WWW
« Reply #4 on: December 16, 2020, 02:12:15 PM »

Its an interesting concept, but yeah, the controls were awkward at best. When your character is upside down the buttons don't flip so you can press left and move right, press up and jump down. Also the goal wasn't clear: I was sort of chasing the other character that was there at the beginning for a while and then I gave up on that b/c there was very little feedback.

Keep it up!
Logged

bayersglassey
Level 0
***



View Profile
« Reply #5 on: December 22, 2020, 11:44:28 PM »

Thanks to all who gave feedback!

It sounded like more than anything else, what was needed was a tutorial. So I've added one.
If you'd like to play it, the demo is updated: http://depths.bayersglassey.com/main.html

First, you learn left and right.
(I'm thinking of renaming the game "Crawl Space" because every domain like "spidergame.com" is apparently taken by filthy squatters, but I was able to get crawlspacegame.com...)


Then, you learn about savepoints.


Then, you learn about the fact that you will be flipped upside down, you stick to walls, there's no gravity, etc.


Then, you learn how to be forced to jump.


Then, you learn how to jump intentionally.


Then... a minor puzzle to make sure you "get" everything so far.


You end up travelling all the way back along the underside of the map.


...until somehow you end up here. Another puzzle, but a bit more open ended, so you can actually play around a bit.


Another puzzle, why not.


Finally you learn how to crawl.


End of tutorial!


...after which, you're thrown into the game proper.


...the controls were awkward at best. When your character is upside down the buttons don't flip so you can press left and move right, press up and jump down.

I thought a bit about whether it would be possible to flip the controls when the player is flipped, and how that might work.

Currently, you can travel around "curves" by holding left or right.
For instance, given a "smooth" shape like these connected hexagons, you can walk around them forever by holding left or right:

  + - +       + - +
 /     \     /     \
+       + - +       +
 \     /     \     /
  + - +       + - +


So, when you're standing on the bottom of something, pressing right makes you move left.
Simply put: pressing right makes you move clockwise, left makes you move counter-clockwise.

Let's say we wanted to change the controls so that when standing on the bottom of something, pressing left made you move left.
I can kind of imagine how that would work; you would need to decide what happens when holding right/left and travelling around a "curve" though: do your controls flip when player rotates?.. so in order to go around something indefinitely, you'd need to hold right for a bit, then switch to holding left as you flipped upside down, then switch back, etc?

There are various ways it could work, but some of them would require a serious rework of the current system.
It uses a mini-language to specify the controls, animations, and map collision detection... for instance, this file contains the following rule which causes you to take a step forward when holding "forwards" (left/right relative to player's orientation), if there is ground there to stand on and nothing blocking you:

    if:
        key: isdown f
        coll: all yes
            ;; ( )  + - +
        coll: all no
            ;;       \*/*
            ;;        + -
            ;;       /*\*
            ;; ( )
    then:
        move: 1 0
        goto: step
        delay: 2


Changing all that stuff for all the different creature types (who all use this same system, with AI players using a virtual keyboard for their "keypresses") would be really rough Cry
But it might be possible to fiddle with the C code which decides e.g. whether "key: isdown f" is true. Hmmmm.

Ultimately I'm hoping that with e.g. the tutorial, I can get players comfortable with the controls as they are...
« Last Edit: October 07, 2021, 11:19:26 PM by bayersglassey » Logged
bayersglassey
Level 0
***



View Profile
« Reply #6 on: December 22, 2020, 11:50:21 PM »

Quote
because it's super hard to draw anything when your "pixels" are squares and triangles.
Never stopped me Wink

Ooooh, those rotating planets though! Addicted

I am always interested to see where other people take these sorts of tech-demo concepts and turn them into a game somehow (the more involved and imaginative, the better; I'm anticipating a full open world, multiplayer co-op, randomly generated dungeons, a full inventory system for powerups and cosmetics, other creatures that live in the tessellation world and a Flatland-esque backstory Grin )

There's already 2-player coop and theoretically more :D
There is 1 room with little islands whose positions are randomly... displaced slightly each time you play. It's actually pretty fun because I've mastered everything else (having written all the maps), so having to actually look and judge jumps is nice. But that's pretty far from a "randomly-generated dungeon". It would actually be cool to try a simple first step towards that, like "implement a Rogue-style algorithm generating some rooms connected by hallways" in this engine. Hmmmmm.

Logged
marcgfx
Level 8
***


if you don't comment, who will?


View Profile WWW
« Reply #7 on: December 23, 2020, 01:39:46 AM »

Gave it another shot and I really like your tutorial. I nearly gave up on the crouching level, I think you should also inform about bridging gaps while standing. I only went back, when I saw that your post showed it had an end of tutorial after that screen.



After the tutorial I felt kind of lost in this possibly open world? I ended up in a green wavy area and I find the visuals very distracting. I think the best course of action would be for you to continue the game as you have now made the tutorial. Level by level giving the player a sense of progression.

I really do not mind the way the controls work, it's the only logical way for me. A little like controlling a remote controlled car.
Logged

bayersglassey
Level 0
***



View Profile
« Reply #8 on: December 24, 2020, 12:35:08 AM »

I nearly gave up on the crouching level, I think you should also inform about bridging gaps while standing.

Ahhhh yup. I take a lot of the mechanics for granted, but yeah knowing how your character behaves around little gaps is pretty crucial.

So I replaced the useless little puzzle right before the crouching room with a veritable playground of small gaps:



After the tutorial I felt kind of lost in this possibly open world? I ended up in a green wavy area and I find the visuals very distracting. I think the best course of action would be for you to continue the game as you have now made the tutorial. Level by level giving the player a sense of progression.

Yeah, the map's a patchwork of odds and ends.
It grew organically as I had ideas for puzzles and features.
I do want to keep it fairly open, but it clearly needs an overarching goal to motivate people to explore it.
Hmm...
Logged
marcgfx
Level 8
***


if you don't comment, who will?


View Profile WWW
« Reply #9 on: December 24, 2020, 04:02:32 AM »

if it really is something to explore it should be fun to traverse the same areas again. but if it's just repeating puzzles, I think that could be tedious. you could maybe add new ways to get through areas you have already managed, but I do believe making it linear will help you game.
Logged

terrarray
Level 0
**


View Profile
« Reply #10 on: March 27, 2021, 04:08:32 AM »

Sort of reminds me of convays game of life for some reason. Keep it up!
Logged
bayersglassey
Level 0
***



View Profile
« Reply #11 on: October 06, 2021, 11:43:17 PM »

I have, I believe, finished the tutorial!
It hopefully teaches you how to move, how to save, how to "spit" (basically shoot), how to unlock things by collecting purple things (coins?) and hitting switches, and what can kill you.

It doesn't teach you about swimming or turning into different types of spider and enemy, because I think those can be learned when you happen across them in-game...

I also tried to add an "explanation" for why you are shown text during the tutorial, but not in the rest of the game. There are eyeballs which talk to you, and they "release" you into the game at the end of the tutorial. What does it all mean? Who knows, but I'm hoping to achieve some kind of "story" giving people a reason to play beyond the tutorial. Maybe the eyes want to you bring back the Amulet of Yendor or whatever?

Some screenshots:











Try it out in your browser! http://depths.bayersglassey.com/


Interestingly, Emscripten seems to generate WASM these days, and Chrome is willing to run it too, as long as I tell nginx to serve up .wasm files with "Content-Type: application/wasm":
Code:
    # in nginx.conf...

    types {
        application/wasm wasm;
    }

It runs well on my laptop, although it makes the fan blow like crazy. I think it's because I'm calling SDL_Delay inside each step of my main loop, which when compiled with Emscripten, is being run using browser's requestAnimationFrame, which probably means... I'm delaying the browser in the middle of it trying to render. On, like, every vsync. O_o Should probably fix that tomorrow.
Edit: fixed!
« Last Edit: October 07, 2021, 08:26:32 AM by bayersglassey » Logged
bayersglassey
Level 0
***



View Profile
« Reply #12 on: October 07, 2021, 10:48:35 PM »

Let's try writing an actual devlog post.


The graphics engine, a linear algebra refresher, and writing parsers instead of GUI editors

The graphics are made out of tiny geometric shapes, each of which can be exactly one colour, much like pixels.
However, they come in various shapes -- currently square, triangle, and diamond -- and are positioned in a 4-dimensional space based on angles of 30 degrees, as opposed to pixels' 2d space based on angles of 90 degrees.
In the graphics engine, the tiny shapes are referred to as "prismels" (because they're like pixels, but can be arbitrary... prisms?.. no, polygons. Hmmmmm. It was many many years ago that I first started thinking about all this, so I guess I have no idea why I went with the term "prismels").

Anyway, here is a motivating example... you play as this spider-looking thing:


And here is a text representation of the "prismels" which make it up:

               -+---+-
              / |   | \
            +-  |   |  -+
            | \ |   | / |
           /   -+---+-   \
          |     |   |     |
          +-   / \ / \   -+---+-
          | \ |   |   | / |   | \
          |  -+---+---+-  |   |  -+
          | / |   |   | \ |   | /
          +-   \ / \ /   -+---+-
          |     |   |     |
           \   -+---+-   /
            | / |   | \ |
            +-  |   |  -+
              \ |   | /
               -+---+-
             -+       +-
            / |       | \
          +-  |       |  -+
          | \ |       | / |
         /   -+       +-   \
        |     |       |     |
        +-   /         \   -+
        | \ |           | / |
        |  -+           +-  |
        | /               \ |
        +-                 -+


They are all squares and triangles. There is a third type of prismel, the diamond, which gets used less frequently than the others. Here are all 3 shown together:

   +---+     +
   |   |     |
   |   |    / \       -+---+
   |   |   |   |     /   /
   +---+   +---+   +---+-



Earlier I said they are positioned in a "4 dimensional" space. What did that mean, though?
It means that the coordinates of these shapes can be expressed as 4-dimensional vectors (a, b, c, d).
Here are the 4 unit vectors I use:

         D  C

         + +
         | |   B
         |/ -+
         | /
         O---+ A

  O = (0, 0, 0, 0) <- The origin
  A = (1, 0, 0, 0)
  B = (0, 1, 0, 0)
  C = (0, 0, 1, 0)
  D = (0, 0, 0, 1)


In case that's not too clear, here are some examples of other vectors:

   2A = (2, 0, 0, 0):

      O---+---+


   -A = (-1, 0, 0, 0):

  +---O


   2B = (0, 2, 0, 0):

             -+
            /
         -+-
        /
      O-


   C - A = (-1, 0, 1, 0):

    +
    |
     \
      |
      O


   ...you can see why that should be C - A, if you put C and -A end-to-end:

     -A

    +---+
        |
       /  C
      |
      O



Why, you might ask, use these 4 dimensions instead of the usual X and Y?
The answer is that basic trigonometry will tell you that if the bottom-left point of the following triangle is the origin (0, 0), then the (x, y) coordinates for its topmost point are, if I recall correctly, (1/2, 1 + sqrt(3)):

      +
      |
     / \
    |   |
    +---+


...and long story short, if you want to represent such coordinates as *integers* (which I do), you actually end up with 4 "dimensions" anyway! The general form just becomes, if I recall correctly, ((a + b * sqrt(3)) / 2, (c + d * sqrt(3)) / 2). You see? Four variables: a, b, c, d. But we've got gross sqrt(3) stuff everywhere, whereas the 4d space I prefer to use has some nice properties when it comes to rotation, as you will now see.


Now, one thing you might notice is that A, B, C, and D can be generated by rotating A by multiples of 30 degrees.
If it helps you visualize this, here they are shown one after the other:

    A: 0 degrees:

         O---+


    B: 30 degrees:

            -+
           /
         O-


    C: 60 degrees:

           +
           |
          /
         |
         O


    D: 90 degrees:

         +
         |
         |
         |
         O


Let's say the function R rotates its argument by 30 degrees.
And note that 30 * 12 = 360 (that is, applying R 12 times gets you back to where you started: R(R(R(R(R(R(R(R(R(R(R(R(x)))))))))))) = x for any x).
Then we have:

  R(A)      = B
  R(B)      = C
  R(C)      = D
  R(D)      = C - A   <- See the visualization of C - A earlier to see how it must be R(D)!
  R(C - A)  = D - B
  R(D - B)  = -A
  R(-A)     = -B
  R(-B)     = -C
  R(-C)     = -D
  R(-D)     = -C + A
  R(-C + A) = -D + B
  R(-D + B)  = A      <- 12 rotations got us back where we started


Also, we can do something cool from linear algebra: we can define vector multiplication, and say that R is a vector instead of a function.
So for any vector x, R * x = R(x).
Now we need to pick a unit vector, let's say that's A.
So A represents the identity transformation: A * x = x
Now since B is A rotated by 30 degrees, we have A * R = B, but since A is the identity, that means R = B.
We can keep going with that:

  R^0  = A
  R^1  = B
  R^2  = C
  R^3  = D
  ...
  R^12 = A  <- 12 rotations of 60 degrees is a 360 degree rotation... i.e. no change


And by the way, 2A represents a "stretch" of size 2. That is, 2A * x stretches x to twice its size.
So I can use vectors to stretch and rotate the graphics.
The graphics engine can also define "mappers" which stretch & rotate all coordinates in an image, and replace each of its prismels with a shape composed of many prismels -- and this whole transformation can be applied repeatedly, basically generating fractals.
For instance, here is a transformation called "curvy" which... well, you'll see:

    "curvy":
        unit: 2 4 0 -2           <---- this is the vector it multiplies the coordinates by
        entries:
            : "vert" -> "vert"
            : "edge" -> "edge"
            : "sq"   -> "curvy_sq"     <---- this says it replaces prismel "sq" with shape "curvy_sq"
            : "tri"  -> "curvy_tri"
            : "dia"  -> "curvy_dia"


...this is what "curvy_sq", "curvy_tri", and "curvy_dia" look like:


...so what happens if we repeatedly apply the "curvy" transformation to some other shape?..

Original:


Transformed with "curvy" a couple of times:


Cool eh?!?!
Anyway, that's enough linear algebra and fractals for now. Apologies?.. or you're welcome?.. but let's get back to games...


So how is all this used by the game engine?
In fact, I "wrote" most of the graphics by hand using 4d coordinates.
Let's see how the spider image is defined. Here it is again for reference:


...and here is how it was defined using the game's graphics language. Note, lines beginning with '#' are comments. This is all taken more or less directly from the game's current code:

    "_head_sixth":
        #      _ +
        #    +    \
        #   / \  _ +
        # (+)- + _ |
        #          +
        prismels:
            : "tri" (0 0 0 0)  0 f eval: 1 + 8 + 1
            : "tri" (1 0 0 0) 11 f eval: 1 + 8 + 2
            : "sq"  (1 0 0 0)  1 f eval: 1 + 8 + 1

    "_head":
        shapes:
            : "_head_sixth" (0 0 0 0)  0 f
            : "_head_sixth" (0 0 0 0)  2 f
            : "_head_sixth" (0 0 0 0)  4 f
            : "_head_sixth" (0 0 0 0)  6 f
            : "_head_sixth" (0 0 0 0)  8 f
            : "_head_sixth" (0 0 0 0) 10 f

    "eye":
        prismels:
            : "tri" (0 0 0 0)  0 f eval: 1 + 8 + 4
            : "tri" (0 0 0 0)  2 f eval: 1 + 8 + 4
            : "tri" (0 0 0 0)  4 f eval: 1 + 8 + 4
            : "tri" (0 0 0 0)  6 f eval: 1 + 8 + 4
            : "tri" (0 0 0 0)  8 f eval: 1 + 8 + 4
            : "tri" (0 0 0 0) 10 f eval: 1 + 8 + 4

    "nose":
        prismels:
            : "sq"  (0 0 0 0) 0 f eval: 1 + 8 + 1
            : "tri" (1 0 0 0) 1 f eval: 1 + 8 + 2

    "head":
        shapes:
            : "_head" (0 0 0  0) 0 f
            : "eye"   (0 0 0  0) 0 f
            : "nose"  (1 1 0 -1) 0 f

    # Back leg
    "bleg":
        prismels:
            : "tri" (  0 -1  0  0) 11 f eval: 1 + 8 + 5
            : "sq"  (  0 -1 -1  0) 11 f eval: 1 + 8 + 3
            : "tri" (  0 -1 -1 -1)  1 f eval: 1 + 8 + 3

    # Front leg
    "fleg":
        shapes:
            # For historical reasons, "bleg" and "fleg" look exactly the same,
            # just "fleg" is usually rotated 180 degrees wherever its used...
            # However, they're separate in case we ever want to tweak one.
            : "bleg" (0 0 0 0) 0 f

    "stand":
        shapes:
            : "head"  ( 0  1  3  1)  0 f
            : "bleg"  ( 0  1  1  1)  0 f
            : "fleg"  ( 2  1  1  1)  6 t


...and just to be clear, that text is parsed by the game engine and used to generate the spider image.

You can maybe see the 4d coordinates in there, e.g. "head"  ( 0  1  3  1)  0 f is saying that the shape called "head" should be rendered at B + 3C + D, and rotated by R^0, and not flipped vertically ("f" is for "false"). The "eval" bits are specifying colours, e.g. eval: 1 + 8 + 2 is light green. (It's a classic 4-bit palette, RGBI where the "I" is for "intensity", e.g. the "light" in "light green". See also: Wikipedia article. And the + 1 is because 0 is reserved for the "transparent colour".)

In any case, somehow after typing up enough of that stuff, we end up with a game which looks like this:


You might argue this does not seem like a particularly efficient way to generate graphics.
And I would agree, which is why I (recently, after already typing up 90% of the graphics the other way) wrote a parser which can understand "text representations" of prismels, like this:

    "_coin_beast_bleg_step1":
        hexpicture:
            ;;                    +
            ;;                   | |
            ;;                   |5|
            ;;                  +---+
            ;;                   | | |
            ;;                   |5|5|
            ;;                    +---+
            ;;                    |   |
            ;;                    |   |
            ;;                    |D  |
            ;;                    +---+
            ;;                    |   |
            ;;                    |   |
            ;;                    |F  |
            ;;                    +---*


...that shape is called "_coin_beast_bleg_step1", because it's the back leg of a "coin beast" during frame 1 of its "stepping" animation, which looks something like this:



Okay, but there are some pieces missing here. How do the animations work?.. also, as you may (or may not) be able to tell from the gameplay clip above, the map's tiles actually form a triangular grid -- the "prismels" and 4d coordinates and whatnot are only used for rendering the sprites, not for the "physics".

First of all, the animations are done by specifying how many frames of animation a "shape" (image) has, and then optionally specifying for which frames its prismels or sub-shapes are visible. For instance, here is the "crawl_step" shape, in which the spider takes a step while crouching:

    "crawl_step":
        animation: cycle 3
        shapes:
            : "crawl_head" (-1  0  2  1)  1 f  0 (0 1)
            : "crawl_head" ( 0  0  2  1)  1 f  0 (1 1)
            : "crawl_head" ( 0  1  2  0)  1 f  0 (2 1)
            : "bleg"  (-1  1  1  1)  0 f
            : "fleg"  ( 2  1  1  2)  7 t  0 (0 1)
            : "fleg"  ( 3  1  1  2)  6 t  0 (1 1)
            : "fleg"  ( 3  1  1  1)  6 t  0 (2 1)


...so, "crawl_head" (-1  0  2  1)  1 f  0 (0 1) says to render the "crawl_head" shape at some 4d coordinate, with a rotation of R^1, not flipped (the "f" is for "false", remember), and finally "0 (0 1)" means don't offset "crawl_head"'s animation by any frames (the "0"), and only make it visible for 1 frame, starting on frame 0 (the "(0 1)").

Look, I enjoy writing parsers more than I enjoy writing GUIs, okay? I'm not saying this was the best way to achieve all this.

Okay, now how is the map defined?
Well, the bottom-left part of the map in this screenshot:


...is defined like this:

                                                    + - + - +   + - +
                                                   /* * *\   \*/
                                          + - + - + - +   + - +
                                         /   /* * *\   \* * */
                    + - + - +   + - + - +   + - +   + - +   +
                   /   /             \           \* * * * */*
      + - + - + - +   +               + - + - +   +   + - + -
     /                 \                       \   \*/  */*\*
    +   + -             +                       +   +
   /   /               /                         \   \
  +       + - + - + - +                           +   +
   \     /                                       /   /
    + - +               .   + - +       + - + - +   +
                        S  /     \     /             \
      .      (+)- + - + - +       + - +     - + -     +
             /                   /     \             /*
            + - +               +       + - + - + - +
           /     \             /                  * * *
          +       + - + - + - +
           \
            +
           /
          +
         /
        +
      */
      .


...that file is called "start.fus", and another file, "worldmap.fus", glues together such maps like this:

submaps:
    :
        file: "data/maps/demo/start.fus"
        pos: (0 0)
        camera: (6 4)
        mapper: ("quadruple")
        palette: "data/maps/demo/pals/start.fus"
    :
        file: "data/maps/demo/start2.fus"
        pos: (7 11)
        camera: (5 4)
        mapper: ("quadruple")
        palette: "data/maps/demo/pals/start.fus"
        submaps:
            :
                file: "data/maps/demo/start3.fus"
                pos: (2 15)
                camera: (5 -5)
                mapper: ("triple")
                tileset: "data/maps/demo/tilesets/shiny.fus"
                submaps:

                    ...ETC...



Terrifying.

And in fact, there is one last major type of file (and associated mini-language), which defines how a character moves and is controlled -- how its animations are glued together, how it responds to keypresses and collisions, and even the logic used by its AI.

Here is a snippet from the file describing the spider's movement:

collmsgs: "touch"
on "crush": goto: dead

collmap "stand":
    ;;   .   .
    ;;   *\*/*
    ;; . - + - .
    ;;   */*\*
    ;;  (.)  .

collmap "crawl":
    ;;     .
    ;;     *
    ;;  (.)  .

stand:
    rgraph: "stand"
    hitbox: collmap("stand")

    # Can't stand on nothing
    if:
        coll: any no
            ;;  (+)- +
    then:
        move: 1 0
        goto immediate: start_jump

    # Crawl
    if:
        key: isdown d
    then:
        goto delay: crawling


    # Forced jump
    if:
        key: isdown f
        coll: all no
            ;;       \*/*
            ;;        + -
            ;;       /*\*
            ;; ( )
    then:
        move: 1 0
        goto immediate: start_jump


    ...ETC...


...so we've got conditionals and "goto"s which send you to other animations, and side-effects like "move: 1 0" which moves you 1 space to the right.
On the one hand, it's a pretty gross example of "not invented here" syndrome (why not embed Lua or something?), but on the other hand, it lets us add syntax and features which might be difficult to express in another language.

For instance, I can express hitboxes as some kind of literal. Here is part of a conditional saying "if the following hitbox, at our sprite's location, would not collide with the map". (Note, the "( )" represents the sprite's location, which for the spider is its back foot):

        coll: all no
            ;;       \*/*
            ;;        + -
            ;;       /*\*
            ;; ( )


For reference, a single triangular map tile, including its 3 points, 3 edges, and 1 triangular face looks like this:

      +
     /*\
    + - +


...the same map tile, not including 2 of its points, 1 of its edges, or its face, looks like:

     .
    /
   + - .


(The "." indicate positions where you could put an "+", that is, a point. The "." are optional, they just make it easier to visualize the triangular grid.)


Hooray, an info dump! I'd say this has been a successful devlog entry. If we can cover enough nitty gritty, maybe I can start writing out my thoughts about how to bring this game project to a close, and actually tie together the features and map I have so far into something which people can play from start to finish, and enjoy.
« Last Edit: October 08, 2021, 12:09:31 AM by bayersglassey » Logged
a-k-
Level 2
**


View Profile
« Reply #13 on: October 08, 2021, 08:46:56 PM »

That was an interesting read! In terms of gameplay, I think it would be nice if tiles changed their color slightly but permanently once you step on them (assuming that e.g. all red tiles are reachable, otherwise that might be confusing).
Logged

bayersglassey
Level 0
***



View Profile
« Reply #14 on: October 09, 2021, 07:09:09 AM »

That's interesting... would the idea be to help the player see where they had been in the world as a whole, sort of a roundabout automapping feature? Or is the idea more to help the player solve individual areas, e.g. so they can remember which places they've tried to jump from in order to get across a gap, kind of thing?

FWIW, there is actually a minimap which gets automatically filled in as you travel. Hold Tab to see it. I'm not sure if I want to keep it in, or have it be unlockable, or what.

Here's an example of a certain area, with its minimap shown below:
Logged
a-k-
Level 2
**


View Profile
« Reply #15 on: October 10, 2021, 10:18:08 AM »

Quote
to help the player solve individual areas, e.g. so they can remember which places they've tried to jump from in order to get across a gap
Exactly this. Also for purely aesthetic reasons, to give players more feedback (even if there's no goal of 100% exploration).
Logged

bayersglassey
Level 0
***



View Profile
« Reply #16 on: October 11, 2021, 07:18:33 PM »

Hmmm, I've definitely been interested in adding tile-editing functionality in general. I don't think I want "edges change colour when you step on them" to be a feature present in every area, but it would definitely be a neat effect to use here and there. And once the groundwork for changing tiles is in place, there are lots of different ways it could be used.

As for whether it's technically possible... currently the map is pasted together from "submaps" which are composed of tiles, and each submap gets rendered as a bitmap (well, 24 bitmaps because all maps allow for a hardcoded 24 frames of looped animation), which is cached. So the engine doesn't currently support changing a submap's tiles. But an easy solution would presumably be to simply re-render a submap when its tiles are changed. Things get a bit more difficult if we want to *save* changes to every submap's tiles, but that would be doable as well (probably as a new data structure in the save data, only included for submaps whose tiles have changed).

Maybe I'll look into this when I try to fix the graphics engine's current "cache every bitmap you render" approach. Basically the graphics are built up as tree structures, whose branch nodes are called "shapes" and leaf nodes are called "prismels" (see the long post above), and in order to render top-level "shapes" the engine needs to render all the intermediate ones down to the prismels, and right now it's caching all those intermediate bitmaps forever, whereas it should probably throw them away and just keep the top-level ones (which are used as e.g. sprites and tiles).
So when I have a look at that, I'll also take a peek at allowing submaps' tiles to be modified...

Thanks for the idea. Smiley
« Last Edit: October 11, 2021, 08:44:22 PM by bayersglassey » Logged
bayersglassey
Level 0
***



View Profile
« Reply #17 on: October 11, 2021, 07:42:06 PM »

I finally figured out how to make saved games persist when running in the browser!
Emscripten doesn't make this easy.
The basic idea makes a lot of sense: Emscripten gives you a virtual filesystem implemented in Javascript, so that C calls to fopen etc work as expected. And you can tell the Emscripten compiler to bake source directories into the Javascript it builds, e.g. if game assets are in data/, then you can say emcc --preload-file data, and then calls to fopen("data/myfile") will work.
However, the default virtual filesystem is 100% in-memory, so all changes to it disappear when you leave the page.
So, Emscripten lets you add persistent storage in a very unix-y way, by letting you "mount" other filesystems, including one ("IDBFS") which uses HTML5's IndexedDB API under the hood.

Long story short, the game now stores savegame files in a subdirectory called "./saves", and when I compile with Emscripten, I tell it to mount an IDBFS filesystem on that directory:
Code:
// This is a little .js file I tell emcc to include with its --post-js option.
Module.preRun.push(function() {
    FS.mkdir("saves");
    FS.mount(IDBFS, {}, "saves");
});


So far so good, now stuff stored in that directory will be magically saved between page refreshes, right??!?!?!
Heck no! To load everything from IndexedDB at the start of the game, and to save everything back when player touches a save point, I need to manually call (from C) a Javascript function, FS.syncfs.
And because modern Javascript does everything asynchronously by default, you have to pass that function a Javascript callback.
So when the game (which is written in C) starts, it calls the following Javascript function (written in a .c file using Emscripten's EM_JS macro):
Code:
EM_JS(void, emccdemo_initial_syncfs, (), {
    console.log("Starting initial syncfs...");
    FS.syncfs(true, function(err){
        console.log("Initial syncfs finished.");
        if(err){
            console.error("There was an error in FS.syncfs.", err);
            if(!window._syncfs_broke)alert(
                "There was an error in FS.syncfs. " +
                "Saved games may not persist between page refreshes! " +
                "See the console for error details.");
            window._syncfs_broke = true;
        }
        ccall('emccdemo_start', 'v');
    });
})

...notice the ccall('emccdemo_start', 'v'), which is Javascript code calling a C function, "emccdemo_start".
That function mostly just does this:
Code:
emscripten_set_main_loop_arg(&emccdemo_step, app, 0, true);

...that is, it calls a C function provided by Emscripten, "emscripten_set_main_loop_arg", which causes the Javascript-based C interpreter at runtime to abandon the current callstack and instead start calling "emccdemo_step" (in this case) every time the browser's requestAnimationFrame thing fires. (You can also ask it to use the more old-fashioned setInterval.)

Anyway, long story short, my C "main" function now contains the following:
Code:
#ifdef __EMSCRIPTEN__

/* Load save files from IndexedDB */
emccdemo_initial_syncfs();

/* Now kill the current call stack!!!
Why? Because emccdemo_initial_syncfs has kicked off an
async JS operation which, when it completes, will set
current thread's "main loop" to emccdemo_step.
So the current call to main() never returns.
Hooray, it's callback hell with C *and* Javascript! */
emscripten_exit_with_live_runtime();

#endif

It's all pretty ridiculous.
But... it works! Try it out:
http://depths.bayersglassey.com/
(And click "play in your browser")

I should mention, much of this would not have been necessary had this Emscripten issue not been marked "wontfix"...

Next... I need to add a menu to the game, so you can choose whether to start a new game or load an old one, etc. As things stand, in order to start a new game, you would need to clear your browser history or something. WTF
Logged
bayersglassey
Level 0
***



View Profile
« Reply #18 on: December 19, 2021, 03:37:38 PM »

The tutorial seems fairly complete at this point, so I've been trying to flesh out the rest of the game.
The game's map has grown organically along with the engine -- each new feature resulted in a few new areas being added to test out that feature.
So the game has a ton of places in it, but no explanation of what you're supposed to be doing in them.
I don't want to throw all that stuff away; I'd rather layer more stuff on top of it until it feels complete.
So I've been trying to add just enough features to the engine that I can have other characters who can talk to you, travel with you, ask you for things and recognize when they get what they want.
At the moment, the kinds of character which appear in the game are:
  • spiders
  • eyeballs
  • eyeball-spiders
  • birds
  • "the purple people" a.k.a. "coin beasts"
  • flying spiders-with-no-legs
  • little rolling guys
  • big rolling triangular guys
  • big rolling circular guys

The ability to show text on the screen was added while I was making the tutorial, and the eyeballs were added at that time.
So currently, for historical reasons, only eyeballs talk.
Since I have zero ideas for a real story, I've been interpreting that as meaning that in this world, only eyeballs can talk.
Spiders, birds, etc cannot.
So, despite the fact that you are yourself a spider, you cannot communicate with your fellow spiders; instead, the eyeballs teach you how to play, and then release you from the tutorial into the main world.
I've been interpreting this as meaning that you are something of a double agent, sent by the eyeballs to... infiltrate the world of spiders?..
None of it makes much sense, but it's enough to inspire some basic dialogue (eyeballs making mysterious remarks about "releasing" you and so forth).

And now, the "main" game (after the tutorial) finally has some direction: you meet a spider, who says nothing but runs ahead, leading you on.
You come across 3 eyeball-spiders, apparently sleeping. The spider you're following spits at them, waking them up, and they each run in a different direction (toward different areas of the world), and hide.
You now have 3 choices of where to go next. Whichever way you go, you will come across one of the 3 hiding eyeball-spiders.
And since eyeballs can talk, I can now make it so that they explain... something to you. Or at least, give you some kind of goal.

Spider waking up some... eyeball-spiders


One eyeball-spider heads left



At the moment, one of the three has a complete "quest" implemented: you find him, he asks you to take him with you, you can pick him up and take him through the mazes on the next couple of screens, and finally you can "spit" him out into a socket in the wall, where he becomes a regular (non-spider) eyeball and can give you some hints about what to do in the following rooms.

Meeting an eyeball-spider


Sounds like a quest


Elevator pitch: you play a spider carrying a chatty eyeball


All this is shown in this video:


(For what it's worth, there is also

which shows a playthrough of most of the tutorial.)

You can also play through it in your browser: http://depths.bayersglassey.com
(Note that, as of this writing, you can save your game, but there is no way to reset the game, other than by clearing your browser's saved data for the page...)


I don't think any of this "story" is quite sufficient to make most people interested in playing through an entire game.
But it's gotten me through adding a ton of features to the engine, so I'm pretty pleased with it for now.
I think it can at least get me to the point where I have *a* finished game, if not a literary and cultural masterpiece. And I'm hopeful that I can work some complexity into it, like maybe developing some kind of description of the different "societies" present in the game (bird / eyeball / spider / "purple people")...
Logged
bayersglassey
Level 0
***



View Profile
« Reply #19 on: December 19, 2021, 09:55:08 PM »

Having just finished adding a ton of features to the engine in the hopes of being able to layer an actual game on top of the existing maps, here's an attempt at a devlog describing the game's engine, and how to hopefully make a game with it...


The game engine (bodies/players/statesets/actors/variables)

The game's engine is a classic map/sprite platformer engine: there is a map, which is a grid of tiles (in this case a triangular grid), and sprites/objects can move around on it.
It uses the following concepts to implement sprites/objects, playable/nonplayable characters, etc:

  • Body
    Bodies are how the game models physical objects. Bodies consist largely of:
    • a position on the map
    • a set of virtual key states (e.g. is the "up" key pressed?.. the "down" key?.. etc)
    • a "stateset" which defines is behaviour (see below)
    • a current state within its stateset, and a frame number within that state
  • Player
    Represents a human player, and allows you to control a body.
    Basically consists of a body plus a set of key mappings (mapping physical keys to the body's virtual keys).
  • Stateset
    Defines a "kind of physical object", for instance a spider, an eye, a coin.
    In particular, it defines the behaviour of an object, in terms of state transitions.
    For instance, a spider starts off in the "stand" state, and in that state, if you press the right or left arrow keys, it might transition to the "step", "step_up", "step_down", or "jump" states (depending on various things, such as the surrounding map tiles).

    This kind of state transition system defining the movement of a character on a triangular grid is exactly what my earliest sketches of this game looked like, before I'd written any code:


    Each state has:
    • a hitbox, for determining how a body in this state collides with other bodies
    • an image, for rendering a body in this state
      (The images are made out of "prismels", see the earlier post about the graphs engine)
    • a series of rules, for determining what a body in this state should do each frame (e.g. move, die, transition to other states, create other bodies, set variables, etc)
    • a set of event handlers, for determining what a body in this state should do when colliding with another body expressing a given "event"
    • a set of expressed "events", for determining which event handlers a body in this state should trigger on bodies it collides with
      Events are just strings. At the moment, the game makes use of the following events: "solid", "crush", "touch", "carryable", "spit"

    For example, here is the spider's "stand" state:

        # This "collmap" is reused as the hitbox of many different states:
        collmap "stand":
            ;;   .   .
            ;;   *\*/*
            ;; . - + - .
            ;;   */*\*
            ;;  (.)  .

        # This is the spider's default state, used when standing still:
        stand:

            # A state's "rgraph" (short for "rendergraph) is its sprite image,
            # which is defined elsewhere (in terms of "prismels"):
            rgraph: $PREFIX NS "stand"

            hitbox: collmap("stand")

            # Can't stand on nothing
            if:
                coll: any no
                    ;;  (+)- +
            then:
                move: 1 0
                goto immediate: start_jump

            # Spit a projectile
            if:
                key: wentdown y
            then:
                call: spit

            # Crawl
            if:
                key: isdown d
            then:

                # To "goto delay" means to goto the indicated state (here "crawling"),
                # *and* to delay by the number of frames in its rendergraph.
                # Otherwise, we would need to explicitly do a "delay" here with
                # the correct number of frames, as well as doing a "goto".
                goto delay: crawling

            # The rules for stepping and jumping are defined in another file,
            # so that they can be reused by other statesets.
            # (e.g. the rules for stepping and jumping are reused by the stateset for
            # "purple people", aka "coin beasts")
            import "anim/_player_move.state.fus"

            # Turn
            if:
                key: isdown b
            then:
                move: 1 0
                turn
                goto delay: turn

            # ...etc...

  • Recording
    Specifies a stateset, a starting location & state & set of virtual key states, and a string of key press/release data (specifying which virtual keys should be pressed/released each frame when playing the recording).
    Example:

        anim: "anim/spider.fus"
        state: "stand"
        pos: ( 5 5)
        rot: 0
        turn: yes
        keys:
            x:
            u:
            d: is was
            l:
            r:
        data: " w4+r w232-r w18+l w1-l"


    ...this lets me "act out" the behaviour for a body, and then save it as a recording.
    Much faster than trying to type out the key presses/releases for each frame by hand...
  • Actor
    Actors are essentially "CPU players".
    Like players, actors have a body which they can control (by modifying its virtual key states directly, or by playing a recording).
    Just like a body, each actor has a stateset, which defines its behaviour.

    Some actors mostly just play back recordings. For example, here are some states from the actor controlling the spider you meet at the beginning of the game (after the tutorial):

        start:

            # If player already passed us (and triggered a hotspot setting a variable),
            # or if player is in front of us (i.e. if a body whose state declares the "touch"
            # event collides with a hitbox in front of us), then play a recording of spider
            # running from the starting area up to the next area...
            if: any:
                expr: mapvar("guide_passed_start")
                coll: bodies("touch") any yes
                    ;;
                    ;;
                    ;;        .   .
                    ;;        *\*/*\*/*\
                    ;; ( )  . - + - + - +
                    ;;        */
                    ;;        .   .   .   .
            then:
                play: "actor/guide/rec/start_to_start2.fus"
                goto: start2

            # Default behaviour: we play a recording of spider sitting at the starting area...
            if() then:
                play: "actor/guide/rec/start.fus"

        start2:
            if: expr: mapvar("guide_passed_start2")
            then:
                play: "actor/guide/rec/start2_to_start3.fus"
                goto: start3
            if() then:
                play: "actor/guide/rec/start2.fus"


    Other actors have complicated rules looking at the map tiles around their body and deciding which virtual keys to press.
    For example, here is one rule from the "spider AI" stateset:

        # Sometimes, crawl!
        if:
            chance: 25%
            any:
                all:
                    coll: any yes
                        # Only crawl somewhere we couldn't just walk
                        ;;       \*/*
                        ;;        + -
                        ;;
                        ;; ( )  .
                    coll: all yes
                        ;; ( )  + - +
                    coll: all no
                        ;;     \*/*
                        ;; (.)  .
                all:
                    coll: any yes
                        # Only crawl somewhere we couldn't just walk
                        ;;       \*/*
                        ;;        + -
                        ;;         \*/*
                        ;; ( )  .   + -
                        ;;           \*
                    coll: all yes
                        ;; ( )  +
                        ;;       \
                        ;;        +
                    coll: all no
                        ;;     \*/*
                        ;; (.)  . -
                        ;;        *
        then:
            key: up f
            key: down d
            set myvar("crawl_time"): 0
            goto: mainloop_crawl




Now, a lot of the features I've been adding recently have to do with variables.
They are shown in some of the snippets above, e.g. the actor checks whether mapvar("guide_passed_start2") is true.
The basics of variables are:
  • There is a global set of variables
  • Each map has a set of variables
  • Each body has a set of variables
  • Each variable has a name and value
    The values can be of the following types: null, boolean, integer, string
    For example, here is a set of variables:

        "nothing": null
        "x": 1
        "name": "hello"
        "yes": T
        "no": F

  • There is a little language for defining expressions with literal values and variable references:

        # Literal values:
        "hello"
        3
        null

        # Refer to a global variable:
        globalvar("x")

        # Refer to a map variable:
        mapvar("x")

        # Refer to a variable of the current body (if any):
        myvar("x")

        # Refer to a variable of the current "other" body (if any):
        # (For example, in an event handler, the "other" body might be the one our
        # body is colliding with)
        yourvar("x")

        # Using variables as variable names:
        mapvar(myvar("x"))

  • Variables can be used in the rules of statesets to control body/actor behaviour, and can also be used by the map to control the display of text, and the visibility of parts of the map.
    For instance, we can have a solid part of the map which blocks the player's progress, but its visibility is controlled by a variable, so that when player achieves some goal, the blocker is removed.
    Here is an example of variables being used to control text & visibility in part of the map (the "vines" area, which has a lot of wiggly seaweed-looking stuff):

        submaps:
            :
                file: "data/maps/demo/vines3_blocker.fus"
                visible: not mapvar("vines3_door")
                pos: (-8 13)
            :
                file: "data/maps/demo/vines2.fus"
                text:
                    if all:
                        mapvar("eyeplayer_vines_ran_away_3")
                        not mapvar("eyeplayer_vines_ran_away_4")
                        == mapvar("eyeplayer_vines_mini_eye_respawn") 0
                    then $GET_STR VINES_COME_BACK
                    else null
                pos: (-26 -5)
                camera: (-7 3)
                mapper: ("quadruple")
                submaps:
                    ...etc...

  • Maps, parts of maps, and statesets can define a default set of variables.
    For example, here is the entirety of the "mini_eye" stateset, which represents an eye as a carryable item (by extending the generic "carryable item" stateset, defined in "anim/_coin.fus"):

        collmsgs: "carryable"

        vars:
            # For use with handlers for collmsg "carryable"
            "carryable": "mini_eye"
            "respawn_mapvar": null
            "carryable_rgraph": "mini_eye_carried"


        $SET_STR RGRAPH_STAND "mini_eye"
        $SET_STR RGRAPH_CROUCH "mini_eye_crouch"
        $SET_BOOL NO_COLLECT_ON_SPIT
        $SET_BOOL NO_COLLECT_ON_TOUCH
        import "anim/_coin.fus"



As an example of how variables are used, here is an event handler defined in the stateset for spiders:

    on "carryable":
        # "me" is a player, "you" is a carryable thing (e.g. coin, food, eye)

        if: not: exists myvar("carrying")
        then:
            # Transfer some vars from carryable thing to the player who
            # will now be "carrying it"
            set myvar("carrying"): yourvar("carryable")
            set myvar("carrying_respawn_mapvar"): yourvar("respawn_mapvar")
            set myvar("carrying_rgraph"): yourvar("carryable_rgraph")
            set_label "carrying": myvar("carrying_rgraph")

            # Increment a mapvar tracking how many of this thing are
            # being carried
            if: exists myvar("carrying_respawn_mapvar")
            then: inc mapvar(myvar("carrying_respawn_mapvar"))

            # When a thing is carried, it is "collected"
            as you:
                goto immediate: _collected



As another example, here is the main state of the stateset for a "hotspot", a kind of object which is invisible, and whose purpose is to set variables when players touch it:

    waiting:
        rgraph: "hotspot_waiting"
        hitbox:
            ;;   .   .
            ;;   *\*/*
            ;; . -(+)- .
            ;;   */*\*
            ;;   .   .

        on "touch":
            if: exists myvar("key")
            then: set mapvar(myvar("key")): T

            if: exists myvar("counter")
            then: inc mapvar(myvar("counter"))

            if: exists myvar("max_cooldown")
            then: set myvar("cooldown"): myvar("max_cooldown")

            goto: touched


...you don't need to bother understanding all that, but now see how hotspots are placed on the map (in this case, with the "key" variable set to the name of a mapvar):

    parts:
        "h": recording "data/recs/hotspot.fus"
            vars:
                "key": "start3b_solved"
    collmap:
        ;; .   .   .   .   + - +   .   .
        ;;                /* * *\
        ;;   .   .   .   +   .   + - .   .
        ;;              /  *   * * * *
        ;; .   . - ! - +   .   .   .   .      [h]
        ;;         * * *         *   *
        ;;   .   .   .   .   .   .   .   .
        ;;         *   *
        ;; .   .   .   .   .   .   .   .


...maybe not very exciting... but it means that at that "!", there is a hotspot, so when player reaches that spot, the hotspot's event handler will fire, setting mapvar("start3b_solved") to true.



...phew!
I'm not sure if writing this all out has gotten me any closer to figuring out what exactly to do with the game, but it feels good to get it all out there in black and white...
« Last Edit: December 20, 2021, 11:59:54 PM by bayersglassey » Logged
Pages: [1] 2
Print
Jump to:  

Theme orange-lt created by panic