0

I'm creating a game in VBA and I don't know how to make Application.OnKey work. Why do I need it? As you can guess, I want an object in my game move when a player presses a key.

A game is basically an infinite loop in a subroutine RunGame(). If I put Application.OnKey as a workbook event without running RunGame(), then it works, so my keyboard and Application.OnKey can understand each other. However, when I run RunGame(), then the key listener doesn't work. I'm sure it's because I put it in a wrong place. The point is I tried it everywhere and have run out of ideas! Could you help me with it? I'm pasting a reproducible example here.

Module "main":

Option Explicit

Sub RunGame()

Call Application.OnKey("{Down}", "OnDownKey")

  Do While True
    '
    '
    'Some game's stuff here
    '
    '

    Call WasteTime(1)
  Loop

End Sub

Module "utils":

Option Explicit

Sub WasteTime(TimeToWaste As Single)

  Dim i As Long
  
  For i = 1 To TimeToWaste * 1000
    VBA.DoEvents
    i = i
  Next
End Sub

Sub OnDownKey()
  MsgBox "You pressed down key"
End Sub

By the way, if there's anything to change in this little example's logic, I'll appreciate your tips.

musialmi
  • 150
  • 9

2 Answers2

2

I've got to get to a solution, so I'll respond to my own question. Tips from @Peyter helped me find the way.

First of all, an infinite loop seems absolutely OK to me, I feel that it works more stably than Application.OnTime. There's also Chip Pearson's method, but I didn't find the reason of using it instead of the infinite loop (the result seemed the same).

Second of all, I left Aplication.OnKey be too. I used a solution from this site. This is some Windows function and works very well. There is one issue, though. From now on, a pressed key will mean to Excel both an extra activity and a normal activity. For example, when I press down arrow, I choose a cell below a cell I'm in. I wanted to turn off the regular behaviour for arrows and letters, but I didn't find any pretty way to do it. So I used an ugly one ;) All in all my functions are as shown below.

Module main:

Option Explicit

Sub RunGame()

  Do While True
    '
    '
    'Some game's stuff here
    '
    '

    Call WasteTime(1)
    IsKeyPressed
  Loop

End Sub

Module utils:

Option Explicit

Declare Function GetAsyncKeyState Lib "User32.dll" (ByVal vKey As Long) As Long

Private Const VK_DOWN = &H28 'DOWN ARROW key

Sub WasteTime(TimeToWaste As Single)

  Dim i As Long
  
  For i = 1 To TimeToWaste * 1000
    VBA.DoEvents
  Next
End Sub

Sub IsKeyPressed()

  If GetAsyncKeyState(VK_DOWN) <> 0 Then
    OnDownKey
  End If
End Sub

Sub OnDownKey()
  MsgBox "You pressed down key"
End Sub

It works. I still don't know where to put VBA.DoEvents, but in the loop it works well. The line Declare Function GetAsyncKeyState Lib "User32.dll" (ByVal vKey As Long) As Long says hello to a Windows function which now will work for us. Private Const VK_DOWN = &H28 'DOWN ARROW key is a definition of down arrow key which now will be called VK_DOWN. IsKeyPressed() checks if the user pressed some key. If GetAsyncKeyState(VK_DOWN) <> 0 checks if it's VK_DOWN which was pressed. Surprisingly for me, this Windows functions works very well with VBA, there are no lags!

Now, what did I do to block the regular behaviour of pressing arrows or letters? I chose one cell in which I wanted to stay - in my example it's AG1. Then I used worksheet events (double click on a sheet on the left in VBA editor and you'll open it). I used two functionalities there:

Option Explicit

Private Sub Worksheet_SelectionChange(ByVal Target As Range)
  [AG1].Select
End Sub

Private Sub Worksheet_Change(ByVal Target As Range)
  If Target.Address = "$AG$1" Then
    Application.EnableEvents = False
    [AG1].Clear
    [AG1].Select
    Application.EnableEvents = True
    RunGame
  End If
End Sub

The first one makes it impossible to escape from AG1, because each time you change the selection, VBA selects AG1 for you.

The second one fires when you try to change value of a cell. The subroutine will clear the cell, select it and then start RunGame(). Why is the last thing needed? Because when the user starts typing something, the infinite loop pauses and when the user hits Enter, the infinite loop doesn't continue. I don't really know if it's paused or finished. But hopefully I will make do without using letter keys, so I don't have to worry.

musialmi
  • 150
  • 9
