前言

异步编程,一个看似用于网络/多线程的编程范式,在游戏开发中也可以大大增加代码书写的便捷性和可读性,本文将介绍这一点。

虽然我想网上应该已经有十一万四千五百一十四篇文章介绍了这个技巧,但是笔者最近亲身体会到了这一点,于是还是写一下吧。

本文将主要使用C#介绍。笔者默认读者有基础的C#异步相关的语法知识。

async/await之力

我们先来介绍一下什么是最基础的异步编程。

在各种编程语言中,你可以通过某些方式标注一个函数是异步的(在C#中,这是async关键字),执行异步函数时,你的主函数将会继续执行。例如:

public async Task Func(){...}
public async Task Main(){
	Func();
	Other(); //并不会等到Func执行完才会执行Other,而是两个“一起”执行
}

相对的,如果用了await,则会等待:

public async Task Func(){...}
public async Task Main(){
	await Func();
	Other(); //【会】等到Func执行完才会执行Other,而【不是】两个“一起”执行
}

异步,让跨帧逻辑更美好~

接下来我们将介绍如何通过await来实现轻松的跨帧逻辑。

场景1:弹幕STG

考虑你有一个弹幕boss,需要实现下列逻辑:“每60帧,发射一圈子弹”。

这是你写的代码,easy and simple:

public void Update(){ //called every frame
	frame++;
	if(frame%60==0){
		SpawnBullets();
	}
}

这很单调,而且太简单了,于是你想实现一个复杂的爆发弹幕逻辑:“boss休息60帧,发射一圈子弹,然后休息90帧,然后以每20帧的频率发射一圈子弹3次,循环”。

你需要维护现在是“一圈子弹”,还是“三圈子弹”的状态,在“三圈子弹”的状态中,你还要记录现在发射了几次。deepseek生成的代码如下:

public void Update()
{
	frameTimer++;

	switch (phase)
	{
		case Phase.Rest1:
			// 休息 60 帧后发射一圈
			if (frameTimer >= RestFrames1)
			{
				frameTimer = 0;
				SpawnBullets();
				phase = Phase.Rest2;
			}
			break;

		case Phase.Rest2:
			// 休息 90 帧后进入连发阶段
			if (frameTimer >= RestFrames2)
			{
				frameTimer = 0;
				burstFired = 1;
				SpawnBullets();     // 进入连发阶段时立即发射第一圈
				phase = Phase.Burst;
			}
			break;

		case Phase.Burst:
			// 每 20 帧发射一次,共 3 次
			if (frameTimer >= BurstInterval)
			{
				frameTimer = 0;
				burstFired++;

				if (burstFired < BurstTimes)
				{
					SpawnBullets();
				}
				else
				{
					// 3 次发射完毕,循环回到初始休息
					burstFired = 0;
					phase = Phase.Rest1;
				}
			}
			break;
	}
}

显然,这个代码很难维护,而且容易写错。(很可惜,有些弹幕系统只支持这样的写法,例如四聖龍神録)