等待点击事件被触发 C#

12
我正在开发一款卡牌游戏,但需要一个功能来使程序停止,直到玩家点击他的手牌PictureBox将其弃置。我的游戏算法如下:
int nextDrawer = 0; // the players which will discard a card are determinated in counterclockwise starting from the human player
for (int i = 0; i < players; i++) // untill all the players hasn't drawed a card
{
    if (i == 0) .... // the human player has to click on a picture box to discard a card
    else .... // an AI player will discard a card which is selected randomly from the 3 cards which AI has got in its hand
}

问题在于当一轮游戏结束时,首先弃牌的人会发生变化。如果玩家编号为0(人类玩家),1(第一个AI玩家),2(第二个AI玩家)和3(第三个AI玩家),在第一轮游戏中,首先弃牌的是人类玩家,但在第二轮游戏中,首先弃牌的可能是2号AI玩家,人类玩家必须等待他前面所有的AI玩家弃牌(在这种情况下,回合将变成2-3-0-1)。
如果AI玩家还没有弃牌,我该如何取消单击事件?
更新:
我不总是需要等待所有 AI 玩家都抽完一张牌:如果本轮游戏的获胜者是编号为2的玩家,则回合将变为2-3-0-1:这意味着玩家必须等待2号和3号AI玩家抽完牌,然后玩家必须点击一个PictureBox,循环将返回到AI玩家,然后允许AI玩家1弃牌。
更新2:
我想到了以下解决方案:
int leader = 0; // who is going to discard first
int nextDiscarder = leader; // next player who's going to discard
for (int i = 0; i < nPlayers; i++) // until all the players hasn't discarded
{
    if (nextDiscarder == 0) // the human has to discard
    {
        enablePictureBoxClickEvent;
        // now before the loop continue the program has to wait the event click on a picture box
    }
    else
    {
        AI[nextDiscarder].discard(); // the ai player will discard
    }
    if (nextDiscarder == players - 1) // if nextDiscarder has reached the end of the table
        nextDiscarder = 0; // return to the begin until all player has discarded a card
    else
        ++nextDiscarder; // continue to discard with the next player
}

在我的点击事件中,我会这样做:

private myEventClick(object sender, EventArgs e)
{
    .... // do the instructions needed to discard a card
    disableMyEventClick;
    returnToLoop;
}

但主要问题是我不知道如何用代码编写我的指令returnToLoop


