Let’s try a Project to manage some JSL
A long time ago I suggested writing a side-scroller https://community.jmp.com/t5/Uncharted/Wild-Road-Map/bc-p/20996/highlight/true#M5 . If you were not playing games in the 80s, https://en.wikipedia.org/wiki/Parallax_scrolling has some examples. Side-scrollers are 2D games where a character moves left to right and the background may have multiple layers that slide at different rates to give a sense of 3-D depth. Other variations might be platform jumpers with a vertical scrolling dimension as well.
There are issues to be solved:
- game development (can JMP make this easier to write?)
- game graphics (what does it look like?)
- game audio (did you hear that?)
- game input (how do I move?)
- game play (does anything happen?)
1. Game Development
I’m using a JMP project (and just recently, switching to a JMP 16 self-contained project.) I’m starting to like the self-contained project for a project that has five to ten independent files. It has tabs for the open files and docking points for the log window. The self-contained project stores everything in one file (zip, under the covers) and automatically unpacks and repacks as it is opened and closed.
Getting this game to work was easier because the file for graphics and the file for audio and the file for game play are separate, relatively small files. That makes it easy to find things, like the JSL that manages the game play between game characters. The project shows the files in tabs, like this:
A JMP project with several files.
There are nine files open as soon as this project opens; the file that holds the class definition for vampires in the game is currently in view. I’m not completely comfortable with when the files are saved back into the project and when the project is saved yet, so I use the highlighted icons at the top left before closing the project. I also docked the log at the bottom left. Projects seem to be isolated from the rest of JMP and from each other, so they have their own log.
The MakeAddin file is not part of the game, but is part of developing the game. If that tab is open and I ^R (run) it, it will regenerate the add-in. What happens if I run another tab? That JSL runs, which might not be the start of the game logic. But I did that all the time, by mistake, so I fixed it like this:
- There is a main file, game.jsl, that includes all the other files in a reasonable order.
- Most of the files are independent and just create a class or a namespace.
- The sidescroller.jsl file needs all the other files loaded (it is the main game loop).
The independent files all end with
try(include("game.jsl"),if(!contains(char(exception_msg),"includes itself through a closed path"),show(exception_msg)));
which will run the game.jsl file after the current file. The game.jsl file will attempt to re-include the current file and throw a recursive include error, which is ignored, or some other error, which is reported. The sidescroller file, which needs to be last, has that try statement as the first line. That way the game.jsl file includes every thing else before the remainder of the sidescroller file runs.
The game.jsl file looks like this
files = {"AUDIO.jsl", "RGBA.jsl", "ASSETS.jsl", "LAYERS.jsl", "VAMPIRE.jsl", "BEAR.jsl",
// currently SIDESCROLLER contains the game logic. All of the files include this file,
// at the end, except for SIDESCROLLER which include it at the start. SIDESCROLLER is
// dependent on all of the other files being loaded. The other files are not order-dependent.
"SIDESCROLLER.jsl"};
For( iFile = 1, iFile <= N Items( files ), iFile += 1,
Try(
Include( files[iFile] ), // this exception is EXPECTED if you start one of the other files; it is OK!
If( !Contains( Char( exception_msg ), "includes itself through a closed path" ),
Show( files[iFile], exception_msg )
)
)
);
Now, whichever file is edited can be run. Except...if more than one file is edited, be sure to save the ones that are not being explicitly run! The include gets the disk copy, not the edit tab copy. But if you only change one file, because of the way the includes are set up above it will use the current edit tab copy without needing to save.
Wish List:
- save all before running (or, make include use the edit tab copy)
- specify a start file when running a project (not the on-project-load file)
- document how saving a tab and saving the project are the same/different
- a way to run the project’s save-all command from JSL
2. Game Graphics
This project uses both JSL-generated graphics and downloaded bitmap graphics. Below, the blue sky, mountains, trees, and gravel path are created by JSL. The rock wall face is a tiled bitmap and the vampires, bats and bear are animated bitmap sequences. Credits at end.
The little bear is idling; the vampires are releasing bats or disappearing.
The RGBA.jsl file is an important part of the JSL graphics. JMP can draw with transparency in a graph and can use bitmaps with transparency in a graph, but there is no easy way to, for example, make a line graph on a transparent background and grab that bitmap with the transparent background. (There is an option, Transparent background for report PNG images, that saves everything except the graph with transparency. You can edit a saved SVG to remove the background, but you need another program to turn that back into a PNG on a transparent background.)
Wish List
- <<BackgroundColor(“none”) for a frame box.
The RGBA file makes a class for creating bitmaps with transparent pixels where nothing is drawn. It might need extending for your favorite primitives. Check out the trees with not-quite-opaque leaves, and gaps between leaves. Bezier Tree using Path() describes drawing the trees, but not with the alpha channel. The mountains are several layers of a hill-drawing algorithm with different parameters, and the sky is a non-transparent blue-to-black shaded rectangle. The gravel path may be quite slow to generate, but fast enough once running; each bit of gravel is drawn in a bitmap.
The ASSETS.jsl file knows about the bitmap assets the game uses, how to open them, how to normalize them, and how to name them. Because the animations are drawn by different people they require normalization to center them the same way. That information is kept in the structure like this
Assets:vampire_images["hit"] = {{"attack/hit_1",.,.,0.80,0.5},
which shows the vampire releasing bats is shifted (.80 vs .5 in others) in the bitmap to handle the extra wide animation. The vampires and the bear get their own layers in the side-scroller and their transparency lets more distant layers show through.
Because the game is actually in a graph, you can peek into it with the zoom tool to understand the layers. Here it is zoomed out and you can see the sky is no bigger, left-to-right, than the window. It does not scroll. But the closer and closer layers get wider and wider so they can scroll more.
Normally the sky fills the display, left-to-right. Zoomed out, see the vampire about to enter the stage.
As the little bear walks to the right, the positions of the bitmaps are adjusted...
Not afraid.
Just past the middle...
The NPCs animate even when off the display.
And at the far end...
The little bear has reached the right-hand end of his world.
The vampires walk slower than the bear. They’ll catch up. And here it is with some of the background and foreground imagery deleted...
Without all the layers, the characters seem to float.
3. Game Audio
Sound is an important part of an immersive experience. I used LMMS to make the sounds in this game, and a proper sound designer could do way better. But it is still pretty good because the sounds can overlap without cutting each other off. On Windows. Probably on Mac if someone with a Mac finds a command line program that can play a sound. Thanks @Jeff_Perkinson for helping me with some Mac audio code. For windows, the AUDIO.jsl file uses RunProgram to run PowerShell which has access to the windows sound system. On Mac it uses a command line sound player, /usr/bin/afplay. You might get JMP to crash if you try to exit JMP before all the playing sounds finish; just let the final progress bar finish playing out all the started sounds.
4. Game Input
This game uses the keyboard as an input device by placing a MouseBox around the Graph Box that holds the game.
New Window( "overlay",
mb = MouseBox(
V List Box(
hlistbox(
leftSpacer=spacerbox(size(5,windowy),color("black")),
gb = Graph Box(
suppressaxes,
framesize( windowx + 1, windowy + 1 ),
X Scale( -1, windowx ),
Y Scale( -1, windowy )
),
The MouseBox forwards keystrokes to a tiny function that remembers the key and the time
lastkey=empty();
lastkeyexpire=tickseconds();
spriteRunner = function({key},{},
lastkey=key;
lastkeyexpire=tickseconds()+1;
);
Elsewhere the key’s action is repeated until it gets stale a second later
While( go,
if( tickseconds() > lastkeyexpire,
lastkey="";
);
…
The keys steer the little bear. They are implemented in the bear’s finite state machine (FSM). An example that changes the bear from idling to walking is in the BEAR.jsl cycle() function:
If( begin,
laststate=m_state;
if( keystroke=="LEFT" & ( pFaceLeft=="faceright" | (pFaceLeft=="faceleft" & pName=="idle") ),
m_state = m_stateWalkLeft;
Begin is true if the current animation cycle is at the beginning, which makes a good place to change states. It looks odd to leave the middle of a jumping animation to an idling animation. It has been a while since I wrote that logic that says the bear should only start moving left if facing left and idle, but if facing right...just start going left? I think that allows instant reversal when walking and maybe prevents a change to the same state which might also be taken care of another way too.
5. Game Play
Making a compelling game is the hard part for me. Telling a story, creating a goal, adding the puzzles. This game is mostly an environment, waiting for a story teller. But I did add one little bit of game play.
,/*else if*/ keystroke=="DOWN" & m_NumRocks<6 & (m_state == m_stateWalkLeft | m_state==m_stateIdleLeft),
m_state = m_statePickLeft;
m_NumRocks = min(6,m_NumRocks+1);
// use SPACE to HIT
,/*else if*/ keystroke==" " & (m_state == m_stateWalkRight | m_state==m_stateIdleRight),
if(m_NumRocks,
m_NumRocks = max(0,m_NumRocks-1);
m_state = m_stateHitRight;
m_RockThrownRightFrom = m_x;
);
Pressing the down arrow picks up rocks; the little bear can hold up to six. Pressing the space bar throws the rock in the facing direction. The bear makes a note of the thrown rock in RockThrownRightFrom or RockThrownLeftFrom which is checked by code in the main loop that can see vampires and the bear.
// check for interaction between bear and vampire
if(!ismissing(bearPlayer:m_RockThrownRightFrom),
if(bearPlayer:m_RockThrownRightFrom<vampire1:getpos()<bearPlayer:m_RockThrownRightFrom+500,
vampire1:m_x = vampire1:getpos()+50;
);
...
Hitting a vampire with a rock knocks it back 50 pixels. (Actually, you can get two vampires with one stone the way I wrote it.) The vampires should probably be in a list that could support more than two. Actually all the NPCs (non-player-characters) should be in a list and should be subclassed from a common base class to make the calls to cycle() in a loop instead of
vampire1:cycle(stagepos);
vampire2:cycle(stagepos);
bearPlayer:cycle(stagepos,lastkey);
but that would just obscure how this simple example works.
There should be two attachments: a project file, and an add-in file. (I can't attach the project to the community right now, but if you install the add-in, there is a copy of the project in the add-in directory.)
Art Credits
The open game art site has a lot more content of varying quality.
You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.