【插件】协程

GameMaker 开发者之家2021/12/5插件教程

协程是由@jujuadams为GameMaker Studio 2.3.6及以上版本设计的一套在GML中异步执行代码的语法扩展


适用GM版本 : 2.3.6+

适用目标平台 :除HTML5之外的一切平台。

下载连接Githubopen in new window

常见问题解答

协程可以在哪些平台上运行?

理论上支持除了HTML5之外的一切。到目前为止,我只在Windows上进行了测试。整个扩展都是原生GML编写的,所以它应该可以开箱即用。您可能会在某个不经常测试的平台上遇到特殊的错误。如果你发现了他们,请报告open in new window给我们。

此代码库的许可证是什么?我可以将其用于商业项目吗?

该库在MIT许可open in new window下发布。这意味着您可以将其用于任何您想要的目的,包括商业项目。你可以把我的名字放在你的感谢列表(Juju Adams)并且说谢谢,这对我来说意义重大,但你没有义务这样做。

我认为您缺少一个有用的功能,我希望您实现它!

很好!请提出Issuesopen in new window。Issues使协程的使用更有趣。而且这可以让我在公共交通上感到无聊时思考一些事情。

我发现了一个错误,解决问题的最佳方法是什么?

请提出Issuesopen in new window。Juju 每天都会检查 GitHub,错误修复通常会在几天后发布。你也可以去Discord服务器open in new window上找我,但这并不能替代一个很好的清晰的错误报告。

帮我!我收到有关异步事件的编译错误。

由于 GMS2.3.6 更改了一些与异步事件相关的常量名称,低于 v2.3.6 的 GameMaker 版本将不可以编译此库。您可以通过在 GM 的文档中找到旧名称并将其插入自己的项目来解决此问题。

协程是否适用于GMLiveopen in new window

我不知道。

说到GMLive,其作者还通过GMEdit实现了协程。这个库有什么不同?

Vadim早在GameMaker Studio 2.3将匿名函数引入GML之前就编写了他的协程实现。这是一个非常聪明的技术解决方案,可以解决旧版GM中许多缺失的功能。 幸运的是,GameMaker现在更加灵活,这个库主要利用了新功能来实现协程。

谁写的代码?

这个库是由@jujuadamsopen in new window经过长时间修补GameMaker而建立和维护的。Juju参与了很多商业GameMaker游戏open in new window

我可以给你捐款吗?

谢谢你想表达你的感激之情,这对我个人来说确实意义重大——但我很幸运的能从Gamedev中获得稳定的收入。如果你想支持我的工作,请在你的游戏里给我一个名字就够了。

协程简介

当你写代码并运行你的游戏时,你写的代码是按顺序执行的,一个命令在前一个命令完成后立即执行。我们将代码存储在函数中,目的是让程序从一个函数进入下一个函数,使游戏运行。当程序进入一个函数时,它必须完成整个函数(或至少到 return 命令为止),然后程序才能退出函数并运行其他代码。这被称为“同步执行”,这就是GameMaker的工作方式——代码是同步运行的,一行接一行,每帧数千行代码分布在数百个函数中。

异步函数的操作方式则不同。异步函数不需要完全完成所有的代码,程序就可以离开该函数,做其他事情,关键是,异步函数可以从他们离开的地方恢复,从他们暂停的地方恢复执行。这意味着你可以写一些代码在它的中间,以允许总体程序去做其他事情,并在稍后返回到该函数。

这个库允许你在GameMaker中编写可暂停的函数。这些可暂停的函数被称为 "coroutines":一个代码块,可以在执行过程中暂停,稍后再继续。

许多语言原生支持编写异步代码,要么是作为核心语言功能,要么是实现异步执行的库。GML则是后者。因此,我们有必要开发自己的系统,以模拟那些支持异步函数的语言的特征和行为。从纯函数中构建corountines是笨拙的,难以遵循的,所以这个库扩展了GML的语法,这样我们就可以用更优雅的方式来描述coroutines。

案例

让我们看一个简单的例子:过场动画。

场景动画是一场噩梦,多年来人们花了很多精力为场景问题设计和建立不同的解决方案。最常见的解决方案是使用一个大的switch...case语句来控制什么实例做什么,什么时候做。构建这些系统是非常耗时的,而且它们最后总是很难看,不实用。在现实中,我们想要的切入函数是一个异步函数——一个可暂停的函数,它可以向游戏传递指令,使物体产生动画并显示文本,而该函数不会阻止游戏的其余部分运行。

下面是一个场景动画的例子。我们将在后面详细介绍正在发生的事情,但这应该能让你体会到用协程可以实现什么样的事情。