1
你尝试过设置布尔标志来帮助你吗?例如:当人类玩家出牌时,将标志设置为false,然后仅在另外3个玩家弃牌时(可以使用计数器)将其设置回true。然后确保只有在标志为true时,人类玩家才能弃牌。希望这能帮到你! - andeart
尝试过这个吗?https://msdn.microsoft.com/zh-cn/library/system.componentmodel.canceleventargs.cancel(v=vs.110).aspx - Zuzlx
我认为“returnToLoop”是不必要的,因为该事件没有终止循环,所以由于该事件没有将您带出线程,因此返回到循环是自动的。 - gridtrak
我认为当你在代码中的“//现在在循环继续之前程序必须等待”点阻塞循环线程时,你可能不会感到高兴。你的GUI将会冻结!我认为使用状态机而不是单线程循环会让你更加愉快。 - gridtrak
使用事件驱动的方法而不是循环可能更加优雅。 - rollsch
你知道,这可能不是太规范的做法,但是:一个非常简单的方法是使用 while(true) {//检查标志变化并退出循环}。当然,这会中断线程,因此该线程上的任何其他操作也将暂停。另一个选项是递归 checkFlag(flag) { if(flag==false) checkFlag(); else //something} - Ashwin Gupta
4个回答

26

我知道大多数人会争辩说你应该采用事件驱动的方法,但是 async/await 特性可以用于轻松实现这样的事情,而不需要手动实现状态机。

我已经在Force loop to wait for an eventA Better Way to Implement a WaitForMouseUp() Function?中发布了类似的方法,因此基本上这与前者相同,只不过将Button替换为Control

public static class Utils
{
    public static Task WhenClicked(this Control target)
    {
        var tcs = new TaskCompletionSource<object>();
        EventHandler onClick = null;
        onClick = (sender, e) =>
        {
            target.Click -= onClick;
            tcs.TrySetResult(null);
        };
        target.Click += onClick;
        return tcs.Task;
    }
}

现在你只需要将你的方法标记为async,并使用await

// ...
if (nextDiscarder == 0) // the human has to discard
{
    // now before the loop continue the program has to wait the event click on a picture box
    await pictureBox.WhenClicked();
    // you get here after the picture box has been clicked
}
// ...

1
+1。有关更多信息,可以在channel9.msdn的Lucian Wischik的Tips 3:将事件封装在返回任务的API中并等待它们视频中找到完全相同的想法描述。 - YuvShap

2

我喜欢Ivan的解决方案,因为它看起来很好,并且可以轻松地在其他需要等待控件的地方重用。

然而,我想提供另一个解决方案,因为我觉得我们现在做的方式比可能更复杂。

所以让我们总结一下:

  • 在游戏的某个时刻,你需要玩家选择一张他们不想扔掉的卡牌
  • 有一个人类玩家,在你的数组中是编号0
  • 人类玩家不总是第一个决定要扔掉哪张牌。
  • 为了决定要扔掉哪张牌,你向玩家显示一个图片框,并等待他点击它。

我认为一个简单的解决方案可能是:

  1. 你先移除AI玩家的卡牌,再移除人类玩家的(如果人类玩家是第一个弃牌,则这里什么也不做;如果人类玩家是最后一个,则所有AI都会在此处弃牌)
  2. 启用PictureBox并结束你的函数
  3. 在PictureBox的点击事件中,你先移除用户卡牌,然后再移除其余AI玩家的卡牌(如果人类玩家是第一个,则所有AI玩家都会在这里移除一张卡牌;如果人类玩家是最后一个,则不做任何操作)

完成了...

所以,这将是这样的:

//We need an instance variable, to keep track of the first player
int _firstPlayerToDiscard = 0;

private void StartDiscardingProcess(int FirstToDiscard)
{
    _firstPlayerToDiscard = FirstToDiscard;
    if (FirstToDiscard != 0) //If the player is the first, we do nothing
    {
        //We discard for every other AI player after the human player
        for (int i = FirstToDiscard; i < nPlayers; i++)
        {
            AI[i].Discard(); 
        }
    }
    //Now we fill the PictureBox with the cards and we display it to the player
    DiscardPictureBox.Enabled = true;
    //or DiscardPictureBox.Visible = true;
    //and we are done here, we know basically wait for the player to click on the PictureBox.
}

private void pictureBox_click(Object sender, EventArgs e)
{
    //Now we remove the card selected by the player
    // ...
    //And we remove the cards from the other AI players
    //Note that if the player was first to discard, we need to change the instance variable
    if (_firstPlayerToDiscard == 0) { _firstPlayerToDiscard = nbPlayers; }
    for (int i = 1; i < _firstPlayerToDiscard; i++)
    {
        AI[i].Discard();
    }
}

你基本上已经完成了...

NB: 如果语法不好或不寻常,请见谅,我通常使用VB .Net编码... 随意编辑语法问题...


1
以下代码演示了一个简单的基于定时器的状态机。在这种情况下,机器的状态是当前玩家的回合。这个例子让每个玩家决定何时让下一个玩家轮流通过将状态设置为下一个玩家。添加其他程序应该检查的附加状态。这种程序架构运行相对平稳,因为程序线程不会被阻塞在紧密循环中。每个玩家能够快速完成和退出回合,即使玩家的回合重复10000次而没有做任何事情也是更好的。
在下面的示例中,单击事件处理程序将机器状态从人类回合转移到AI回合。这有效地暂停游戏,直到人类点击。由于回合没有被阻塞在紧密循环中,您可以有其他按钮供人类点击,如“跳过”,“重新开始”和“退出”。
using System;
using System.Windows.Forms;
using System.Timers;

namespace WindowsFormsApplication1
{
  public partial class Form1 : Form
  {
    private System.Timers.Timer machineTimer = new System.Timers.Timer();

    // These are our Machine States
    private const int BEGIN_PLAY = 0;
    private const int HUMAN_PLAYER_TURN = 1;
    private const int AI_PLAYER_TURN = 2;

    // This is the Current Machine State
    private int currentPlayer = BEGIN_PLAY;

    // Flag that lets us know that the Click Event Handler is Enabled
    private bool waitForClick = false;

    // The AI members, for example 100 of them
    private const int AIcount = 100;
    private object[] AIplayer = new object[AIcount];
    private int AIcurrentIndex = 0;    // values will be 0 to 99


    public Form1()
    {
        InitializeComponent();
        this.Show();

        // The Timer Interval sets the pace of the state machine. 
        // For example if you have a lot of AIs, then make it shorter
        //   100 milliseconds * 100 AIs will take a minimum of 10 seconds of stepping time to process the AIs
        machineTimer.Interval = 100;  
        machineTimer.Elapsed += MachineTimer_Elapsed;

        MessageBox.Show("Start the Game!");
        machineTimer.Start();
    }


    private void MachineTimer_Elapsed(object sender, ElapsedEventArgs e)
    {
        // Stop the Timer
        machineTimer.Stop();
        try
        {
            // Execute the State Machine
            State_Machine();

            // If no problems, then Restart the Timer
            machineTimer.Start();
        }
        catch (Exception stateMachineException)
        {
            // There was an Error in the State Machine, display the message
            // The Timer is Stopped, so the game will not continue
            if (currentPlayer == HUMAN_PLAYER_TURN)
            {
                MessageBox.Show("Player Error: " + stateMachineException.Message, "HUMAN ERROR!",
                                MessageBoxButtons.OK, MessageBoxIcon.Error);
            }
            else if (currentPlayer == AI_PLAYER_TURN)
            {
                MessageBox.Show("Player Error: " + stateMachineException.Message, "AI ERROR!",
                                MessageBoxButtons.OK, MessageBoxIcon.Error);
            }
            else
            {
                MessageBox.Show("Machine Error: " + stateMachineException.Message, "Machine ERROR!",
                                MessageBoxButtons.OK, MessageBoxIcon.Error);
            }
        }
    }



    private void State_Machine()
    {
        // This routine is executing in the Timer.Elapsed Event's Thread, not the Main Form's Thread
        switch (currentPlayer)
        {
            case HUMAN_PLAYER_TURN:
                Play_Human();
                break;

            case AI_PLAYER_TURN:
                Play_AI();
                break;

            default:
                Play_Begin();
                break;
        }
    }


    private void Play_Human()
    {
        // This routine is executing in the Timer.Elapsed Event's Thread, not the Main Form's Thread
        // My Turn!
        if (!waitForClick)
        {
            // Please Wait until I take a card...
            // I am using this.Invoke here because I am not in the same thread as the main form GUI
            // If we do not wrap the code that accesses the GUI, we may get threading errors.
            this.Invoke((MethodInvoker)delegate
            {
                pictureBox1.Click += PictureBox1_Click;
            });

            // set this flag so we do not re-enable the click event until we are ready next time
            waitForClick = true;
        }
    }


    private void PictureBox1_Click(object sender, EventArgs e)
    {
        // This routine is executing in the Main Form's Thread, not the Timer's Thread

        // Stop the game for a little bit so we can process the Human's turn
        machineTimer.Stop();

        // Disable the Click Event, we don't need it until next time
        pictureBox1.Click -= PictureBox1_Click;
        waitForClick = false;

        // To Do:  Human's Turn code...

        // Let the AI Play now
        currentPlayer = AI_PLAYER_TURN;
        machineTimer.Start();
    }


    private void Play_AI()
    {
        // This routine is executing in the Timer.Elapsed Event's Thread, not the Main Form's Thread
        if (AIcurrentIndex < AIcount)
        {
            // If we do not wrap the code that accesses the GUI, we may get threading errors.
            this.Invoke((MethodInvoker)delegate
            {
                // To Do:  AI Player's Turn code...
            });

            // Advance to the next AI
            AIcurrentIndex++;
        }
        else
        {
            // Reset to the beginning
            AIcurrentIndex = 0;
            currentPlayer = BEGIN_PLAY;
        }
    }


    private void Play_Begin()
    {
        // This routine is executing in the Timer.Elapsed Event's Thread, not the Main Form's Thread
        // If we do not wrap the code that accesses the GUI, we may get threading errors.
        this.Invoke((MethodInvoker)delegate
        {
            // ... do stuff to setup the game ...
        });

        // Now let the Human Play on the next Timer.Elapsed event
        currentPlayer = HUMAN_PLAYER_TURN;

        // After the Human is done, start with the first AI index
        AIcurrentIndex = 0;
    }

  }
}

1
根据事件而非循环,我本来会以不同的方式设计该过程,但按照您的方式,您应该使用自动重置事件来通知循环myEvent已被触发。
AutoResetEvent clickEventFired = new AutoResetEvent(false); // instanciate event with nonsignaled state
AutoResetEvent clickEventFired = new AutoResetEvent(true); // instanciate event with signaled state

clickEventFired.Reset(); // set state to nonsignaled
clickEventFired.Set();  // set state to signaled
clickEventFirect.WaitOne(); // wait state to be signaled

https://msdn.microsoft.com/en-us/library/system.threading.autoresetevent(v=vs.110).aspx

public static void yourLoop()
{
    int leader = 0; // who is going to discard first
    int nextDiscarder = leader; // next player who's going to discard

    // instanciate auto reset event with signaled state
    AutoResetEvent clickEventFired = new AutoResetEvent(true);

    for (int i = 0; i < nPlayers; i++) // until all the players hasn't discarded
    {
        if (nextDiscarder == 0) // the human has to discard
        {
            enablePictureBoxClickEvent;
            clickEventFired.WaitOne(); // wait for event to be signaled
        }
        else
        {
            AI[nextDiscarder].discard(); // the ai player will discard
            clickEventFired.Reset(); // set event state to unsignaled
        }

        if (nextDiscarder == players - 1) // if nextDiscarder has reached the end of the table
            nextDiscarder = 0; // return to the begin until all player has discarded a card
        else
            ++nextDiscarder; // continue to discard with the next player
    }
}

private myEventClick(object sender, EventArgs e)
{
    .... // do the instructions needed to discard a card
    disableMyEventClick;
    clickEventFired.Set(); // signal event
}

问题可能是,如果您使用UI线程进入yourLoop,则会调用WaitOne()并阻塞UI线程,直到用户单击为止,这将导致死锁...除非WaitOne()不会阻塞UI线程,但我不知道如何做到这一点... - Martin Verjans
我之所以问这个问题,是因为在文档中指出:“阻止当前线程,直到当前WaitHandle接收到信号。”,但他们从未谈论过它是否完全异步。 - Martin Verjans
它并不是真正的异步,但感觉上却是一样的。waitHandle 等待当前线程而不像 UI 线程那样锁定其他线程。 - freakydinde
这就是我所说的,如果你使用UI线程进入了 yourLoop() 方法,你就会创建一个死锁。 - Martin Verjans
要使用UI线程启动yourLoop方法,您必须使用UI Dispatcher,除了更新UI之外,为什么还要使用它? - freakydinde

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接