GameMaker Studio 2

创建于:2017-04-19

创建人: dougen

150 信息 607 成员
游戏开发工具 GameMaker Studio 2 的讨论小组
GameMaker Studio 2小组导航 :介绍、愿景、内容
Linpean 2017-10-27

我最近半年开始学习GameMaker Studio 2(以下简称GMS2),向往着变成一名独立开发者,同时也在了解GMS2的引擎操作,阅读F1帮助文档,趴教程。可以说 GMS2 的中文用户很少,百度贴吧关注人数也未破万,资料很不成体系,很多前人的资料和文章都未被人整理,压在不知名的角落,我希望能够帮助其他的中文用户了解GMS2,所以建立一个快速导航,帮助大家快速开始GameMaker Studio 2的学习。

愿景和管理工作

希望大家不要在公开的帖子中留下联系方法或询问他人的联系方式,而是将有关一个话题的讨论都公开在帖子中,如果真的是要私下联系,可以使用私信的功能。我不希望看到的是一个人提出一个问题后通过私下的沟通解决了问题,而关于解决问题的过程并没有留在帖子中,无法给搜索到这个帖子的人提供帮助。

  • 希望大家不要重复发帖,将与同一话题的讨论集中到一个帖子中
  • 希望大家在发表帖子和回复之前先考虑一下措辞,事后发现错误也及时订正
  • 希望大家不要发表无价值的回复,当你只是觉得某一个帖子好的时候,请使用「点赞」功能

管理员的工作主要是维护秩序,维护的主要目标是方便他人检索和阅读信息,希望大家不要感到不满。

  • 修改帖子标题以符合帖子内容,通常会修改几乎所有帖子标题
  • 修改正文和回复中明显的错别字、将代码编辑代码块
  • 将重复的帖子导向讨论更多的帖子,并移除新的重复的帖子
  • 删除重复或无意义的回复,删除不当内容

导航模块

  • 快速索引社区:国内比较大的GameMaker中文社区和推荐的QQ群
  • 求助:提出你在使用过程中遇到的问题,寻找需要的功能和插件,管理员会不定期整理QA内容,防止日经QA
  • 开发:讨论与游戏开发有关的话题,或发布你自己开发的游戏
  • 分享:原创文章、资料汇总、翻译自官网的文章和文档

快速索引社区

  1. Yoyogames官网
  2. GMS2官方文档
  3. GMS2文档红色激情汉化版
  4. GameMaker开发者之家(维护中)
  5. GameMaker百度贴吧
  6. GameMakerStudio2 Wiki(正在创建中)
  7. GameMaker开发者之家QQ群:235271204
  8. GameMaker:Studio2 菜鸟群:102797189
  9. GameMaker:Studio2入门小站

求助整合

  1. 官网如何购买,如何注册?
  2. 国内更新太慢,怎么办?


开发

    待续

[ 分享:入门 ] Make an RPG:开始我们的RPG之旅
Linpean 2017-10-23

文档说明

本系列是油管上的HeartBeast[Beginner] Make an RPG课程的中文笔记,主要形式是截图的方式进行步骤上的说明。

由于原教程是基于GMS1版本的,我这里是用GMS2版本进行制作的,界面和部分函数都有变化,有错漏的地方,请参考原视频和官方帮助文档。接触GameMaker时,苦于国内没有完整的一个RPG教程,诺娃上的青铜的幻想GameMaker: Studio 中文教程可惜因为作者的工作,没有持续更新下去。而红激的教程也是以FC上的小游戏为切入点,只好上油管,HeartBeast的教程很丰富,有平台跳跃、射击、也有角色扮演。

面向对象:GameMaker新手,以学习一门脚本编程语言,制作一个RPG游戏为目标的爱好者。

主要包括以下内容(蓝色标识已完成,目前进度 :18/30):

相关参考资料:

  1. GMS官方说明文档
  2. [Beginner] Make an RPG--HeartBeast
  3. GameMaker: Studio 中文教程--青铜的幻想
  4. GMS2官方中文教程系列--顺子
sprite_duplicate() 无法正常工作
notlsd 2019-05-22

有人遇到相同的问题么?

【译】浮岛物语(FORAGER): 在 GameMaker 中做优化
顺子 2019-05-20

原文地址:Forager: Optimization In GameMaker | Blog | YoYo Games
关于作者: 一年多以前,Gabe “lazyeye” Weiner 发现了开发游戏的乐趣,这种乐趣迅速转化成激情并促使他从大学辍学开始全职进行游戏开发。之前他发布了一系列游戏工具和一些 Game Jam 作品, Forager(浮岛物语) 是他第一个商业作品。你可以关注他的 Twitter来跟进他的日常工作情况,也可以在 Steam 上直接购买 Forager(浮岛物语) 。


Forager 是如何管理成千上百个实例的

有些时候,你很幸运有机会在项目启动时就参与其中,这样可以对你的代码库有更全面的了解和掌控。但另一种情况是,你需要接手一个比较复杂的,将近 50000 行代码的项目然后被告知“搞定这个问题”。嗨,我是 lazyeye ,Forager的程序负责人。

在我成为这个游戏的程序负责人之前,我就定下了一项任务:优化。Forager 是一个规模庞大的游戏,玩家可以在巨大的地图上收集资源并制作各种装置,这意味着游戏中很容易就会出现 5000 个实例同时运作的状况,甚至更多。这种特殊的游戏类型会遇到一个常见的优化难题——玩家具备创建几乎无限实例的能力,而我们还要保证游戏可以在所有的平台上顺畅运行。

实例数量过多是 GameMaker 制作的游戏中常见的一个问题;几乎在所有我参与过的项目中,过多的实例数量都是给性能拖后腿的一环。通常人们会陷入使用对象而不是去学习更有效率的工具的陷阱,比如:粒子系统(particle systems),数据结构(data structures),素材图层(asset layers)等等。了解如何使用这些方法应对不同场合其实非常重要,因为这样可以提升性能并让一些复杂的工作变得更加简单。比如某些物体不会移动,也不需要根据深度进行排序,也没有任何碰撞事件,那就不该用对象来实现它。

