让我们尝试一个项目来管理一些 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 游戏,其中角色从左向右移动,背景可能有多个以不同速率滑动的层,以提供 3-D 深度感。 其他变体也可能是具有垂直滚动尺寸的平台跳线。
有问题需要解决:
- 游戏开发(JMP 可以让这更容易编写吗?)
- 游戏图形(它看起来像什么?)
- 游戏音频(你听到了吗?)
- 游戏输入(如何移动?)
- 玩游戏(有什么事吗?)
1. 游戏开发
我正在使用一个 JMP 项目(并且就在最近,切换到一个 JMP 16 独立项目。)我开始喜欢一个有五到十个独立文件的项目的独立项目。 它具有用于打开文件的选项卡和用于日志窗口的停靠点。 自包含项目将所有内容存储在一个文件中(zip,在封面下),并在打开和关闭时自动解包和重新打包。
让这个游戏运行起来更容易,因为图形文件、音频文件和游戏文件是独立的、相对较小的文件。 这使得查找东西变得很容易,例如管理游戏角色之间游戏的 JSL。 该项目在选项卡中显示文件,如下所示:
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 文件将尝试重新包含当前文件并抛出一个递归包含错误,该错误被忽略,或者报告其他一些错误。 需要放在最后一个的侧滚轮文件将 try 语句作为第一的线。 这样,game.jsl 文件就包含了所有其他内容,然后再运行其余的 sidescroller 文件。
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 运行项目的全部保存命令的方法
2. 游戏画面
该项目使用 JSL 生成的图形和下载的位图图形。 下面,蓝天、山脉、树木和碎石路是由JSL创造的。 岩壁表面是平铺位图,吸血鬼、蝙蝠和熊是动画位图序列。 学分在最后。
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 对 0.5)以处理超宽动画。 吸血鬼和熊在横向卷轴中有自己的图层,它们的透明度可以让更远的图层显示出来。
因为游戏实际上是在一个图表中,所以您可以使用缩放工具查看它以了解图层。 在这里它被缩小了,你可以看到天空从左到右并不比窗户大。 它不滚动。 但是越来越近的层会变得越来越宽,因此它们可以滚动更多。
Normally the sky fills the display, left-to-right. Zoomed out, see the vampire about to enter the stage.通常天空会从左到右填满显示屏。 缩小,看到吸血鬼即将登场。
随着小熊向右走,位图的位置也在调整……
Not afraid.不怕。
刚过了中间...
The NPCs animate even when off the display.NPC 动画即使在屏幕关闭时也是如此。
而在远端...
The little bear has reached the right-hand end of his world.小熊已经到达了他世界的右手边。
吸血鬼走得比熊慢。 他们会赶上的。 在这里,它删除了一些背景和前景图像......
Without all the layers, the characters seem to float.没有所有的层,字符似乎漂浮。
3. 游戏音频
声音是沉浸式体验的重要组成部分。 我使用 LMMS 来制作这个游戏中的声音,一个合适的声音设计师可以做得更好。 但它仍然非常好,因为声音可以重叠而不会相互切断。 在 Windows 上。 如果使用 Mac 的人找到可以播放声音的命令行程序,则可能在 Mac 上。 谢谢@Jeff_Perkinson帮助我处理一些 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="";
);
…钥匙引导小熊。 它们在熊的有限状态机 (FSM) 中实现。 在 BEAR.jsl cycle() 函数中,有一个将熊从空闲变为行走的示例:
If( begin,
laststate=m_state;
if( keystroke=="LEFT" & ( pFaceLeft=="faceright" | (pFaceLeft=="faceleft" & pName=="idle") ),
m_state = m_stateWalkLeft;如果当前动画循环在开始,则 Begin 为真,这是更改状态的好地方。 将跳跃动画的中间部分留给空闲动画看起来很奇怪。 我已经有一段时间没有写过这样的逻辑了 我认为这允许在行走时立即反转,并且可能防止更改为相同的状态,这也可能以另一种方式处理。
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;
);按向下箭头捡起石头;小熊最多可以容纳六个。 按下空格键会朝正对的方向扔石头。 熊记下了扔在里面的石头RockThrownRightFrom或者摇滚乐剩下从这由可以看到吸血鬼和熊的主循环中的代码进行检查。
// 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);
但这只会掩盖这个简单示例的工作原理。
应该有两个附件:一个项目文件和一个加载项文件。 (我现在无法将项目附加到社区,但是如果您安装插件,插件目录中有该项目的副本。)
艺术学分
开放式游戏艺术网站有更多不同质量的内容。