C# Manually stopping an asynchronous for-statement (typewriter effect)

前端 未结 4 1426
我寻月下人不归
我寻月下人不归 2020-12-29 18:06

I\'m making a retro-style game with C# .NET-Framework, and for dialogue I\'m using a for-statement, that prints my text letter by letter (like

4条回答
  •  盖世英雄少女心
    2020-12-29 18:24

    I pondered on your task a bit more and it occurred to me that it is a good job for the Rx.Net library.

    An advantage of this approach is that you have less mutable state to care about and you almost don't need to think about threads, synchronization, etc.; you manipulate higher-level building blocks instead: observables, subscriptions.

    I extended the task a bit to better illustrate Rx capabilities:

    • there are two pieces of animated text, each one can be fast-forwarded separately;
    • the user can fast-forward to the final state;
    • the user can reset the animation state.

    Here is the form code (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 _stateChanges = new Subject();
    Dictionary _lines;
    IDisposable _eventsSubscription;
    IDisposable _animationSubscription;
    public Form1()
    {
        InitializeComponent();
        _lines = new Dictionary
        {
            { DialogState.NpcSpeaking, ("NPC speaking...", lblNpc) },
            { DialogState.PlayerSpeaking, ("Player speaking...", lblCharacter) },
        };
        // tick = 1,2...
        IObservable tick = Observable
            .Interval(TimeSpan.FromSeconds(0.15))
            .ObserveOn(this)
            .StartWith(-1)
            .Select(x => x + 2);
        IObservable> fastForwardClicks = Observable.FromEventPattern(
            h => btnFastForward.Click += h,
            h => btnFastForward.Click -= h);
        IObservable> skipToEndClicks = Observable.FromEventPattern(
            h => btnSkipToEnd.Click += h,
            h => btnSkipToEnd.Click -= h);
        // On each state change animationFarames starts from scratch: 1,2...
        IObservable animationFarames = _stateChanges
            .Select(
                s => Observable.If(() => _lines.ContainsKey(s), tick.TakeUntil(_stateChanges)))
            .Switch();
        var animationFinished = new Subject();
        _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 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();
    }
    

提交回复
热议问题