function CutsceneFindMyFroggy()
{
	return CO_BEGIN
		//阻止玩家使用正常的控制手段进行移动
		oPlayer.inCutscene = true;
		
		//让玩家走进房间
		WHILE (oPlayer.x != 55) THEN
			oPlayer.x = min(oPlayer.x + 2, 55);
			YIELD THEN
		END
		
		//显示一些对话
		oTextbox.text = "我的青蛙在哪里?";
		oTextbox.expression = sPlayerSad;
		AWAIT keyboard_check_pressed(vk_space) THEN
		
		oTextbox.text = "...";
		
		//为达到喜剧效果而短暂停顿片刻
		DELAY 350 THEN //毫秒
		
		oTextbox.text = "Ribbit!";
		oTextbox.expression = sFrog;
		audio_play_sound(sndLonelyRibbit, 1, false);
		
		//让青蛙跳到玩家的怀里
		WHILE (oFrog.x != 55) THEN
			oFrog.x = max(oFrog.x - 2, 55);
			YIELD THEN
		END
		
		oTextbox.text = "啊,她来了!我美丽的两栖动物?";
		oTextbox.expression = sPlayerHappy;
		AWAIT keyboard_check_pressed(vk_space) THEN
		
		oTextbox.text = "Ribbit! :)";
		oTextbox.expression = sFrog;
		audio_play_sound(sndHappyRibbit, 1, false);
		AWAIT keyboard_check_pressed(vk_space) THEN
		
		//清空文本框,然后释放玩家
		oTextbox.text = "";
		player.inCutscene = false;
	CO_END
}

这种语法与普通的GML有些不同。特别值得注意的是,它使用了新的流程控制关键字。毫无疑问,WHILE你已经见过了(尽管是小写的while循环命令),而许多其他的命令是新的或不熟悉的。

CO_BEGINCO_END定义了循环程序的开始和结束点。这两个标记之间的一切都构成了协程的代码。普通的GML可以在整个协程中使用,比如修改其他实例中的变量,但是为了实现协程的特定行为,必须使用特殊的协程语法。THENEND是结构性命令,在使用上类似于打开{和关闭}的大括号。

AWAITDELAY是为了方便而提供的命令。AWAIT将暂停该行代码的循环程序,直到条件返回true。在上面的例子中,我们在等待用户按下空格键,然后再推进到后面的指令。DELAY将等待一定的时间后再继续进行(以毫秒为单位)。

最后,我们有一个不寻常的命令YIELD。这个功能是循环程序的核心。当一个协程碰到YIELD命令时,它会立即跳出协程,允许你的游戏执行其他指令。在下一步中,该程序将从YIELD命令中恢复,继续执行代码。

让剪辑变得更容易处理只是一个开始:协程可用于UI动画、复杂的网络协议、REST API和OAuth流程、多阶段视觉效果、异步保存/加载(控制台开发需要),还有更多。它们是非常有用的,现在它们在GameMaker中出现了。

配置

__CoroutinesConfig()是一个包含少量宏的脚本,可以用来定制协程库的全局行为。如果你想改变库的默认行为,你应该编辑这些宏。

COROUTINES_CHECK_SYNTAX

预期值: Boolean, true or false

是否对协程进行运行时语法检查。这在启动协程时有性能上的损失,但在执行协程时没有。此外,语法检查是一个实验性的功能。如果你发现语法检查器没有帮助的话,它可能会抛出假的结果。

  1. 提交一个错误报告!open in new window 这个问题很可能是可以解决的!
  2. 将此宏设置为 false

此外,运行时语法检查在创建一个协程程序时有一点小的性能损失。如果你觉得你需要更多的速度,你可能想把这个宏设置为"false"来回收一点CPU时间。

COROUTINES_DELAY_REALTIME

预期值: Boolean, true or false

将此宏设置为true,以测量DELAY命令的持续时间,单位为毫秒。如果你需要每一帧的准确性,那么将这个宏设置为false

COROUTINES_DEFAULT_CANCEL_WHEN_ORPHANED

预期值: Boolean, true or false

当协程的主体被销毁或被垃圾回收时,取消轮子的行为。一个特定的程序在成为孤儿时是否被取消,也可以用.CancelWhenOrphaned()方法来控制。

注意: 一个停用的实例算作一个不存在的实例。

COROUTINES_DEFAULT_CREATOR_WEAK_REFERENCE

预期值: Boolean, true or false

这个宏与上面那个宏有关。如果你在一个结构的范围内创建一个coroutine,coroutine需要保持对该结构的引用,以便.GetCreator()方法能够返回一个值。如果你期望(或打算)该结构在某一时刻被垃圾回收,这将导致一个问题,因为如果coroutine持有一个强引用,coroutine将保持该结构的活力。将此宏设置为"true "将默认每个结构的引用为弱引用,以避免这一问题。可以使用.WeakReference()方法来进一步调整单个coroutine持有的引用类型。

