3

ゲームで敵の動作をスクリプト化する際に、生活を楽にするために、C# でコルーチンを実装しようとしています。ゲームは固定フレームレートです。スクリプトを書くための理想的な構文は次のとおりです。

wait(60)
while(true)
{
  shootAtPlayer();
  wait(30);
}

つまり、60 フレーム待って、プレイヤーを撃って、30 待って、プレイヤーを撃って、30 待って…などです。

C# で yield return を使用して、このソリューションを実装しました。

public IEnumerable update()
{
  yield return 60;
  while(true)
  {
    shootAtPlayer();
    yield return 30;
  }
}

呼び出し元は、実行中のルーチン (IEnumerable カウンターで 0) とスリープ中のルーチン ( > 0) のスタックを保持します。フレームごとに、呼び出し元は各スリープ ルーチンのスリープ フレーム カウンタを 0 になるまで 1 ずつ減らします。このルーチンは再び有効になり、次の yield が返されるまで実行を続けます。ほとんどの場合、これで問題なく動作しますが、以下のようにサブルーチンを分割できないのは面倒です。

public IEnumerable update()
{
  yield return 60;
  while(true)
  {
    doSomeLogic()
    yield return 30;
  }
}

public IEnumerable doSomeLogic()
{
  somethingHappening();
  yield return 100;
  somethingElseHappens();
}

上記の構文は、yield return を別のメソッド内にネストできないため、正しくありません。つまり、実行状態を維持できるのは 1 つのメソッド (この場合は update()) だけです。これは yield return ステートメントの制限であり、実際に回避する方法を見つけることができませんでした。

以前、C# を使用して目的の動作を実装する方法を尋ねたところ、この状況では await キーワードがうまく機能する可能性があるとのことでした。コードを await を使用するように変更する方法を理解しようとしています。yield return でやりたかったように、呼び出されたメソッドで await をネストすることは可能ですか? また、awaitsを使用してカウンターを実装するにはどうすればよいですか? ルーチンの呼び出し元は、フレームごとに 1 回呼び出されるため、各待機ルーチンに残された待機フレームをデクリメントする制御を行う必要があります。これを実装するにはどうすればよいですか?

4

1 に答える 1

3

async/await がこれに適しているかどうかはわかりませんが、確実に可能です。考えられるアプローチを説明するために、小さな (まあ、少なくともできるだけ小さくしようとした) テスト環境を作成しました。コンセプトから始めましょう:

/// <summary>A simple frame-based game engine.</summary>
interface IGameEngine
{
    /// <summary>Proceed to next frame.</summary>
    void NextFrame();

    /// <summary>Await this to schedule action.</summary>
    /// <param name="framesToWait">Number of frames to wait.</param>
    /// <returns>Awaitable task.</returns>
    Task Wait(int framesToWait);
}

これにより、複雑なゲーム スクリプトを次のように記述できるようになります。

static class Scripts
{
    public static async void AttackPlayer(IGameEngine g)
    {
        await g.Wait(60);
        while(true)
        {
            await DoSomeLogic(g);
            await g.Wait(30);
        }
    }

    private static async Task DoSomeLogic(IGameEngine g)
    {
        SomethingHappening();
        await g.Wait(10);
        SomethingElseHappens();
    }

    private static void ShootAtPlayer()
    {
        Console.WriteLine("Pew Pew!");
    }

    private static void SomethingHappening()
    {
        Console.WriteLine("Something happening!");
    }

    private static void SomethingElseHappens()
    {
        Console.WriteLine("SomethingElseHappens!");
    }
}

エンジンを次のように使用します。

static void Main(string[] args)
{
    IGameEngine engine = new GameEngine();
    Scripts.AttackPlayer(engine);
    while(true)
    {
        engine.NextFrame();
        Thread.Sleep(100);
    }
}

これで、実装部分に到達できます。もちろん、カスタムの awaitable オブジェクトを実装することもできますが、ここでは and のみに依存しますTask(TaskCompletionSource<T>残念ながら、非ジェネリック バージョンはないため、単に を使用しますTaskCompletionSource<object>)。

class GameEngine : IGameEngine
{
    private int _frameCounter;
    private Dictionary<int, TaskCompletionSource<object>> _scheduledActions;

    public GameEngine()
    {
        _scheduledActions = new Dictionary<int, TaskCompletionSource<object>>();
    }

    public void NextFrame()
    {
        if(_frameCounter == int.MaxValue)
        {
            _frameCounter = 0;
        }
        else
        {
            ++_frameCounter;
        }
        TaskCompletionSource<object> completionSource;
        if(_scheduledActions.TryGetValue(_frameCounter, out completionSource))
        {
            Console.WriteLine("{0}: Current frame: {1}",
                Thread.CurrentThread.ManagedThreadId, _frameCounter);
            _scheduledActions.Remove(_frameCounter);
            completionSource.SetResult(null);
        }
        else
        {
            Console.WriteLine("{0}: Current frame: {1}, no events.",
                Thread.CurrentThread.ManagedThreadId, _frameCounter);
        }
    }

    public Task Wait(int framesToWait)
    {
        if(framesToWait < 0)
        {
            throw new ArgumentOutOfRangeException("framesToWait", "Should be non-negative.");
        }
        if(framesToWait == 0)
        {
            return Task.FromResult<object>(null);
        }
        long scheduledFrame = (long)_frameCounter + (long)framesToWait;
        if(scheduledFrame > int.MaxValue)
        {
            scheduledFrame -= int.MaxValue;
        }
        TaskCompletionSource<object> completionSource;
        if(!_scheduledActions.TryGetValue((int)scheduledFrame, out completionSource))
        {
            completionSource = new TaskCompletionSource<object>();
            _scheduledActions.Add((int)scheduledFrame, completionSource);
        }
        return completionSource.Task;
    }
}

主なアイデア:

  • Waitメソッドは、指定されたフレームに到達すると完了するタスクを作成します。
  • スケジュールされたタスクの辞書を保持し、必要なフレームに達したらすぐに完了します。

更新:不要な を削除してコードを簡素化しましたList<>

于 2013-06-05T02:27:15.590 に答える