C# 手动停止异步 for 语句(打字机效果)

thi*_*m24 48 .net c# winforms

我正在制作一个复古风格的游戏C# .NET-Framework,对于对话,我使用了一个 for 语句,它会一个字母一个字母地打印我的文本(就像打字机效果):

在此处输入图片说明

我正在处理不同的场景,我有一个跳过按钮(右下角)可以跳过当前对话并传递到下一个场景。当显示所有文本时,我的打字机效果会自动停止,但是当我单击跳过按钮时,它会自动跳到下一个场景。

我希望在打字机仍处于活动状态时,如果我单击跳过按钮,它首先显示所有文本,而不是跳到下一个场景。

这样它只会在显示所有文本时(自动或手动)跳到下一个场景。

这是我用于打字机方法(+ 变量)的(工作代码):

    public string FullTextBottom;
    public string CurrentTextBottom = "";
    public bool IsActive;
    
    public async void TypeWriterEffectBottom()
    {
        if(this.BackgroundImage != null) // only runs on backgrounds that arent black
        {
            for(i=0; i < FullTextBottom.Length + 1; i++)
            {
                CurrentTextBottom = FullTextBottom.Substring(0, i); // updating current string with one extra letter
                LblTextBottom.Text = CurrentTextBottom; // "temporarily place string in text box"
                await Task.Delay(30); // wait for next update
                
                #region checks for IsActive // for debugging only!

                if(i < FullTextBottom.Length + 1)
                {
                    IsActive = true;
                    Debug1.Text = "IsActive = " + IsActive.ToString();
                }
                if(CurrentTextBottom.Length == FullTextBottom.Length)
                {
                    IsActive = false;
                    Debug1.Text = "IsActive = " + IsActive.ToString();
                }

                #endregion
            }
        }

    }
Run Code Online (Sandbox Code Playgroud)