COROUTINES_GAMEMAKER_BROADCASTS_TRIGGER_NATIVE

预期值: Boolean, true or false

Coroutines有它自己的本地广播系统。广播可以用CoroutineBroadcast()函数进行,监听器可以用AWAIT_BROADCAST设置。GameMaker有自己的广播系统,精灵和序列可以发射事件。GameMaker的广播可以被Coroutine使用AWAIT_ASYNC_BROADCAST接收,并且GameMaker的全局变量event_data将被访问,正如你所期望的那样。Coroutine的广播和GameMaker的广播是两个不同的系统,通常不会相互影响。

有时能够使用AWAIT_BROADCAST来接收GameMaker的广播是很有用的。将这个宏设置为true将允许GameMaker广播触发本地Coroutine广播监听器。然而,如果GameMaker广播触发了一个本地监听器,那么event_data将不能被访问。将此宏设置为 "true "不会禁用 AWAIT_ASYNC_BROADCAST,所以要注意不要混淆行为。

协程语法

以下是用于定义coroutine的有效命令宏。请记住,每个cououtine命令宏之间的代码都在独立的GML函数中。这些函数在同一范围内执行(coroutine根结构),但由于在不同的函数中,它们不能共享局部变量("var"变量)。

基础

CO_BEGINCO_END

coroutineRootStruct = CO_BEGIN
    show_debug_message("This is an example coroutine.");
CO_END

CO_BEGINCO_END被要求放在所有Coroutine代码的括号内。COROUTINE命令必须放在这两条命令中才有效(否则你可能会遇到致命的编译错误)。CO_BEGIN为创建的coroutine实例返回一个coroutine根结构。如果你想从coroutine中读取数值或使用方法控制其执行,那么你将需要保持对coroutine根结构的引用。

创建一个程序将自动把它添加到一个全局的程序列表中,以便每一帧都被执行。一旦该程序完成,它将被从全局执行中移除,并可用于内存回收。当然,如果你在该程序完成后还保持着对该程序的引用,那么在你持有的引用也被丢弃之前,它将不会被垃圾回收。

coroutineRootStruct = CO_BEGIN
    show_debug_message("This is an example parent coroutine.");
    
    CO_BEGIN
        show_debug_message("This is an example child coroutine.");
    CO_END
CO_END

Coroutine定义可以相互嵌套,因此一个coroutine父级可以创建额外的coroutine子级。在一个父下创建的子将继续执行,无论父是否被暂停、取消或以其他方式进行交互。每个子程序都存在于它自己的范围内,因此每个子程序内的变量对该程序来说是唯一的。父和子不共享变量,子代程序之间也不共享变量。

子程序不会阻止父程序的执行——如果你想让子程序阻止其父程序的执行,请使用RACESYNC命令(或者使用AWAIT创建你自己的功能)。

THEN

coroutineRootStruct = CO_BEGIN
    show_debug_message("This will");
    THEN
    show_debug_message("be displayed");
    THEN
    show_debug_message("in the");
    THEN
    show_debug_message("same frame");
CO_END
coroutineRootStruct = CO_BEGIN
    REPEAT 5 THEN
        show_debug_message("Five messages!");
    END
CO_END

THEN本身没有特别的意义,如果在没有上下文的情况下使用,只是将代码块附加到前面的代码块的末尾。然而,THEN在许多地方是必需的语法,应该按照这些命令的指示来使用。

CO_PARAMS.<variable>

function ShowPopUpMessageTwice(_message)
{
    CO_PARAMS.message = _message;
    
    return CO_BEGIN
        show_message(_message);
        DELAY 1000 THEN
        show_message(_message);
    CO_END
}

CO_PARAMS允许你在定义程序之前在程序中设置变量。如果你的coroutine位于一个启动新coroutine的函数内,这对传递参数很有帮助。

CO_ON_COMPLETE

coroutineRootStruct = CO_BEGIN
    list = ds_list_create();
CO_ON_COMPLETE
    //Clean up the list to avoid a memory leak
    ds_list_destroy(list);
CO_END

CO_ON_COMPLETE增加了额外的、最终的代码,当它完成时,将由coroutine执行。CO_ON_COMPLETE在调用.Restart()方法时也将被执行。

请注意CO_ON_COMPLETE代码的内容必须是简单的GML。这意味着你不能在代码块内使用coroutine命令。

CO_SCOPE = <struct/instance>