但是,Forager 很不一样,它与我见过的大多数项目不同,游戏中几乎每一个物体都应当以实例来实现,并且尝试不用实例来完成类似的效果简直是一场噩梦。这让人非常沮丧,这让我意识到我必须卷起袖子然后跳出这个框架重新思考一下。

Image title

我说的大量实例……是真的超级多!

绘制优化

因此,我们坚持使用了实例。但是仍然有许多方法可以改进每一帧运行的代码性能。性能下降通常源于对绘制方法的工作原理不够了解。不幸的是,这个问题对我而言非常超纲(我并不是专家),但一些基本的理解还是有助于避免一些常见的问题。

你之前可能听说过“指令集中断(batch breaks)”,一个指令集(batch)指 GameMaker 发送给 GPU 的一组用于绘制画面的指令。GPU 在绘制成组内容时非常高效,但当发送到 GPU 的指令集过多时,在指令集直接切换的时间就越多,从而影响了性能。每当更新 GameMaker 的绘制配置时都有可能发生指令集中断,比如在修改颜色/Alpha,字体,混合模式等时,以下是一些常见的“指令集断路器

  • Primitives (draw_rectangle, draw_circle, draw_primitive_begin, etc.)
  • Surfaces (surface_set_target, surface_reset_target)
  • Shaders (shader_set_target, shader_reset_target)
  • Draw settings (draw_set_font, draw_set_colour, etc.)
  • GPU settings (gpu_set_blendmode, gpu_set_alphaenable, etc.)

这种指令集中断是难免会遇到的,不同平台的处理方式也有差异。而关键是要仔细构建代码尽量减少这些中断的发生。通常情况下,我们可以通过将类似的指令集组合到一起来进行优化。比如,与其让大量实例各自采用以下绘制事件(Draw Event):

// Outline
shader_set(shd_outline);
draw_self();
shader_reset();

你可以尝试在一个控制器对象中统一完成此类操作

// Instance outlines
shader_set(shd_outline);
with (par_outlined) {
  draw_self();
}
shader_reset_target();

深度顺序的差异时常会影响这个操作,有时候会使得画面看起来异常。你可以使用图层脚本来进行辅助,更精确地控制图层绘制的先后顺序从而进行优化。

步(Step)事件优化

优化步事件需要不断扪心自问:“这个操作需要每一帧都执行吗?”通常情况下,我会反复思考这个问题几次,刚开始答案总会是“没错,这个操作必须每帧都执行”,但可能到了第八次时就会变成“哦,那个法子可能行得通。”

实例需要每一帧都从全局的数据结构里获取信息吗?也许你在创建(Create)事件里执行一次就够了。你会在步事件里不断刷新玩家的背包吗?也许只需要当发生添加或删除操作时更新一下就行了。没有以上状况需要处理?也许根本没人在意这些代码是不是隔几帧执行一次。

还有一个小技巧是利用 GML 的短路(short-circuiting )特性。这是 GML 里当获得一个 FALSE 后决定是否停止继续执行后续代码的机制。 比如以下这个例子。

if (1 + 1 == 3) && (instance_place(x, y, obj_enemy)) {
  // stuff
}

由于 1+1 不可能等于 3,因此 GameMaker 根本不会去判断 instance_place的调用。因为判断条件里使用了&&因此两个条件必须都为 TRUE ,因此当第一个条件为 FALSE 时则整个条件不可能返回 TRUE。因此你可以在制定判断条件时控制好顺序,如果你确定某个条件比其它的都重要,那务必把它放在最前面!另外要注意哪些条件最容易返回 FALSE ——当你前五个条件几乎总是 TRUE ,后面跟一个经常出现 FALSE 的条件时,那前面那些判断就都是在浪费时间。

更进一步:动态加载实例

这些优化机制都很棒,但是对于浮岛物语而言,我们真正需要的是一个“宏观”解决方案。作为一个优化者,你的工作是骗过玩家,让他们以为某件事情正在发生即可,而实际上可以把很多东西藏到幕后去处理。这种技巧是优化的基础:再聪明的玩家也想象不到游戏里的处理机制,而只能基于自己看见的内容进行理解。

虽然我们已经确保了所有的实例都是必须的,但这不意味着每一个实例都是那么关键。以“objTree”为例,当玩家和它们发生交互时,树木需要处理深度排序,具备碰撞效果,还有各种视觉效果。但是,如果玩家必须靠近树木才能进行交互,那我们在 95%的状况下可能都不用去实例化这棵树。如果一棵树在森林里消失了,但玩家并没有站在边上看到这一切,他们会在意这些吗?

这需要让我们的动态加载系统来进行处理了。如果玩家看不见某个实例,我们就可以将其停用。如果玩家移动到可以看到该实例的位置,我们可以在它出现在视野里之前就迅速激活它。下面这个 GIF 图再现了这个过程——白色矩形表示了我们的视野区域,在该范围以外的实例会被临时禁用进行动态加载处理。

Image title

注意边界位置

一些实际代码

下面这个CullObject的脚本代码,就是用来在步事件中检测活动的实例是否需要被暂时禁用

Image title

所有的截图代码的高亮风格都基于TonyStr的 Dracula 主题

我们把需要动态加载的对象作为参数传入,来检测该实例的图像是否在视野之外。如果在视野外,我们创建一个数组来保存这个实例的 ID 和边界框(Boundary box ),然后把这个数组塞进记录停用实例的列表里。要注意这里的边界框是基于精灵图像缩放绘制的尺寸,而不是基于实例的碰撞盒。我们添加了少许的缩放,因为浮岛物语中偶尔会基于某些变量绘制稍小尺寸的图像。

下一个脚本——ProcessCulls用于把停用的实例“恢复

Image title

注意:浮岛物语中实际使用的这些脚本中会有更多内容,但是为了方便演示,我把这些脚本都脱水只保留了核心代码

这个脚本里我们只是处理停用实例列表,检测相机视野是否移动到了能展示出某个实例的位置,如果是就立刻激活该实例并将相关数据从列表中删除即可。

等一下,我搞砸了

当我把以上修改代码推送到代码库后不久,我想到“嗯,我需要知道游戏里有没有用到instance_existsinstance_number和其它实例函数,这些代码可能会因为实例被停用而出纰漏”

我立刻搜索了instance_findinstance_exists和 instance_number这些关键词,然后看到了 500 多行结果。

糟糕…

