DEV Community

Cover image for OpenGL Game in Clojure CLR (Mono/.Net) - Part3 (Handling Input)
Pankaj Doharey
Pankaj Doharey

Posted on • Edited on

1

OpenGL Game in Clojure CLR (Mono/.Net) - Part3 (Handling Input)

Handling Keyboard input and Game loop.

Most 3D applications like games run in a game loop where are series of events modify the game and OpenGL state to draw things on screen. One such important event is user input event. OpenTK GameWindow class has
one such method called OnUpdateFrame which as the name suggest gets triggered every time the frame is rendered.

Right now it is getting it isnt doing anything since it is in the parent class and we are dealing with an instance of GameWindow.

What we really need to do is inherit the GameWindow and override the OnUpdateFrame methods and capture the window events as they happen.

In order to do so there are primarily 3 mechanisms in in Clojure to deal with Inheritance. gen-class, proxy and reify. But since we are dealing with the instance of the inherited class reify cannot be used.

Since i aim to keep the tutorial largely functional my tool of choice is proxy.

So let us modify our core namespace and create a proxy object and override the OnUpdateFrame method like so:

(ns core
(:gen-class))
(assembly-load-from "./extern/OpenTK/lib/net20/OpenTK.dll")
(import [System]
[System.IO]
[System.Console]
[System.Drawing]
[OpenTK]
[OpenTK.Graphics.ES30 ClearBufferMask]
[OpenTK.Graphics.OpenGL GL]
[clojure.lang])
(defn new-window [width height]
(proxy [OpenTK.GameWindow] [width height nil "Mono Chromium BSU"]
(OnUpdateFrame [event]
(Console/WriteLine event))))
(defn -main []
(Console/WriteLine "Starting OpenTK Window.")
(let [window (new-window 800 600)]
(GL/ClearColor Color/CornflowerBlue)
(GL/Clear ClearBufferMask/ColorBufferBit)
(.SwapBuffers window)
(.Run window 30.0)))

As you can see OpenTK.GameWindow constructor has one more overloading function which can take four parameters, width,height, GraphicsMode and Title of the window. For now we are sticking with default graphics mode thus a nil is passed.

If you compile and run this program it will print the event object to the terminal with every refresh.

Print Event Object

Since, much of our game logic will go inside the OnUpdateFrame method it will be better to dispatch the events to a separate on-update-frame function to handle the inputs and keep only the override definition in the proxy object like so:

(ns core
(:gen-class))
(assembly-load-from "./extern/OpenTK/lib/net20/OpenTK.dll")
(import [System]
[System.IO]
[System.Console]
[System.Drawing]
[OpenTK]
[OpenTK.Graphics.ES30 ClearBufferMask]
[OpenTK.Graphics.OpenGL GL]
[OpenTK.Input Keyboard Key]
[clojure.lang])
(defn on-update-frame [event]
(let [input (Keyboard/GetState)]
(if (.IsKeyDown input Key/Escape)
(Console/WriteLine "Escape Pressed!"))))
(defn new-window [width height]
(proxy [OpenTK.GameWindow] [width height nil "Mono Chromium BSU"]
(OnUpdateFrame [event]
(on-update-frame event))))
(defn -main []
(Console/WriteLine "Starting OpenTK Window.")
(let [window (new-window 800 600)]
(GL/ClearColor Color/CornflowerBlue)
(GL/Clear ClearBufferMask/ColorBufferBit)
(.SwapBuffers window)
(.Run window 30.0)))

Lets us compile and run this application:

Handle Keypress

You see every time we press the escape key it would print Escape Pressed on the terminal multiple times because the refresh rate of the window is so fast that it ends up capturing the escape key press event multiple times.

For now we want the Escape key to exit the current window. But the on-update-frame does not have a handle of the proxy instance.

There is a way though C# has a special keyword this which gives a handle on the current execution context. Thus we would modify our OnUpdateFrame override and on-update-frame function and pass this as a parameter. Then we can simply call (.Exit this) in the input let block.

Let us modify our code like so:

(ns core
(:gen-class))
(assembly-load-from "./extern/OpenTK/lib/net20/OpenTK.dll")
(import [System]
[System.IO]
[System.Console]
[System.Drawing]
[OpenTK]
[OpenTK.Graphics.ES30 ClearBufferMask]
[OpenTK.Graphics.OpenGL GL]
[OpenTK.Input Keyboard Key]
[clojure.lang])
(defn on-update-frame [this event]
(let [input (Keyboard/GetState)]
(if (.IsKeyDown input Key/Escape)
(.Exit this))))
(defn new-window [width height]
(proxy [OpenTK.GameWindow] [width height nil "Mono Chromium BSU"]
(OnUpdateFrame [event]
(on-update-frame this event))))
(defn -main []
(Console/WriteLine "Starting OpenTK Window.")
(let [window (new-window 800 600)]
(GL/ClearColor Color/CornflowerBlue)
(GL/Clear ClearBufferMask/ColorBufferBit)
(.SwapBuffers window)
(.Run window 30.0)))

This exits the window when you press escape.

Next we will refactor and properly organise the code, and learn how to draw shapes on the screen ...

To be continued ...

Previous Posts:

Part 1 - The Setup
Part 2 - The Game Window

Qodo Takeover

Introducing Qodo Gen 1.0: Transform Your Workflow with Agentic AI

While many AI coding tools operate as simple command-response systems, Qodo Gen 1.0 represents the next generation: autonomous, multi-step problem-solving agents that work alongside you.

Read full post

Top comments (0)

Billboard image

The Next Generation Developer Platform

Coherence is the first Platform-as-a-Service you can control. Unlike "black-box" platforms that are opinionated about the infra you can deploy, Coherence is powered by CNC, the open-source IaC framework, which offers limitless customization.

Learn more