这是为方便而提供的高级功能,在使用时不应不加注意。

////Create Event of an object
//Set the scope of the next coroutine to ourselves (CO_SCOPE is reset by CO_END)
CO_SCOPE = self;
//Start 
CO_BEGIN
    WHILE true THEN //Repeat forever!
        //Randomize our position and angle
        image_angle = random(360);
        x = xprevious + random_range(-5, 5);
        y = yprevious + random_Range(-5, 5);
        
        //Wait 120ms before doing this again
        DELAY 120 THEN
    END
CO_END
////Draw Event
draw_self();

时不时地,让一个coroutine直接与一个实例(或结构)的状态进行交互是很有用的。CO_PARAMS.GetCreator()是为了帮助coroutine和游戏中的其他数据容器之间顺利互动而提供的,但在特定范围内调用代码可能是有利的。

当一个协程被生成时,每个命令之间的所有代码被收集在一个本地GameMaker函数中。这个函数的范围,默认情况下,被强制为根coroutine结构。这确保了实例变量总是在一个隔离的环境中被创建和修改。虽然这比其他方法要安全得多,但也会有不便之处。CO_SCOPE覆盖了默认的行为(范围为coroutine结构),这样函数的范围为你选择的实例或结构。

然而,coroutine结构仍然被生成,并将作为调用coroutine方法的端点而存在。所有的程序方法仍然可以通过直接引用由CO_BEGIN返回的程序结构来访问。

CO_SCOPE适用于下一个程序定义,并且只适用于下一个定义。当CO_END被调用时,CO_SCOPE将被重置为默认行为(范围为根冠状结构)。在这方面,CO_SCOPE类似于CO_PARAMS

请注意,通过使用CO_SCOPE,很容易产生冲突,即两个coroutine争相为一个实例设置相同的值。这可能会导致令人不快和棘手的bug的修复。使用该功能的风险由你自己承担

CO_LOCAL.<variable>

CO_PARAMS.cells_to_travel = 10;
CO_SCOPE = self;
CO_BEGIN
    CO_LOCAL.i = 0; //Use a coroutine variable to count how many times we've moved
    WHILE CO_LOCAL.i < CO_LOCAL.cells_to_travel THEN
	    
        //Move down the grid, 32px at a time
        y += 32; 
        
        //Change our sprite
        sprite_index = sprMoveDown;
        
        //Wait 90ms before doing this again
        DELAY 90 THEN
    END
CO_END

CO_LOCAL包含一个对当前正在处理的循环程序的引用。默认情况下,CO_LOCAL将是coroutine代码块内部的self作用域。如果你使用CO_SCOPE(见上文),情况就会改变,因为现在的coroutine代码块是在其他实例/结构的范围内运行。为了能够引用由coroutine根结构持有的沙盒变量,需要CO_LOCAL

返回值

YIELD <expression> THEN

coroutineRootStruct = CO_BEGIN
    show_debug_message("This will");
    YIELD THEN
    show_debug_message("be displayed");
    YIELD THEN
    show_debug_message("over several");
    YIELD THEN
    show_debug_message("different frames");
CO_END
coroutineRootStruct = CO_BEGIN
    i = 1;
    REPEAT 5 THEN
        YIELD i THEN //Yield the values 1, 2, 4, 8, 16 over 5 frames
        i *= 2
    END
CO_END

YIELD指示协程程序暂时停止执行协程程序并返回一个值。与PAUSE...THENRETURN不同,执行将在下一帧恢复,不需要任何其他动作。由YIELD发出的值可以使用.Get()方法从coroutine中读取。如果在YIELDTHEN之间没有指定值,那么.Get()将返回undefined

请注意YIELD命令后面必须有THEN命令。如果你忘记了THEN命令,那么代码将神秘地不能运行,并且会出现"跳过"。

PAUSE <expression> THEN

coroutineRootStruct = CO_BEGIN
    show_debug_message("Look left");
    PAUSE "left" THEN
    show_debug_message("Look right");
    PAUSE "right" THEN
    show_debug_message("Look left again");
    PAUSE "left" THEN
    show_debug_message("Then cross the road");
CO_END

PAUSE指示coroutine立即暂停执行并返回一个值。这与YIELD的行为类似,但与YIELD不同的是,一个暂停的coroutine不会在下一帧恢复执行。你将需要调用.Resume()方法来恢复暂停的 coroutine 的执行。由PAUSE发出的值可以使用.Get()方法从该协程程序中读取。如果在PAUSETHEN之间没有指定值,那么.Get()将返回undefined

请注意PAUSE命令后面必须有THEN命令。如果你忘记了THEN命令,那么代码将神秘地不能运行,并出现"跳过"。

