Skip to main content

Indie game storeFree gamesFun gamesHorror games
Game developmentAssetsComics
SalesBundles
Jobs
TagsGame Engines

oofoe

252
Posts
9
Topics
21
Followers
6
Following
A member registered Mar 23, 2018 · View creator page →

Creator of

Recent community posts

Yeah... I'm planning to fix that at some point. It's a jam game, something's gotta' give... Thanks for taking a look!

Quick archival note... These articles have been collected and checked in to the Canned Heat documentation at https://hg.sr.ht/~oofoe/candheat/browse/dox?rev=tip.

Silly and entertaining was just what I was going for... life's too short for serious and boring. Thanks for playing!

Thanks for checking it out!

Hi!

If anyone's interested, you're welcome to join us by videoconference (Jitsi) at the Toronto Lisp Users Group this coming Tuesday (2024-11-12) to discuss what went right, what went wrong and lessons learned about the jam. There may be other topics as well...

https://torlisp.neocities.org/

Nice Rogue imp, the graphical tiles add a lot and the background music is a good, creepy fit. Appreciate that it's fairly constrained and only uses about a third of the letters on the keyboard instead of _all_ of them... And just like the original, you can be doing great until you aren't!

Nice neon-noir look, like the vector stroke fonts. Second the call for a soundtrack! ; - )

Season cycling and the "judgement haiku" were nice touches. Music was a perfect choice, but unfortunately too short. Enjoyed it.

I like the "roving musician" idea. Too bad there aren't more juke joints -- I was just getting started!

"...had a great time!" <-- That, that right there. That's the goal. ; - ) Really glad you enjoyed it! (And yes, it should probably place a limit on jumps...)

I like the idea of going between cloud layers. And with space, everything's so far away, you don't have to worry about parallax backgrounds... ; - ) FWIW, even if you can't get the game as complete as you want, submit anyway -- it's a good way to keep a record. And when rating, even if the game is not fully playable, I still try to take into account the criteria.

Had some text overrun issues at points. Liked the collage effects. If you're developing further, would be neat if you were able to make them self-assemble during the dialogue!

Ballistic control was hard to get used to, but I liked the graduated introduction of the compounding elements. Lots of action on screen!

(1 edit)

Smiting somewhat less effective than hoped... ; - )

Liked the non-real-time card idea... Reminded me a little of an old ZX81 game called Kamikaze Drive, which would show you road signs and you had to input(!) your brake and accelerator percentages to make it through the next segment of track... This one has much better mechanics (so to speak...)!

Well, you got that one! It seems to work fine now for as far as I can get -- Napoleon and Genghis Khan have nothing to fear from me... Looks really good!

I liked the ASCII cloud over the windmill (I'm kind of big on clouds...). One thing I ran into -- I planted so much that I couldn't scroll down to work in the workshop!

It actually assembles the layout (randomly) from a "library" of "phrases" (PHRASES if you look at the source). So you sort of have the best of both worlds -- a bit of human design by a human, but dynamically assembled to keep things interesting. The level editor outputs phrases in the web console when you hit space. You can copy and paste that into the source (%game.js if you're going to recompile, or %index.html if you're just messing with the thing without breaking out Racket). Glad you liked it!

Fingerguns! ; - )

(1 edit)

Thanks for trying it out! Mr. MacLeod comes through again for the music... And, yeah, I have to admit that physics isn't my strong suit here. ; - )

(2 edits)

Hint #7 -- Changing the World

Remember I talked briefly about "live" objects (or entities...) in the Store instead of "dead" ones that get drawn implicitly? One of the benefits of the former is that you can easily change them dynamically. This has important benefits for development beyond just unifying way you store things.

When the objects are live, you can directly access them in the Store, modify their properties and see instant updates. To get started, pop open the web console in your browser while your game is running. The Entity Store (JavaScript variable STORE) is a global variable you can access:


You can use all the web debugging tools at your command (and there are many powerful ones!) to access and interrogate what's happening in your game. You can also use the Canned Heat query functions:

Remember that Urlang compiles directly to JavaScript, right? And because you're able to access the entities, you can change their properties, right in your console window. That's how I fine tuned a lot of my text placement for the title and credit screens in Kipple Kat. I would put in rough coordinates for things, then I would adjust them on the fly to get them where they really needed to be:

As I change it in the console, the text moves up and down (or down and up, in this case). Once I have it where I want it, I note the latest number and update the source. Not completely smooth, but way better than trying to update a number, recompile, renavigate to wherever it is in the game, deciding it's wrong, then updating a number... The point is, live objects are your friend!