这是我为我的跳过按钮(名为Pb_FastForward获取的代码:

    private void PbFastForward_Click(object sender, EventArgs e)
    {
        if( //typewriter is active)
        {
             //print all text into the textbox
        }
        
        else if( //all text is printed)
        {
             // skip to the next scene
        }
    }   
Run Code Online (Sandbox Code Playgroud)

但我不知道如何制定代码的第二部分。我尝试了许多不同的方法,例如使用在单击按钮时增加的计数器(并使用它来检查 if 语句),以及许多不同类型的 if 语句来查看打字机是否仍然处于活动状态,但我还没有任何工作。

编辑

这是不同组件需要加载的顺序(点击按钮时),这与不同变量的更新方式有关:

  1. Gamestate_Cycle() --> 调用加载新场景。
  2. FullTextBottom = LblTextBottom.Text --> 调用以刷新打字机的变量。
  3. TypeWriterEffectBottom() --> 调用执行打字机效果。

aep*_*pot 46

避免async void。否则你会得到一个Exception会破坏你的游戏并且你将无法catch做到的。

然后在async方法中尽可能少地使用全局变量。

我建议CancellationTokenSource作为线程安全的方式来停止 Type Writer。

public async Task TypeWriterEffectBottom(string text, CancellationToken token)
{
    if (this.BackgroundImage != null)
    {
        Debug1.Text = "TypeWriter is active";
        StringBuilder sb = new StringBuilder(text.Length);
        foreach (char c in text)
        {
            if (token.IsCancellationRequested)
            {
                LblTextBottom.Text = text;
                break;
            }
            sb.Append(c);
            LblTextBottom.Text = sb.ToString();
            await Task.Delay(30);
        }
        Debug1.Text = "TypeWriter is finished";
    }
}
Run Code Online (Sandbox Code Playgroud)

定义 CTS。它是线程安全的,因此可以在全局范围内使用它。

private CancellationTokenSource cts = null;
Run Code Online (Sandbox Code Playgroud)

async方法调用 TypeWriter就可以await了。

// set button layout as "Skip text" here
using (cts = new CancellationTokenSource())
{
    await TypeWriterEffectBottom(yourString, cts.Token);
}
cts = null;
// set button layout as "Go to the next scene" here
Run Code Online (Sandbox Code Playgroud)

最后

private void PbFastForward_Click(object sender, EventArgs e)
{
    if (cts != null)
    {
        cts?.Cancel();
    }
    else
    {
        // go to the next scene
    }
}   
Run Code Online (Sandbox Code Playgroud)

  • SB 比常规字符串操作快得多。- 当您在每次操作后执行 ToString 时,我并不完全相信这是真的。 (7认同)

Geb*_*ebb 19

我对你的任务思考了更多,我突然想到 Rx.Net库是一项很好的工作。

这种方法的一个优点是你需要关心的可变状态较少,你几乎不需要考虑线程、同步等;您可以操作更高级别的构建块:可观察对象、订阅。

我稍微扩展了任务以更好地说明 Rx 功能:

  • 有两段动画文字,每一段都可以分别快进;
  • 用户可以快进到最终状态;
  • 用户可以重置动画状态。

演示

这是表单代码(C# 8,System.Reactive.Linq v4.4.1):

private enum DialogState
{
    NpcSpeaking,
    PlayerSpeaking,
    EverythingShown
}

private enum EventKind
{
    AnimationFinished,
    Skip,
    SkipToEnd
}

DialogState _state;
private readonly Subject<DialogState> _stateChanges = new Subject<DialogState>();
Dictionary<DialogState, (string, Label)> _lines;
IDisposable _eventsSubscription;
IDisposable _animationSubscription;
public Form1()
{
    InitializeComponent();
    _lines = new Dictionary<DialogState, (string, Label)>
    {
        { DialogState.NpcSpeaking, ("NPC speaking...", lblNpc) },
        { DialogState.PlayerSpeaking, ("Player speaking...", lblCharacter) },
    };
    // tick = 1,2...
    IObservable<long> tick = Observable
        .Interval(TimeSpan.FromSeconds(0.15))
        .ObserveOn(this)
        .StartWith(-1)
        .Select(x => x + 2);
    IObservable<EventPattern<object>> fastForwardClicks = Observable.FromEventPattern(
        h => btnFastForward.Click += h,
        h => btnFastForward.Click -= h);
    IObservable<EventPattern<object>> skipToEndClicks = Observable.FromEventPattern(
        h => btnSkipToEnd.Click += h,
        h => btnSkipToEnd.Click -= h);
    // On each state change animationFarames starts from scratch: 1,2...
    IObservable<long> animationFarames = _stateChanges
        .Select(
            s => Observable.If(() => _lines.ContainsKey(s), tick.TakeUntil(_stateChanges)))
        .Switch();
    var animationFinished = new Subject<int>();
    _animationSubscription = animationFarames.Subscribe(frame =>
    {
        (string line, Label lbl) = _lines[_state];
        if (frame > line.Length)
        {
            animationFinished.OnNext(default);
            return;
        }

        lbl.Text = line.Substring(0, (int)frame);
    });
    IObservable<EventKind> events = Observable.Merge(
        skipToEndClicks.Select(_ => EventKind.SkipToEnd),
        fastForwardClicks.Select(_ => EventKind.Skip),
        animationFinished.Select(_ => EventKind.AnimationFinished));
    _eventsSubscription = events.Subscribe(e =>
    {
        DialogState prev = _state;
        _state = prev switch
        {
            DialogState.NpcSpeaking => WhenSpeaking(e, DialogState.PlayerSpeaking),
            DialogState.PlayerSpeaking => WhenSpeaking(e, DialogState.EverythingShown),
            DialogState.EverythingShown => WhenEverythingShown(e)
        };
        _stateChanges.OnNext(_state);
    });
    Reset();
}

private DialogState WhenEverythingShown(EventKind _)
{
    Close();
    return _state;
}

private DialogState WhenSpeaking(EventKind e, DialogState next)
{
    switch (e)
    {
        case EventKind.AnimationFinished:
        case EventKind.Skip:
        {
            (string l, Label lbl) = _lines[_state];
            lbl.Text = l;
            return next;
        }
        case EventKind.SkipToEnd:
        {
            ShowFinalState();
            return DialogState.EverythingShown;
        }
        default:
            throw new NotSupportedException($"Unknown event '{e}'.");
    }
}

private void ShowFinalState()
{
    foreach ((string l, Label lbl) in _lines.Values)
    {
        lbl.Text = l;
    }
}

private void Reset()
{
    foreach ((_, Label lbl) in _lines.Values)
    {
        lbl.Text = "";
    }
    _state = DialogState.NpcSpeaking;
    _stateChanges.OnNext(_state);
}

protected override void OnClosed(EventArgs e)
{
    _eventsSubscription?.Dispose();
    _animationSubscription?.Dispose();
    base.OnClosed(e);
}

private void btnReset_Click(object sender, EventArgs e)
{
    Reset();
}
Run Code Online (Sandbox Code Playgroud)

  • 对于 C# 8,也可以使用全新的“IAsyncEnumerable”来完成。 (3认同)