RETURN <expression>

coroutineRootStruct = CO_BEGIN
    IF oPlayer.x > 55 THEN
        RETURN "Too far right"
    ELSE
        CutsceneFindMyFroggy();
        RETURN "Playing cutscene"
    END_IF
CO_END

RETURN指示coroutine立即完成执行并返回给定值。与YIELDPAUSE不同的是,该程序的执行被完全停止(尽管该程序可以通过.Restart()方法重新启动)。由RETURN发出的值可以用.Get()方法从该协程程序中读取。如果在RETURN后没有指定任何值,那么.Get()将返回undefined

请注意RETURN命令后面不需要有THEN命令。任何写在RETURN命令后面的东西当然不会被执行,就像GML的本地return命令。

RESTART

coroutineRootStruct = CO_BEGIN
    CreateSmokeParticle(oChimney.x, oChimney.y);
    DELAY random_range(300, 350) THEN
    RESTART
CO_END

RESTART指示异步协程产生,然后在下一个异步协程框架中,restart执行。如果调用coroutine结构上的.Get()方法,一个被重新启动的coroutine将返回undefined。通过将RESTART放在一个循环程序的末尾,你可以让一个循环程序无休止地循环,直到被cancel。CO_ON_COMPLETE将在重新启动一个循环程序时被调用。

请注意,循环程序中的变量不会被重置。

请注意RESTART命令后面不需要有THEN命令。任何写在RESTART命令后面的东西都不会被执行,就像GML的本地return命令。

循环

END

coroutineRootStruct = CO_BEGIN
    REPEAT 5 THEN
        show_debug_message("Five messages!");
    END
CO_END

它本身没有任何作用。然而,END对于终止REPEATWHILEFOREACH循环是必要的。它也应该被用来终止RACESYNC块。在其他情况下,不能使用它。

REPEAT <expression> THEN <function> END

coroutineRootStruct = CO_BEGIN
    REPEAT 5 THEN
        show_debug_message("Five messages!");
    END
CO_END

类似于GameMaker自己的repeat()循环。没有必要在所有的情况下使用这个宏来取代标准的repeat()循环。只有当repeat()循环包含一个coroutine命令时,才需要使用REPEAT...END循环。

WHILE <condition> THEN <function> END

coroutineRootStruct = CO_BEGIN
    fireball = instance_create_depth(oPlayer.x, oPlayer.y, 0, oFireball);
    
    //Wait until the fireball has risen above the player by 30 pixels
    WHILE fireball.y <= fireball.ystart - 30 THEN
        fireball.y -= 5;
        YIELD THEN
    END
    
    //Then shoot the fireball at the nearest enemy!
    nearest = instance_nearest(fireball.x, fireball.ystart, oEnemy);
    fireball.direction = point_direction(fireball.x, fireball.y, nearest.x, nearest.y);
    fireball.speed = 11;
CO_END

WHILE类似于GameMaker自己的while()循环。没有必要在所有的情况下使用这个宏来取代标准的while()循环。只有当while()循环包含一个coroutine命令时,才需要使用WHILE...END循环。

FOREACH <iteratorVariable> IN <iterableData> THEN <function> END

coroutineRootStruct = CO_BEGIN
    highestHP = 0;
    highestInstance = noone;
    
    //Find the enemy from our array with the highest HP
    FOREACH instance IN global.arrayOfEnemies THEN
        if (instance.hp > highestHP)
        {
            highestHP = instance.hp;
            highestInstance = instance;
        }
    END
    
    //Bash them!
    if (instance_exists(lowestInstance)) hp -= 100;
CO_END

FOREACH...THEN循环是一个方便的功能,它在以下两种情况下进行迭代

  1. 一个数组。
  2. 一个结构。
  3. 一个对象的实例。
  4. 或从一个循环程序输出的`YIELD'。

当在数组上迭代时,迭代器变量是由数组本身给出的值。当迭代结构时,迭代器变量从结构中获得数值;要迭代结构的键,请使用variable_struct_get_names()

当迭代对象的实例时,迭代器变量被赋予实例引用(实例的结构表示,例如,在实例的范围内调用`self'得到的)。请注意,"FOREACH "循环的行为与GameMaker的本地 "with() "循环不同:"FOREACH "循环中的代码范围不会改变。

当迭代一个循环的输出时,YIELD值被分配给迭代器变量。FOREACH...THEN循环将在可迭代的循环程序完成时终止。

请注意不要修改你正在迭代的数组或结构。当FOREACH...THEN循环开始时,迭代的总数被计算出来,如果数组或结构的大小发生变化,则可能导致崩溃和其他错误。

BREAK