Because the E-S encourages a generic approach, I can do the same thing with almost any displayed object -- they all work the same way because they're displayed or operated on by the same mechanisms. 

Have you ever wanted a level editor, or some way to dynamically modify your game world? With Canned Heat, you get it almost for free. To add the layout editor (because Kipple Kat doesn't really have traditional levels...) was about 150 or so lines of code. Most of that was new key handlers to drive a cursor -- you guessed it! -- Entity around and select what it rolled over. I didn't have much time to be fancy, but I was still able to: move a cursor, add new entities at the cursor, move them around with the cursor, delete selected objects, delete all objects (clean slate!) and dump the current state of the layout to the web console. Rather rough, but effective -- In about twenty minutes, I was able to do most of the layouts seen in the game. And that time includes fixing a bug a found along the way!

When I first made Canned Heat available, I provided four simple example games that I used to validate the process. My jam entry (demonstrating the editor among other more advanced techniques) is now one of those examples. And, while I'm very happy with it and how it worked out, it's only scratching the surface.

So, if you're writing games in Lisp for the web, I hope you'll have a chance to check out Canned Heat. Thanks for reading!

Very happy you liked it! I'm going to drop one more entry here tonight, but later I'll try to summarize all these hints into the repo documentation.

Quick note, tried "Campaign" and got this:


Hint #6 -- Cheap(ish) particle systems...

Everybody loves particles, right? They really liven things up. 

Canned Heat has the basics for particle handling built right in -- it uses (sys-tick), which compares the entities "age" (incremented every tick) with its "span" (if it has one). If age has exceeded the span, the entity is marked for destruction, which means, next redraw, it will be gone for good. 

A super simple case for particles is to make a trail, like the one Tinkerbell (or Nyancat, if you like) sports. To do this, create a particle object every time the entity is moved. Here, I have a simple System that will emit a particle whenever any entity that has a "trail" component is set to true:

   (define (sys-trail)
     "( --) Track trajectory of objects with trail component."
     (for ([e in-array (qq 'trail #t)])
       (let ([x (+ e.x (/ e.w 2))]
             [y (+ e.y (/ e.h 2))])
         (when (and (< 0 x) (< x SV.screen.width)
                    (< 0 y) (< y SV.screen.height))
           (store (object [tag 'trail] [scene #t] [x x] [y y] [age 0] [span 90])))))
     )
Don't be put off by that goofy math in the middle -- I'm just centreing the particle in the footprint of the trailing entity. The last bit checks to make sure the location is within the displayable area of the screen before creating it. I've had thousands of entities created with Canned Heat, but i don't want to let them get away from me... The parameters of the particle entity will snuff it out after ninety frames.

Oh, right... How would you display the particles? Try this in your render section:

     (for ([e in-array (qq 'scene #t)])
       (cond ...
             [(= e.tag 'trail)               (:= cx 'fillStyle 'red)               (cx.fillRect e.x e.y 4 4)]              ))
I just draw a red square for every trail particle. Easy as that.

You can supply deltas and use (sys-move) to animate the particles so they don't just hang around. With some trivial extensions, there are tons of possibilities -- add a colour ramp that interpolates as the entity ages and a rising dy with some jitter? You've got fire! Collisions can be pretty fast, since you just test for the point (x, y) instead of having to take the particle size into account. 

Canned Heat was partially inspired by the data structures used to manage particle systems, so it's no surprise that it handles them well!

(1 edit)

Hint #5 -- Taking advantage of E-S to avoid special cases.

It's very tempting at times to just draw something in the render as a special case. And that's not necessarily bad! Especially if you're trying to get something working. However, if you can take advantage of Canned Heat's entity store, your code will be a lot more flexible and maintainable. Not to mention probably shorter!

Here's an example... I wanted to put a "GAME OVER" message up, so I dropped it in at the end of the main draw function as a special case just so I could see it:

       (when (= 'dead hero.state)
         (:= cx 'fillStyle 'orange)
              (:= cx 'font "Bold 100px sans-serif")
              (:= cx 'textAlign 'center)
              (cx.fillText "GAME OVER"
                           (+ (/ SV.screen.width 2)  (- (* 5 (Math.random)) 3))
                           (+ (/ SV.screen.height 2) (- (* 5 (Math.random)) 3)))
              (:= cx 'textAlign 'start))
       )

The drawing is conditional on the hero being dead, which is simple enough. However that's one place where stuff like this can get you into trouble, when you have too many special cases to keep track of. Then, I set special font attributes and text alignments. While I clean up the alignment, I didn't bother resetting anything else. And, notice that I'm computing during the draw with the jitter factor being applied to the location of the cx.filllText. That should be really happening in a System.

So, to clean this up, I used mk-text instead. You can specify all the properties you need and let the draw loop handle it for you:

   (define (sys-lose?)
     "( --) Check to see if Kat has fallen and can't get up."
     (let ([hero (q1 'hero)])
       (when (and (not (= 'dead hero.state))
                  (< (+ SV.screen.height (/ SV.screen.height 4)) hero.y))
         (:= hero.state 'dead)
         (mk-text '(("GAME OVER"))
                  (/ SV.screen.width 2) (/ SV.screen.height 2) 'orange
                  '((font "Bold 100px sans-serif")
                    (jitter 3) (textAlign center)))
         )))

Now I set create the GAME OVER message in the System where I determine that the hero has died. The jitter property is set so that the sys-jitter System will animate it properly. But best of all, instead of dead pixels drawn irretrievably into a frame buffer, I have a live object for the GAME OVER text that I could do other things with if I wanted. The other advantage, of course, is that I have a general way to create text from the data in the Store instead of having to write code to do it.

Are you on a particular Discord server? Wasn't able to find you. Thanks!

(1 edit)

Hint #4 -- Audio!

Sound is to games what the frame is to a painting. Even a simple effort makes a huge improvement in the overall feel of the game. And thanks to sites like Open Game Art, or Incompetech, there are a wealth of audio resources available. And, of course, every game author who is not me, is also an acid-house-trance DJ on the side. So make sure you use these resources to bring your games to life!

Oh? What's that? You say Canned Heat doesn't have any audio handling capabilities? Hmmm. I guess I didn't get around to putting those in before the jam started. Let's see if we can fix that. First we need to load some sound:

   (define (mk-sound name path)
     "( name path -- entity) Create audio entity."
     (let ([a (new Audio)])
       (:= a.src path)
       (store (object [tag 'audio] [name name] [au a]))))

This function makes an "audio" entity in the Store. It's pretty simple. It creates a web Audio object, sets the source to whatever file you might have for it and drops it in the Store, ready for use. Most modern browsers can load most any audio file format you're likely to use (although FireFox seems to have some problems with %.wav format for some reason). If you don't have a particular requirement, I would recommend %.ogg for sound effects and %.mp3 for longer pieces, like the orchestral score you  composed in Ableton last night. Here's how to use mk-sound when initializing stuff:

(mk-sound 'ponk "a/Interface Element 3.mp3")
(mk-sound 'spring "a/spring.ogg")
(mk-sound 'pop  "a/Pop.mp3")

I generally try to load it before my image resources. It probably doesn't matter that much, but...

And now, maestro, you need to play those sounds where they'll do the most good. Maybe in a key handler when the player presses the jump key, or perhaps in the collision detection System when they run into something. To do that, you'll need a way to play them:

   (define (play name)
     "( name --) Play named audio entity."
     (let ([a (grab (λ (e) (and (= e.tag 'audio)
                                (= e.name name))))])
       (when a
         (:= a.au.currentTime 0)
         (a.au.play))))

With this function, you can just say "(play 'spring)" and the heady sound of coiled metal will grace your ears!

Oh! One quick note -- sometimes browsers will be reluctant to play sounds without user interaction. This is a defense against all those awful auto-playing adverts that were popular a while back. Haven't ever seen that? Thank your browser for protecting you. However, it does mean that you may not hear any sound after all the trouble you took to retrofit your game. Fortunately, the solution is (fairly) simple. You can tell the browser to allow auto-playing media on the game page, or... You can just have the user do something on the page, which usually unblocks the audio. This can be as simple as hitting a key or clicking a button. When you publish your web game on itch.io, if you set it for "click to play", then that initial click will do that for you.

Make mine music!

(1 edit)

Interesting. It seemed very natural to provide an abstraction for running across the Store and selecting the appropriate things. What do "normal" ECS imp's do? Or is it expected that the user will provide that part?

Hint #3 -- Powerful queries and collision detection...

So, if you've looked at my presentation or read through some of the examples, you know that the "grab/s" functions can query the Store for entities with a particular tag or key value setting. However, you can also feed grab and friends a query function, which can be arbitrarily powerful...

Here is a system that implements 2D collision detection. Yes, there's an example of this in %invade.rkt, but this one is better.

   (define (sys-hit)
     "( --) Collision detection."
     
     (let* ([hero (q1 'hero)]
            [items (grabs (λ (e) (and e.hit
                                      (< hero.x (+ e.x e.w))
                                      (> (+ hero.x hero.w) e.x)
                                      (< hero.y (+ e.y e.h))
                                      (> (+ hero.y hero.h) e.y))))]
            )
       (when (not (empty? items))
         (for ([item in-array items])
           (item.hit item hero)))
       ))
It gets the "hero" entity and runs a function query to check it's position against everything that has a collision handler ("hit"). When it finds them, it runs the handler with the item and the hero as arguments (this is a Canned Heat convention). 

Notice that I check to see if the entity has a collision handler before I attempt the geometry testing -- this make things faster because it only does the expensive comparisons on things that warrant it.

(1 edit)

Hint #2 -- Self documenting controls...

If you add a few extra fields (components) to your key specifications, you can use it to document the controls for the user:

     (store (object [tag 'key] [key "F1"]
                    [do (λ (e) (:= SV.helping (not SV.helping)))]
                    [label "help"] [help "Show/dismiss control help screen."]))
     (store (object [tag 'key] [key "ArrowLeft"] [do key-left]
                    [label "left"] [help "Press to go left."]))

Then add a function to display it when you render:

   (define (draw-help cx)
     "( cx --) Show controls help."
     (let* ([qw (/ SV.screen.width 4)]
            [qh (/ SV.screen.height 4)]
            [x (+ qw 32)] [y (+ qh 32)])
               
       (:= cx 'globalAlpha 0.75)
       (cx.fillRect qw qh (* 2 qw) (* 2 qh))
       (:= cx 'globalAlpha 1.0)
       (:= cx 'fillStyle 'white)
       (:= cx 'font "Bold 16px sans-serif")
       (cx.fillText "CONTROL HELP" x y)
       (+= y 32)
       (:= cx 'font "12px sans-serif")
       (for ([k in-array (qq 'key)])
         (cx.fillText k.key x y)
         (cx.fillText k.label (+ x 80) y)
         (cx.fillText k.help (+ x 128) y)
         (+= y 16))
       ))

Now, when I press [F1], I see this:


One of the great things about this is that if you're using the overlay system, the help will update to whatever keys you currently have assigned.

Just thought of this when wiring up the keys for my own entry and thought I'd share!

(1 edit)

Just because I needed it to start my own entry, there's now a %skel.rkt file in the repo. It's a bare-bones version of Canned Heat, ready for you to plug in all the clever bits!

(1 edit)

Greetings!

It is with great pride and a certain amount of trepidation (or maybe it's the other way around?) that I would like to announce the first public release of the Canned Heat Game "Engine"! It uses the power of Racket Scheme and @soegaard's Urlang to produce self-contained browser based games that can be enjoyed standalone, or served from itch.io.

Why is "engine" in quotes? Because it's not really a library or framework, but more of a mindset. It implements a simple (very simple) Entity Store and provides some bracketing functionality. This may not sound like so much, but you can use it to build almost any style of computer game, from text-based to 2D or 3D. There are four (more-or-less) complete example 2D games which you are welcome to use as starting points. Or delete most of what's in them and go from there. I don't care...

Aside from the README file, here is the presentation that I just gave at the Toronto Lisp User's Group about it. The examples are also commented (and most of the comments may even be up to date!). More documentation and examples will be added as I have time.

Support? Ask in this topic and I'll try to answer if I can.

Thank you and good luck!

Appreciate you trying it out! Hopefully the measures required were not excessively heroic... ; - )

Check the credits for links, but basically Incompetech and OpenGameArt. I trimmed "Gary"'s gun handling noises down to get that nice solid "click", but didn't have to do anything else to it. It worked perfectly! (In my opinion.)

Liked the combination of 3D with text adventure. One suggestion -- if parser only has a few choices for me, go ahead and make the appropriate choice, i.e. shouldn't have to do "go[space]ki[tab]", but "g[o ]k[itchen]". You already know what the valid choices are, so don't make user jump through hoops...

Oh hey, no problem at all, was just confused. I thought you might have meant that I had too much logging turned on but when I checked that didn't seem to be it. My goal with this game jam is to come back with something that you can actually play. Maybe next time!

Um, sorry "great logs" (WRT "Square Meal")? What problem are you describing? Thanks!

Well, funny beats dire! Thanks for trying it!

Hi! Thanks for taking a look! Someday I will find the perfect isometric control mapping. Until then, we must all suffer! ; - ) Right now, the reason it stops when you get to a height of 10 is leftover from testing. Unfortunately, the scoring/level clear detection code broke when I did my last push and I can't fix for the contest.

I think everybody's doing it these days... Unfortunately for very good reason. Good luck!