0

Why are you using an infinite loop? Gah.

Instead use VBA's WithEvents model to wait for input.

And use Application.Ontime to call a macro, which calls itself again at a set interval. If you Infinite loop, you never destroy objects in that sub, and VBA will run out of memory, crashing the game. When you have even like a side scroller, that moves the frame or something, and you use AOT, then instead of one fluid big sub, you get 1 frame, then 2 frame, and you can change the trajectory of the AOT calls, by using WithEvents to change logic in your AOT calls so that it branches to a different AOT macro.

Peyter
  • 474
  • 3
  • 14
  • The purpose of infinite loop is not only to catch events. Actually, not at all. The purpose is to keep images going. Why am I using it? A book about creating games which I read told me to do so (however, the language used in that textbook wasn't VBA). I saw a game example in VBA on YT and there the author used: InfLoop: \n 'Everything \n GoTo: InfLoop '''''''''''''''''''''' Effectively, to me it seems the same solution. In my subroutine there are no local objects, so I don't need to destroy them. Do you still think that using a loop here is wrong? – musialmi Jul 12 '20 at 15:40
  • 1
    But you're stating in your question, that when you press a button, it's not working. This is because VBA is single threaded. So while your sub is infinitely looping, the application isn't able to process the key press. So my solution here is that you replicate your infinite loop (lol) by making an application event which is self-sustainable, but also leaves a gap between events of like <1 second. To the user, the sub will seem like its just looping, but to the computer, there's a <1 second gap for a button press using WithEvents. Thats going to let Application hear that button press event – Peyter Jul 12 '20 at 15:49
  • I'm sorry, but it seems to not be working as it should on my computer (let's leave reaction to events for now). Instead of the loop, I put `Application.OnTime Now + (ms * 0.00000001), "RunGame" ` at the end of the subroutine. I found on StackOverflow that `Now + (ms * 0.00000001)` should give delay of ms miliseconds. But this function behaves crazily. If ms >= 580, then the game is as slow as a snail. If ms <= 570, then the game is way too fast and is unstoppable, that is I can't click "stop macro", I can't do anything, I must close Excel via task manager. This is mathematically impossible. – musialmi Jul 12 '20 at 16:07
  • It probably means it doesn't work with values less than a second, perhaps? – musialmi Jul 12 '20 at 16:11
  • You're right on that, Its the Chip Pearson method that works less than a second I was confusing. Here take a look at this one: https://stackoverflow.com/questions/25116231/ontime-for-less-than-1-second-without-becoming-unresponsive – Peyter Jul 12 '20 at 16:16
  • OK... But now, when I stop the subroutine, Excel crashes. When I click on the sheet during the run, it crashes too! This solution is far from perfect (apart from not working): it uses some external Windows functions, it requires a lot of more code and it's ugly :D I'm sorry :D But I really hope there's something which would work. Anyway, is it really that it's impossible to listen to events when looping in VBA? – musialmi Jul 12 '20 at 16:36
  • OK, I've got it. It crashes when I stop because this timer must be stopped by the provided by Pearson subroutine, which I didn't use. Now I did and it works. However, if I accidentally click on some cell, Excel crashes. Even if I didn't set any event on cell change. So... it's quite a huge downside. Now I'll check if events started to work ;) – musialmi Jul 12 '20 at 19:03
  • Yay, it's working! Thank you. Now I'd be very grateful for a solution to the crashing when changing cell problem... – musialmi Jul 12 '20 at 19:04
  • I must say it's very crashable. It even crashes when I click "start" and then "stop", and then "start", and then "stop", and then "start"... There must be some other way to handle it! – musialmi Jul 12 '20 at 19:14
  • By the way, I used what is called "Windows timers" on that website. Now I tried also the method described before it and it works like my infinite loop - it works, it doesn't crash, but it doesn't understand events. – musialmi Jul 12 '20 at 19:37
  • 1
    Yeah you're going to have to get really crafty to try and get the withevents to handle. Off the top of my head, I'm thinking the sub should check for input somehow. Of course there are some fun weird little work arounds to try and get Excel to behave multi-threaded, if you want to deep dive into this, https://codereview.stackexchange.com/questions/185212/a-new-approach-to-multithreading-in-excel <- this thread is interesting. But you could also spawn workers using VBS scripts / Wscripts that run on the OS, and report back a value to excel after they're done running. It all going to b a waround – Peyter Jul 12 '20 at 23:45