This document describes the implementation of the MonoGame Chapter 11 input management system in Common Lisp using DotCL and CLOS.
The system provides centralized input handling with frame-to-frame state tracking, enabling detection of "just pressed" and "just released" transitions for keyboard, mouse, and gamepad input.
The implementation followed the Chapter 11 design:
-
Create an
input-manager.lispfile with four CLOS classes mirroring the C#MonoGameLibrary.Inputnamespace:KeyboardInfo- tracks previous/currentKeyboardStateMouseInfo- tracks previous/currentMouseState, position deltas, scroll wheel changesGamePadInfo- tracks previous/currentGamePadStateand manages timed vibrationInputManager- aggregates all three device classes, updates them each frame
-
Define a new Lisp package
:dungeon-slime-inputwith local nickname:inputin the:dungeon-slimepackage. -
Integrate into the
coregame class (mg-core.lisp):- Add an
input-managerslot (class allocation, singleton per game) - Create the input manager in
initialize - Call
input-manager-updateat the start ofupdate(before base class update), matching the C#Core.Updatepattern
- Add an
-
Refactor
game-1.lispto remove inline input handling and use the new input manager, following the one-frame-latency pattern as in the C# tutorial.
input-manager.lisp- The complete input management system
-
packages.lisp:- Added pre-declarations for 3 missing C# packages
- Added new
:dungeon-slime-inputpackage definition with local nicknames for all needed C# packages and exports for all public functions - Added
(:input :dungeon-slime-input)local nickname to:dungeon-slimepackage
-
csharp.lisp:- Added
ts-(TimeSpan subtraction) function - Added
ts<=(TimeSpan less-than-or-equal) function - Fixed
ts>=which incorrectly calledop_GreaterThaninstead ofop_GreaterThanOrEqual
- Added
-
mg-core.lisp:- Added
input-managerslot tocoreclass - Create input manager in
initializemethod - Update input manager in
updatemethod before base call
- Added
-
game-1.lisp:- Refactored
check-keyboard-inputto useinput:im-keyboardandinput:is-key-downetc. instead of rawKeyboard.GetState - Refactored
check-gamepad-inputto useinput:im-game-padsandinput:is-button-downetc., with timed vibration viainput:game-pad-set-vibration - Changed Escape key from continuous
key-down?toinput:was-key-just-pressedpattern - Changed F7 test error from continuous to just-pressed pattern
- Refactored
-
dungeon-slime.asd:- Added
input-managercomponent betweenmg-classesandmg-core, depending on"mg-classes"and"csharp" - Added
"input-manager"dependency tomg-core
- Added
The :dungeon-slime-input package uses :use for :cl, :mg-classes,
and :csharp. It references MonoGame C# wrapper packages through local
nicknames (:kb-state, :gp-state, :ms, etc.) to avoid symbol
conflicts while keeping code concise.
The input state update follows the same pattern as the C# tutorial:
input states are updated in core:update, which is called via
call-next-method from game-1:update. This means game-1:update
reads input states from the previous frame's update. This ~16ms
latency at 60fps is standard in many game engines and matches the
Chapter 11 behavior.
The GamePadInfo class manages timed vibration internally:
game-pad-set-vibrationstores the remaining duration and starts vibrationgame-pad-updatedecrements the timer each frame usingGameTime.ElapsedGameTime- When the timer expires,
game-pad-stop-vibrationis called automatically
The C# TimeSpan operations for the vibration timer are provided by
the :csharp package's ts- and ts<= functions.
Instead of a C# MouseButton enum, keyword symbols are used:
:left, :middle, :right, :x-button1, :x-button2, with
corresponding +mouse-left+ etc. constants. A case dispatch in
mouse-button-from-state maps these to the appropriate
MouseState property accessors.
The input manager functions use the :input nickname in
:dungeon-slime code, producing calls like:
(input:was-key-just-pressed (input:im-keyboard (input-manager game))
key:+escape+)
(input:is-button-down (aref (input:im-game-pads (input-manager game)) 0)
button:+a+)The input manager depends on several auto-generated C# packages
(cspackages/). These are pre-declared as empty packages in
packages.lisp before the :dungeon-slime-input package definition
to satisfy the :local-nicknames requirement during compilation.
The actual package contents are loaded later in the ASDF load order,
which is fine since the nickname registration only needs the package
name to exist, not its contents.
This document describes the implementation of the MonoGame Chapter 12 collision detection system in Common Lisp using DotCL and CLOS.
The system provides circle-based collision detection, AABB rectangle collision, screen boundary blocking for the player, screen boundary bouncing for the enemy bat, and a trigger collision response when the slime "eats" the bat.
collision.lisp— CLOScircleclass and collision utility functionscollision-test.lisp— Comprehensive test suite for all collision logic
-
mg-classes.lisp:- Added
v2-distance-squared— wrapsVector2.DistanceSquaredstatic method - Added
v2-reflect— wrapsVector2.Reflectstatic method (for bounce response) - Added
v2-normalize— returns a normalized copy of a Vector2 viaop_Division - Added
rect-intersects— wrapsRectangle.Intersects(Rectangle)method - Added
rect-contains-p— wrapsRectangle.Contains(Rectangle/Point/Vector2)method - Exported all new functions from the
:mg-classespackage
- Added
-
packages.lisp:- Added pre-declaration for
:microsoft-xna-framework-rectangle - Added pre-declaration for
:microsoft-xna-framework-graphics-graphics-device - Added pre-declaration for
:microsoft-xna-framework-graphics-presentation-parameters - Added local nicknames
(:rect ...),(:gd ...),(:pp ...)to:dungeon-slime - Added
circle,circle-intersects,circle-left/right/top/bottomexports - Added
bat-pos,bat-velto exports - Added new
:mg-classesexports
- Added pre-declaration for
-
game-1.lisp:- Added
bat-pos(Vector2) andbat-vel(Vector2) slots to thegame-1class - Added
assign-random-bat-velocityfunction — generates random angle, converts to direction vector scaled by MOVEMENT_SPEED - Updated
initializeto set initial bat position (10px right of slime) and callassign-random-bat-velocityafter the C# lifecycle (which loads sprites) - Updated
updatewith three collision responses:- Blocking response: slime bounding circle checked against screen edges; if slime drifts outside via keyboard/gamepad input, position is clamped back inside
- Bounce response: bat bounding circle checked at its new position;
if outside, velocity is reflected about the nearest screen-edge normal
(axis-aligned or diagonal), then normalized via
v2-normalizebeforev2-reflect - Trigger response: if slime circle intersects bat circle, bat respawns at a random grid-aligned position with new random velocity
- Updated
drawto usebat-posslot instead of hardcoded position
- Added
-
dungeon-slime.asd:- Added
"collision"component between"input-manager"and"mg-core", depends on"mg-classes" - Added
"collision-test"component between"game-1"and"clr-defmethod-test", depends on"collision"and"mg-classes" - Added
"collision"dependency to"game-1" - Added
"collision-test"dependency to"test-harness"
- Added
-
test-harness.lisp:- Added
(run-collision-tests)call inrun-all-tests
- Added
The C# tutorial defines Circle as a readonly struct implementing IEquatable<T>,
with fields X, Y, Radius, computed properties Top/Bottom/Left/Right,
and an Intersects(Circle) method.
In Lisp, circle is a standard CLOS class with x, y, radius slots accessed
via circle-x, circle-y, circle-radius. Boundary computations (circle-left,
circle-right, circle-top, circle-bottom) are pure arithmetic functions.
circle-intersects uses v2-distance-squared (wrapping Vector2.DistanceSquared)
to compare squared-distance vs squared-radii-sum, avoiding the sqrt as in the C#
version.
No IEquatable implementation is needed — CLOS instances use equalp or eql.
The Microsoft.Xna.Framework.Rectangle cspackage provides property accessors:
left, right, top, bottom, center, location, size. Instance methods
(Intersects, Contains) are not generated by the assembly package generator
(which only handles properties), so they are provided as Lisp functions via
dotnet:invoke in mg-classes.lisp.
Vector2.DistanceSquared, Vector2.Reflect, and Vector2.Normalize are all
static methods not generated by the cspackage. They are wrapped as Lisp functions
in mg-classes.lisp:
v2-distance-squaredcallsVector2.DistanceSquared(v1, v2)v2-reflectcallsVector2.Reflect(vector, normal)v2-normalizemanually computesv / Lengthviaop_Division(sinceVector2.Normalize()is an instance void-method on a value-type struct, which cannot be safely called through DotCL interop)
The assign-random-bat-velocity function generates a random angle in [0, 2π),
computes (cos, sin) direction vector, and multiplies by MOVEMENT_SPEED.
For screen bouncing, the bat's new position (current + velocity) is checked
against screen edges. The normal is constructed component-by-component (starting
from zero) as a plain Lisp vector. If either axis triggered, the normal is built
and normalized before calling v2-reflect.
Unlike the bat (which bounces), the slime is blocked at screen edges using a "teleport back" approach: if the slime's bounding circle extends past a screen edge, the slime's position is clamped to place it back inside. This matches the C# tutorial's blocking behavior.
When circle-intersects detects slime-bat overlap, the bat is respawned at a
random grid position: the screen is divided into columns/rows based on bat
sprite dimensions, and a random cell is chosen. A new random velocity is also
assigned.
collision-test.lisp contains 30 individual test assertions covering:
- Circle construction (default and explicit values)
- Circle boundary computation (
left/right/top/bottom) - Circle intersection (true cases: identical, partially overlapping, nested, diagonal overlap)
- Circle intersection (false cases: separated, tangent, far apart)
- Circle degenerate cases (zero-radius, zero-radius touching normal)
v2-distance-squared(3-4-5 triangle, same point, negative coords)v2-reflect(bottom edge, right edge, top edge bounces)v2-normalize(3-4-5 normalization, zero vector)rect-intersects(overlapping, non-overlapping, identical, edge-touching, containing)rect-contains-p(center point, outside point, contained rectangle, larger rectangle, edge point, outside edge)
All tests are run via make test and produced 0 failures.