GameMaker Studio 2

创建于:2017-04-19

创建人: dougen

190 信息 1080 成员
游戏开发工具 GameMaker Studio 2 的讨论小组
顺子@GameMaker Studio 2 的内容(查看所有内容
【译】浮岛物语(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 对于用它制作的那些游戏而已非常合用——大部分开发人员永远都不需要担心这些问题。

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

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

(转发自:原日志地址
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 2中实现2D动态光影效果(Part 1)
顺子 2018-11-10

原文是YoYo官网的教程,共有两篇,这是第一篇:Realtime 2D Lighting In GameMaker Studio 2 - Part 1

第二篇可能要再等等,这两天有点事儿orz

——————————

对于开发人员而言想实现一套 2D 照明引擎是比较高阶的操作,其实现过程通常困难重重。本文我将简单介绍一些光影的基础知识,比如什么是“遮挡物”,以及如何以合理有效的方式来投射阴影。

制作遮挡物最快速的方法(同时也最易于编辑)是使用一个新的 tilemaps 。这允许我们来检测哪些单元格更靠近光源,这比使用实例要方便的多,因为使用实例你必须使用循环来遍历检测哪些单元格最靠近光源。因此,我们坚持使用 tilemaps。

首先,我们创建一个简单的 tileset 和 tilemap 进行测试。(先创建 tileset)

Image title

较暗的瓷片(tile)用于铺满第一层我们称其为地面(ground),浅色的瓷片则用来铺设第二层我们称为墙(walls)。

Image title

然后我们创建一个对象并将其作为我们的光源,把它放在我们的地图上,然后我们看一下需要处理些什么。如下图所示,红色圆圈是光线的范围,黄色的矩形是我们需要处理的 tilemap 区域。

Image title


要实现以上需求,我们需要遍历这些瓷片,并核实其中是否有墙的瓷片。

var lx = mouse_x;       // 光源坐标,基于鼠标的位置
var ly = mouse_y;
var rad = 96            // 发光半径
var tile_size = 32;     // 瓷片的尺寸
var tilemap = layer_tilemap_get_id("walls");    //获取摆放墙的 tilemap id

//定位待检测矩形区域顶点坐标
var startx = floor((lx-rad)/tile_size);
var endx = floor((lx+rad)/tile_size);
var starty = floor((ly-rad)/tile_size);
var endy = floor((ly+rad)/tile_size);

//绘制黄色矩形区域
draw_set_color(c_yellow);
draw_rectangle(startx*tile_size,starty*tile_size, endx*tile_size,endy*tile_size,true);  

把以上代码放进对象(光源位置的那个对象)的绘制事件(draw event)中,这将选择我们需要处理的瓷片,并且在范围上绘制一个黄色的矩形,紧贴瓷片的边界。接下来,我们需要遍历这个区域中的瓷片来查找非空的瓷片单元格,如下:

for(var yy=starty;yy<=endy;yy++)
{
    for(var xx=startx;xx<=endx;xx++)
    {
        var tile = tilemap_get(tilemap,xx,yy);
        if( tile!=0 ){
        }
    }
}

那么,我们现在已经准备好真正做点什么了!首先,我们在这个对象的创建事件(create event)中添加一些代码来处理绘制阴影的多边形顶点缓冲和我们需要使用的顶点格式

/// @description 初始化阴影构成
vertex_format_begin();
vertex_format_add_position();
vertex_format_add_color();
VertexFormat = vertex_format_end();

VBuffer = vertex_create_buffer();

完成以上工作后,我们就可以准备构建出一些阴影了,但是在我们开始之前,我们要搞清楚我们要如何投射阴影呢?我们先回去看一下之前的光照半径图,从光照中心投射光线到其中一个块儿上——每个角都要很精确。在这个块儿背后的光线范围都是我们稍后要绘制阴影的区域。现在你会注意到,前面的两条边缘跟后面的形状是一样的——只是他们由更靠近光源的点开始。这很方便操作,这意味着我们只需要处理面向光源的边缘。

Image title


让我们再试一次,这次我们只处理靠前的两条边——靠近光源的面。我们使用前面两条边作为 QUAD(由两个三角形构成的四边形)的前缘。另外希望它的投影范围更长,因此我们让这两条边尽可能的长——硬件会在 viewport 边缘直接进行裁剪,我们不用担心这两条边会太长。我们先确定好这个块儿各个顶点在场景(room)中的坐标,然后创建一些线条(向量,如下所示)

Image title


现在,我们已经清楚投影区域的四条边,让我们重新处理一下循环来构建我们的缓冲区

vertex_begin(VBuffer, VertexFormat);
for(var yy=starty;yy<=endy;yy++)
{
    for(var xx=startx;xx<=endx;xx++)
    {
        var tile = tilemap_get(tilemap,xx,yy);
        if( tile!=0 )
        {
            // get corners of the 
            var px1 = xx*tile_size;     // top left
            var py1 = yy*tile_size;
            var px2 = px1+tile_size;    // bottom right
            var py2 = py1+tile_size;


            ProjectShadow(VBuffer,  px1,py1, px2,py1, lx,ly );
            ProjectShadow(VBuffer,  px2,py1, px2,py2, lx,ly );
            ProjectShadow(VBuffer,  px2,py2, px1,py2, lx,ly );
            ProjectShadow(VBuffer,  px1,py2, px1,py1, lx,ly );                      
        }
    }
}
vertex_end(VBuffer);    
vertex_submit(VBuffer,pr_trianglelist,-1);

ProjectShadow 中的技巧是获取光源到遮挡物上的每一个点的向量。我们通过计算Point1X-LightX和Point1Y-LightY以及Point2X-LightX和Point2Y-LightY 来做到这一点。这会给我们两个需要用到的向量,接下来我们要实现向量“单元”,这是一个长度为 1.0 的向量,这样可以便于我们将单位向量进行缩放,如果你特别靠近障碍物,其中一条边可能会特别近(在屏幕上),而另一条边则偏远。这会使这两者的投影比例一致并且更均匀,下面是计算单元向量的代码:

Adx = PointX-LightX;        
Ady = PointY-LightY;        
len = sqrt( (Adx*Adx)+(Ady*Ady) );
Adx = Adx / len;    
Ady = Ady / len;    

Adx 和 Ady包含了长度为 1 的向量,即 sqrt((Adx * Adx)+(Ady * Ady))== 1.0。单元向量将在计算过程中覆盖所有区域,包含普通的 3D 照明模型的法线及运动方向矢量。比如,如果你想以恒定的速度进行对角线斜向移动(当你使用 x++和 y++时,对角线移动速度将比横纵轴更快)时,你可以使用单位向量乘以你想要的速度。一旦我们得到了这个单位向量,我们可以以大量缩放并将其添加到坐标位置。这将会帮助我们填补“QUAD”的远点,以下是 ProjectShadow 的脚本

/// @description 根据光源位置投射某线段的阴影
/// @param VB Vertex buffer
/// @param Ax  x1
/// @param Ay  y1
/// @param Bx  x2
/// @param By  y2
/// @param Lx  Light x
/// @param Ly  Light Y

var _vb = argument0;
var _Ax = argument1;
var _Ay = argument2;
var _Bx = argument3;
var _By = argument4;
var _Lx = argument5;
var _Ly = argument6;

// 阴影是无限的——至少要完全覆盖整个屏幕
var SHADOW_LENGTH = 20000;

var Adx,Ady,Bdx,Bdy,len

// 获取点 1 的单元向量长度
Adx = _Ax-_Lx;      
Ady = _Ay-_Ly;      
len = (1.0*SHADOW_LENGTH)/sqrt( (Adx*Adx)+(Ady*Ady) );      // unit length scaler * Shadow length
Adx = _Ax + Adx * len;
Ady = _Ay + Ady * len;

// 获取点 2 的单元向量长度
Bdx = _Bx-_Lx;      
Bdy = _By-_Ly;      
len = (1.0*SHADOW_LENGTH) / sqrt( (Bdx*Bdx)+(Bdy*Bdy) );    // unit length scaler * Shadow length
Bdx = _Bx + Bdx * len;
Bdy = _By + Bdy * len;


// 构造 QUAD(即投影本体)
vertex_position(_vb, _Ax,_Ay);
vertex_argb(_vb, $ff000000);
vertex_position(_vb, _Bx,_By);
vertex_argb(_vb, $ff000000);
vertex_position(_vb, Adx,Ady);
vertex_argb(_vb, $ff000000);

vertex_position(_vb, _Bx,_By);
vertex_argb(_vb, $ff000000);
vertex_position(_vb, Adx,Ady);
vertex_argb(_vb, $ff000000);
vertex_position(_vb, Bdx,Bdy);
vertex_argb(_vb, $ff000000);

因此,当你把以上内容整合起来,并将创建事件中的 rad 变量设为 256 后,您将得到下图所示的画面,当你移动鼠标时阴影也会随之移动并正确投影。最后,我们来修正一下确保四条边都能得到正常的投影。

Image title


如上所述,我们可以通过投影两条边就得到同样的效果。我们需要测试光位于向量哪一侧(障碍物的边缘)来实现这一点:

  if( !SignTest( px1,py1, px2,py1, lx,ly) ){
        ProjectShadow(VBuffer,  px1,py1, px2,py1, lx,ly );
    }
    if( !SignTest( px2,py1, px2,py2, lx,ly) ){
        ProjectShadow(VBuffer,  px2,py1, px2,py2, lx,ly );
    }
    if( !SignTest( px2,py2, px1,py2, lx,ly) ){
        ProjectShadow(VBuffer,  px2,py2, px1,py2, lx,ly );
    }
    if( !SignTest( px1,py2, px1,py1, lx,ly) ){
        ProjectShadow(VBuffer,  px1,py2, px1,py1, lx,ly );                      
    }

如你所见,我们的内部循环调用只需稍稍变化即可,现在可以在正式投影之前检测边缘。这样做边缘检测非常快(你甚至可以直接使用内联),只检测边缘肯定比投影更快,代码如下:

/// @description 检测点位于线条哪一侧
/// @param Ax 
/// @param Ay 
/// @param Bx
/// @param By
/// @param Lx
/// @param Ly

var _Ax = argument0;
var _Ay = argument1;
var _Bx = argument2;
var _By = argument3;
var _Lx = argument4;
var _Ly = argument5;

return ((_Bx - _Ax) * (_Ly - _Ay) - (_By - _Ay) * (_Lx - _Ax));

对于那些好奇心旺盛的人而言,这是,但你并不需要真的了解——或关心它究竟如何工作——它就是能用!现在,添加这个可能并没有明显的效果,事实上它看上去应该跟之前完全一样,但我们现在只需要绘制一半的量——高效总是值得的。根本而言,你现在已经又了一套基础的 2D 光照引擎。捕获到障碍物的边缘,然后进行投影并绘制阴影。如何创建这些障碍物完全取决于你,但是一旦您确保边缘进入了光照范围并且面朝光源,你就可以使用以上的代码来绘制出阴影。最后…把实例所在的图层移动到墙壁所在的图层下方,你将得到以下画面…

Image title


本文内容到此为止,下一次我们的任务是使光源半径外的东西都变黑,然后你会感受到真正的点光源效果。在那之后,你可能会想要添加一些颜色和“淡出”效果,以使得光线在到达光照半径边缘时慢慢消失,然后也许添加更多的光。


 

(转发自:原日志地址
GMS2:draw_text_ext小坑一枚
顺子 2018-09-04

大多数情况下,我们做游戏总要用到文字,而大量的文字显示就需要考虑排版。

文字最基本的排版手段就是换行,那么我们该怎么在游戏中让文字自动换行呢?

如果看过一些教程的朋友应该都知道再 GMS2 里普通的文字绘制函数是:

draw_text(x,y,string)

而另外还有一个略微高阶一点的函数是:

draw_text_ext(x, y, string, sep, w);

理论上,这个带 ext 后缀的函数就是自带的具备自动换行功能的函数了,其中“sep”即行间距,而“w”正是设定的单行文字像素宽度,理论上当文字长度超过这个宽度时就会自动换行,许多关于对话框实现、文字显示的教程里也都会使用这个函数,但是当你自己去尝试使用这个函数时却往往会发现,自动换行并没有起效……

记得最早在 indienova 上有人介绍 FriendlyCosmonaut 的对话框教程时下面就展开过讨论,后台也有朋友提过这个问题,不过当时一直没明白原因,因为测试的时候发现有的时候可以,有的时候又不行,非常不稳定。直到有一天 QQ 群里的朋友一语道破:

因为根据拉丁语系都是以单词为单位组成句子的,而单词跟单词之间必有空格,为了保证单词完整可读,所以 draw_text_ext 这个函数是在当单行文字宽度超过了你设置的限制并且有空格时才进行换行

而我们中文的字与字之间不需要空格,于是就出现了这种很神奇的无法自动换行的“BUG”解决的方法其实也简单,第一种最简单的就是你在本该换行的地方敲上一个空格,让这个函数发现此处有空格该换行了。但这种做法只适合做测试或者一两处文字随手改一下的情况,如果文字量大,而且时不时需要重新调整排版,那工作量就非常感人了。

所以,我们还有一个办法,自己写一个脚本来给中文自动换行即可,此处 QQ 群里的“口十”同学贡献了自用的一个脚本可供诸位参考,其中还针对行末是标点符号的情况做了特殊处理,确保把标点符号留在行尾(不过符号不够完整,各位可根据自己的使用需求进行修改调整)。

/// @func draw_chinese_text_ext(x, y, str, sep, w, less_or_more);
/// @param x_ordinate        x 坐标
/// @param y_ordinate        y 坐标
/// @param string                绘制的字符串内容
/// @param sepration          行间距
/// @param width                 单行最大像素宽度
/// @param l_or_m              单行取大于或小于上述宽度

var xx = argument[0];
var yy = argument[1];
var str = argument[2];
var sep = argument[3];
var w = argument[4];
var l = noone;
if (argument_count > 5) l = argument[5] else l = true;

var x0 = xx;
var y0 = yy;

while!(str == ""){
    var s = "";
    var i = 0;
    var ww = 0;
    var con = "";
    do{
        i++;
        s = string_copy(str, 1, i);
        ww = string_width(s)
        con = string_copy(s, i, 1);
    }until(ww >= w) or (i >= string_length(str)) or (con == "#");
    if (con!="#"){
        if (l) and (ww > w){
            var char = string_char_at(s, string_length(s));
            var t = char == ")" or char == "," or char == "。" or char == "!" or char == "?";
            if !(t){
                i--;
                s = string_copy(str, 1, i);
            }
        }
        draw_text(x0, y0, s);
        y0 += sep;
        str = string_delete(str, 1, i);
    } else {
        s = string_copy(str, 1, i-1);
        draw_text(x0, y0, s);
        y0+=sep;
        str = string_delete(str, 1, i);
    }
}
return noone;


(转发自:原日志地址
状态机:史上最棒的机制
顺子 2018-03-17
原文地址:[Tutorial/Example] Finite State Machines: The most awesome thing in the history of ever.
原作者:PixelatedPope
译者注:之前HeartBeast的2D横版教程里曾经有一集介绍过简单的状态机,之前FC妹子也做过状态机的视频,我搬了生肉其实自己也没仔细看,这两天因为自己想尝试一下,而且群里之前也有好几个朋友都问过状态机的原理和机制,恰好找到了这篇教程,虽然是前两年的老文章,示例也是GMS1.4的,但对状态机介绍还是蛮清楚的,希望大家喜欢

嗨~诸位,也许有些人还记得我之前一篇关于状态机的教程,但考虑到状态机的使用恰好是本月挑战中的“专家级挑战”,我觉得是时候再写一篇更正式的教程了。在这个教程里,我们会简单复刻一些马里奥世界的内容。

最终效果的演示:状态机效果演示

项目文件:网盘下载

我不会逐行去介绍代码,但是应该足够让大家明白基本原理并实践操作了。让我们正式开始吧。

有限状态机:这是个啥?

如果你有关注YoYo Games的官方技术博客,也许你曾经见过这篇文章,这篇文章很好的解释了什么事状态机,强烈对此有兴趣的朋友仔细阅读,不过我会先简单定义一下。

一个有限状态机(后文以状态机缩写指代)是一种特殊的组织代码的方式,用这种方式你能确保你的对象随时都知道自己所处的状态以及所能做的操作。其中每一个状态都是独立的代码块,与其他不同的状态分开独立运行,这么做可以使得游戏的调试变得更加方便,同时也更易于增加新的功能(比如一些特殊的能力和动画之类)。玩家角色在跳跃的时候看起来有点奇怪?那就直接去“跳跃”的状态里找问题吧!

同样这个机制也可以用于给敌人实现基本的AI逻辑,让敌人可以根据状态做出不同的决策。

恰好状态机机制是本月挑战里的“专家级”难度,但我十分希望当我刚开始学习编程时就能了解这个知识点,正确运用状态机可以为你免去很多不必要的麻烦。接下来我们就来看一下如何使用吧。

有限状态机适合我的项目吗?

这个问题俨然是“世上没有愚蠢的问题”这句话的最佳反证。状态机系统永远适合你的项目,这个问题可以修改成这样“状态机是否适合我这个对象?”确实,并不是所有的对象都需要用上状态机机制,但你可能会惊讶地发现有那么多对象都适合使用状态机去进行管理。

显然,可控的角色和敌人都需要使用状态机,但实际上我的游戏控制器对象也采用了这一机制,用来区分在主菜单、设置菜单和关卡选择等不同的场景的用途,甚至我的镜头控制器也用了状态机,比方说“跟随玩家状态”,“过场动画状态”和“显示特定对象状态”等等。

那么如何才能确认某个对象需要使用状态机呢?其实非常简单,对于每个对象都要问一问自己:“这个对象可以做些什么?”

如果这个对象需要处理超过2件事情以上的功能,那你就应该考虑去做一个状态机。让我们来试着问一下这个问题,比如马里奥,尤其是在超级马里奥世界里,马里奥可以做什么呢?

他可以:

  • 站立
  • 行走/跑
  • 躲闪
  • 爬墙

显然上面这个列表还有更多没列出来的,但这是个好的开始。因此,显然马里奥有很多事情可做,而且几乎所有这些事情都是独立的状态,那你现在就已经有了一个对象应该要做的功能列表了,现在是时候画一个流程图了。

认真脸:流程图。

不开玩笑,港真,流程图是你的好伙伴。下面是几个示例(从最上面那个链接里借用的)

Image title

Image title

上一张图的汉化版



设计好流程图并梳理好你所有需要处理的状态是非常重要的第一步。在你正式开始编码之前,你需要制定出基本的状态和各自的规则。你不需要彻底搞清楚你的角色能做到每一个操作,这是状态机最棒的特性,它总是易于扩展,但是基本的设计是非常重要的。

OK,设计好了。怎么实现呢?

最好的办法当然是一头扎进去然后直接动手了,是吧?为了让这个过程变得更轻松简单,我做了一个小脚本可供使用

点击前往网盘下载脚本

让我们来看一下这个脚本并了解一下它是怎么工作的:

state_machine_init()

当你在创建需要使用状态机的对象时,可以在Create事件中调用这个脚本。它会创建一对数据结构和一系列十分有用的变量。现在我们先来看看这些变量

  • state - 这个变量是当前状态的标志位。这个脚本中将会包含在对象step事件中执行的代码。
  • state_next - 当我们切换状态时,我们希望在切换之前当前状态能执行完毕,因此我们调用这个脚本来切换状态,同时更新这个值,然后上述"state"变量将在"End Step"事件中发生变化。
  • state_name - 这个变量用于获取保存当前状态的名称(创建时设定的名称)
  • state_timer - 这个变量用于记录当前状态持续的“step”数(即运行了多少帧),实用程度绝对超乎想象
  • state_map - 一个ds_map数据结构,把你设定的状态名称作为键名保存进来,
  • state_stack - 一个用来记录你历史状态的数据结构。可以用来实现一些状态机的进阶功能,比如变回到之前的状态。
  • state_new - 这是个非常有用的变量,当你切换到某个状态时,有可能你希望该状态处于初始化状态,比如速度设为0,或者更新精灵等等,这些情况十分常见。这时候你只需要在状态的最开始将这个值设为“true”即可完成这一切操作。
  • state_var[0] - state_var 这个变量比较特别。这是一个用来存储某个状态的持续时间的数组。因为我发现我自己经常会有这样的需求——“我想要清楚地跟踪并记录这个状态下的一些信息……但是别的状态没这种需求”那我该怎么办?每次有这种需求的时候都新建一个变量?这不是很荒谬嘛?所以,我用“state_var”来作为替代品,把这个数组作为针对该状态的一个便笺本,或是剪贴板,取决于你的用法。这个数组可以记录我所需要的值,因此我不必新建变量来进行记录,可能有点说不太清楚,但这个非常有用。

如果你下载并仔细看了我提供的脚本,你可能会注意懂我在里面放了一些建议性的变量(也许你的游戏用得着)。比如,通常情况下"state_can_interrupt"或"state_is_invincible" 这些变量都会派得上用场。

State_create("state name",state_script)

当状态机引擎实例化以后,我们需要创建我们自己的状态。比如说,我们要创建几个马里奥的状态,那我们就可以像下面这样操作

state_create("Stand",state_mario_stand);    //调用"state_mario_stand"脚本处理“站立”状态
state_create("Walk",state_mario_walk);        //调用"state_mario_walk"脚本处理“行走”状态
state_create("Air",state_mario_air);            //调用"state_mario_air"脚本处理“空中”状态
state_create("Crouch",state_mario_crouch);    //调用"state_mario_crouch"脚本处理“蹲下”状态

一个对象可以创建任意多的状态,尽可能根据你的需求去随意创建即可

state_init("State Name")

一旦创建好所有的状态,现在就可以来设置对象初始的默认状态了,对于马里奥而言,这应该是站立状态

state_init("Stand");     //把“站立”状态设置为默认初始状态

非常简单

state_execute()

这是状态机的核心,在“step”事件中调用这个方法就可以调用你当前状态的脚本

state_update()

这个脚本应当放在“end step”事件中调用,用于处理不同状态之间的切换

state_cleanup()

这个脚本调用最好放在“destroy”事件里。因为之前我们在处理状态机的过程中会创建一些数据结构,因此当对象实例被销毁时应当彻底销毁那些数据结构来释放内存。重要提醒 也许你还不知道,当你切换“room”时,如果你的非持久化对象(没有勾选“persistant”)具备“destory”事件,那在切换场景的时候这个对象会直接消失但是并不触发“destory”事件里的代码,因此,如果你的游戏中会出现这种情况,请务必谨慎处理。

state_switch("State Name" or state_script)

这个方法是用来在不同状态间进行切换的。你可以把创建状态时起的名字或状态的脚本名作为参数(推荐使用名字更直观)。比如当马里奥在站立状态下,我可能会按下方向键来控制他下蹲:

if(keyboard_check_pressed(vk_down))
    switch_state("Crouch");

你也可以用相同的方法去利用左右方向键来控制走动状态,或用跳跃键来使他执行跳跃状态的脚本。

state_switch_previous()

之前提过,这是状态机的进阶功能。在某些情况下,你可能需要对象恢复到上一阶段的状态。比如我有一个角色拥有施放咒语的状态,并且有另一个状态是击中时被击退,他有可能在任何状态下被击中:站立、行走、下蹲甚至丢道具等等,但当他被“击退”后不能总是让他恢复成站立状态,我希望他能恢复到被击退之前的状态。那在这种情况下,这个脚本就能派上用场了,这个利用了"state_stack"这一数据结构

好吧,也许用说的还不够清楚,下面这个图可以简要的说明具体的使用情况

你可看到"create"、"step"、"end step"、"destory"等不同的事件中的代码,这是状态机系统的基础设置,这其实非常容易,其复杂程度取决于你的状态到底有多复杂。

动手编写一个状态

让我们从最简单的状态开始:“站力状态”. 打开文章开头的那个工程链接并下载下来打开,然后找到“ Scripts>Platform Boy States>pb_state_stand”。

然后你会发现我写了一行"if(state_new)". 让我们来看一下这个状态里都做了些什么. 我把所有的速度变量都设为0,并确保对象处于默认精灵下(马里奥步行精灵的第一帧就是站立状态),为了确保他确实显示的是第一帧海拔image_index设为了0。只要我处于站立状态,所有这些值都应当保持如此,这样就没必要反复去设置了。

在第12行,你会看到我正在检测输入操作,看马里奥是不是马上就要撞墙,我倾向于在状态脚本运行之前先检测用户的输入操作。这不是100%必要的,但是做一次检测然后去调用状态脚本中的内容是个很好的习惯。

为什么我要在12-13行检测碰撞呢?因为如果我不检测碰撞只单纯运行控制事件,那当我们向左或向右朝着墙走过去就会走到墙里去,我不希望马里奥钻进墙里去,所以必须时刻检测,当你试图操作马里奥钻进墙壁时立刻进行切换,因此这个检测在你切换到“行走”状态前是必须完成的。

接下来,我会检测是否按下了跳跃按钮。如果按下了跳跃键,那就会切换到“state_air”空中状态,并将y速度设置为“jump_strength”。因为我的空中状态没有区分下落和跳跃,仅仅是在空中,所以要跳起来的话我需要设置跳跃的力度。

你可能会注意到,在这里我用了两个“if”来处理“state_switch”(嗯,你注意到这点了吗?)没错,设想一下如果现在我们冲着没有墙的方向走去,同时按下了跳跃键……会发生什么呢?所以,当这种情况发生时最后被下达的指令优先级应当更高。因此在这种情况下我会执行跳跃的操作而不是向左走。你可以简单的按照你代码中的顺序来排列不同状态的优先级,25行就是一个示例。

在这里,我正在检测马里奥的脚底的地面。比如当我站在地面上时,也许因为一些原因下面的地面突然消失了,那现在他就要掉下来,而此时应当切换到“空中”状态,迫使他掉下去,并且优先级要高于跳跃或落下。

当然,在同一时间任意组合所有的操作可能性也不大,但是正确梳理不同状态之间的优先级和关系是十分必要的,可以避免很多不必要的意外状况。

这就是我的站立状态中所需要的全部功能,但想一想我们还可以在里面加些什么东西?比如我们可以检测马里奥当前是不是站在一个移动的平台或者传送带上。如果是,那我会获取这个对象的速度,并根据实际情况将这个速度设到马里奥的x轴或y轴的速度上,以便他可以跟着平台运动。而当我处于“空中”状态时则完全不用操心这些问题,甚至我都用不着做相关的判断和检测。但是在“行走”状态下还是需要考虑这种情况的,所以也许你也要考虑在行走状态的脚本中添加相应的检测代码。

尾声

这篇文章写得有点长,我也不确定你是否能从我的代码中得到更多启发(我的碰撞检测有一点疯狂,并)。所以,自己试玩一下范例工程,然后尝试建立自己的状态机角色,如果有任何问题欢迎私信或给我留言。

当你第一次开始使用状态机时,你可能觉得你之前学编程知识都要被丢掉了。因为不再需要像“on_ground”或“can_jump”这样的变量了。 如果你处于站立状态,你就能确认自己正在地面上并一定能进行跳跃操作......其他状态下则肯定不是站立状态,没错吧? 请务必信我这一次,状态机机制十分有用,值得你学习和尝试去根据自己的需求实现一个定制版的状态机系统,请相信我,这是一个令人难以置信的有价值的东西。 不要放弃,不断续尝试,不断改进,最后你一定会实现你想要的效果。

希望这些内容能派得上用场,而不是单纯耗费了你的时间。再次声明,如果有任何问题可以私信或给我留言。

很荣幸你能读到这里,现在亲自去试试看吧!

(转发自:原日志地址
[ 分享 ]GMS2摄像机系统指南
顺子 2018-03-07

原作者:Madde Michael

译者:highway★ 

译注:由于gms1的view已经被干掉,新接触2代的用户对摄像机系统并不太熟悉。在游戏的前期(prototype或demo)开发中,摄像机处于3C(Character, Control, Camera)中一个非常重要的核心位置,一个设计合理的摄像机系统会让您的游戏体验起来更juicy,我觉得花些时间翻译一下这篇指南很值得,希望能帮助到更多GMS2用户更了解新的摄像机系统。话了,请您继续看。

版本: IDE: 2.0.2.44

RUNTIME: 2.0.1.27

导出平台: ALL

工程下载: Built Project

译注:部分图片由于原文是beta版,IDE有些命名与现版本不同,已经修正为现有版本。

概要

这篇指南提供了有关新的摄像机系统的基本信息,以及views的使用方法。由于摄像机系统跟GMS1时相比是全新的,这篇指南面向那些对gml有基本了解并对views有点儿懵逼的初学者。

项目工程示意:

Image title

效果没那么屌,但是实现了摄像机创建、分配,view的移动、缩放、和插值。对应操作:“点击聚焦”和“滚轮缩放”。

相关链接:

教程

首先,我要说明一下 - 实际上你不用摄像机也能获取一个视图(view) - 场景(Room)编辑器的视图(view)跟GMS1的工作方式基本一样。但是……它现在可没原来那么灵活了 - 你不能像以前那样在代码里修改view_[x,y,h,w]view了(译注:如果你没接触过GMS1,那么请不要在意这段) - 除非你先设置一个摄像机才能进行高级的操作。

注意:

  • Gamemaker Studio 2缩写为GMS2
  • 请用GML,英雄!
  • 困了惑了,请随时按F1查手册,英雄!
  • 发现了错误,或者有什么问题请email我,英雄!(去这里找原作者)
  • 译注:视图对应view,摄像机对应camera,视图端口对应viewport,加括号有点恶心……sry,mate

这篇指南会讲到这些内容:

  • 设置一个视图(用来使用摄像机)
  • 创建/删除摄像机并分配给视图
  • 通过代码设置摄像机跟随对象(object)
  • 除了绑定视图之外,用其他方法绘制摄像机
  • 摄像机脚本 - 更新、开始和结束
  • 比较简单的摄像机位置插值
  • 比较简单的摄像机缩放
  • 根据场景边界限定摄像机位置
  • 其余的摄像机函数和功能

设置一个视图(用来使用摄像机)

OKay,你可能想跳过这里……我知道,你要是知道这玩意的话就跳过吧。实际上就是在一个Room内,通过编辑器或者代码来启用视图。

在你的游戏里有两种方法可以设置视图,一个是用Room编辑器里的视图工具,第二就是用代码。嗯……我又重复了一遍。

我个人推荐后者,这意味着你无须在每个单独的Room里设置视图(译注:如果你的关卡很多,肯定很烦人,而且说不定哪一个room就忘了)。你只需要在Room start的时候运行代码,完事儿~ 但是,代码并不会改变游戏的窗口(基于视图端口),但是Room编辑器里就可以改变游戏窗口,假如它是游戏中的第一个房间 - 有时候称为“启动房间”。如果之后出于一些原因需要改变窗口的尺寸的话,windows_*函数可以帮你搞定。

两种方法,我们都可以设置: 视图0对应的视图端口尺寸为640*480,跟GMS一样。

这里是在Room编辑器中设置视图的方法:首先,在Resource树窗口打开你的房间,GMS2在默认情况下就为你创建好了一个名为room0的房间。可以双击,也可以拖到工作区来打开。

Image title

然后在左侧下方,找到"Viewports and Cameras"。来进行设置:

Image title

接下来,在“Enable Viewports”前打钩

Image title

打开Viewport 0的设置区,向下面这样填:

Image title


OK,我们搞定了基本的视图设置。

注意我们并没有改变视图边界(border)、视图速度(speed)还有跟随对象(Object Following)。这里解释一下他们的功能:

  • 跟随对象(Object Following):如字面意思,让摄像机跟随选中的对象。
  • 视图边界(View Border):摄像机跟随对象之前,需要靠近视图边界的距离(以像素为单位)。
  • 视图速度(Camera Speed):当越过边界时,摄像机跟随对象移动的速度。速度设为-1会让视图即时移动来显示跟随对象。

下面是以代码来设置View的方法:这段代码最好写在“Room Start”事件中。你也可以把它放到创建房间的代码中(room creation code,在room编辑器里可以找到),但你可以把代码写在一个永久存在(persistent)的实例中,来节省一些时间。(尽管使用新的房间父子系统,在Room编辑器中创建默认视图要比以前更容易)。

这里我没有写整段代码,只是展示注释过的部分

代码:

//启用视图
view_enabled = true;

//设置view 0为可见
view_set_visible(0, true);

//设置view 0的视图端口尺寸为640x480,边界为0
view_set_wport(0, 640);
view_set_hport(0, 480);

在这里设置这些边界不会自适应游戏窗口尺寸 - 即使是在启动房间。启动房间(或者有一个启用的视图)的尺寸要和你希望你的游戏的显示窗口尺寸一样,这样才会自动适应。

你也可以手动调整窗口尺寸来适应,用window_set函数。但是,在同一个事件中调用window_set_size()和window_center()是无效的,因为窗口调整要在当前的事件结束之后才能工作。

如果你想要同时调整游戏窗口并居中,看一下下面的示例:

代码:

//调整尺寸并居中
window_set_rectangle((display_get_width() - view_wport[0]) * 0.5, (display_get_height() - view_hport[0]) * 0.5, view_wport[0], view_hport[0]);
surface_resize(application_surface,view_wport[0],view_hport[0]);

这样就可以调整view 0对应的视图端口的窗口尺寸,并将"application_surface"的尺寸设为相同。

我们还没有通过代码来设置视图的尺寸、位置、目标对象、边界或者速度 - 因为我们要通过摄像机来进行设置,在后面我会解释如何操作。

从这里开始,开始全是代码了,英雄!

创建和删除摄像机并分配给视图

如果你习惯了GMS1的视图,那么GMS2的摄像机会有些不同。你可以将其理解为……嗯,有点像实例(instance) - 你要创建一个摄像机,然后赋予它一些信息,当你用过之后可以删了它。

这一部分将帮助你通过代码来设置视图的参数,从这开始这些属性就都由摄像机来管理了。

当创建摄像机时候,有两个函数我们要了解:

"camera_create()" 和 "camera_create_view()"两个函数都返回唯一的摄像机ID,通过ID我们可以用来使用其他所有的摄像机相关函数。

"camera_create()"用来创建一个实际上是空白画布的摄像机 - 你需要手动指定位置和尺寸,然后才能在2D中正确使用 - 我这样说是因为你可以在3D中使用它而无需指定这些参数。"camera_create_view()"更适合2D开发来使用,因为它强制你在创建时就要指定所有的视图参数。

我们将使用“camera_create_view()”,因为这篇指南的目的是帮助那些意图开发2D游戏的英雄们~如果你要关于3D方面的“camera_create”,那可以看我另一篇文章 "Getting Started with 3D in GMS2"。

下面是创建摄像机的方法。再磨叽一遍,640x480的尺寸,位置是0,然后跟随对象参数设为默认:

代码:

//创建摄像机
//在(0,0)的位置创建一个摄像机,640x480,角度为0,没有跟随实例,摄像机水平和垂直速度为即时跳转,边界为32像素
camera = camera_create_view(0, 0, 640, 480, 0, -1, -1, -1, 32, 32);

上面代码中我们指定一个角度,这里取代了GMS1的view_angle[0..7]。其余关于视图角度的函数为 "camera_get_view_angle" 和 "camera_set_view_angle"。

这就是摄像机创建了,用完摄像机要销毁的话:

代码:

camera_destroy(camera);

下面我们要将摄像机绑定到视图,比较简单:

代码:

//设置view0来使用摄像机 "camera"
view_set_camera(0, camera);

从这开始,当我们要更新视图时,我们就要使用"view_camera[view_index]"引用一个绑定的摄像机实例(本例中为view_camera[0])。这是因为我们要保证我们正在更新的摄像机是分配给视图的那个。

用view_camera[x] = camera也可以正常工作,但我感觉用view_set_camera()更安全更可靠。

我们实际上并不需要将摄像机绑定到视图来使用它 - 我会简单的介绍它的工作原理 - 但绑定会更方便,因为它可以访问自动摄像机“更新/开始/结束”脚本,我将对此进行介绍。

你可以通过这两个函数"view_camera[0..7]"和"view_get_camera(view)",来获取视图正在使用的摄像机的ID。

通过代码设置摄像机跟随对象(object)

如果你用Room编辑器来设置视图,就不用管这个了。但是,如果用代码的话,你有2个选择:第一,在camera_create_view()里设置标准参数,比如:

代码:

//创建一个摄像机,位置(0,0), 尺寸640x480, 角度0, 跟随的目标实例"objPlayer", 即时跳转的水平垂直速度,边界为32像素
camera = camera_create_view(0, 0, 640, 480, 0, objPlayer, -1, -1, 32, 32);

另一个选择是手动的创建每一个参数 - 这允许动态的改变属性,比如增加边界尺寸、聚焦在敌人身上等等。

就像这样设置:

代码:

//基础设置
camera_set_view_pos(view_camera[0], 0, 0);
camera_set_view_size(view_camera[0], 640, 480);

//设置目标对象的信息
camera_set_view_target(objPlayer);
camera_set_view_speed(view_camera[0], -1, -1);
camera_set_view_border(view_camera[0], 32, 32);

搞定!

除了绑定视图之外,用其他方法绘制摄像机

如果,你不想将摄像机指定给某个视图(或者想使用摄像机,但不创建视图的话),我们就要用到camera_apply函数了。

代码:

//应用摄像机并以此来绘制
camera_apply(camera);

如果你只想要将摄像机应用于某些视图(无需绑定),只需要将其包装在view_current测试中,就像这样写:

代码:

if(view_current == 0)
{
camera_apply(camera);
...
}
...

这样弄之后,照常允许绘制代码。

之后要将摄像机重置为默认值,只要这样调用:

代码:

camera_apply(camera_get_default());

这会将摄像机视图重置为GMS2默认的摄像机 - 当没有视图启用的情况下所使用的默认摄像机。注意你要用camera_set_default来操作,虽然我还没想到过合理的使用案例,也没测过。

当使用camera_apply时有个比较大的问题是,并不会自动调用摄像机“更新/开始/结束”脚本 - 这个脚本非常好用,后面我会详细解释。当然,如果你自己写脚本来配合apply方法来使用的话,你要分开调用它们。

摄像机脚本 - 更新、开始和结束

好了,终于到了新摄像机函数里有趣的地方之一了,脚本!请看手册,英雄!这里有三种摄像机脚本,更新/开始/结束。信息如下:

所有可见和活跃视图端口的摄像机都会调用其更新脚本。

然后对于每个独立的视图:

视图对应的摄像机会调用开始脚本绘制事件为该视图执行(包括绘制开始draw begin和绘制结束draw end事件)调用摄像机的结束脚本

那么,这有什么好处呢?嗯,首先,脚本只会被分配给视图的可见摄像机调用 - 这意味着你可以把代码放到这些脚本中,如果视图没开始绘制,它不会浪费时间来运行 - 当跳帧时这就很好。你也不必在绘制开始时使用高深度对象运行设置代码。

这些脚本好像只能自动为绑定到视图的摄像机运行。如果你的脚本没有运行,检查一下是不是没有正确绑定!

OK,这些函数都可以正常工作,并以类似的方式使用 - 你可以告诉它们什么时候开始工作。在这里,我要将脚本赋予视图0的摄像机的开始函数。

这个名为Update_script的脚本,包含下面的内容 - 简单但是比较挫的震屏效果:

代码:

//对分配给当前视图的摄像机进行震动
camera_set_view_pos(view_camera[view_current], random_range(-6,6), random_range(-6,6));

我们用view_camera[view_current]来确保我们将此脚本绑定到任意视图摄像机。

这是震屏功能,现在我们将其绑定到一个视图,用下面的代码绑定到camera_begin:

代码:

//将很挫的震屏脚本update_script绑定到所需的摄像机"begin"
camera_set_begin_script(view_camera[target_view],update_script);

为了绑定到其他摄像机事件,我们可以使用以下两种之一:

代码:

camera_set_end_script(view_camera[target_view],update_script);
camera_set_update_script(view_camera[target_view],update_script);

注意:在写代码的时候,无法从脚本中获知哪台摄像机正在运行当前的更新脚本(只使用view_camera[view_current] 或camera_get_active()来标识"开始"和"结束"脚本),除非你严格的设置所有的变量连接。我已经提交了一个bug报告,但我不知道它是否会被yoyo看做是个bug,因为绘制实际上还没有开始。(译注:由于作者写指南的时候是BETA版,现在这个问题是否存在我还没有测试过)

要清除摄像机中的脚本,只需按照正常运行函数,而不是使用脚本参数中的“-1”。每个脚本设置函数都有一个“getter”equivelant(我不确定这玩意汉语到底该叫什么=_= sry~),它返回当前绑定到摄像机的脚本内容。

再注意:你不能在摄像机脚本中执行任何绘制。这些玩意必须在绘制事件中完成(或者在surface上)。

OK,脚本内容先聊到这儿。

比较简单的摄像机位置插值

对于这一点,我们只是将摄像机从当前位置插入到用户点击的任意位置。这个代码适合...比如,追踪一个对象,这很简单,从简单的东西开始做,是个好选择~

假设 - 你已经创建了一个2D摄像机,分配给了视图0。如果你忘了怎么操作,翻到上面重新看看。

首先:我们在create事件中初始化2个变量:

代码:

click_x = 0;
click_y = 0;

它们用来存储鼠标最后一次点击的位置。因此,我们首次启动项目时,它会将摄像机对准原点,因为我们已经将其赋值为0了。

下面这些代码都写在step事件(在同一个object里,命名叫什么随你,英雄)

代码:

//检测鼠标左键是否点击,是的话,更新点击位置
if(mouse_check_button_pressed(mb_left))
{
click_x = mouse_x;
click_y = mouse_y;
}

//获取目标视图位置和尺寸,尺寸减半所以视图将聚焦在中心
var vpos_x = camera_get_view_x(view_camera[0]);
var vpos_y = camera_get_view_y(view_camera[0]);
var vpos_w = camera_get_view_width(view_camera[0]) * 0.5;
var vpos_h = camera_get_view_height(view_camera[0]) * 0.5;

//插值率
var rate = 0.2;

//将视图位置插入新的对应位置
var new_x = lerp(vpos_x, click_x - vpos_w, rate);
var new_y = lerp(vpos_y, click_y - vpos_h, rate);

//更新视图位置
camera_set_view_pos(view_camera[0], new_x, new_y);

总结一下,这段代码检测是否按下鼠标左键,是的话,更新摄像机到“点击位置”。 然后不管任何情况,视图都会插入到鼠标按下的最后位置的中心。

这是如何完成的?

  • 我们获取当前位置(即左上角)和视图的尺寸
  • 将视图尺寸减半
  • 我们建的变量rate,用来指定插值的速率(这样后面改起效果来会方便一些)
  • 我们用lerp函数将当前视图的位置定位到新的点击位置,视图的宽高减半会从点击位置移除,让视图聚焦在 中心
  • 将新的位置坐标赋予视图,过程结束

一个简单的视图插值demo完成了~

摄像机缩放

这跟之前的工作原理类似 - 你要修改视图的宽和高,但保持视图端口的尺寸。唯一的区别是你需要一个摄像机才能使用一些函数。

假设 - 你已经创建了一个2D摄像机,分配给了视图0。如果不幸你又忘了怎么操作,翻到上面重新看看。

我们要在create事件中创建一些缩放相关的变量(在摄像机创建之后):

代码:

zoom_level = 1;
//获取初始视图尺寸,后面要用于插值
default_zoom_width = camera_get_view_width(view_camera[0]);
default_zoom_height = camera_get_view_height(view_camera[0]);

下面这些代码都写在step事件(在同一个object里,命名叫什么随你,英雄)

代码:

//根据鼠标滚轮来缩放,用clamp函数可以让效果看起来没那么蠢
zoom_level = clamp(zoom_level + (((mouse_wheel_down() - mouse_wheel_up())) * 0.1), 0.1, 5);

//获取当前视图尺寸
var view_w = camera_get_view_width(view_camera[0]);
var view_h = camera_get_view_height(view_camera[0]);

//设置插值率
var rate = 0.2;

//通过插值计算当前和目标缩放尺寸获取新视图尺寸
var new_w = lerp(view_w, zoom_level * default_zoom_width, rate);
var new_h = lerp(view_h, zoom_level * default_zoom_height, rate);

//应用新尺寸
camera_set_view_size(view_camera[0], new_w, new_h);

这些是干嘛的? 我们一步步拆开来看:

  • 根据鼠标滚轮当前的滚动方式调整缩放级别 - 向上放大向下缩小。缩放级别也用clamp函数处理,防止过度缩放
  • 获取当前的视图尺寸
  • 设定插值率(这样后面改起效果来会方便一些)
  • 通过当前视图尺寸插入到原始尺寸乘以缩放级别来确定新尺寸的大小
  • 更新新的视图尺寸

这就是缩放的处理。请记住,视图尺寸从左上角开始变化,而不是中心。如果你想让视图保持居中,需要加上这些:

代码:

//获取重新对齐视图所需的位移值
var shift_x = camera_get_view_x(view_camera[0]) - (new_w - view_w) * 0.5;
var shift_y = camera_get_view_y(view_camera[0]) - (new_h - view_h) * 0.5;

//更新视图位置
camera_set_view_pos(view_camera[0],shift_x, shift_y);
这将使用视图的宽高的差异来确定如何移动视图以此来重新居中,然后再应用新的位置。

这一章节结束~

根据场景边界限定摄像机位置

我在twitter上被问到过这个问题,所以我觉得下面这段代码有必要讲一下:

代码:

camera_set_view_pos(Camera_ID,
clamp( camera_get_view_x(Camera_ID), 0, room_width - camera_get_view_width(Camera_ID) ),
clamp( camera_get_view_y(Camera_ID), 0, room_height - camera_get_view_height(Camera_ID) ));

这行代码通过存储在Camera_ID变量中的ID来获取摄像机的位置,然后将其位置控制在(0,0)和房间界限减去摄像机尺寸之间。

如果你已经计算好或者已经知道摄像机尺寸和位置,你可以不用camera_get_view_*这个函数,直接用值就可以(译注:如果能用变量的话,后期查阅或者修改代码的时候会比单纯用值来写的可读性更好)。

其余的摄像机函数和其功能

上面说了那么多之后,我想你应该对新的摄像机系统有了一定的了解,用它们来创造奇迹吧~英雄!当然,还有点东西最好说一下,很方便的几个函数:

  • 我们可以用view_set_[x,y,w,h]port()函数来改变视图端口相关的东西
  • 可以通过view_set_surface_id()告诉视图绘制到surface
  • camera_get_active()返回当前活跃摄像机的唯一ID(正在绘制的摄像机)
  • 有两个函数使用了矩阵 - camera_set_view_mat()和camera_set_proj_mat()。这些主要用于设置3D投影(译注:如果你有兴趣扩展学习,可以去Shaun Spaulding的youtube频道看看,他比较喜欢用这个,有几个视频专门讲了GMS2的摄像机)
  • camera_set_proj_mat()采用投影矩阵,通常用三个矩阵函数之一 - matrix_build_projection_ortho,matrix_build_projection_perspective或matrix_build_projection_perspective_fov
  • camera_set_view_mat()采用视图矩阵,通常用matrix_build_lookat构建
  • 我在另一篇指南“GMS2的3D开发起步”讲了这些矩阵函数的基本用法
  • 本指南中讨论的所有setter函数都对应的有getter函数

值得一提的是,你可以用"camera_get_view_[x/y/width/height]()"来绘制与摄像机视图相关的东西,尽管使用Draw GUI事件来绘制HUD会好的多。译注:HUD,即head-up display,西方游戏行业对游戏界面的定义,从飞机的抬头显示系统拿来的名词,国内游戏业更喜欢叫UI,如果你希望查阅国外游戏的资料,建议还是更改习惯,称之为HUD。

就写到这吧,你现在应该可以去玩这个新的摄像机系统了,如果还有什么你想了解的,或者有什么问题,需要什么帮助的话,联系我,我看看能不能帮到你~

*译注:阿西吧,真tm长………

扩展阅读 : [GDC2015] Scroll Back - 2D卷轴游戏的摄像机理论与实例

这篇文章是Itay Keren(Mushroom11的开发者,IGN9分游戏,貌似IOS上过推荐)在2015年的游戏开发者大会上的演讲的图文版,超长超屌,理论研究+实例分析~ 如果你在开发2D卷轴游戏,那你一定要精读精读精读这篇文章,里面涉及到很多行业前辈带来的超级精彩精心设计的摄像机系统。在看过文章之后,再去回顾以前玩过的游戏,或者新玩到的游戏,就会更加体会到摄像机系统对游戏体验的强力加分或者……折扣……。废话不多说了,请去GMS2实验吧~ 期待您的新成果~~~~~ 一起加油!

(转发自:原日志地址
[ 分享 ]【GML脚本】瓷片碰撞检测
顺子 2018-01-27

脚本说明

这是在reddit发现的一个脚本,作者为“CptLeafBlower 

原帖地址:Super Easy Tile Collision - GMS2

跟内置函数“place_meeting”的用法十分类似,只是这个检测的不是对象而是瓷片地图只需传入要检测的坐标点以及需要检测的瓷片地图ID即可

参数说明

参数序号 参数名 参数说明
argument0 x1 待检测坐标的x
argument1 y1 待检测坐标的y
argument2 x2 需要检测碰撞的瓷片地图ID

代码正文

///@description tiles_collision(x , y , tilemap)
///@arg x
///@arg y
///@arg tilemap

var xx, yy, tilemap, xp, yp, meeting;

xx = argument0;
yy = argument1;
tilemap = argument2;

//save our current position
xp = x;
yp = y;

//move to the position where we wanna check for a tile collision
x = xx;
y = yy;

//check for collision on all four corners of the collision mask
meeting =       tilemap_get_at_pixel(tilemap, bbox_right, bbox_top)
                ||
                tilemap_get_at_pixel(tilemap, bbox_right, bbox_bottom)
                ||
                tilemap_get_at_pixel(tilemap, bbox_left, bbox_top)
                ||
                tilemap_get_at_pixel(tilemap, bbox_left, bbox_bottom);

//Move back to the original position
x = xp;
y = yp;

//Return wether or not there was a collision
return(meeting);

使用范例

var tile_solid = layer_tilemap_get_id("tiles_ground")
if(!tiles_collision(x, y + 1, tile_solid))                            
{
    speed_down  += gravity_;
}

以上代码首先使用"layer_tilemap_get_id"的方法获取了"tiles_ground"图层中设置的瓷片地图的ID,然后将该ID保存到临时变量"tile_solid"中,然后检测在当前对象向下一个像素的位置有没有"tile_solid"对应的瓷片地图,如果没有则受重力加速度影响下落。

(转发自:原日志地址
[ 分享 ]在GMS中绘制技能冷却动画
顺子 2018-01-11

这一篇是之前曾经就说过的冷却时间的实现方式,废话不说我们先看效果:

Image title

上图左边的柱形动画是之前在QA里就介绍过的,今天我们要实现的就是右侧那种钟形的冷却时间动画,其实这个方法也是借花献佛,主要借用了Yellowafterlife的这篇文章——GameMaker: Circular cooldown rectangle 中的脚本,严格来说,本文只算个简单的脚本使用说明,原文详细的解释了脚本实现的思路和原理,推荐喜欢研究的同学深入阅读~

简单说明

这个脚本利用了绘制基本几何体的方法来绘制这个动画,同时使用了扇形图元,基本的原理就是先绘制一个矩形蒙版图像,然后根据冷却时间的变化(时间进度)来逐步缩小绘制的区域,期间需要根据进度值来不断计算绘制的角度变化。

Image title

在使用这个脚本时有一些注意事项:

  • 可以在调用方法前使用draw_set_alpha和draw_set_colour来定义绘制的不透明度和颜色(用完记得要恢复)
  • 原方法绘制了一个矩形,如果你想绘制圆形,可以使用纹理贴图来实现
  • 对调x1和x2的传入顺序可以切换顺时针/逆时针动画方向
  • 传入的value并不是冷却时间而应当是当前剩余的冷却进度(百分比)

代码

/// draw_rectangle_cd(x1, y1, x2, y2, value)
///@param x1
///@param y1
///@param x2
///@param y2
///@param value

var v, x1, y1, x2, y2, xm, ym, vd, vx, vy, vl;
v = argument4
if (v <= 0) return 0 // 进度为0则不绘制
x1 = argument0; y1 = argument1; // 定义左上角
x2 = argument2; y2 = argument3; // 定义右下角
if (v >= 1) return draw_rectangle(x1, y1, x2, y2, false) // 填满冷却区域
xm = (x1 + x2) / 2; ym = (y1 + y2) / 2; // 定义中心点
draw_primitive_begin(pr_trianglefan)
draw_vertex(xm, ym); draw_vertex(xm, y1)
// 绘制不同的角:
if (v >= 0.125) draw_vertex(x2, y1)
if (v >= 0.375) draw_vertex(x2, y2)
if (v >= 0.625) draw_vertex(x1, y2)
if (v >= 0.875) draw_vertex(x1, y1)
// 计算角度:
vd = pi * (v * 2 - 0.5)
vx = cos(vd)
vy = sin(vd)
// 向量控制
vl = max(abs(vx), abs(vy))
if (vl < 1) {
    vx /= vl
    vy /= vl
}
draw_vertex(xm + vx * (x2 - x1) / 2, ym + vy * (y2 - y1) / 2)
draw_primitive_end()

示例下载

第一张图中的例子放在网盘了欢迎下载,包含了这个CD时间的脚本以及很粗糙的一个技能拖拽的实现orz

链接:https://pan.baidu.com/s/1bqf44Th 密码:y022

(转发自:原日志地址
[ 分享 ]伤害数字的实现方式
顺子 2017-11-22

前几天有朋友在后台问我如何能让文字移动,以及缩放大小,经过一番沟通确认了他的目的是想要做那种人物受到伤害时头顶飘出的伤害数字,想了一下原理并不复杂,于是有了这篇简单的教程,下面先看下效果:

Image title

其中使用的最重要的一个函数是

draw_text_transformed_color(x,y,string,xscale,yscale,angle,c1,c2,c3,c4,alpha)

通过这个函数我们可以控制绘制文字的绘制属性

  • x,y——文字绘制坐标
  • string——需要绘制的文字内容
  • xscale,yscale——文字的横纵向缩放比
  • angle——文字绘制角度
  • c1,c2,c3,c4——左上左下右上右下四个顶点的颜色值,中间的颜色会自动过渡
  • alpha——透明度

之前的文字放大就是通过xscale和yscale的设定实现的,文字淡出的效果则是调整了alpha值。

当然这样的效果需要根据实际使用场景来进行具体的调整才会达到最佳。

下面我们说一下这样的文字具体的实现方式

思路

首先我们需要一个对象来绘制文字内容,并且我们希望这个文字有一个位移,然后在位移的过程中逐渐变淡同时略微放大,最后直接消失

首先是位移,在draw事件中我们可以指定绘制文字的坐标,理论上直接修改绘制文字的坐标也可以达到这个目的,为了方便我选择直接修改绘制文字的对象本身的坐标,然后把绘制坐标跟对象的坐标绑定关联,同样可以让文字移动。

然后在对象的step事件中把对象自身的image_xscale和image_yscale进行递增操作,同时将image_alpha进行递减操作,最后把文字绘制方法中的xscale、yscale和alpha跟与之对应关联,这里其实也可以直接设定三个递增/减的自定义变量而不使用内置属性。

最后是设定如果alpha小于等于0即完全透明不见时自动销毁该对象,避免冗余的对象消耗性能。

实现

首先创建一个对象

Image title

然后在create事件中添加了一个随机数值的代码,这部分是为了演示用的,在实际应用时需要根据自己的需求去定义

Image title

然后在step事件中是如下内容

Image title

第一部分就是设定对象的y坐标递减以达到向上飘升的效果

第二部分是设定了自身的横纵图像缩放比以每帧0.05的速率递增

第三部分是设定自身的图像透明度以每帧0.05的速率递减

最后第四部则是设定当图像透明度小于等于0以后直接销毁当前实例

最后是文字绘制的方法

Image title

这就是最关键的文字绘制,我这里用step中修改的image_xscale和image_yscale来约束了文字的缩放比,同时用image_alpha约束了文字的透明度

不过如果只按照以上方法绘制的效果会有一些问题,最主要是当文字放大以后会出现锯齿而不清晰。

Image title


优化

为了解决文字放大锯齿的问题,我们首先需要自定义一个足够大的字体,然后在平时使用时可以缩小显示,之后放大显示就会平滑一些。

然后我们会尝试调整角度和颜色的变化来使文字更有动感,大概效果如下:

Image title


这部分优化代码直接放出,不截图了,代码里会做一些注释

//Create事件
//随机获取四个顶点的颜色
color = [c_green,c_white,c_red,c_purple,c_yellow,c_blue,c_lime,c_maroon,c_orange]
color1_to_draw = color[random(7)];//c1 
color2_to_draw = color[random(7)];//c2
color3_to_draw = color[random(7)];//c3
color4_to_draw = color[random(7)];//c4
//随机设定显示的数字
text_to_draw = floor(random(150));//String
//Step事件
//up
y -= 5 ;

//scale
image_xscale += 0.05;
image_yscale += 0.05;

//fade out
image_alpha -= 0.05;

//destroy
if (image_alpha <= 0 ) instance_destroy();
//Draw事件
//设定自定义字体,这个字体的字号我设置了100
draw_set_font(fnt_test);       
//文字居中排版 
draw_set_halign(fa_center)  
//绘制文字,角度用了wave脚本,这个脚本以前介绍过
draw_text_transformed_color(x,y,string(text_to_draw),image_xscale/10,image_yscale/10,wave(-10,10,5,0),color1_to_draw,color2_to_draw,color3_to_draw,color4_to_draw,image_alpha)

示例下载

链接:http://pan.baidu.com/s/1kUBddAZ 密码:htvt

这么个简单的功能都能水一整篇也是蛮佩服我自己的XDDD

(转发自:原日志地址
[ 分享 ]在GMS2中实现动态遮罩效果
顺子 2017-11-04
这是翻译的YoYo官方博客里的一篇教程——DYNAMIC RENDERING MASKS

这是一篇小教程,内容不多,但是效果很酷而且十分实用。当我们在屏幕上绘制内容时,大多数人都只考虑颜色,即红、绿、蓝三个颜色通道,但其实还有一个Alpha通道,但如果你想要借此来构建精灵或处理画面上的内容时可能会遇到很多问题,但是有一件事用Alpha通道绝对合适,那就是制作蒙板遮罩。

因为玩家只会看到处理过的最终结果画面,所以我们可以使用Alpha通道来处理画面以获取我们想要的效果。首先我们要知道有这样一个命令——“gpu_set_colorwriteenable()”——这个方法可以用来打开或关闭颜色和Alpha通道是否允许写入。

首先,我们在屏幕上取任意像素点,每个像素点都具有4个通道——红、绿、蓝以及Alpha,你可以任意启用或关闭其中的任何通道。然后我们要定义一个“特殊”的遮罩,然后把精灵图像写入这个“特殊”的遮罩。这样一来这个RGB通道就会变成这个样子——比如说,一个有趣的按钮,我们可以正常渲染这个按钮图像,然后我们用屏幕上的Alpha通道来“遮”住按钮外面的内容,这样我们就只绘制按钮内部的画面了。

我们现创建两个精灵,一个按钮,以及一个我们希望绘制的“遮罩”的造型精灵。

Image title


现在我们的思路是这样的,我们先按照常规的方式把所有内容绘制出来,包括背景、按钮以及按钮的阴影等等,然后要来限制整个按钮绿色区域内部的内容。当然要实现这个目标也有很多其他的方法,比如使用着色器等等,但哪些方法更复杂。接下来我们还要准备一个背景和用来绘制按钮的精灵。

Image title


一切准备就绪,如果我们此时来创建测试对象和绘制事件,我们只需要按部就班,首先是背景,接着是按钮阴影,最后是按钮本身,一切照旧。


draw_sprite(sBackground,0, 0,0);
draw_sprite_ext(sButton,0, 32+4,32+4,1,1,0,c_black,0.5);
draw_sprite(sButton,0, 32,32);

接下来的操作才是重点!我们要禁用所有的颜色通道和混合模式,然后在alpha通道写入“0”,如果混合模式是开启状态,这个0会被混合模式按正常的数值进行处理(即完全透明)就不会被正确写入——或者说不是我们希望的样子。而关闭混合模式以后,0就可以被写入alpha通道,而此时其他颜色通道中的图像则不会受影响。


gpu_set_blendenable(false)
gpu_set_colorwriteenable(false,false,false,true);
draw_set_alpha(0);
draw_rectangle(0,0, room_width,room_height, false);

如上,我们禁用了混合模式和颜色通道,并将alpha设置为0,然后绘制了一个全屏的矩形——当然这个矩形只会填充alpha通道。接下来,我们要来实现“遮罩”,遮罩即整个屏幕内我们希望展示的区域。这可以是任何形状或区域,不过在本文中,特制按钮中的绿色区域。


draw_set_alpha(1);
draw_sprite(sMask,0, 32,32);
gpu_set_blendenable(true);
gpu_set_colorwriteenable(true,true,true,true);

这里我们把alpha值设回了1,并且绘制了“sMask”这个遮罩精灵——同样是关闭了混合模式的,因此。

现在我们画面上是这样的:正常的背景画面上有一个按钮,然后上面被一个alpha值是0的矩形覆盖,但按钮的绿色区域哪一块的alpha值是1。接下来我们要打开alpha模式,但并不是读取精灵的alpha通道,而是去读取整个屏幕画面的alpha通道。因为精灵中并没有alpha值因此我们需要打开alphatest模式,这样会尝试从精灵中读取alpha值,然后在最后要记得再把这个测试模式关闭。


gpu_set_blendmode_ext(bm_dest_alpha,bm_inv_dest_alpha);
gpu_set_alphatestenable(true);
draw_sprite(sPlayer,0, mouse_x,mouse_y);
gpu_set_alphatestenable(false);
gpu_set_blendmode(bm_normal);

然后最后我们再把混合模式设置好,启用alpha通道来进行测试,然后我们设置以鼠标所在位置来绘制这个精灵,把所有的设置恢复到正常模式。最后我们点击运行,就会看到以下画面。

Image title



我们可以用类似的机制去实现许多类似上图的效果,比如高亮标记游戏中的一些元素等。甚至可以用来实现水的效果。

最后两段因为跟教程没什么关系就不翻了,不过这个教程确实蛮有意思,我尝试了一个移动遮罩的效果,感觉如果在一些解谜游戏中使用也是蛮有趣的。

Image title


(转发自:原日志地址
月度挑战——多人游戏(本地)
顺子 2017-11-03


这个挑战是Reddit的GameMaker版块的每月挑战,有兴趣的朋友可以一起参与一下:-)

原帖——Monthly Challenge 30 - November 2017

————————————————————————

各位游戏开发者大家好,本月是第三十期月度挑战!这个挑战系列是希望通过一些有趣的小任务,刺激大家运用自己的知识来挑战特定主题的游戏开发。


说到有趣,什么最有趣呢?当然是玩游戏!那比玩游戏更有趣的呢?那就是跟朋友一起玩游戏!


没错,本次的月度挑战是要挑战双人游戏,不过这里不涉及网络联机,而是单纯的本地多人游戏,如果你想尝试多人联机,那就自己去尝试一下,不过本次的挑战任务并不包含网络联机的内容。


通常来说,本地多人游戏至少需要为两名(或更多)玩家设计单独的控制器,另外游戏的玩法上需要有一定的竞争性或合作趣味,另外需要显示不同玩家的进度或者得分情况。游戏类型当然是可以任意的,但请注意要控制规模,不然很容易失控。如果你一下子想不到适合的游戏类型,可以试试看传统的双人游戏,比如Pong、双人青蛙跳等~



你可以通过以下方式来参与这次挑战:


  • 在你正在做的游戏里实现这个功能
  • 做一个小的Demo演示
  • 把相关的代码直接发出来
  • 任何方式,你喜欢就好!


完成这些挑战后直接回复来展现你的成果即表示完成挑战:)

难度标题描述
初学者随意行动完成两个玩家同时操控即可,比如玩家1使用键盘按键,玩家2使用鼠标,二者控制的内容并不重要,重要的是确保都能独立的输入事件,并且都能正确响应即可。
进阶试炼同生共死让两个玩家实现一定程度上的互动,比如相互碰撞,或以某种特殊方法修改对象,比如可以让他们在竞赛游戏中互相射击或是合作游戏中互相开门之类
高阶挑战随进随出允许随时加入游戏,让玩家2可以随时按“开始键”加入游戏中,如果没有玩家2时你也可以设置一个AI来替代。
在GMS2中配置iOS设备调试
顺子 2017-10-11

GameMaker Studio 2的移动版可以支持iOS和Android双平台的导出,之前我曾经写过Android平台的真机调试的配置方式,前阵子有朋友在后台问我怎么导出iOS版本,因此今天就来介绍一下。

注:GameMaker Studio 2导出iOS版本必须有一台Mac用于编译,并且必须拥有苹果开发账号,本文不涉及注册账号的内容,假定有这需求的朋友自己已经搞定账号和证书了

概述

Image title

首先,在IDE的右上角可以切换编译和调试用的平台,选择iOS即可,同样这里有VM和YYC两个选项,这里稍微说明一下这两种编译方式的区别:

VM模式是通过一个叫“YOYO Runner”的特殊运行器直接运行你的代码,而YYC模式则是完全编译成对应平台的原生代码再执行,前者可以理解成是使用GMS2自带的一个万能模拟器来运行你的游戏。

VM模式的运行效率稍微低一些,但是编译速度更快,并且可以使用调试模式(Debugger),而在YYC模式下是无法调试游戏的,当然YYC模式的运行效率更高性能更可靠,不过如果游戏比较大逻辑比较复杂的话编译的时间可能会比较长,各位可以自行权衡取舍。

如果你是用Windows开发的游戏,想要最终输出iOS版的话,也务必需要准备一台Mac电脑,因为GMS2只会导出Xcode的工程文件,还是要在Mac中使用Xcode来最终打包编译游戏的,而且官方说必须是真机,Mac虚拟机用不了。

设备配置

Image title

在YYC模式中点击笔形图标即可管理用来运行和调试的设备,跟之前Android设备的管理类似。

在设备列表中默认会同步Xcode中的设备列表进行展示,而进入设备管理界面后如下图:

Image title

点击“Detect Device”可以重新检测设备的连接情况,如果你的设备没有显示出来可以检查一下数据线有没有问题并重新插拔一下设备,也有可能是Xcode的同步有问题,可以先启动Xcode然后等几分钟再尝试检测设备。

另外,这里的列表不仅会显示已连接的iOS设备,在Xcode中创建的各个iOS模拟器也都会显示出来,并同样可以用于游戏调试,不过如果使用模拟器调试游戏,最好现在Xcode中把模拟器打开,因为临时启动模拟器耗时较长,另外如果你的Mac内存不够的话,可能会在这个过程中出错导致编译失败。

平台设置

在GMS2的设置项中可以设置输出Xcode工程的路径以及默认的证书信息

Image title


如果你的Mac尚未完成开发环境的配置,可能会导致后续的编译工作出问题,另外图中的复选框如果勾选上,在后续调试时项目会自动编译并打开,如果没有勾上,则项目只会被发送到Xcode中需要你手动编译运行。

测试项目

当完成以上所有配置以后,无论使用VM或YYC模式,只要点击“运行”按钮就可以编译游戏,项目会被编译成Xcode工程发送到Xcode中。

如果这是第一次测试该项目,还需要在项目的配置项中做一些调整,如下图所示:

Image title

你会发现在“Status”处有红色感叹号,这就意味着配置有问题无法正确编译执行,此处需要修改对应的“Bundle Identifier”。

你需要修改默认配置的包名“com.company.game”为你自定义的游戏包名,这个包名可在此处手动修改,在GMS2中也可事先定义,当修改完成后正常显示应该如下图所示:

Image title

当所有的警告信息消失后就可以开始运行游戏了,当你点击Xcode的运行按钮时,游戏将在你选择的设备(或模拟器)上运行起来。

以上最后关于Xcode的使用部分未进行太过详细的说明,相信准备编译iOS版本的同学自己应该已经从其它渠道对iOS的开发和编译有一定程度的了解了:)

(转发自:原日志地址
游戏暂停的简单实现(2/2)
顺子 2017-10-10

大家好,距离上一篇说游戏暂停实现机制的文又过去快半个月了,恰好HeartBeast第14集教程也是介绍如何实现游戏暂停,并且就是我们上次提到的将当前场景对象临时禁用的这种方法,今天我们就来介绍一下这一种实现游戏暂停的方式。

不同于我们上次介绍的方式,这个方法是在游戏当前场景中进行暂停,不涉及游戏场景的切换,同时引入了持久化对象的概念,对象属性勾上了持久化(Persistent)以后,该对象一旦创建实例,对应的实例就会存在于后续所有的游戏场景中(除非手动销毁),从而可以用于控制一些所有场景中都需要调用的方法和机制。

本文的具体操作会有别于HB的教程,但思路是一致的

HeartBeast的教程已经传到B站了——简单实现游戏暂停

控制暂停对象

如上文所说,我们需要创建一个新的对象,并把对象的持久化属性打上勾

Image title接着我们创建一个“Create”事件,并在其中定义两个全局变量:

global.paused_ = false ;
global.screen_pic = noone;

这里我们使用全局变量的目的是便于不同的对象都能正确调用和修改这两个值,这两个变量分别是:

  • paused_:标识当前是否暂停状态
  • screen_pic:用来保存暂停前的游戏画面

因为刚才我们创建的是一个暂停按钮,我希望用户在点击这个按钮时能触发暂停操作,因此可以创建一个点击事件,这里使用“Tap”或“Mouse->Left Down”都可以

Image title

然后在这个事件里写暂停相关的代码:

if !global.paused_{
    global.paused_ = true
    if sprite_exists(global.screen_pic) sprite_delete(global.screen_pic) ;
    global.screen_pic = sprite_create_from_surface(application_surface,0,0,room_width,room_height,false,false,0,0)

    instance_deactivate_all(all)
    instance_create_layer(room_width/2,room_height/2,"pipe",obj_continue)
}

这段代码整体在一个if判断中,第一哈昂的if判断就是判断当玩家点击这个暂停按钮时,当前游戏是否处于“非暂停”即正常游戏状态,注意这里我们前面用了一个感叹号"!"来表示否定,这里也可以使用下面这种写法(HB就是这么写的)

if not global.paused_

当判断条件成立时就直接进入执行暂停的操作,首先是把这个暂停的变量设置为“true”标记将要进入暂停状态

然后我们要给当前画面截屏,在截屏之前我们也做了一个if判断,这个判断是判断当前游戏中是不是已经有相同名称的截屏存在,如果有则要先把之前的截屏删除

这里用了sprite_exists()来判断精灵图像是否存在,然后用sprite_delete()来执行删除操作

if sprite_exists(global.screen_pic) sprite_delete(global.screen_pic) ;

然后我们创建一个新的截屏图像

global.screen_pic = sprite_create_from_surface(application_surface,0,0,room_width,room_height,false,false,0,0)

这里我们使用sprite_create_from_surface()这个方法获取当前应用窗口的界面图像并生成一个精灵图像,然后将其保存到了之前创建的全局变量global.screen_pic中。

这里使用的application_surface是一个默认的surface表面,就是当前整个应用窗口的表层图像窗口。

然后我们用 instance_deactivate_all()这个方法禁用所有的实例,并单独新建了一个用于恢复游戏的按钮"obj_continue"

instance_deactivate_all(all)
instance_create_layer(room_width/2,room_height/2,"pipe",obj_continue)

在HB的教程中因为他使用的是键盘按键来控制暂停事件,因此直接禁用和恢复实例就能实现暂停的效果,而我们这里是通过点击画面中的按钮来实现的,因此恢复游戏的按钮也需要单独创建出来才可以。

恢复游戏对象

好,在刚才我们已经完成了游戏的暂停操作,但是我们要如何解除暂停状态恢复正常游戏呢?

首先同样新建一个对象,由于这个对象也是一个按钮,我们同样创建一个“Tap”事件来执行恢复游戏的操作

Image title

在这个“Tap”事件中我们放入的代码如下:

instance_activate_all();
global.paused_ = false;
instance_destroy();

第一行是用instance_activate_all()恢复整个场景中所有的实例,这些实例会保留禁用之前的所有属性,包括坐标、状态等等。

第二行则是重新将暂停状态标记为“未暂停”的状态,表示即将正常恢复游戏

最后则是把自身这个恢复游戏的按钮销毁,因为在正常的游戏过程中这个按钮应该是不会出现的。

到此为止,其实游戏的暂停和恢复基本已经完成了,但是还不够完美。

因为在暂停按钮被按下时所有的实例都被禁用了,因此整个场景中所有的对象实例都会消失不见,整个画面是空的(tiles和背景等还会保留)

希望你还记得我们之前曾经在暂停时单独保存了一张游戏画面的截屏,也希望你在上一张图片中有留意到这个恢复游戏的按钮事件中有一个“draw”事件,我们就要在这个“draw”事件中把之前的截屏绘制到屏幕上,用这张图片来欺骗玩家的眼睛,让他以为因为游戏被“暂停”了所以上面的内容都静止不动了。

draw_sprite(global.screen_pic,0,0,0)
draw_self()

在这个draw事件中,我们首先把刚才截屏画面的精灵图像直接绘制到屏幕上,然后同样不要忘记当我们自定义“draw”事件以后一定要加上一个"draw_self",否则这个按钮本身就不会被绘制出来了。

这里还要注意这两行代码的顺序,因为根据绘制顺序不同的内容的深度也不同,后绘制的内容在上方,如果你先写了"draw_self",那绘制截图的时候就会把按钮给挡住,玩家同样会看不到那个恢复游戏的按钮了。

以上就是通过禁用实例和恢复实例的方式来实现游戏暂停机制的方法。

HB的教学视频中使用了用户事件来封装暂停的操作,而其实也可以用脚本来单独封装,HB没有使用脚本的原因是他的教学视频采用了试用版,对脚本的数量有限制。

而我也没有封装这个暂停操作,是因为感觉没什么必要,好像并没有很多不同的地方会去触发暂停,既然没什么复用的需求也就偷个懒了

(转发自:原日志地址
GMS2每次版本更新都卡住?如何手动下载GMS2的runtime工具
顺子 2017-10-09
前两天因为GMS2更新,又在首次更新runtime的时候无限卡了,这次似乎特别严重,以前等一会儿多试几次总能成功,这次却怎么也不行,昨天在公众号稍稍吐槽了一下,就有朋友来介绍手动更新的方法了,推荐给所有卡屏的朋友~
本文系知乎的爱德华所写,原文地址:每次版本更新都卡住?如何手动下载GMS2的runtime工具

前言

GMS2每次版本更新时都会卡住很久,而且因为更新界面比较简陋,用户无法通过界面信息及时了解到更新究竟是卡在了哪里,只能干瞪眼,让人无名火起。

实际上,版本更新所花费的时间,主要都耗在了下载上,因为国内链接yoyogames的服务器的速度很慢,所以每次更新都会花费巨量的时间来下载并不算大的文件。因此,如果你有VPN或者海外加速网络服务的,可以开启这类功能,也许会大幅加快你的更新速度。

而没有科学上网条件的朋友,我们也可以手动下载更新文件,通过下载工具,同样可以提高GMS的版本更新速度。

操作

  • step1

打开yoyogames官网runtimes页面(需登陆你的账号)

https://account.yoyogames.com/runtimes

  • step2

点击最新的runtime

Image title

  • step3

根据提示,下载右侧的文件(建议用支持断点续传的下载工具,不然你懂的),并根据页面提示拷贝到指定文件夹

Image title

上图为下载区,download按钮即指向对应文件的下载地址

Image title

上图为手动操作提示,红框内文字意思是:

如果你是windows,就拷贝到C盘的%programdata%\GameMakerStudio2\Cache\runtimes\runtime-2.1.1.158\downloads

如果你是mac,就拷贝到/Users/Shared/GameMakerStudio2/Cache/runtimes/runtime-2.1.1.158/downloads

如果没有这个文件夹的话,你需要手动创建下(不过如果你已在下载前开启GMS2进行了自动更新的话,GMS2应该已经创建这个文件夹)

注:我在实际操作过程中发现也可能是download,所以如果发现复制到地址栏打不开的话把末尾的downloads换成download试试,可能是官网打错了

注2:文件夹地址中的runtime-2.1.1.158这个是根据tuntime版本号来的,不同版本也会有相应的改变

  • step4

还没有结束,因为我在操作时发现,更新系统可能还会下载其他的文件,我们在将这些runtime文件复制到指定目录后,可以打开GMS2,看到软件还在更新后,刷新下指定目录,会发现文件夹内又有了新的文件,然而runtime页面并没有,怎么办?

没关系,其实这个更新的文件的地址就是,http://gm2016.yoyogames.com/xxxxx.zip (末尾为文件名),所以你只要复制这个文件名,加个http://gm2016.yoyogames.com/地址,复制到浏览器或者下载工具中,同样可以手动下载。下载完后复制到step3中提示的文件夹里即可。

如果打开GMS2后发现还有新文件要下载,重复本操作即可。

注:亦可通过官网的runtime RSS订阅地址下载最新runtime的所有RSS,地址如下:

http://gms.yoyogames.com/Zeus-Runtime.rss

(请留意版本号,其中后缀为zip的文档即是runtime文件)

Image title

把这些下载的文件都复制到指定的文件夹后,你会惊喜地发现更新只需数秒就完事了……

真是的是下载几小时,安装几秒钟。

本文完。

亦可参考官方指导:How To Manually Download GMS2 "Runtime" Build Tools

https://help.yoyogames.com/hc/en-us/articles/115001342672-How-To-Manually-Download-GMS2-Runtime-Build-Tools

(转发自:原日志地址
[ 开发 ] Reddit的GMS十月挑战——鬼怪特效
顺子 2017-10-06

好吧,十月份的挑战已经到来,本次的挑战主题是制作几款不同效果的鬼怪幻影——一个忽隐忽现的幽鬼,突然消失又突然出现幻步——迈着鬼步来回游荡的妖怪,特点是移动时会留下残影幻视——浑身散发妖异光芒的恐惧化身OK,所以就是需要你尝试用GameMaker来实现如上的三种特效——忽隐忽现、移动残影以及发光效果~

你可以用以下不同的方式参与到活动中来:

  • 直接在你正在制作的项目中使用该功能
  • 制作一个全新的demo
  • 提交一份相关的代码
  • 以及其他任何你喜欢的形式!

下面的表中有三项不同难度的挑战,可根据自己的实际情况选择挑战:

难度标题描述
初学者幻影创建一个若隐若现的对象,提示:你可以用draw_set_alpha来修改绘制图像的透明度以实现这个功能。
进阶难度幻步创建一个可移动的对象,移动是身后要跟出一串影子。提示:移动时创建一系列实例然后利用定时器使其自动消失
高阶挑战幻视创建一个发光的物体,提示:你可以使用着色器(shader)或任何你喜欢的方式,甚至粒子效果(particles)

你可以把画面做的很恐怖,或是很Q很可爱,只要你能在过程中学会一些新的技巧:)记住,最重要的是开心????

最后是原帖中给出的上述三种对象的示意图(不得不佩服找图的这位兄die~找的都是些啥)

幻影

Image title

幻步

Image title

幻视

Image title

游戏暂停的实现方式(1/2)
顺子 2017-09-23

不知道还有没有人记得我曾经说过自己在做一个FlappyBird用来练手,当时的进度是完成了基本的开始游戏、计分和死亡检测之类,但是后来就没再继续,这两天想了想还是准备捡一捡,然后就想先做一个暂停的功能。

Flappy Bird的暂停界面属于现在手机游戏特别典型那种,当玩家按下暂停按钮以后在当前的画面至上浮出一个暂停菜单,菜单里一般有“继续”,“重新开始”,“退出游戏”等一些基本的选项操作,如果玩家按下“继续”那就要回到游戏界面可以继续操作。

说起来并不复杂,而原理其实同样简单,通常来说有两种思路:

  • 制作一个专门的“暂停菜单”的游戏场景(room),当用户点击暂停以后跳转到那个场景
  • 在用户按下暂停按钮的同时把当前画面截屏并设置为背景画面,同时把场景中所有的对象全部禁用(deactivate),然后创建一个新的暂停菜单的对象供用户操作

今天先介绍第一种方法(才不是因为这种方法比较简单orz)

在介绍这之前,我们首先要了解游戏场景的一个自身属性"persistence"——持久化,当把这个属性设置为Ture并离开该场景后,场景中的所有数据都会保存为你离开时的状态,并会以该状态被直接带入下一个场景,之后你再回到这个场景就可以继续所有刚才未完成的操作。

了解了这个属性以后就很容易了,我们首先需要制作一个没有背景的暂停菜单的游戏场景

Image title

然后当我们在游戏场景中按下暂停按键后只需要简单的两行即可

room_persistent = true;     //把持久化属性设置为true
room_goto(room_pause);  //跳转去暂停菜单场景
global.current_room = room //把当前场景保存到一个全局变量中

如果我们要继续游戏,只需要给“Continue”按钮的对象中增加一个点击事件

room_goto(global.current_room);   //返回暂停前的游戏场景

这里我们用了一个全局变量来记录暂停前的游戏场景,这个操作是针对于一些拥有多个游戏场景的游戏而设置的,如果你的游戏十分简单,像FlappyBird一样只有一个固定的游戏场景,完全也可以写死跳转的roomid也能达到相同效果,不过为了培养良好的习惯最好还是不要用写死的方法。

今天介绍的方法真的很简单,不过可能会相对稍微多占用一些内存。

毕竟要把暂停前的游戏数据都临时存起来,但是对于一些小游戏而言也基本够用了:)


(转发自:原日志地址
在GMS中使用DS MAP存储/读取游戏信息
顺子 2017-09-19

之前介绍了在GameMaker Studio中使用自带函数和ini文件进行存/读档的方法,其实还有另一种方法,即采用GMS中的几种数据结构来保存数据——ds_map、ds_list、ds_grid等并且在GMS中自带了十分方便的方法可以直接把这些数据结构保存成物理文件(对应也有方法可以读取这些文件),今天以ds_map为例介绍一下这种方法,希望能有帮助。而保存和读取文件主要会使用到的方法是:

  • ds_map_secure_save
  • ds_map_secure_load

而要使用ds_map来保存数据,首先我们要对这种数据结构有一个大致的了解:

Image title

如上图,我们可以把ds_map想像成这样一个数据集合,其实它的数据结构跟ini配置文件十分类似,只是少了一个"section"的分类,其中的数据同样是由一对对"key"、"value"构成。而GMS中针对ds_map也有大量内置函数可以用于数据的增删改查:

  • ds_map_exists
  • ds_map_create
  • ds_map_add
  • ds_map_clear
  • ds_map_copy
  • ds_map_replace
  • ds_map_delete
  • ds_map_empty
  • ds_map_size
  • ds_map_find_first
  • ds_map_find_last
  • ds_map_find_next
  • ds_map_find_previous
  • ds_map_find_value
  • ds_map_read
  • ds_map_write
  • ds_map_destroy

有关ds_map的相关函数可以看这里——映射表(DS Map)

接下来,我们用一个实际的例子来说明一下如何用ds_map来保存游戏信息:

假设我们现在有一个简单的RPG游戏,现在要保存游戏进度,我们需要保存的内容有英雄的等级、血量、攻击力,以及英雄身上的装备的信息

save_1 = ds_map_create(); //创建一个ds_map保存到save_1变量中

//
ds_map_add(save_1,"Head",hero_head);// 在save_1中增加一个key为"Head"的数据,值为hero_head
ds_map_add(save_1,"Right Hand",hero_right_hand);//同上
ds_map_add(save_1,"Left Hand",hero_left_hand);//同上
ds_map_add(save_1,"Level",hero_level);//在save_1中增加一个key为“Level”的数据,值取hero_level对应的值
ds_map_add(save_1,"HP",hero_hp);//同上
ds_map_add(save_1,"Attack",hero_attack);//同上

ds_map_secure_save(save_1, "save1.dat" ) ;//把save_1中的ds_map保存为一个名为"save1.dat"的物理文件
ds_map_destroy(save1.dat);//释放内存

通过以上操作我们就可以完成我们之前的目标,将需要保存的信息通过ds_map进行储存并加密保存为一个物理文件,由于这种保存方式自带加密操作,因此存档文件的安全性较ini配置文件要更高。而当游戏中执行加载存档的方法时我们可以这么做:

load_save = ds_map_secure_load("save1.dat")//加载"save1.dat"文件中的ds_map保存到"load_save"中

//加载ds_map中的数据保存到游戏变量中
hero_head = ds_map_find_value(load_save, "Head");
hero_right_hand = ds_map_find_value(load_save, "Right Hand");
hero_left_hand = ds_map_find_value(load_save, "Left Hand");
hero_level = ds_map_find_value(load_save, "Level");
hero_hp = ds_map_find_value(load_save, "HP");
hero_attack  = ds_map_find_value(load_save, "Attack");

ds_map_destroy(load_save);//释放内存

以上,其实从原理上来说跟ini配置文件的使用方式是十分类似的,但是个人更推崇这种方式

  • 加密性更好
  • ds_map的数据结构也更好与ds_list和ds_grid等联动
  • ini配置文件同时只能开启一个,ds_map只要内存不炸理论上可以多个同时操作

在写这篇教程的时候找到了几篇详细介绍数据结构的教程,好像很棒,过几天找时间翻译一下:)

(转发自:原日志地址
GameMaker Studio 2中简易存/读档
顺子 2017-09-18

除了Flappy Bird之类的小游戏,许多游戏都需要保存和载入进度,以便玩家可以随时中断游戏,之后又能继续玩下去,在GameMaker Studio中有几种方法都可以实现类似的功能,常用的有以下几种方法:

  • 自带函数game_save()
  • ini配置文件保存
  • ds_map/ds_list存储

大家可以根据自己游戏的复杂程度和自己的喜好来选用,今天先介绍前两种。

自带函数game_save()

在GameMaker Studio中自带两个存档和读档的方法:

  • game_save()
  • game_load()

这两个方法在调用时都只需要在括号中填入字符串格式的文件名作为参数即可

game_save("save.dat")
game_load("save.dat")

如上的代码就是将当前游戏的一些静态内容保存到一个名为"save.dat"的文件中(或从中读取),如各种变量等,但这个方法无法指定哪些内容需要保存,而哪些又需要排除,并且无法保存游戏中创建的动态资源,诸如数据结构(ds_list、ds_map等)。并且当你开启了某个场景(room)自带的物理特性以后这个方法就会失效无法使用。另外好像这种方法保存的文件在重新编译更新游戏版本时是无法继承的。所以这个方法其实还是十分有局限性的,只有当游戏非常简单对保存的数据内容非常有限时可以考虑用一下。

ini配置文件保存

这可能是最普遍的一种方法,可以自定义需要保存的值,而且不算复杂,但是使用这种方法首先要对ini配置文件有基本的理解,不清楚的可以先去了解一下——INI文件

Image title


使用ini配置文件保存数据的方法如下:

ini_open("saveData.ini"); //打开ini文件,如果不存在则新建
ini_write_real("Variables", "score", score); //第一个值是section,第二个值是key,第三个值是对应要写入的value
ini_write_string("Variables","string",string); //同上,不过这是写入字符串的方法
ini_close(); //用完记得关闭文件否则容易内存溢出

在使用ini_open的方法打开/新建了一个ini配置文件以后,就可以将你所需要的值全部存入这个文件,比如血量、分数、人物等级等等,甚至可以把每一个对象的坐标都保存到文件里,方便下次载入时能让画面完全恢复原装。而当你载入游戏时就需要用以下方法进行读取:

ini_open("saveData.ini");
score = ini_read_real("Variables","score",0); //如果文件中没有对应的值则会将第三个参数保存到文件里
string = ini_read_string("Variables","string","Hello world"); //同上,只是字符串的方法而已
ini_close();

用这个方法你可以把你之前保存的数据依次读取出来,然后加载到你当前的游戏中来,这里要注意的是如果在存档文件中没有获取到对应的值时,会将此处第三个参数的内容作为默认值写入存档文件,切记。

(转发自:原日志地址
GMS常用脚本介绍(3&4&5)
顺子 2017-09-15

刚发现介绍脚本的这最后的集合一直留在草稿里没发……抓紧发了orz

Wrap()

Wrap这个脚本是用来做什么的呢?先来看一下这个GIF图其实就是把传入的一个值约束在指定的范围内来回递增/减,并且钟摆循环。

Image title


代码正文

///@description Wrap(value,min,max)
///@function Wrap
///@param value
///@param min
///@param max
//returns the value wrapped

var _val = argument[0];
var _max = argument[1];
var _min = argument[2];

if(_val mod 1==0 )
{
    while(_val > _max) || _val < _min)
    {
        if(_val > _max)
        {
            _val = _min + _val - _max -1 ;
        }
        else if (_val < _min)
        {
            _val = _max + _val - _min + 1 ;
        }
        else
            _val = _val ;
    }
    return(_val);
 }
 else
 {
     var _old = argument[0] + 1 ;
     while(_val != _old)
     {
         _old = _val ;
         if (_val < _min)
                _val = _max - (_min - _val) ;
        else if(_val > _max)
                _val = _min + (_val - _max) ;
        else
                _val = _val ;
     }
     return(_val);
 }

JumpInDirection()

下面这个脚本其实很简单,就是在你指定跳跃的角度和距离后,即可让对象朝着指定的方向移动对应距离的脚本,而脚本中的那两句代码在HeartBeast的横版射击游戏教程中也有使用,就是把官方内置的方法"lengthdir_x"和"lengthdir_y"组合使用,这个脚本只是单纯简化了操作。

代码正文

/// @description JumpInDirection(length,direction)
/// @param length
/// @param direction
var len = argument0 ;
var dir = argument1;

x += lengthdir_x(len,dir);
y += lengthdir_y(len,dir);

Chance()

这个脚本更简单,只有一行,理论上来说也完全可以直接把代码写到需要实用的地方去,但是通过脚本可以使得可读性更强,这个脚本的目的是计算概率,下面举个例子

if Chance(0.7)
{
    //写在这里的代码就有70%的概率被触发
}

在参数里只要填入0~1的小数即可直接设定触发代码的概率,从而实现触发概率的功能。而实现机制则异常简单,稍加思考应该就能明白:)

代码正文

return argument0 > random(1) ;
(转发自:原日志地址
Reddit的GMS九月挑战——系统时间
顺子 2017-09-06

从这个月开始继续同步更新reddit的每月挑战吧,一个月一个主题压力也不大,有兴趣的朋友也可以一起参与一下,这是第28期了。

本月的主题是关于“系统时间”的运用,很多人可能会觉得奇怪,“系统时间”能用来做什么呢?首先,通过读取系统时间,你的游戏可以模拟真实世界的时间流逝,你可以让你游戏中的太阳随着实际的时间而改变位置,也可以根据实际时间切换白天/黑夜。或者让敌人的力量随着时间的变化而得到调整,也可以随着时间来动态调整游戏的元素以提供更多的趣味性。

你可以用以下不同的方式参与到活动中来:

  • 直接在你正在制作的项目中使用该功能
  • 制作一个全新的demo
  • 提交一份相关的代码
  • 以及其他任何你喜欢的形式!

下面的表中有三项不同难度的挑战,可根据自己的实际情况选择挑战:

难度 标题 描述
初学者 当前的时间 读取你本机的时间然后显示在游戏中
进阶难度 实时变化 在游戏内设置一些会随着时间变化而发生改变的变量(比如游戏中太阳坐标随着时间而移动,或者设定一扇只有在特定时间才会打开/关闭的门)
高阶挑战 时光飞逝 在游戏中设置一些随着时间动态变化的可见的元素(比如从种子长成一株植物,或游戏背景从白天变成夜晚)

小Tips: 可以使用以下这些函数

  • current_second
  • current_minute
  • current_hour
  • date_current_datetime
  • date_get_second
  • date_get_minute
  • date_get_hour

不用太刻意去迎合示例的要求,只要符合主题,想怎么做都是可以的:)

加入 indienova

  • 建立个人/工作室档案
  • 建立开发中的游戏档案
  • 关注个人/工作室动态
  • 寻找合作伙伴共同开发
  • 寻求线上发行
  • 更多服务……
登录/注册