这个状况非常棘手——这些函数将无法返回正确的结果,因为这些函数只能作用于激活状态的实例。如果游戏中一直在使用这些实例函数处理相关逻辑……那在动态加载中被禁用的实例就会出大问题。

但我没有放弃,我决定在动态加载系统中再增加一层判断。我需要一个方法来检测实例是否存在,无论这个实例是否处于激活状态。我还需要获得所有这些实例的准确数量并能快速检索它们。处理instance_exists函数将是一个挑战,这个函数我们可以传入三种类型的参数—— ID,对象名称或者父对象名称

真·实例 函数

第一步是在创建一个可以动态加载的实例时把它添加到一个全局的数据结构里:

Image title

我们把这个实例的 ID 添加到我们的实例缓存列表中。接下来,我们遍历所有可能的父对象 ID 数组,检查该实例是否是某个父对象的子对象。如果是,我们把它的 ID 也添加到这个父对象的实例列表中。这是因为 GameMaker 中,当我们把一个父对象属性的实例作为参数传入某个实例函数时,这会影响到这个父对象所有的子对象,因此我们的脚本中同样需要实现这一点。

接下来,我们需要在销毁实例时从缓存中释放掉相关资源,因此在清理事件(Clean Up)中需要以下内容:

Image title

这跟之前的过程刚好相反

现在我们的实例已经创建好了,我们需要准备好我们用来替换的函数。

Image title

你可能已经注意到了,在所有这些函数中,同时兼容了不属于我们用自定义的“真·实例”系统所处理的实例函数。请记住,在程序中有 500 多处相关代码,因此我试图尽可能节省时间。可以快速进行查找和替换这一点至关重要。

Image title

现在我们可以看到这个激活变量的用途——这样一来我们就可以在结果返回之前就确认这个实例是否处于激活状态,如果不是,那就可以将其激活并在临时激活列表中加以记录。但是我们并不会将其真正激活,这样我们就可以很好的区分出真正被激活的实例和那些临时激活的实例。

在返回结果之前临时激活实例是很有必要的,因为这样就可以在实例被恢复之前就通过代码来修改其中的一些值。在 GameMaker 中被停用的实例只能从中读取值而不能直接写入修改。而在一个理想的系统之中,这种激活状态应该是可选的,但因为我需要保持参数格式不变以兼容系统原有的实例函数,因此我把激活状态设为必要的前置条件了。

终于,我们在TrueInstanceExists可以通过检测传入参数是否大于 100000 来确定这是不是一个实例或对象 ID ,而TrueInstanceFind可以用来确保在返回某个实例之前先将它激活。

Image title


最后,我们必须停用掉我们临时激活的实例,这里会有一个小问题—— GameMaker 在每一个步事件(step)和绘制事件(draw)之间会重建事件队列。这意味着我们必须确保我们的控制器对象在每个事件里都运行以下脚本,否则我们可能会遇到某个实例在不恰当的时机试图触发它的某个事件。

Image title


再快一点

回想一下我最开始说的,我们必须反复质疑我们步事件中的代码是否需要每一帧都运行?这同样适用于此——宏(macro)。SYSTEM_CHECK_INTERVAL,它控制着我们的系统级脚本(比如动态加载机制)的运行频率。其中的脚本会根据特定系数来进行调控,比如,在控制植物生长的系统脚本中,增长值将根据我们的间隔系数进行增长。如果系统每 20 帧刷新一次,则增长值将以 20 为单位进行增长 。我们把需要控制的脚本放进一个 switch 循环,这可以把这些操作平均分配到 20 帧里去。

Image title


结语

Image title

游戏优化是个极其庞大的话题,我们在这里只是讨论了其中的一部分内容。实例数量是影响大部分项目的一个方面,在 GameMaker 引擎里,正确理解纹理渲染的机制和其它一些游戏开发过程中的问题,对于优化游戏至关重要。

但是,类似“不要太早优化你的游戏”这种建议仍然非常有道理。在你明确自己的游戏有性能上的问题之前,没必要过分担心性能问题,并因此妨碍了开发进度。事实上,GameMaker 对于用它制作的那些游戏而已非常合用——大部分开发人员永远都不需要担心这些问题。

也就是说,如果你最后做了一个体量巨大的游戏而面临重大的性能问题,我支持你充分调动智慧和好奇心,进行各种试验和研究。

或者,你懂的,来找我吧 ;)

