让我们尝试一个项目来管理一些 JSL
很久以前我建议写一个横向卷轴游戏https://community.jmp.com/t5/Uncharted/Wild-Road-Map/bc-p/20996/highlight/true#M5 。 如果你在80年代不玩游戏, https://en.wikipedia.org/wiki/Parallax_scrolling有一些例子。 横向卷轴游戏是 2D 游戏,其中角色从左向右移动,背景可能有多层,以不同的速率滑动,以提供 3D 深度感。 其他变体也可能是具有垂直滚动尺寸的平台跳线。
有以下问题需要解决:
- 游戏开发(JMP 可以让这个更容易编写吗?)
- 游戏图形(看起来像什么?)
- 游戏音频(你听到了吗?)
- 游戏输入(如何移动?)
- 玩游戏(有什么事情发生吗?)
1. 游戏开发
我正在使用 JMP 项目(最近才切换到 JMP 16 独立项目)。我开始喜欢具有五到十个独立文件的项目的独立项目。 它具有用于打开文件的选项卡和用于日志窗口的停靠点。 这个独立的项目将所有内容存储在一个文件中(zip,在幕后),并在打开和关闭时自动解压和重新打包。
让这个游戏运行起来更容易,因为图形文件、音频文件和游戏文件是独立的、相对较小的文件。 这使得查找内容变得很容易,例如管理游戏角色之间的游戏的 JSL。 该项目在选项卡中显示文件,如下所示:
A JMP project with several files.A JMP project with several files.A JMP project with several files.A JMP project with several files.A JMP project with several files.包含多个文件的 JMP 项目。
该项目一打开就有九个文件打开;当前可以看到保存游戏中吸血鬼类定义的文件。 我对何时将文件保存回项目以及何时保存项目不太满意,因此在关闭项目之前我使用左上角突出显示的图标。 我还将日志停靠在左下角。 项目似乎与 JMP 的其余部分以及彼此之间是隔离的,因此它们有自己的日志。
MakeAddin 文件不是游戏的一部分,而是开发游戏的一部分。 如果该选项卡打开并且我^R(运行)它,它将重新生成加载项。 如果我运行另一个选项卡会发生什么? JSL 运行,这可能不是游戏逻辑的开始。 但我总是错误地这样做,所以我像这样修复了它:
- 有一个主文件 game.jsl,其中包含按合理顺序排列的所有其他文件。
- 大多数文件都是独立的,只是创建一个类或一个命名空间。
- sidescroller.jsl 文件需要加载所有其他文件(它是主游戏循环)。
独立档案全部结尾和
try(include("game.jsl"),if(!contains(char(exception_msg),"includes itself through a closed path"),show(exception_msg)));它将在当前文件之后运行 game.jsl 文件。 game.jsl 文件将尝试重新包含当前文件并引发递归包含错误(该错误将被忽略)或其他错误(该错误将被报告)。 sidescroller 文件需要放在最后,该 try 语句作为第一的线。 这样,在 sidescroller 文件的其余部分运行之前,game.jsl 文件就包含了所有其他内容。
game.jsl 文件如下所示
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 )
)
)
);现在,无论编辑哪个文件都可以运行。 除非...如果编辑了多个文件,请务必保存未显式运行的文件! 包含获取磁盘副本,而不是编辑选项卡副本。 但是,如果您只更改一个文件,由于上面设置的包含方式,它将使用当前的编辑选项卡副本,而无需保存。
愿望清单:
- 在运行之前保存所有内容(或者,使用编辑选项卡副本进行包含)
- 运行项目时指定启动文件(不是项目加载文件)
- 记录保存选项卡和保存项目的相同/不同之处
- 从 JSL 运行项目的 save-all 命令的方法
2. 游戏画面
该项目使用 JSL 生成的图形和下载的位图图形。 下面的蓝天、山峦、树木、碎石路都是JSL创造的。 岩壁表面是平铺位图,吸血鬼、蝙蝠和熊是动画位图序列。 学分在最后。
The little bear is idling; the vampires are releasing bats or disappearing.The little bear is idling; the vampires are releasing bats or disappearing.The little bear is idling; the vampires are releasing bats or disappearing.The little bear is idling; the vampires are releasing bats or disappearing.The little bear is idling; the vampires are releasing bats or disappearing.小熊无所事事;吸血鬼正在释放蝙蝠或消失。
RGBA.jsl 文件是 JSL 图形的重要组成部分。 JMP 可以在图形中以透明度进行绘制,并且可以在图形中使用具有透明度的位图,但是没有简单的方法,例如,在透明背景上制作折线图并抓取具有透明背景的位图。 (有一个选项,报告 PNG 图像的透明背景,可以保存除透明图形之外的所有内容。 您可以编辑保存的 SVG 以删除背景,但您需要另一个程序才能将其转回透明背景上的 PNG。)
愿望清单
RGBA 文件创建了一个类,用于创建具有透明像素的位图,其中不绘制任何内容。 它可能需要扩展您最喜欢的原语。 检查叶子不太透明的树木以及叶子之间的间隙。 使用 Path() 的贝塞尔树描述了绘制树木,但不使用 Alpha 通道。 山脉是具有不同参数的山体绘制算法的多层,天空是不透明的蓝到黑阴影矩形。 碎石路的生成速度可能相当慢,但一旦运行就足够快了;每一块砾石都绘制在位图中。
ASSETS.jsl 文件了解游戏使用的位图资源、如何打开它们、如何标准化它们以及如何命名它们。 由于动画是由不同的人绘制的,因此需要标准化才能以相同的方式居中。 该信息保存在这样的结构中
Assets:vampire_images["hit"] = {{"attack/hit_1",.,.,0.80,0.5},该图显示释放蝙蝠的吸血鬼在位图中发生了偏移(0.80 vs 0.5)以处理超宽动画。 吸血鬼和熊在横向卷轴中拥有自己的图层,它们的透明度让更远的图层显示出来。
因为游戏实际上是在图表中,所以您可以使用缩放工具查看它以了解各个图层。 在这里,它被缩小了,你可以看到天空从左到右并不比窗户大。 它不滚动。 但是越来越近的层变得越来越宽,因此它们可以滚动更多。
Normally the sky fills the display, left-to-right. Zoomed out, see the vampire about to enter the stage.Normally the sky fills the display, left-to-right. Zoomed out, see the vampire about to enter the stage.Normally the sky fills the display, left-to-right. Zoomed out, see the vampire about to enter the stage.Normally the sky fills the display, left-to-right. Zoomed out, see the vampire about to enter the stage.Normally the sky fills the display, left-to-right. Zoomed out, see the vampire about to enter the stage.通常,天空从左到右充满显示屏。 拉近镜头,看到吸血鬼即将登场。
随着小熊向右走,位图的位置也随之调整……
Not afraid.Not afraid.Not afraid.Not afraid.Not afraid.不怕。
刚过了中间...
The NPCs animate even when off the display.The NPCs animate even when off the display.The NPCs animate even when off the display.The NPCs animate even when off the display.The NPCs animate even when off the display.即使不在显示器上,NPC 也会有动画。
而在远端...
The little bear has reached the right-hand end of his world.The little bear has reached the right-hand end of his world.The little bear has reached the right-hand end of his world.The little bear has reached the right-hand end of his world.The little bear has reached the right-hand end of his world.小熊已经到达了他世界的右端。
吸血鬼比熊走得慢。 他们会赶上的。 这里删除了一些背景和前景图像......
Without all the layers, the characters seem to float.Without all the layers, the characters seem to float.Without all the layers, the characters seem to float.Without all the layers, the characters seem to float.Without all the layers, the characters seem to float.没有了所有的层次,人物似乎漂浮起来。
3. 游戏音频
声音是沉浸式体验的重要组成部分。 我使用 LMMS 来制作这个游戏中的声音,一个合适的声音设计师可以做得更好。 但它仍然相当不错,因为声音可以重叠而不会相互切断。 在 Windows 上。 如果使用 Mac 的人找到可以播放声音的命令行程序,则可能是在 Mac 上。 谢谢@杰夫·珀金森帮助我编写一些 Mac 音频代码。 对于 Windows,AUDIO.jsl 文件使用 RunProgram 来运行可以访问 Windows 声音系统的 PowerShell。 在 Mac 上,它使用命令行声音播放器 /usr/bin/afplay。 如果您在所有播放声音结束之前尝试退出 JMP,则 JMP 可能会崩溃;只需让最后的进度条播放完所有开始的声音即可。
4. 游戏输入
该游戏通过在容纳游戏的图形框周围放置一个鼠标框来使用键盘作为输入设备。
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 )
),MouseBox 将击键转发给一个可以记住按键和时间的小函数
lastkey=empty();
lastkeyexpire=tickseconds();
spriteRunner = function({key},{},
lastkey=key;
lastkeyexpire=tickseconds()+1;
);在其他地方重复按键的操作,直到一秒钟后变得陈旧
While( go,
if( tickseconds() > lastkeyexpire,
lastkey="";
);
…钥匙控制小熊的方向。 它们在 Bear 的有限状态机 (FSM) 中实现。 将熊从闲置更改为行走的示例位于 BEAR.jsl Cycle() 函数中:
If( begin,
laststate=m_state;
if( keystroke=="LEFT" & ( pFaceLeft=="faceright" | (pFaceLeft=="faceleft" & pName=="idle") ),
m_state = m_stateWalkLeft;如果当前动画周期处于开始处,则 Begin 为 true,这是更改状态的好地方。 将跳跃动画的中间留给空闲动画看起来很奇怪。 自从我写下这样的逻辑以来已经有一段时间了,即熊只有在面向左且闲置时才应该开始向左移动,但如果面向右……就开始向左移动? 我认为这允许在行走时立即逆转,并且可能防止改变到相同的状态,这也可以通过另一种方式来处理。
5. 游戏玩法
制作一款引人入胜的游戏对我来说是困难的部分。 讲故事,设定目标,添加谜题。 这个游戏主要是一个环境,等待一个讲故事的人。 但我确实添加了一点游戏玩法。
,/*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;
);按向下箭头可以捡起岩石;小熊最多可以容纳六个。 按下空格键会将岩石朝正面的方向扔出。 熊记下扔出的石头向右扔石头或者扔石头左边从这是由主循环中的代码检查的,可以看到吸血鬼和熊。
// check for interaction between bear and vampire
if(!ismissing(bearPlayer:m_RockThrownRightFrom),
if(bearPlayer:m_RockThrownRightFrom<GETPOS></GETPOS>
用石头击中吸血鬼会将其击退 50 像素。 (实际上,按照我写的方式,你可以用一块石头得到两个吸血鬼。)吸血鬼可能应该在一个可以支持两个以上的列表中。 实际上,所有 NPC(非玩家角色)都应该在一个列表中,并且应该从一个公共基类派生出来,以便在循环中调用 Cycle(),而不是
vampire1:cycle(stagepos);
vampire2:cycle(stagepos);
bearPlayer:cycle(stagepos,lastkey);
但这只会掩盖这个简单示例的工作原理。
应该有两个附件:一个项目文件和一个加载项文件。 (我现在无法将项目附加到社区,但如果您安装了加载项,加载项目录中就会有该项目的副本。)
艺术作品
开放游戏艺术网站有更多质量参差不齐的内容。