coroutineRootStruct = CO_BEGIN
    healthRemaining = oPlayer.hp;
    FOREACH heart IN global.heartInstances THEN
        heart.sprite_index = min(4, healthRemaining);
        healthRemaining -= 4;
        if (healthRemaining <= 0) BREAK;
    END
CO_END

类似于GameMaker自己的break命令。立即脱离REPEAT...THENWHILE...THENFOREACH...THEN循环,不执行循环中的其他代码。循环中的其余代码将正常执行。

请注意,标准的GML break 命令不会在coroutine循环中发挥作用。

CONTINUE

coroutineRootStruct = CO_BEGIN
    FOREACH enemy IN objEnemy THEN
        IF point_distance(oPlayer.x, oPlayer.y, enemy.x, enemy.y) > 100 THEN
            CONTINUE
        END_IF
        
        enemy.vspeed -= 4;
    END
CO_END

类似于GameMaker自己的continue命令。强制执行一个循环(无论是 REPEAT...THENWHILE...THEN,还是 FOREACH...THEN 循环),立即进入下一个迭代,而不执行循环中的其他代码。

请注意,标准的GMLcontinue命令在coroutine循环中不起作用。

分支

IF <condition> THEN <function> END_IF (and ELSE and ELSE_IF)

coroutineRootStruct = CO_BEGIN
    healthRemaining = oPlayer.hp;
    
    FOREACH heart IN global.heartInstances THEN
        heart.sprite_index = min(4, healthRemaining);
        healthRemaining -= 4;
        
        IF (healthRemaining <= 0) THEN
            BREAK;
        END_IF
        
        YIELD THEN
    END
CO_END

类似于GameMaker自己的ifelse命令。一个IF必须由一个END_IF来匹配。通常不需要使用这些特定的命令。如果if-else(或else-else等)本身包含一个coroutine命令,你应该使用这些宏,但ASYNC_COMPLETE除外。ELSEELSE_IF也被支持。

请注意ELSE IF是不正确的语法,会导致编译错误,请确保使用ELSE_IF

其他

DELAY <expression> THEN

coroutineRootStruct = CO_BEGIN
    WHILE instance_exists(oRainbow) THEN
        oRainbow.image_blend = c_red;
        DELAY 500 THEN
        oRainbow.image_blend = c_orange;
        DELAY 500 THEN
        oRainbow.image_blend = c_yellow;
        DELAY 500 THEN
        oRainbow.image_blend = c_lime;
        DELAY 500 THEN
        oRainbow.image_blend = c_aqua;
        DELAY 500 THEN
        oRainbow.image_blend = c_purple;
        DELAY 500 THEN
    END
CO_END

DELAY是一个方便的行为,它将暂停一个循环程序的实际时间量。延迟的时间是以毫秒为单位的;一秒钟是1000毫秒,在60FPS下,一个单帧是(大约)16.66ms。

请注意,当一个程序在等待DELAY命令时,.GetPaused()方法将不会返回"true"。

AWAIT <condition> THEN

coroutineRootStruct = CO_BEGIN
    fireball = instance_create_depth(oPlayer.x, oPlayer.y, 0, oFireball);
    fireball.hspeed = -5;
    
    //Wait until the fireball has risen above the player by 30 pixels
    AWAIT fireball.y <= fireball.ystart - 30 THEN
    
    //Then shoot the fireball at the nearest enemy!
    nearest = instance_nearest(fireball.x, fireball.ystart, oEnemy);
    fireball.direction = point_direction(fireball.x, fireball.y, nearest.x, nearest.y);
    fireball.speed = 11;
CO_END

AWAIT是一个方便的行为,它在继续执行代码之前会检查其条件。如果条件返回 true,那么将立即继续执行。然而,如果条件返回 "false",那么coroutine将暂时停止执行,直到下一帧(很像YIELD...THEN,尽管AWAIT将产生一个 "undefined "值)。

请注意,当一个coroutine在等待AWAIT命令时,.GetPaused()方法将不会返回true

AWAIT_FIRST <coroutine> ... END

coroutineRootStruct = CO_BEGIN
    AWAIT_FIRST
        CO_BEGIN
            DELAY 200 THEN
            show_debug_message("First coroutine finished");
        CO_END
        
        CO_BEGIN
            DELAY 100 THEN
            show_debug_message("Second coroutine finished");
        CO_END
    END
    
    show_debug_message("Race finished (second coroutine should finish first)");
CO_END

AWAIT_FIRST允许父程序暂时停止执行,直到定义的子程序中的一个完成。一旦任何一个子程序完成执行,父程序的执行将继续;其余未完成的子程序将立即被取消。只有在AWAIT_FIRST...END块中定义的程序才会被考虑到这种行为,任何先前创建的子程序将被忽略,以达到AWAIT_FIRST逻辑的目的。