(转发自:原日志地址
北京独立游戏团队诚寻美术同伴!有追求的小伙伴请随时勾兑~
ethliu 2019-03-13

坐标北京,团队配备有资深策划一名、美术策划剧情全能的程序一名、发行商务一名、兼职美术好友若干

目前寻求可全职一起讨论和工作的美术伙伴一名,有偿~

我们做游戏的目的是追求优质的游戏体验,欢迎有同样追求自我追求的美术小伙伴前来勾兑勾兑!!

随时联系我都可以~ QQ: 139103270

译:[GMS2] SEALS OF THE BYGONE中随机生成平台游戏关卡的方法
highway★ 2019-02-23

作者:Logan Foster
译:highway★

Image title

游戏体验的多样性是Roguelite游戏的支柱之一。算法生成的关卡,从平台游戏的角度来说,存在很多挑战,对于刚入手GMS的新手来说也比较复杂。比较折中的方式是将预制的关卡区块链接组合在一起,下面是我在Seals of the Bygone中制作随机关卡的方法,用到了GMS2的图层系统。


摘要

从本质上说,我们要为关卡创建一些不同的区块(section)。每个区块包含子区块(subsections),其中包含我们的资产(asset)。通过使用代码来动态切换子区块的开关(on/off),我们就可以随机关卡了。(译注:当然这不如dead cells那种算法加预制的随机方法效果好,不过对于刚入坑这里的朋友来说应该也是种不错的选择,而且在区块中来控制关卡设计,也相对纯算法容易一些,比如做risk of rain类型的游戏,应该是很够用了)

Image title

我们没有在代码或外部资源中存储关卡区块信息,而是在Room Editor中创建它们,并存储在图层中,就跟做其他关卡一样。这也是我创建这个系统的主要原因,我想保留现有的设计流程,该流程基于Room Editor。


配置

第1步:创建父区块

首先,我们要为每个主要结构创建空白资产层。区块的数量并没有限制,在下面的示例关卡里,我们创建了3个区块,名为"Section1","Section2","Section3"。这些资产层基本上就相当于结构文件夹,也可以在代码中引用。

Image title

第2步:创建子区块

现在,创建子区块的其他空白资产层。这些图层将保存我们构成关卡的tile和object。这里我们将其命名为"Sub1_1","Sub1_2"等。


第3步:子区块的图层

最后,我们将构成关卡的tile、asset和object层也加入到这些子区块中。上图中,子区块"Sub2_1"是打开的,并存在"Collision2_1"和"Tiles2_1"这类图层。这些就是我们构建关卡要用到的层。

Image title

现在我们完成了所有设置,你可以切换父组的可见性,以便查看子区块的不同组合。我建议你在完成设计子区块后使用锁定功能,以确保不会意外的编辑它。


代码

我们已经搞定了关卡区块,并存储在不同的层中,现在可以来试试在关卡开始的时候随机选择哪些层了。在Room Start事件中敲下面的代码。

Image title

首先,我们定义要用于区块和子区块的前缀。我们将使用它们的名字来找到我们要用的层,请确保这些命名与你在Room Editor中的层相同。然后我们循环查看有多少不同的区块。

Image title

现在我们知道有多少区块了,遍历每个区块并查看有多少个子区块。然后选择一个随机的子区块来使用。

Image title

在决定使用哪个子区块之后,我们循环并销毁其他所有子区块(在该区块内)。最后我们通过设置层的可见性进行清理,以确保一切正确。如图,这里的图层名称是"硬编码"的,所以你需要将它们更改为你的区块包含的内容。我们可以动态的执行此操作,但这里没什么必要。

结论

完事儿了,很简单的解决方案,可以为你的关卡增添一些随机性。如果你想做一些更强大的东西,可以试试用layer_xlayer_y移动一些东西来扩展这个系统。

如果你有什么问题,可以随时联系我@rologfos
谢谢阅读!




2/23/2019
H

(转发自:原日志地址
求助帖,200毫秒内双击两次方向键进入奔跑状态
陈康 2019-01-21

部分步事件代码
/// @description 人物状态更新
#region 按键输入
CtrlArr[PRO_CTRL.Null] = keyboard_check(vk_nokey);
CtrlArr[PRO_CTRL.Left] = keyboard_check(vk_left);
CtrlArr[PRO_CTRL.Right] = keyboard_check(vk_right);
CtrlArr[PRO_CTRL.Up] = keyboard_check(vk_up);
CtrlArr[PRO_CTRL.Down] = keyboard_check(vk_down);
CtrlArr[PRO_CTRL.Fight] = keyboard_check(ord("X"));
CtrlArr[PRO_CTRL.DeputyFight] = keyboard_check_pressed(ord("Z"));
CtrlArr[PRO_CTRL.Elude] = keyboard_check_pressed(vk_space);
CtrlArr[PRO_CTRL.DeputyAction] = keyboard_check_pressed(ord("V"));
CtrlArr[PRO_CTRL.Jump] = keyboard_check_pressed(ord("C"));
CtrlArr[PRO_CTRL.Rest] = keyboard_check_pressed(ord("E"));

#endregion


JumpTrigger = CtrlArr[PRO_CTRL.Jump];//跳跃触发
IsTop = place_meeting(x,y-1,obj_land);//头顶
IsGround = place_meeting(x,y+1,obj_land);//地面
#region 垂直状态判定
switch (InitializeYState)
{
case PRO_YACTION.Stop :
if (!IsTop && JumpTrigger && (JumpMin < JumpMax))//跳跃按键和跳跃次数
{
InitializeYState = PRO_YACTION.Skip;
}else if (!IsGround)
{
InitializeYState = PRO_YACTION.Fall;
}
break;
case PRO_YACTION.Skip :
{
InitializeYState = PRO_YACTION.Rise;//上升状态
}
break;
case PRO_YACTION.Rise :
{
if (IsTop)
{
InitializeYState = PRO_YACTION.Stop;
}else if (!IsTop && JumpTrigger && (JumpMin < JumpMax))//按下跳跃且跳跃次数大于可跳跃次数
{
InitializeYState = PRO_YACTION.Skip;
}else if (Vsp >= 0)
{
InitializeYState = PRO_YACTION.Fall;//Y向上大于0,大于零在空中
}
}
break;
case PRO_YACTION.Fall :
{
if (JumpTrigger && (JumpMin < JumpMax))//按下跳跃且跳跃次数大于可跳跃次数
{
InitializeYState = PRO_YACTION.Skip;
}else if (IsGround)
{
InitializeYState = PRO_YACTION.Stop;
}
}
break;

}
#endregion


LeftHold = CtrlArr[PRO_CTRL.Left];
RightHold = CtrlArr[PRO_CTRL.Right];

SquatHold = CtrlArr[PRO_CTRL.Down];

LeftMeet = place_meeting(x-1,y,obj_land);
RightMeet = place_meeting(x+1,y,obj_land);

#region 水平状态判定
switch (InitializeXState)
{
case PRO_XACTION.Stand :
{
if (!LeftMeet && LeftHold && !RightHold && IsGround )//左边没碰到仅左键按下
{
InitializeXState = PRO_XACTION.LeftRush;
......
夜太深,脑子转不过来.如果有志同道合的人士,请赐教

下蹲移动和跳跃算是共存了

左右移动,C跳跃,下蹲,X攻击.问双击方向键的想法分享.

菜鸟学到的吐血教训:别写类似ds_list[| 0] = [ds_map1,"aaa",0]这样的代码……
Rusty 2018-12-02

因为在做一个带有卡牌要素的游戏,用ds_map和ds_list管理卡牌信息、牌序等等十分方便,于是当时就随手写出了类似ds_list[| 0] = [ds_map1,"aaa",0]这样的代码。现在想写存档系统了,结果一看教程,懵逼了。如果需要通过json_encode这种函数将ds数据转化为json,同时又是ds_map中含有ds_list或者ds_list中含有ds_map这种情况时,需要先使用ds_list_mark_as_map、ds_map_add_list等函数让ds_map和ds_list可以被正确写入和读取。而如果是前面这种ds_list含数组,数组里又含ds_map的形式,ds_list_mark_as_map、ds_map_add_list等函数貌似就不适用了,转成json来存档读档就会出错。于是乎不得不把ds连同相关的自定义function也改一遍……不幸中的万幸就是这部分代码本来就是刚学GMS的时候写的,也该优化了……

总之给同为新手的朋友一句话:如果想用json的格式来保存存档信息,就千万别因为一时方便就写成类似ds_list[| 0] = [ds_map1,"aaa",0]这样的代码,可以写成ds_list[| 0]=ds_map1; ds_map1[? "string"]="aaa"; ds_map[? "number"]=0;这样,把"aaa"、0这类写在ds_map里,才能正常用json函数创建/读取存档文件……

不过也可能有把ds_list[| 0] = [ds_map1,"aaa",0]这种函数正常转写成json的方法,只不过我太菜没注意到……

总之,供各位朋友参考了……含泪发布。

(转发自:原日志地址
【求助】steam版本的GMS2,代码中的中文显示不正常
慢好多拍 2018-11-20

Image titleImage title

如图中显示,steam版本的GMS2,中文在代码中显示为“?”,字体设置里面的中文也被显示为“?”,排除是输入法的问题。之后我即便整个汉化之后,问题还是存在,不知道有什么解决办法吗?非常感谢!


GAMEMAKER STUDIO 2中实现2D动态光影效果(PART 2)
顺子 2018-11-15

教程原文:Realtime 2D Lighting in GameMaker Studio 2 - Part 2

翻译:顺子

在上一篇文章中,我想您展示了如何创建 2D 光影系统的基础知识,本文将继续沿用上一篇中的示例,我们将进一步完善它使其更像真实的点光源。我们还将介绍一些着色器的使用以便于实现这一效果——没错,使用着色器会更方便!——然后让光源也具备颜色,让我们先来看看上一次我们做到哪儿了。

Image title

在上一篇文章最后,我们说接下来的任务是让光线半径以外所有的区域都变黑,因为这更像一个“点光源”,因此我们先来实现这个效果。在我们真正了解这个机制之前,我们先把之前绘制的阴影都放到一个表面(surface)上,因为这么做更方便后期使用着色器进行处理。请注意,以下的变化将穿插于不同的脚本之间,因此可能会有一点跳——让我们开始吧!在光源对象的创建事件中我们要添加一个新的变量:

surf = -1 ;

然后在绘制事件的顶部添加以下内容,这将会在首次运行绘制事件时创建一个新的表面——如果这个表面不见了也会重新创建。

if( !surface_exists(surf) ){
    surf = surface_create(room_width,room_height);
}

接下来我们找到检测 tile 的循环,并且在vertex_begin()之前,我们将该表面设置为当前表面,这样我们的阴影就会被渲染到这个表面上而不是屏幕上。我们将修改我们的循环代码,它现在看起来像这样......

surface_set_target(surf);
draw_clear_alpha(0,0);

vertex_begin(VBuffer, VertexFormat);
for(var yy=starty;yy<=endy;yy++)
{
    for(var xx=startx;xx<=endx;xx++)
    {
            // Shadow volume creation. 
    }
}
vertex_end(VBuffer);    
vertex_submit(VBuffer,pr_trianglelist,-1);
surface_reset_target();

draw_surface(surf,0,0);

现在,除了我们修改了阴影渲染的表面以外应该跟之前没有任何变化,你可能已经注意到了在代码中我们把这个表面设置为黑色并且 alpha 为 0。这一点非常重要,这样做我们才能把这个表面混合显示,然后使得没有阴影的区域正确显示出地面来。

OK,现在进入真正有趣的部分!我们现在要创建一个着色器(shader),并让它来处理这个新的表面。同样,目前来看这一切都没有任何的不同。我们在资源树中着色器一栏单击右键并选择“创建”,这将会自动添加一个新的着色器并打开编辑窗口。这将为我们自动生成一个简单的默认着色器,我们要做的就是在绘制先前那个表面之前先设置这个着色器,然后在绘制完表面后进行重置。这个操作(假设你调用了你的着色器 shader0)将只影响中间调用 draw_surface() 的部分。

Image title

shader_set(shader0);
draw_surface(surf,0,0);
shader_reset();

照旧,目前运行起来还是跟之前没有任何区别,但是我们现在可以做一些有趣的事情了!首先我们需要传递几个用户自定义的值,直接传给我们绘制灯光的事件。我们把这段代码添加到 Fragment Shader 中 void main() 的上方。

uniform vec4 u_fLightPositionRadius;        // x=lightx, y=lighty, z=light radius, w=unused

接下来,我们需要把场景的坐标从 Vertex Shader 传递到 Fragment Shader 中。这很容易,因为之前我们在场景中哪些坐标绘制阴影都已经保存下来了,只需要把这些坐标传递过去就行了。为此我们需要另外为 Vertex shader添加一个新的值,如下所示:

varying vec2 v_vTexcoord;
varying vec4 v_vColour;
varying vec2 v_vScreenPos;

然后在 Vertex Shader 脚本的底部“}”前添加

v_vScreenPos =  vec2( in_Position.x, in_Position.y );

你还需要把“varying vec2 v_vScreenPos;”这一行放进 Fragment shader 里。现在 Fragment shader 已经有了一切要素,只需要把光源的坐标传到绘制事件中即可。为此我们需要获取光源对象的创建事件中 u_fLightPositionRadius 变量的值。我们要在最开始就去处理,因为这个过程实在不够快,因此如果我们在一开始就去处理,在后面真正要用的时候就会更高效一些。让我们在光源的创建事件底部添加以下内容:

LightPosRadius = shader_get_uniform(shader0,"u_fLightPositionRadius");

现在我们可以直接在光源的绘制事件中使用它了,应当在我们设置 shader 以及绘制阴影表面之间

shader_set(shader0);
shader_set_uniform_f( LightPosRadius, lx,ly,rad,0.0 );
draw_surface(surf,0,0);
shader_reset();

现在,我们运行一下程序——没错,看上去还是没有任何变化——但至少要确保能正常运行!

到此为止,我们已经可以在 fragment shader 中使用这些额外的信息。包括获取场景坐标(v_vScreenPos)并计算其到光源的位置(u_fLightPositionRadius.xy)的距离,然后检查这个距离是否大于光照半径,如果不是则输出黑色像素。你还可以继续增大光照半径(绘制事件中的 rad变量 256 左右)。让我们来看看我们如何在 Fragment Shader 中使用它,以下是 shader 全文:

varying vec2 v_vTexcoord;
varying vec4 v_vColour;
varying vec2 v_vScreenPos;


uniform vec4 u_fLightPositionRadius;        // x=lightx, y=lighty, z=light radius, w=unused

void main()
{
    // Work out vector from room location to the light
    vec2 vect = vec2( v_vScreenPos.x-u_fLightPositionRadius.x, v_vScreenPos.y-u_fLightPositionRadius.y );

    // work out the length of this vector
    float dist = sqrt(vect.x*vect.x + vect.y*vect.y);

    // if in range use the shadow texture, if not it's black.
    if( dist< u_fLightPositionRadius.z ){
        gl_FragColor = v_vColour * texture2D( gm_BaseTexture, v_vTexcoord );
    }else{
        gl_FragColor = vec4(0.0, 0.0, 0.0, 1.0);
    }
}

如你所见,这是一个非常简单的计算,但是会给我们一个很棒的效果,可以很好的区分出点光源,并将光照半径之外的所有区域都变成黑色。

Image title

看这效果多酷!移动鼠标时我们可以发现所有的投影都会跟随我们鼠标而移动位置。我们还可以进一步提升效果,只需要略微降低ALPHA 值使画面更透明一些即可,只需要在 Fragment shader 最后的“}”之前添加以下内容

gl_FragColor.a *= 0.5 ;

这么做是为了获取最终的 alpha 值(在我们的示例中是在影子区域为 1.0,其它区域则为 0.0),降低透明度就会得到以下画面


Image title

在上面你可能会注意到,当我计算出 vect 变量时我调用了 * vec2 (xx,yy)* 。我这么做是为了让它更清晰,但实际上这并不是必须的。着色器作用于向量,这意味着我们可以同时执行多个操作。这条线应该是这样的:

vec2 vect = v_vScreenPos.xy-u_fLightPositionRadius.xy;

这么做是为了能在一个操作中获取屏幕上某个点的 xy 坐标并减去光源的坐标 xy 然后直接存储到 vec2 中。如果你能明白这一点,就使用这个新的语句,如果不明白就还是用原来的写法,这对这个着色器而言没什么区别但在其它某些着色器中可能区别就大了,你可以试着去尝试一些新的东西,这么做可以让你的代码更精炼,启动更快。

接下来我们要添加一些颜色,但在此之前,我们想要让光线的边缘变得不那么硬,而是一个渐渐削弱的效果。毕竟在现实中除非你非常靠近光源本身,否则你看不到这么强烈的光线边缘,事实上光线会逐渐消失,所以我们在这里添加这样的效果会使得整体效果更好。我们要让光线随着光照半径线性削弱,如果你足够聪明,可以在这里使用曲线或类似的方法来实现。这个衰减的计算非常简单,实际上我们已经拥有了所有需要的值。跟光源的距离以及光照半径就是我们所需要的所有数值。目前我们已经有一些简单的代码是在光照半径内去调节 alpha 值在 0.0 到 1.0 之间的缩放 (光照半径内外),因此现在我们用 LERP(线性插值)来处理阴影的值来完成整个阴影的渲染绘制,以下是 Fragment shader 中的内容:

//在范围内使用阴影纹理,否则全黑。
if( dist< u_fLightPositionRadius.z ){
    // 从光源中央往半径将值从 0 变为 1
    float falloff = dist/u_fLightPositionRadius.z;          
    // 获得阴影纹理
    gl_FragColor = v_vColour * texture2D( gm_BaseTexture, v_vTexcoord );        
    // 阴影线性变为全黑
    gl_FragColor = mix( gl_FragColor, vec4(0.0,0.0,0.0,0.7), falloff);          
}else{
    // 超出半径的直接全黑
    gl_FragColor = vec4(0.0, 0.0, 0.0, 0.7);
}

着色器中的 LERP 方法名为 mix(很奇怪),但事实上就是一个东西。这个值会根据衰减值逐渐从 gl_FragColor 混合(个人猜测)处理成 0,0,0,0.7 (全黑)。当你从脚本尾部删除 gl_FragColor.a *= 0.5;以后,这就会呈现出一个很棒的衰减效果,更像真实的明暗处理。

Image title

接下来我们要添加颜色。首先,我们双击场景中的实例,并将颜色设置为绿色,如下所示:

Image title

完成这个操作后,再修改我们绘制表面的光线绘制事件,这样我们可以把颜色传进去,比如下面这样:

draw_surface_ext(surf,0,0,1,1,0,image_blend,0.5);

这将会把绿色(以及 0.5 的 alpha 值)传递给着色器,然后我们就可以用来设置光线颜色。接下来我们需要修改 Fragment shader 的代码让着色器也处理颜色。所以我们之前写着**gl_FragColor = v_vColour * texture2D()**的地方现在要改成:

    vec4 col =  texture2D( gm_BaseTexture, v_vTexcoord );
    if( col.a<=0.01 ){
        gl_FragColor = v_vColour;           
    }else{      
        gl_FragColor = col;
    }

我们检测 alpha 值(col.a 的值),当它为 1.0 时则为全黑阴影区域,0.0 则为非阴影区,所以这是个非常容易理解的数字。我们也没有必要拿这个值跟 0.0 进行精确比较,因为我们永远也不能确定这个值到底是多少,所以我们需要预留一点误差空间。现在我们运行程序的话,你会看到一道可爱的绿光!(译者:要想生活过得去,嗯~)

Image title

要想点亮多个光源,你只要重复以上操作即可,混合区域的处理会稍微有些复杂(即如何让黑色区域和着色区域保持本色),但这才是核心关键。如果你找不到正确的混合模式,那就把所有的内容传递给着色器并让它来帮你处理一切,记住默认的 Application Surface 也只是一个表面,同样可以用着色器来渲染。另外要记住你没法同时读取并写入同一个表面,因此你可能会需要一个临时表面。

终于……你做到了,你已经完成了一个属于自己的实时光影系统!在下面的图里你可以看到看到我把一个光源穿越多个动态光源的效果。点击此处可以查看相关效果的视频,其中使用的就是与我介绍的类似的系统,尽管我还做了一些更复杂的处理(比如在单个表面上绘制多个阴影),你当然没有必要去尝试并把这些东西复制到你自己的光影系统里,你的系统已经能够实现各种基础的功能了。

译者:最后这个图的效果,光照着本教程是无法完成的,算是原作者留的课后思考题吧,另外自己按照教程实现了一个小示例——网盘下载:密码:bymz,仅供参考

Image title


(转发自:原日志地址
【译文】GAMEMAKER STUDIO系列:简单状态机
highway★ 2018-11-15

作者:Nathan Ranney

翻译:highway★

Image title

按照设计,状态机一次只能处于一种状态。 由我们来定义对我们的情况有意义的状态,以及它们之间的关系。 在本文中,我们将使用状态机来控制在任何给定时间可用的玩家操作,允许我们设置角色并定义角色可以执行的操作。


大家好, 今天我想告诉你如何设置一个简单的状态机。 状态机是一种数据结构,顾名思义,它跟踪不同的状态。 例如,我们的游戏可能有三种状态:“游戏运行”,“游戏暂停”和“游戏结束”。我们可能会使用状态机来记住哪一个处于活动状态,并定义如何从一个状态转换到另一个状态(请参阅 上面的图片)。

基础设置

此条目需要比上一个篇文章(绘制精灵)多一些动画,因此在开始之前,你需要将这些动画添加到工程中。 从此链接下载精灵并将其添加到你的项目中。 我已经恰当地命名了文件,因此只要确保精灵的名称与文件名相匹配,就可以将其添加到GMS中。 继续添加所有精灵 - 甚至是敌人的精灵 - 因为我们将在以后的文章中需要这些精灵。 确保每个精灵的原点是(16,32)。

枚举,控制器和持久性

为了设置我们的状态机,我们首先确定哪些状态是可能的,以及我们如何在代码中识别它们。 由于这个例子都是关于角色动作的,所以让我们定义那些动作是什么,并给每个动作一个整数id。 最简单的方法是使用enum(枚举,Enumeration的缩写),它是在自定义变量类型下保存的常量的集合。 如果你熟悉Gamemaker中的Macros(宏),那么枚举就是这样的。 我更喜欢使用枚举,因为它们比宏更容易管理和跟踪。

创建一个脚本并将其命名为enum_init。 添加以下行。
//states enum states 
{     
    normal,     
    crouch,      
    attack,     
    hit 
}

请注意,我们不必设置每个条目的值,枚举自动将值0指定为“正常”,然后在每个条目后递增。 我们可以随时获得值

var example = states.attack; 
show_message(string(example));  //output: 3

实际上,你可以通过为每个条目指定值来覆盖枚举的自动编号,但重要的是要重申枚举是常量。 它们定义后无法更改!

枚举也是全局的,这意味着任何对象都可以访问它们。 这对我们的状态机来说非常完美。

现在我们有了枚举,我们在哪里实例化它? 从非持久对象调用这些变量,比如我们的oPlayer对象,不是最好的主意。 我们想要做的是创建一个持久的控制器对象(它始终存在),以管理许多对象可以访问的枚举和其他数据类型之类的东西。 继续创建一个新对象并将其命名为“con”(译注:建议还是oController这样与其他对象保持相同的命名前缀,在看代码时会比较易读。)。 我喜欢保持我的控制器名称简短,因为它更容易返回。选中新对象上的“持久(Persistent)”框。 最后,将Create事件添加到对象,并添加以下面的代码:

///init 初始化 
enum_init();

将con对象放在你的房间里。 由于此对象是持久的,因此除非您明确销毁,否则它将继续存在! 无需在每个房间放置此物体。

Switch cases

既然我们已经在枚举中定义了状态,我们就可以从我们的玩家对象中访问它们了。 打开我们在上一个条目中创建的oPlayer对象,并将以下行添加到create事件中。

attack = false; 

//states 
currentState = 0; 
lastState = 0; 

//movement 
xSpeed = 0; 
ySpeed = 0; 
lastSprite = sprite;

End Step事件添加到oPlayer,然后添加一些代码。

xPos = x; 
yPos = y; 

x += xSpeed; 
y += ySpeed;  

//animation 
frame_reset();

现在让我们跳到step事件。 我们可以删除我们在之前那篇文章中添加的几乎所有代码,因为大多数代码只是为了展示draw_sprite_ext的不同部分。 查看下面的代码,并确保你的step事件看起来完全相同。

//buttons 
player_buttons(); 
 
//animation 
frame_counter();  

//state switch 
switch currentState 
{     
    case states.normal:         
        normal_state();     
    break;      
    
    case states.crouch:         
        crouch_state();     
    break;      

    case states.attack:         
        attack_state();     
    break; 
}

如果你之前从未见过switch语句,你可能会想知道到底发生了什么。 我稍后会解释,但首先我们需要创建三个新脚本:normal_state,crouch_stateattack_state。 我喜欢使用不同状态的脚本,因为它使代码更容易阅读。 你可以弹出所需的任何脚本(译注:在GMS2中,在代码中的脚本上按下鼠标中键即可弹出对应的脚本,并链接在当前对象窗口),并在该特定状态下工作。

好吧,所有这一切究竟意味着什么呢? 什么是switch语句以及它是如何工作的? 将switch语句视为if语句的更具体版本。if语句用于布尔值检查,条件满足则执行,switch语句用于根据变量的值执行代码。 看看下面的代码块。

//if statement 
if(currentState == states.normal)
{     
    normal_state(); 
}else if(currentState == states.crouch)
{     
    crouch_state(); 
}  

//switch statement 
switch currentState 
{     
    case states.normal:         
        normal_state();     
    break;  
    
    case states.crouch:         
        crouch_state();     
    break; 
}

这两段代码在功能上都是相同的。 它们都将根据currentState变量的当前值运行我们想要的脚本,但switch语句要清晰得多。 当我们添加状态时,使用if语句变得难以管理。 switch语句更容易管理。

最后,我们需要在player_buttons脚本中添加一个新的按钮变量。 打开该脚本并添加此行:

attack = keyboard_check_pressed(ord("Z"));

状态机

我们已经定义了一个可能状态的枚举,以及变量currentState来跟踪哪个状态是活动的。 现在我们知道了switch语句的工作原理,我们可以创建在每个状态下执行的代码,以及在它们之间进行转换的规则。 switch语句可以很容易地显示我们的状态机是什么以及它正在做什么。 如果我们的currentState变量等于语句中的一个case,则执行与该case相关的代码。 由于我们为每个状态创建了脚本,因此请继续打开normal_state脚本并添加以下代码

//移动 
if(left)
{     
    xSpeed = -2; 
}else if(right)
{     
    xSpeed = 2; 
}else
{     
    xSpeed = 0; 
}  
//切换到下蹲状态 
if(down)
{     
    currentState = states.crouch; 
}  
//切换到攻击状态 
if(attack)
{     
    currentState = states.attack; 
}

这段代码非常简单。 对我来说,正常状态意味着角色的默认状态。 他们没有执行任何特殊操作,例如攻击或使用道具,玩家可以完全控制角色。 在这里,我们有左右移动,并转换到蹲和攻击状态。 如果你现在运行游戏,你将无法看到我们的状态机的全部效果。 如果你按下或Z,你将改变状态,不再能够移动。 接下来让我们定义蹲状态。 打开crouch_state脚本并添加以下代码:

xSpeed = 0;  
if(!down)
{     
    currentState = states.normal; 
}

蹲下时(按住向下箭头键)我们停止玩家的水平移动(xSpeed = 0)。 如果他们释放向下键,我们将返回正常状态。 这将是一个在蹲下时添加不同动作的好地方,比如爬行或者可能是蹲下的攻击。
打开我们创建的最后一个状态脚本,attack_state,并添加以下代码:

xSpeed = 0;  

if(frame > sprite_get_number(sprite) - 1)
{     
    currentState = states.normal; 
}

我们再次将水平速度归零,并且当动画结束时我们将玩家状态设置回正常。 但是......我们还没有设置我们的动画,是吗? 动画控制是状态机和switch的另一个重要用途! 创建一个新脚本并将其命名为animation_control。 添加以下代码:

xScale = approach(xScale,1,0.03); 
yScale = approach(yScale,1,0.03);  

//动画控制 
switch currentState 
{     
    case states.normal:         
        if(left)
        {             
            facing = -1;         
        }else if(right)
        {             
            facing = 1;         
        }                  
        if(left || right)
        {             
            sprite = sprPlayer_Run;         
        }else
        {             
            sprite = sprPlayer_Idle;         
        }     
    break; 
     
    case states.crouch:         
        sprite = sprPlayer_Crouch;     
    break;      

    case states.attack:         
        sprite = sprPlayer_Attack;     
    break; 
} 
 
//如果精灵更改,则将帧重置为0 
if(lastSprite != sprite)
{     
    lastSprite = sprite;     
    frame = 0; 
}

通过使用另一个switch语句,我们可以轻松控制播放器动画。 请注意,我们可以在switch case中使用if语句! 我们没有将动画控制与我们创建的初始switch语句组合在一起的原因有几个。 首先,我们希望我们的动画在所有代码的最后发生。 动画是之前发生的一切的结果! 其次,它让代码更好读。 上面代码底部的最后一个表达式会在精灵更改时将帧重置为0。 这可以防止在更改精灵时动画在错误的帧上启动。

请注意,我们将xScale和yScale代码移动到animation_control的顶部。 这对以后很重要。

oPlayer对象中打开end step事件,并将以下行添加到代码的底部。 这将确保它在其他一切之后发生。

animation_control();

来吧,运行游戏。 你应该有一个能够左右奔跑,空闲,蹲和攻击的角色了! 我们能够根据当前状态区分角色的行为。 除了管理优势之外,设置状态机还可以更轻松地跟踪错误,添加新行为以及跟踪对象的整体结构。 我经常使用状态机和switch语句来控制比如要显示的菜单屏幕,当前的游戏模式以及定义给敌人的AI类型等内容。

谢谢阅读! 在Twitter上关注我,并在我的网站上关注更多与游戏开发相关的内容。

(转发自:原日志地址
【译文】GAMEMAKER STUDIO系列:构建更好的动画系统
highway★ 2018-11-15

作者:Nathan Ranney

翻译:highway★


在开发Kerfuffle(译注:游戏挂了,过于追求视觉效果、没钱、再加上一些其他问题,他们现在在做Knight Club)时,我需要一个动画系统,允许我在游戏中保持任何单独的动画帧(译注:格斗游戏/动作游戏为了提升打击感,会采用帧冻结的技术),而无需手动添加或删除精灵帧。 我还需要能够根据当前动画帧触发某些动作。 使用此设置,我可以创建hitbox,播放声音或更改状态,同时完全控制屏幕上绘制的所有内容。

变量

这些是与动画系统相关的重要变量。 如果后面你感到困惑,请回头再仔细看看。


frameSpeed

The speed in FPS that the game counts through the frames to be displayed. Default is 1.游戏通过要显示的帧计算的FPS速度。 默认值为1。

frameCounter

Increases every frame by the frameSpeed.

每帧按frameSpeed递增


currentFrame

The frame of the sprite currently being drawn on the screen.

当前正在屏幕上绘制的精灵帧


frameData

Current list of frame data the game is reading from, based on the animation that needs to play. Idle, run, attack, etc.

游戏正在读取的当前帧数据列表,基于需要播放的动画。 如空闲,奔跑,攻击等


frameDuration

Total number of in game frames to display the current sprite frame.

显示当前精灵帧的游戏帧总数。


maxFrames

The total number of frames in any given sprite.

任何给定精灵中的帧总数。


animSprite

The actual name of the sprite resource in GameMaker. sprMomo_Idle, for example.

GameMaker中精灵资源的实际名称。 例如,sprMomo_Idle。


脚本

后面我们要用到的脚本。

frame_reset();

//将frameCounter和currentFrame重置为0
frameCounter = 0;
currentFrame = 0;

animation_set();

该脚本接受两个参数。 首先,frameData(相关的帧数据列表)和第二个是animSprite(你想要绘制的精灵资源)

//animation_set ( argument0, argument1 );

frameData = argument0;
animSprite = argument1;