Perfect your strategy and test your luck in knakk - the relaxed flip-and-write game that challenges you to master the art of card play and strategic slot-filling!
You can play in your browser or get the released game on itch.io and on Google Play.
To get the source code, you have multiple options:
- Clone the repository using git (Recommended): You will need Git LFS to download large assets. After setting it up, you can clone the repository like any other.
- Download the source archive as a zip file from GitHub.
This game contains many examples that show you how to use Godot's built-in features to build a game. Here is an overview of topics covered and where to find code relevant to each of them.
A lot of the game is implemented using custom controls. They are very similar to Node2D
nodes, but have a size
property that makes it easy to adapt them to different sizes. One example for this is the RewardMarker
node which is used to great effect to show rewards associated with slots on the field, the current score, and the score on the game over screen.
The default_theme.tres
file defines font family, font size and button colors for the whole project. It's set in the project settings and applies to all control nodes by default.
The Hearts
and Clubs
areas on the playing field use HBoxContainer
s to automatically lay out their slots in a row. Diamonds
and Spades
are similar to a GridContainer
but contain a custom layout that is tailored to their triangle-shaped grid.
The camera is used to transition between the different scenes such as the game itself or the game over scene. It also centers everything in the middle of the screen when the window's aspect ratio does not match the reference window size.
The Camera
node is animated by an AnimationPlayer
that animates its offset
and zoom
properties.
The animation uses Bezier curve tracks for precise control over way the animation feels. You can see the curves by switching to the bezier curve editor in the animation editor.
Tweens are used in two situations:
- We don't know the start or end values of an animation in advance. For an example of this, see the
shrink_to_played_size
method inCard.gd
. It animates the card's rotation to a random value. - It's a super simple animation that is more convenient to create with a short line of code. See the
visualize_interaction_state
method inCard.gd
for an example. It sets the card's scale and rotation to constant values but uses a tween to smoothly transition to those values.
When cards move around on the board, their movement is smoothed. This is not implemented using a tween but a custom movement step in the _process
method.
This is to have precise control over the speed and smoothing curve of the movement, and to avoid creating a new tween object every frame when users drag a card.
The whole codebase uses type annotations to improve autocomplete and catch bugs.
Since there are no nullable types at the time of writing, types that have to be null at some point are typed as Variant
.
This can also be implicit, e.g. Array[Variant]
being written as just Array
.
Methods returning nothing are not explicitly annotated using -> void
to reduce visual clutter, since the overwhelming majority of them returns nothing.
The game is designed for retina screens and consequently has a large reference window size. To reduce aliasing artifacts when scaling down images and fonts on smaller resolutions, all assets generate mipmaps on import. This comes at the expense of additional memory usage, but since this project doesn't use a lot of textures, it should be an acceptable tradeoff.
The viewport stretches using the canvas_items
method, meaning that the viewport changes its resolution to the window size and nodes are rendered using their actual pixel size on screen.
This also reduces aliasing artifacts on smaller screens.
By setting the aspect ratio to expand
, we allow the game to fill the window with its white background instead of using black bars.
The "emulate touch input using mouse" project setting allows the code to always work using touchscreen events and still "just work" with a mouse.
The drag-and-drop mechanic in Card.gd
works by capturing all input events and checking whether they apply to the card node that runs the script:
- For a touch start event, if it is inside the card's bounds, set the card's status to "being dragged"
- For a touch movement (
InputEventScreenDrag
), if the card is being dragged, move it to the new finger position - For a touch stop event, if the card is being dragged and is above a "drop zone", dispatch an action signal to play the card
I noticed that there can be a slight pause between a touch start event and a subsequent touch movement, causing a dragged card to "jump" between its original position and the position indicated by the user's finger. To prevent that jump, cards always interpolate between their current and their target position, resulting in a quick smooth motion.
The code mostly follows the Official GDScript Style Guide and the GDQuest GDScript Style Guide.
There are a few debugging shortcuts that are only enabled in debug mode:
- R Draw a new hand without spending a turn
- O End the current round
- Ctrl+O End the game instantly
- B Add a redraw bonus
- A Add a play again bonus
- D Toggle debug view
There are a couple good usage examples of coroutines:
Main.gd
andCamera.gd
use coroutines to wait for camera transitions to complete before performing subsequent actionsCard.gd
uses coroutines to wait for tweens to finishMenuScreen.gd
,Tutorial.gd
andNewRoundAnimation.gd
use coroutines to wait for a specified amount of time before doing something
- A landscape layout: Use Godot's UI features to make the controls automatically resize and reflow depending on different window aspect ratios.
- A highscore system: Use the filesystem abstraction to save, load and show highscores of previous games.
- Add an entry to
CHANGELOG.md
. - Use
just tag-version $VERSION
to write the new version into the export preset configuration. - Verify that the build works by running
just export
. - Create a new commit:
git commit -m "Version $VERSION"
. - Use
git tag $VERSION
to tag that commit with the new version. - Use
git push --tags
to push the changes (including your new tag) to export and release the new version via CI. - Upload the
.aab
release to the Google Play Store manually.
This repository uses git-crypt to encrypt both the keystore and the .godot/export_credentials.cfg
file. This way, the file contents remain private.
Do not add your release keystore or export_credentials.cfg file to your git repository without encrypting or adding them to your .gitignore!
As of April 2023, knakk has reached version 1.0 and is playable and polished. We'll keep adding features, our plans for this are outlined in the roadmap.
Where not specified otherwise, this project is licensed under the terms of the MIT License.
The card graphics found in the GameScreen/Card
directory were designed by Peter Wood and are licensed under the CC-BY license.
The font files in the Fonts
directory belong to the font family Dosis, designed by Pablo Impallari, licensed under the SIL Open Font License.