每个子程序都存在于它自己的范围内,因此每个子程序内的变量对该程序来说是唯一的。父级和子级程序不共享变量,子级程序之间也不共享变量。所有角逐程序将执行CO_ON_COMPLETE函数,无论该角逐程序是否是第一个结束的。

请注意与普通的子程序不同,暂停或取消父程序将暂停或取消在AWAIT_FIRST...END块内创建的子程序。

请注意当一个协程在AWAIT_FIRST命令下等待时,.GetPaused()方法不会返回true

AWAIT_ALL <coroutine> ... END

coroutineRootStruct = CO_BEGIN
    AWAIT_ALL
        CO_BEGIN
            DELAY 200 THEN
            show_debug_message("First coroutine finished");
        CO_END
        
        CO_BEGIN
            DELAY 100 THEN
            show_debug_message("Second coroutine finished");
        CO_END
    END
    
    show_debug_message("Sync finished (both coroutines should have finished)");
CO_END

AWAIT_ALL允许一个父程序暂时停止执行,直到所有定义的子程序完成。一旦所有的子程序完成了执行,父程序的执行将继续进行。只有在AWAIT_ALL...END块内定义的程序才会被考虑,任何先前创建的子程序将被忽略,以用于AWAIT_ALL逻辑。

每个子程序都存在于它自己的范围内,因此每个子程序中的变量对该程序来说是唯一的。父类和子类程序不共享变量,子类程序之间也不共享变量。

请注意与普通的子程序不同,暂停或取消父程序将暂停或取消在AWAIT_ALL...END块中创建的子程序。

请注意,当一个协程在AWAIT_ALL命令下等待时,.GetPaused()方法将不会**返回`true'。

AWAIT_BROADCAST <name> THEN

coroutineRootStruct = CO_BEGIN
    //Rotate the door 30 degrees so it's ajar
    WHILE image_angle < 30 THEN
        image_angle += 5
        YIELD
    END
    
    //Wait for the player to push right...
    AWAIT_BROADCAST "push right" THEN
    
    //...then open the door all the way!
    WHILE image_angle <= 90 THEN
        image_angle = min(90, image_angle + 5);
        YIELD
    END
CO_END

///Elsewhere in the player object...
if (keyboard_check(vk_right))
{
    CoroutineBroadcast("push right");
    hspeed = 2;
}

AWAIT_BROADCAST是一个有用的命令,它允许间接的,对 coroutine 的控制。当coroutine遇到AWAIT_BROADCAST命令时,coroutine将在该命令处暂停。为了使该程序继续执行,必须使用与AWAIT_BROADCAST命令相同的名称调用CoroutineBroadcast()。然后,当CoroutineEventHook()在一个Step事件中被调用时(通常是在下一帧),该程序将继续执行。如果多个程序在等待同名的广播,只需要调用一次CoroutineBroadcast()就可以恢复所有这些程序。

请注意,当一个协程在等待AWAIT_BROADCAST命令时,.GetPaused()方法将不会返回true

AWAIT_BROADCAST默认情况下,只响应本地Coroutines广播。要收听来自精灵和序列的GameMaker广播,请使用AWAIT_ASYNC_BROADCAST

异步事件

AWAIT_ASYNC_* <function> THEN

coroutineRootStruct = CO_BEGIN
    show_debug_message("Starting leaderboard pull");
    handle = steam_download_scores("Game Scores", 1, 10);
    
    AWAIT_ASYNC_STEAM
        if (async_load < 0)
        {
            show_debug_message("Leaderboard request timed out");
        }
        else if (async_load[? "id"] == handle)
        {
           show_debug_message("Leaderboard data received");
            global.scores = array_resize(0);
            
            var _list = map[? "entries"];
            var _i = 0;
            repeat(ds_list_size(_list))
            {
                var _data = _list[| _i];
                array_push(global.scores, {name : _data[? "name"], score : _data[? "score"], rank : _data[? "rank"]});
                _i++;
            }
            
            ASYNC_COMPLETE
        }
    THEN
    
    show_debug_message("Leaderboard pull complete");
CO_END

AWAIT_ASYNC_*命令(完整列表见下文)允许一个coroutine与GameMaker的本地异步事件系统进行交互。当coroutine遇到AWAIT_ASYNC_*命令时,coroutine将暂停该行代码并等待相关的异步事件被触发。一旦GameMaker的运行时出现正确类型的异步事件,AWAIT_ASYNC_* ... THEN的异步代码将被执行。如果该代码块调用了ASYNC_COMPLETE,那么coroutine立即执行进一步的代码,否则AWAIT_ASYNC_*继续监听新的事件。

位于AWAIT_ASYNC_*THEN之间的标准GML函数在每次触发异步事件时都会被执行,而不管该异步事件是否与coroutine有关。这很不幸,但这也是GameMaker的设计方式。你应该总是检查你收到的async_loadevent_data ds_map是否与你所期望的异步事件相匹配。

AWAIT_ASYNC_*命令后面的代码不能包含任何coroutine宏(除了ASYNC_COMPLETE之外)。这是因为async_loadevent_data可能包含易失性数据,在异步事件结束后不会持久。如果你想对异步事件返回的数据进行广泛的操作,你应该对其进行拷贝,然后在AWAIT_ASYNC_*代码块之外处理这些数据。

AWAIT_ASYNC_*代码可以在一个操作超时时执行。默认情况下,没有设置超时时间,操作可能永远挂起。你可以使用ASYNC_TIMEOUT宏来定制超时时间(见下文)。当一个异步操作超时时,async_load是一个负数。你应该总是写代码来检查一个异步操作是否超时,也就是说,你应该总是处理async_loadevent_data为负数的情况。

请注意,AWAIT_ASYNC_BROADCAST会专门接收GameMaker的精灵和序列广播;它不会接收Coroutines库中的广播(使用AWAIT_BROADCAST来代替)。此外,在AWAIT_ASYNC_BROADCAST代码块中,你应该检查event_data而不是async_load

支持以下异步等待命令:

  • AWAIT_ASYNC_HTTP
  • AWAIT_ASYNC_NETWORKING
  • AWAIT_ASYNC_SOCIAL
  • AWAIT_ASYNC_SAVE_LOAD
  • AWAIT_ASYNC_DIALOG
  • AWAIT_ASYNC_SYSTEM
  • AWAIT_ASYNC_STEAM
  • AWAIT_ASYNC_BROADCAST

这些是GameMaker中最常见的异步事件。如果你想添加更多的异步事件,那么请让我知道open in new window,它们将被纳入正式发布。

ASYNC_COMPLETE

coroutineRootStruct = CO_BEGIN
    handle = get_string_async("Please enter your name", "Juju Adams");
    result = "";    
    AWAIT_ASYNC_DIALOG
        if (async_load[? "id"] == handle)
        {
            if (async_load[? "status"]) result = async_load[? "string"];
            ASYNC_COMPLETE
        }
    THEN
    
    RETURN result;
CO_END

ASYNC_COMPLETEAWAIT_ASYNC_*命令的一个重要组成部分。它表示异步操作已经完成,并且coroutine应该可以继续执行代码。如果你不在你的异步代码块中调用ASYNC_COMPLETE,那么异步操作可能会无限期地挂起。

请注意ASYNC_COMPLETE不应该在AWAIT_ASYNC_*代码块之外调用,否则你会看到不可预测的行为。

AWAIT_ASYNC_* <function> ASYNC_TIMEOUT <duration> THEN

coroutineRootStruct = CO_BEGIN
    show_debug_message("HTTP GET started");
    handle = http_get("https://www.jujuadams.com/");
    AWAIT_ASYNC_HTTP
        if (async_load < 0) //Handle the timeout case
        {
            show_debug_message("HTTP GET timed out");
            ASYNC_COMPLETE
        }
        if (async_load[? "id"] == handle)
        {
            if (async_load[? "status"] == 0)
            {
                show_debug_message("HTTP GET succeeded");
                show_debug_message(async_load[? "result"]);
                ASYNC_COMPLETE
            }
            else if (async_load[? "status"] < 0)
            {
                show_debug_message("HTTP GET failed with error code " + string(async_load[? "http_status"]));
                ASYNC_COMPLETE
            }
        }
    ASYNC_TIMEOUT 6000 THEN //Wait 6 seconds before timing out (6000 milliseconds)
    show_debug_message("HTTP GET complete");
CO_END

异步操作,特别是对服务器的操作,经常会遇到问题,请求超时。ASYNC_TIMEOUT...THENAWAIT_ASYNC_*命令增加了一个超时行为,以处理可能出现未报告失败的情况。默认情况下,AWAIT_ASYNC_*命令没有超时时间,操作有可能会永远挂起。超时持续时间(ASYNC_TIMEOUTTHEN之间的数字)是以毫秒为单位的。

当异步代码被执行但操作超时时,async_load将被设置为一个负数。你应该总是在你的AWAIT_ASYNC_*块中写入行为,以处理async_load为负数的情况,避免出现意外问题。

Last Updated 6/3/2024, 1:49:47 PM