Welcome link
Welcome to DragonRuby Game Toolkit!
The information contained here is all available in your the zip file at ./docs/docs.html
. You can browse the docs in a local website by starting up DragonRuby and going to http://localhost:9001
.
Tips for Learning DragonRuby Game Toolkit link
The following tips will help you learn the DragonRuby quickly.
Tip #1: Join the Community link
Our Discord server is extremely supportive and helpful. It's the best place to get answers to your questions. The developers of DragonRuby are also on this server if you have any feedback or bug reports.
The Link to Our Discord Server is: http://discord.dragonruby.org.
The News Letter will keep you in the loop with regards to current DragonRuby Events: http://dragonrubydispatch.com.
Tip #2: Read the Book link
Brett Chalupa (one of our community members) has written a book to help you get started: https://book.dragonriders.community/
Tip #3: Watch the Tutorial Video link
Here are some videos to help you get the lay of the land.
- Building Tetris - Part 1: https://youtu.be/xZMwRSbC4rY
- Building Tetris - Part 2: https://youtu.be/C3LLzDUDgz4
Tip #4: Go Through the Sample Apps in Order link
The sample apps are located in the ./samples
directory. The samples are ordered by increasing difficulty and cover all aspects of the game engine.
Getting Started Tutorial link
This is a tutorial written by Ryan C Gordon (a Juggernaut in the industry who has contracted to Valve, Epic, Activision, and EA... check out his Wikipedia page: https://en.wikipedia.org/wiki/Ryan_C._Gordon).
Introduction link
Welcome!
Here's just a little push to get you started if you're new to programming or game development.
If you want to write a game, it's no different than writing any other program for any other framework: there are a few simple rules that might be new to you, but more or less programming is programming no matter what you are building.
Did you not know that? Did you think you couldn't write a game because you're a "web guy" or you're writing Java at a desk job? Stop letting people tell you that you can't, because you already have everything you need.
Here, we're going to be programming in a language called "Ruby." In the interest of full disclosure, I (Ryan Gordon) wrote the C parts of this toolkit and Ruby looks a little strange to me (Amir Rajan wrote the Ruby parts, discounting the parts I mangled), but I'm going to walk you through the basics because we're all learning together, and if you mostly think of yourself as someone that writes C (or C++, C#, Objective-C), PHP, or Java, then you're only a step behind me right now.
Prerequisites link
Here's the most important thing you should know: Ruby lets you do some complicated things really easily, and you can learn that stuff later. I'm going to show you one or two cool tricks, but that's all.
Do you know what an if statement is? A for-loop? An array? That's all you'll need to start.
The Game Loop link
Ok, here are few rules with regards to game development with GTK:
- Your game is all going to happen under one function ...
- that runs 60 times a second ...
- and has to tell the computer what to draw each time.
That's an entire video game in one run-on sentence.
Here's that function. You're going to want to put this in mygame/app/main.rb, because that's where we'll look for it by default. Load it up in your favorite text editor.
def tick args
args.outputs.labels << [580, 400, 'Hello World!']
end
Now run dragonruby
...did you get a window with "Hello World!" written in it? Good, you're officially a game developer!
Breakdown Of The tick
Method link
mygame/app/main.rb
, is where the Ruby source code is located. This looks a little strange, so I'll break it down line by line. In Ruby, a '#' character starts a single-line comment, so I'll talk about this inline.
# This "def"ines a function, named "tick," which takes a single argument
# named "args". DragonRuby looks for this function and calls it every
# frame, 60 times a second. "args" is a magic structure with lots of
# information in it. You can set variables in there for your own game state,
# and every frame it will updated if keys are pressed, joysticks moved,
# mice clicked, etc.
def tick args
# One of the things in "args" is the "outputs" object that your game uses
# to draw things. Afraid of rendering APIs? No problem. In DragonRuby,
# you use arrays to draw things and we figure out the details.
# If you want to draw text on the screen, you give it an array (the thing
# in the [ brackets ]), with an X and Y coordinate and the text to draw.
# The "<<" thing says "append this hash onto the list of them at
# args.outputs.labels)
args.outputs.labels << { x: 580, y: 400, text: 'Hello World!' }
end
Once your tick
function finishes, we look at all the arrays you made and figure out how to draw it. You don't need to know about graphics APIs. You're just setting up some arrays! DragonRuby clears out these arrays every frame, so you just need to add what you need _right now_ each time.
Rendering A Sprite link
Now let's spice this up a little.
We're going to add some graphics. Each 2D image in DragonRuby is called a "sprite," and to use them, you just make sure they exist in a reasonable file format (png, jpg, gif, bmp, etc) and specify them by filename. The first time you use one, DragonRuby will load it and keep it in video memory for fast access in the future. If you use a filename that doesn't exist, you get a fun checkerboard pattern!
There's a "dragonruby.png" file included, just to get you started. Let's have it draw every frame with our text:
def tick args
args.outputs.labels << { x: 580, y: 400, text: 'Hello World!' }
args.outputs.sprites << { x: 576, y: 100, w: 128, h: 101, path: 'dragonruby.png' }
end
(Pro Tip: you don't have to restart DragonRuby to test your changes; when you save main.rb, DragonRuby will notice and reload your program.)
That .sprites
line says "add a sprite to the list of sprites we're drawing, and draw it at position (576, 100) at a size of 128x101 pixels". You can find the image to draw at dragonruby.png.
Coordinate System and Virtual Canvas link
Quick note about coordinates: (0, 0) is the bottom left corner of the screen, and positive numbers go up and to the right. This is more "geometrically correct," even if it's not how you remember doing 2D graphics, but we chose this for a simpler reason: when you're making Super Mario Brothers and you want Mario to jump, you should be able to add to Mario's y position as he goes up and subtract as he falls. It makes things easier to understand.
Also: your game screen is _always_ 1280x720 pixels. If you resize the window, we will scale and letterbox everything appropriately, so you never have to worry about different resolutions.
Ok, now we have an image on the screen, let's animate it:
def tick args
args.state.rotation ||= 0
args.state.rotation -= 1
args.outputs.labels << { x: 580, y: 400, text: 'Hello World!' }
args.outputs.sprites << { x: 576,
y: 100,
w: 128,
h: 101,
path: 'dragonruby.png',
angle: args.state.rotation }
end
Now you can see that this function is getting called a lot!
Game State link
Here's a fun Ruby thing: args.state.rotation ||= 0
is shorthand for "if args.state.rotation isn't initialized, set it to zero." It's a nice way to embed your initialization code right next to where you need the variable.
args.state
is a place you can hang your own data. It's an open data structure that allows you to define properties that are arbitrarily nested. You don't need to define any kind of class.
In this case, the current rotation of our sprite, which is happily spinning at 60 frames per second. If you don't specify rotation (or alpha, or color modulation, or a source rectangle, etc), DragonRuby picks a reasonable default, and the array is ordered by the most likely things you need to tell us: position, size, name.
There Is No Delta Time link
One thing we decided to do in DragonRuby is not make you worry about delta time: your function runs at 60 frames per second (about 16 milliseconds) and that's that. Having to worry about framerate is something massive triple-AAA games do, but for fun little 2D games? You'd have to work really hard to not hit 60fps. All your drawing is happening on a GPU designed to run Fortnite quickly; it can definitely handle this.
Since we didn't make you worry about delta time, you can just move the rotation by 1 every time and it works without you having to keep track of time and math. Want it to move faster? Subtract 2.
Handling User Input link
Now, let's move that image around.
def tick args
args.state.rotation ||= 0
args.state.x ||= 576
args.state.y ||= 100
if args.inputs.mouse.click
args.state.x = args.inputs.mouse.x - 64
args.state.y = args.inputs.mouse.y - 50
end
args.outputs.labels << { x: 580, y: 400, text: 'Hello World!' }
args.outputs.sprites << { x: args.state.x,
y: args.state.y,
w: 128,
h: 101,
path: 'dragonruby.png',
angle: args.state.rotation }
args.state.rotation -= 1
end
Everywhere you click your mouse, the image moves there. We set a default location for it with args.state.x ||= 576
, and then we change those variables when we see the mouse button in action. You can get at the keyboard and game controllers in similar ways.
Coding On A Raspberry Pi link
We have only tested DragonRuby on a Raspberry Pi 3, Models B and B+, but we believe it _should_ work on any model with comparable specs.
If you're running DragonRuby Game Toolkit on a Raspberry Pi, or trying to run a game made with the Toolkit on a Raspberry Pi, and it's really really slow-- like one frame every few seconds--then there's likely a simple fix.
You're probably running a desktop environment: menus, apps, web browsers, etc. This is okay! Launch the terminal app and type:
sudo raspi-config
It'll ask you for your password (if you don't know, try "raspberry"), and then give you a menu of options. Find your way to "Advanced Options", then "GL Driver", and change this to "GL (Full KMS)" ... not "fake KMS," which is also listed there. Save and reboot. In theory, this should fix the problem.
If you're _still_ having problems and have a Raspberry Pi 2 or better, go back to raspi-config and head over to "Advanced Options", "Memory split," and give the GPU 256 megabytes. You might be able to avoid this for simple games, as this takes RAM away from the system and reserves it for graphics. You can also try 128 megabytes as a gentler option.
Note that you can also run DragonRuby without X11 at all: if you run it from a virtual terminal it will render fullscreen and won't need the "Full KMS" option. This might be attractive if you want to use it as a game console sort of thing, or develop over ssh, or launch it from RetroPie, etc.
Conclusion link
There is a lot more you can do with DragonRuby, but now you've already got just about everything you need to make a simple game. After all, even the most fancy games are just creating objects and moving them around. Experiment a little. Add a few more things and have them interact in small ways. Want something to go away? Just don't add it to args.output
anymore.
Starting a New DragonRuby Project link
The DragonRuby zip that contains the engine is a complete, self contained project structure. To create a new project, unzip the zip file again in its entirety and use that as a starting point for another game. This is the recommended approach to starting a new project.
Considerations For Public Git Repositories link
You can open source your game's code given the following options.
Option 1 (Recommended) link
Your public repository needs only to contain the contents of ./mygame
. This approach is the cleanest and doesn't require your .gitignore
to be polluted with DragonRuby specific files.
Option 2 (Restrictions Apply) link
IMPORTANT: Do NOT commit dragonruby-publish(.exe)
, or dragonruby-bind(.exe)
.
dragonruby
dragonruby.exe
dragonruby-publish
dragonruby-publish.exe
dragonruby-bind
dragonruby-bind.exe
/tmp/
/builds/
/logs/
/samples/
/docs/
/.dragonruby/
If you'd like people who do not own a DragonRuby license to run your game, you may include the dragonruby(.exe)
binary within the repo. This permission is granted in good-faith and can be revoked if abused.
Considerations For Private Git Repos link
The following .gitignore
should be used for private repositories (commercial games).
/tmp/
/logs/
You'll notice that everything else is committed to source control (even the ./samples
, ./docs
, and ./builds
directory).
The DragonRuby binary/package is designed to be committed in its entirety with your source code (it’s why we keep it small). This protects the “shelf life” for commercial games. 3 years from now, we might be on a vastly different version of the engine. But you know that the code you’ve written will definitely work with the version that was committed to source control. For private repositories, it's strongly recommended that you do NOT keep DragonRuby Game Toolkit in a shared location and instead unzip a clean copy for every game (and commit everything to source control).
IMPORTANT: File access functions are sandoxed and assume that the dragonruby
binary lives alongside the game you are building. Do not expect file access functions to return correct values if you are attempting to run the dragonruby
binary from a shared location. It's recommended that the directory structure contained in the zip is not altered and games are built using that starter template.
Deploying To Itch.io link
Once you've built your game, you're all set to deploy! Good luck in your game dev journey and if you get stuck, come to the Discord channel!
Creating Your Game Landing Page link
Log into Itch.io and go to https://itch.io/game/new.
- Title: Give your game a Title. This value represents your `gametitle`.
- Project URL: Set your project url. This value represents your `gameid`.
- Classification: Keep this as Game.
- Kind of Project: Select HTML from the drop down list. Don't worry, the HTML project type _also supports binary downloads_.
- Uploads: Skip this section for now.
You can fill out all the other options later.
Update Your Game's Metadata link
Point your text editor at mygame/metadata/game_metadata.txt and make it look like this:
NOTE: Remove the #
at the beginning of each line.
devid=bob
devtitle=Bob The Game Developer
gameid=mygame
gametitle=My Game
version=0.1
The devid
property is the username you use to log into Itch.io. The devtitle
is your name or company name (it can contain spaces). The gameid
is the Project URL value. The gametitle
is the name of your game (it can contain spaces). The version
can be any major.minor
number format.
Building Your Game For Distribution link
Open up the terminal and run this from the command line:
./dragonruby-publish --only-package mygame
(if you're on Windows, don't put the "./" on the front. That's a Mac and Linux thing.)
A directory called ./build
will be created that contains your binaries. You can upload this to Itch.io manually.
Browser Game Settings link
For the HTML version of your game, the following configuration is required for your game to run correctly:
- Check the checkbox labeled
This file will be played in the browser
for the html version of your game (it's one of the zip files you'll upload). - Ensure that
Embed options -> SharedArrayBuffer support
is checked. - Be sure to set the
Viewport dimensions
to1280x720
for landscape games or your game will not be positioned correctly on your Itch.io page. - Be sure to set the
Viewport dimensions
to540x960
for portrait games or your game will not be positioned correctly on your Itch.io page.
For subsequent updates you can use an automated deployment to Itch.io:
./dragonruby-publish mygame
DragonRuby will package _and publish_ your game to itch.io! Tell your friends to go to your game's very own webpage and buy it!
If you make changes to your game, just re-run dragonruby-publish and it'll update the downloads for you.
Consider Adding Pause When Game is In Background link
It's a good idea to pause the game if it doesn't have focus. Here's an example of how to do that
def tick args
# if the keyboard doesn't have focus, and the game is in production mode, and it isn't the first tick
if !args.inputs.keyboard.has_focus && args.gtk.production && args.state.tick_count != 0
args.outputs.background_color = [0, 0, 0]
args.outputs.labels << { x: 640,
y: 360,
text: "Game Paused (click to resume).",
alignment_enum: 1,
r: 255, g: 255, b: 255 }
# consider setting all audio volume to 0.0
else
# perform your regular tick function
end
end
If you want your game to run at full speed even when it's in the background, add the following line to mygame/metadata/cvars.txt
:
renderer.background_sleep=0
Consider Adding a Request to Review Your Game In-Game link
Getting reviews of your game are extremely important and it's recommended that you put an option to review within the game itself. You can use args.gtk.open_url
plus a review URL. Here's an example:
def tick args
# render the review button
args.state.review_button ||= { x: 640 - 50,
y: 360 - 25,
w: 100,
h: 50,
path: :pixel,
r: 0,
g: 0,
b: 0 }
args.outputs.sprites << args.state.review_button
args.outputs.labels << { x: 640, y: 360, anchor_x: 0.5, anchor_y: 0.5, text: "Review" }
# check to see if the review button was clicked
if args.inputs.mouse.intersect_rect? args.state.review_button
# open platform specific review urls
if args.gtk.platform? :ios
# your app id is provided at Apple's Developer Portal (numeric value)
args.gtk.openurl "itms-apps://itunes.apple.com/app/idYOURGAMEID?action=write-review"
elsif args.gtk.platform? :android
# your app id is the name of your android package
args.gtk.openurl "https://play.google.com/store/apps/details?id=YOURGAMEID"
elsif args.gtk.platform? :web
# if they are playing the web version of the game, take them to the purchase page on itch
args.gtk.openurl "https://amirrajan.itch.io/YOURGAMEID/purchase"
else
# if they are playing the desktop version of the game, take them to itch's rating page
args.gtk.openurl "https://amirrajan.itch.io/YOURGAMEID/rate?source=game"
end
end
end
Deploying To Mobile Devices link
If you have a Pro subscription, you also have the capability to deploy to mobile devices.
Deploying to iOS link
To deploy to iOS, you need to have a Mac running MacOS Catalina, an iOS device, and an active/paid Developer Account with Apple. From the Console type: $wizards.ios.start
and you will be guided through the deployment process.
$wizards.ios.start env: :dev
will deploy to an iOS device connected via USB.$wizards.ios.start env: :hotload
will deploy to an iOS device connected via USB with hotload enabled.$wizards.ios.start env: :sim
will deploy to the iOS simulator.$wizards.ios.start env: :prod
will package your game for distribution via Apple's AppStore.
Deploying to Android link
To deploy to Android, you need to have an Android emulator/device, and an environment that is able to run Android SDK. dragonruby-publish
will create an APK for you. From there, you can sign the APK and install it to your device. The signing and installation procedure varies from OS to OS. Here's an example of what the command might look like:
# generating a keystore
keytool -genkey -v -keystore APP.keystore -alias mygame -keyalg RSA -keysize 2048 -validity 10000
# deploying to a local device/emulator
apksigner sign --ks mygame.keystore mygame-android.apk
adb install mygame-android.apk
# read logs of device
adb logcat -e mygame
# signing for Google Play
apksigner sign --min-sdk-version 21 --ks ./profiles/APP.keystore ./builds/APP-googleplay.aab
Deploying To Steam link
If you have a Indie or Pro subscription, you also get streamlined deployment to Steam via dragonruby-publish
. Please note that games developed using the Standard license can deploy to Steam using the Steamworks toolchain https://partner.steamgames.com/doc/store/releasing.
Testing on Your Steam Deck link
Easy Setup link
- Run
dragonruby-publish --only-package
. - Find the Linux build of your game under the
./builds
directory and load it onto an SD Card. - Restart the Steam Deck in Desktop Mode.
- Copy your game binary onto an SD card.
- Find the game on the SD card and double click binary.
Advanced Setup link
- Restart the Steam Deck in Desktop Mode.
- Open up Konsole and set an admin password via
passwd
. - Disable readonly mode:
sudo steamos-readonly disable
. - Update pacman
sudo pacman-key --populate archlinux
. - Update sshd_config
sudo vim /etc/ssh/sshd_config
and uncomment thePubkeyAuthentication yes
line. - Enable ssh:
sudo systemctl enable sshd
. - Start ssh:
sudo systemctl start sshd
. - Run
dragonruby-publish --only-package
. - Use
scp
to copy the game over from your dev machine without needing an SD Card:scp -R ./builds/SOURCE.bin deck@IP_ADDRESS:/home/deck/Downloads
Note: Steps 2 through 7 need only be done once.
Note: scp
comes pre-installed on Mac and Linux. You can download the tool for Windows from https://winscp.net/eng/index.php
Setting up the game on the Partner Site link
Getting your App ID link
You'll need to create a product on Steam. This is unfortunately manual and requires identity verification for taxation purposes. Valve offers pretty robust documentation on all this, though. Eventually, you'll have an App ID for your game.
Go to https://partner.steamgames.com/apps/view/$APPID, where $APPID is your game's App ID.
Specifing Supported Operating Systems for your game link
Find the "Supported Operating Systems" section and make sure these things are checked:
- Windows: 64 Bit Only
- macOS: 64 Bit (Intel) and Apple Silicon
- Linux: Including SteamOS
Click the "Save" button below it.
Setting up SteamPipe Depots link
Click the "SteamPipe" tab at the top of the page, click on "depots"
Click the "Add a new depot" button. Give it a name like "My Game Name Linux Depot" and take whatever depot ID it offers you.
You'll see this new depot is listed on the page now. Fix its settings:
- Language: All Languages
- For DLC: Base App
- Operating System: Linux + SteamOS
- Architecture: 64-bit OS only
- Platform: All
Do this again, make a "My Game Name Windows Depot", set it to the same things, except "Operating System," which should be "Windows," of course.
Do this again, make a "My Game Name Mac Depot", set it to the same things, except "Operating System," which should be "macOS," of course.
Push the big green "Save" button on the page. Now we have a place to upload platform-specific builds of your game.
Setting up Launch Options link
Click on the "Installation" tab near the top of the page, then "General Installation".
Under "Launch Options," click the "Add new launch option" button, edit the new section that just popped up, and set it like this:
(Whenever you see "mygamename" in here, this should be whatever your game_metadata's "gameid" value is set to. If you see "My Game Name", it's whatever your game_metadata's "gametitle" value is set to, but you'll have to check in case we mangled it to work as a filename.)
- Executable: mygamename.exe
- Launch Type: Launch (Default)
- Operating System: Windows
- CPU Architecture: 64-bit only
- Everything else can be default/blank.
Click the "Update" button on that section.
Add another launch option, as before:
- Executable: My Game Name.app
- Launch Type: Launch (Default)
- Operating System: macOS
Add another launch option, as before:
- Executable: mygamename
- Launch Type: Launch (Default)
- Operating System: Linux + SteamOS
- CPU Architecture: 64-bit only
Publish Changes link
Go to the "Publish" tab at near the top of the page. Click the "View Diffs" button and make sure it looks sane (it should just be the things we've changed in here), then click "Prepare for Publishing", then "Publish to Steam" and follow the instructions to publish these changes.
Go to https://partner.steamgames.com/apps/associated/$APPID For each package, make sure all three depots are included.
Configuring dragonruby-publish
link
You only have to do this part once when first setting up your game. Note that this capability is only available for Indie and Pro license tiers. If you have a Standard DragonRuby License, you'll need to use the Steamworks toolchains directly.
Go add a text file to your game's metadata
directory called steam_metadata.txt
... note that this file will be filtered out dragonruby-publish
packages the game and will not be distributed with the published game.
steam.publish=true
steam.branch=public
steam.username=AAA
steam.appid=BBB
steam.linux_depotid=CCC
steam.windows_depotid=DDD
steam.mac_depotid=EEE
If steam.publish is set to false
then dragonruby-publish will not attempt to upload to Steam. false
is the default if this file, or this setting, is missing.
Where "AAA" is the login name on the Steamworks Partner Site to use for publishing builds, "BBB" is your game-specific AppID provided by Steam, "CCC", "DDD", and "EEE" are the DepotIDs you created for Linux, Windows, and macOS builds, respectively.
Setting a branch live link
Once your build is uploaded, you can assign it to a specific branch through the interface on the Partner site. You can make arbitrary branches here, like "beta" or "nightly" or "fixing-weird-bug" or whatever. The one that goes to the end users without them switching branches, is "default" and you should assume this is where paying customers live, so be careful before you set a build live there.
You can have dragonruby-publish set the builds it publishes live on a branch immediately, if you prefer. Simply add...
steam.branch=XXX
...to steam_metadata.txt, where "XXX" is the branch name from the partner website. If this is blank or unspecified, it will _not_ set the build live on _any_ branch. Setting the value to public
will push to production.
A reasonable strategy is to create a (possibly passworded) branch called "staging" and have dragonruby-publish always push to there automatically. Then you can test from a Steam install, pushing as often as you like, and when you are satisfied, manually set the latest build live on default for the general public to download.
If you are feeling brave, you can always just set all published builds live on default, too. After all, if you break it, you can always just push a fix right away. :) (or use the Partner Site to roll back to a known-good build, you know.)
Publishing Build link
Run dragonuby-publish as you normally would. When it is time to publish to Steam, it will set up any tools it needs, attempt to log you into Steam, and upload the latest version of your game.
Steam login is handled by Valve's steamcmd
command line program, not dragonruby-publish
. DragonRuby does not ever have access to your login credentials. You may need to take steps to get an authorization token in place if necessary, so you don't have to deal with Steam Guard in automated build processes (documentation on how to do this is forthcoming, or read Valve's SteamCMD manual for details).
You (currently) have to set the new build live on the partner site before users will receive it. Optionally automating this step is coming soon!
Questions/Need Help? link
You probably have several. Please come visit the Discord and ask questions, and we'll do our best to help, and update this document.
DragonRuby's Philosophy link
The following tenants of DragonRuby are what set us apart from other game engines. Given that Game Toolkit is a relatively new engine, there are definitely features that are missing. So having a big check list of "all the cool things" is not this engine's forte. This is compensated with a strong commitment to the following principles.
Challenge The Status Quo link
Game engines of today are in a local maximum and don't take into consideration the challenges of this day and age. Unity and GameMaker specifically rot your brain. It's not sufficient to say:
But that's how we've always done it.
It's a hard pill to swallow, but forget blindly accepted best practices and try to figure out the underlying motivation for a specific approach to game development. Collaborate with us.
Continuity of Design link
There is a programming idiom in software called "The Pit of Success". The term normalizes upfront pain as a necessity/requirement in the hopes that the investment will yield dividends "when you become successful" or "when the code becomes more complicated". This approach to development is strongly discouraged by us. It leads to over-architected and unnecessary code; creates barriers to rapid prototyping and shipping a game; and overwhelms beginners who are new to the engine or programming in general.
DragonRuby's philosophy is to provide multiple options across the "make it fast" vs "make it right" spectrum, with incremental/intuitive transitions between the options provided. A concrete example of this philosophy would be render primitives: the spectrum of options allows renderable constructs that take the form of tuples/arrays (easy to pickup, simple, and fast to code/prototype with), hashes (a little more work, but gives you the ability to add additional properties), open and strict entities (more work than hashes, but yields cleaner apis), and finally - if you really need full power/flexibility in rendering - classes (which take the most amount of code and programming knowledge to create).
Release Early and Often link
The biggest mistake game devs make is spending too much time in isolation building their game. Release something, however small, and release it soon.
Stop worrying about everything being pixel perfect. Don't wait until your game is 100% complete. Build your game publicly and iterate. Post in the #show-and-tell channel in the community Discord. You'll find a lot of support and encouragement there.
Real artists ship. Remember that.
Sustainable And Ethical Monetization link
We all aspire to put food on the table doing what we love. Whether it is building games, writing tools to support game development, or anything in between.
Charge a fair amount of money for the things you create. It's expected and encouraged within the community. Give what you create away for free to those that can't afford it.
If you are gainfully employed, pay full price for the things you use. If you do end up getting something at a discount, pay the difference "forward" to someone else.
Sustainable And Ethical Open Source link
This goes hand in hand with sustainable and ethical monetization. The current state of open source is not sustainable. There is an immense amount of contributor burnout. Users of open source expect everything to be free, and few give back. This is a problem we want to fix (we're still trying to figure out the best solution).
So, don't be "that guy" in the Discord that says "DragonRuby should be free and open source!" You will be personally flogged by Amir.
People Over Entities link
We prioritize the endorsement of real people over faceless entities. This game engine, and other products we create, are not insignificant line items of a large company. And you aren't a generic "commodity" or "corporate resource". So be active in the community Discord and you'll reap the benefits as more devs use DragonRuby.
Building A Game Should Be Fun And Bring Happiness link
We will prioritize the removal of pain. The aesthetics of Ruby make it such a joy to work with, and we want to capture that within the engine.
Real World Application Drives Features link
We are bombarded by marketing speak day in and day out. We don't do that here. There are things that are really great in the engine, and things that need a lot of work. Collaborate with us so we can help you reach your goals. Ask for features you actually need as opposed to anything speculative.
We want DragonRuby to *actually* help you build the game you want to build (as opposed to sell you something piece of demoware that doesn't work).
Frequently Asked Questions, Comments, and Concerns link
Here are questions, comments, and concerns that frequently come up.
Frequently Asked Questions link
What is DragonRuby LLP? link
DragonRuby LLP is a partnership of four devs who came together with the goal of bringing the aesthetics and joy of Ruby, everywhere possible.
Under DragonRuby LLP, we offer a number of products (with more on the way):
- Game Toolkit (GTK): A 2D game engine that is compatible with modern gaming platforms.
- RubyMotion (RM): A compiler toolchain that allows you to build native, cross-platform mobile apps. http://rubymotion.com
All of the products above leverage a shared core called DragonRuby.
NOTE: From an official branding standpoint each one of the products is suffixed with "A DragonRuby LLP Product" tagline. Also, DragonRuby is _one word, title cased_.
NOTE: We leave the "A DragonRuby LLP Product" off of this one because that just sounds really weird.
NOTE: Devs who use DragonRuby are "Dragon Riders/Riders of Dragons". That's a bad ass identifier huh?
What is DragonRuby? link
The response to this question requires a few subparts. First we need to clarify some terms. Specifically _language specification_ vs _runtime_.
Okay... so what is the difference between a language specification and a runtime?
A runtime is an _implementation_ of a language specification. When people say "Ruby," they are usually referring to "the Ruby 3.0+ language specification implemented via the CRuby/MRI Runtime."
But, there are many Ruby Runtimes: CRuby/MRI, JRuby, Truffle, Rubinius, Artichoke, and (last but certainly not least) DragonRuby.
Okay... what language specification does DragonRuby use then?
DragonRuby's goal is to be compliant with the ISO/IEC 30170:2012 standard. It's syntax is Ruby 2.x compatible, but also contains semantic changes that help it natively interface with platform specific libraries.
So... why another runtime?
The elevator pitch is:
DragonRuby is a Multilevel Cross-platform Runtime. The "multiple levels" within the runtime allows us to target platforms no other Ruby can target: PC, Mac, Linux, Raspberry Pi, WASM, iOS, Android, Nintendo Switch, PS4, Xbox, and Stadia.
What does Multilevel Cross-platform mean?
There are complexities associated with targeting all the platforms we support. Because of this, the runtime had to be architected in such a way that new platforms could be easily added (which lead to us partitioning the runtime internally):
- Level 1 we leverage a good portion of mRuby.
- Level 2 consists of optimizations to mRuby we've made given that our target platforms are well known.
- Level 3 consists of portable C libraries and their Ruby C-Extensions.
Levels 1 through 3 are fairly commonplace in many runtime implementations (with level 1 being the most portable, and level 3 being the fastest). But the DragonRuby Runtime has taken things a bit further:
- Level 4 consists of shared abstractions around hardware I/O and operating system resources. This level leverages open source and proprietary components within Simple DirectMedia Layer (a low level multimedia component library that has been in active development for 22 years and counting).
- Level 5 is a code generation layer which creates metadata that allows for native interoperability with host runtime libraries. It also includes OS specific message pump orchestrations.
- Level 6 is a Ahead of Time/Just in Time Ruby compiler built with LLVM. This compiler outputs _very_ fast platform specific bitcode, but only supports a subset of the Ruby language specification.
These levels allow us to stay up to date with open source implementations of Ruby; provide fast, native code execution on proprietary platforms; ensure good separation between these two worlds; and provides a means to add new platforms without going insane.
Cool cool. So given that I understand everything to this point, can we answer the original question? What is DragonRuby?
DragonRuby is a Ruby runtime implementation that takes all the lessons we've learned from MRI/CRuby, and merges it with the latest and greatest compiler and OSS technologies.
How is DragonRuby different than MRI? link
DragonRuby supports a subset of MRI apis. Our target is to support all of mRuby's standard lib. There are challenges to this given the number of platforms we are trying to support (specifically console).
Does DragonRuby support Gems?
DragonRuby does not support gems because that requires the installation of MRI Ruby on the developer's machine (which is a non-starter given that we want DragonRuby to be a zero dependency runtime). While this seems easy for Mac and Linux, it is much harder on Windows and Raspberry Pi. mRuby has taken the approach of having a git repository for compatible gems and we will most likely follow suite: https://github.com/mruby/mgem-list.
Does DragonRuby have a REPL/IRB?
You can use DragonRuby's Console within the game to inspect object and execute small pieces of code. For more complex pieces of code create a file called repl.rb
and put it in mygame/app/repl.rb
:
- Any code you write in there will be executed when you change the file. You can organize different pieces of code using the
repl
method:
repl do
puts "hello world"
puts 1 + 1
end
- If you use the `repl` method, the code will be executed and the DragonRuby Console will automatically open so you can see the results (on Mac and Linux, the results will also be printed to the terminal).
- All
puts
statements will also be saved tologs/puts.txt
. So if you want to stay in your editor and not look at the terminal, or the DragonRuby Console, you cantail
this file.
4. To ignore code in repl.rb
, instead of commenting it out, prefix repl
with the letter x
and it'll be ignored.
xrepl do # <------- line is prefixed with an "x"
puts "hello world"
puts 1 + 1
end
# This code will be executed when you save the file.
repl do
puts "Hello"
end
repl do
puts "This code will also be executed."
end
# use xrepl to "comment out" code
xrepl do
puts "This code will not be executed because of the x in front of repl".
end
Does DragonRuby support pry
or have any other debugging facilities?
pry
is a gem that assumes you are using the MRI Runtime (which is incompatible with DragonRuby). Eventually DragonRuby will have a pry based experience that is compatible with a debugging infrastructure called LLDB. Take the time to read about LLDB as it shows the challenges in creating something that is compatible.
You can use DragonRuby's replay capabilities to troubleshoot:
- DragonRuby is hot loaded which gives you a very fast feedback loop (if the game throws an exception, it's because of the code you just added).
- Use
./dragonruby mygame --record
to create a game play recording that you can use to find the exception (you can replay a recording by executing./dragonruby mygame --replay last_replay.txt
or through the DragonRuby Console using$gtk.recording.start_replay "last_replay.txt"
. - DragonRuby also ships with a unit testing facility. You can invoke the following command to run a test:
./dragonruby mygame --test tests/some_ruby_file.rb
. - Get into the habit of adding debugging facilities within the game itself. You can add drawing primitives to
args.outputs.debug
that will render on top of your game but will be ignored in a production release. - Debugging something that runs at 60fps is (imo) not that helpful. The exception you are seeing could have been because of a change that occurred many frames ago.
Frequent Comments About Ruby as a Language Choice link
But Ruby is dead. link
Let's check the official source for the answer to this question: isrubydead.com: https://isrubydead.com/.
On a more serious note, Ruby's _quantity_ levels aren't what they used to be. And that's totally fine. Everyone chases the new and shiny.
What really matters is _quality/maturity_. Here's a StackOverflow Survey sorted by highest paid developers: https://insights.stackoverflow.com/survey/2021#section-top-paying-technologies-top-paying-technologies.
Let's stop making this comment shall we?
But Ruby is slow. link
That doesn't make any sense. A language specification can't be slow... it's a language spec. Sure, an _implementation/runtime_ can be slow though, but then we'd have to talk about which runtime.
Here's a some quick demonstrations of how well DragonRuby Game Toolkit Performs:
- DragonRuby vs Unity: https://youtu.be/MFR-dvsllA4
- DragonRuby vs PyGame: https://youtu.be/fuRGs6j6fPQ
Dynamic languages are slow. link
They are certainly slower than statically compiled languages. With the processing power and compiler optimizations we have today, dynamic languages like Ruby are _fast enough_.
Unless you are writing in some form of intermediate representation by hand, your language of choice also suffers this same fallacy of slow. Like, nothing is faster than a low level assembly-like language. So unless you're writing in that, let's stop making this comment.
NOTE: If you _are_ hand writing LLVM IR, we are always open to bringing on new partners with such a skill set. Email us ^_^.
Frequent Concerns link
DragonRuby is not open source. That's not right. link
The current state of open source is unsustainable. Contributors work for free, most all open source repositories are severely under-staffed, and burnout from core members is rampant.
We believe in open source very strongly. Parts of DragonRuby are in fact, open source. Just not all of it (for legal reasons, and because the IP we've created has value). And we promise that we are looking for (or creating) ways to _sustainably_ open source everything we do.
If you have ideas on how we can do this, email us!
If the reason above isn't sufficient, then definitely use something else.
All this being said, we do have parts of the engine open sourced on GitHub: https://github.com/dragonruby/dragonruby-game-toolkit-contrib/
DragonRuby is for pay. You should offer a free version. link
If you can afford to pay for DragonRuby, you should (and will). We don't tell authors that they should give us their books for free, and only require payment if we read the entire thing. It's time we stop asking that of software products.
That being said, we will _never_ put someone out financially. We have income assistance for anyone that can't afford a license to any one of our products.
You qualify for a free, unrestricted license to DragonRuby products if any of the following items pertain to you:
- Your income is below $2,000.00 (USD) per month.
- You are under 18 years of age.
- You are a student of any type: traditional public school, home schooling, college, bootcamp, or online.
- You are a teacher, mentor, or parent who wants to teach a kid how to code.
- You work/worked in public service or at a charitable organization: for example public office, army, or any 501(c)(3) organization.
Just contact Amir at [email protected] with a short explanation of your current situation and he'll set you up. No questions asked.
But still, you should offer a free version. So I can try it out and see if I like it. link
You can try our web-based sandbox environment at http://fiddle.dragonruby.org. But it won't do the runtime justice. Or just come to our Discord Channel at http://discord.dragonruby.org and ask questions. We'd be happy to have a one on one video chat with you and show off all the cool stuff we're doing.
Seriously just buy it. Get a refund if you don't like it. We make it stupid easy to do so.
I still think you should do a free version. Think of all people who would give it a shot. link
Free isn't a sustainable financial model. We don't want to spam your email. We don't want to collect usage data off of you either. We just want to provide quality toolchains to quality developers (as opposed to a large quantity of developers).
The people that pay for DragonRuby and make an effort to understand it are the ones we want to build a community around, partner with, and collaborate with. So having that small monetary wall deters entitled individuals that don't value the same things we do.
What if I build something with DragonRuby, but DragonRuby LLP becomes insolvent. link
We want to be able to work on the stuff we love, every day of our lives. And we'll go to great lengths to make that continues.
But, in the event that sad day comes, our partnership bylaws state that _all_ DragonRuby IP that can be legally open sourced, will be released under a permissive license.
RECIPIES: link
How To Determine What Frame You Are On link
There is a property on state
called tick_count
that is incremented by DragonRuby every time the tick
method is called. The following code renders a label that displays the current tick_count
.
def tick args
args.outputs.labels << [10, 670, "#{args.state.tick_count}"]
end
How To Get Current Framerate link
Current framerate is a top level property on the Game Toolkit Runtime and is accessible via args.gtk.current_framerate
.
def tick args
args.outputs.labels << [10, 710, "framerate: #{args.gtk.current_framerate.round}"]
end
How To Render A Sprite Using An Array link
All file paths should use the forward slash /
*not* backslash . Game Toolkit includes a number of sprites in the
sprites
folder (everything about your game is located in the mygame
directory).
The following code renders a sprite with a width
and height
of 100
in the center of the screen.
args.outputs.sprites
is used to render a sprite.
NOTE: Rendering using an Array
is "quick and dirty". It's generally recommended that you render using Hashes
long term.
def tick args
args.outputs.sprites << [
640 - 50, # X
360 - 50, # Y
100, # W
100, # H
'sprites/square-blue.png' # PATH
]
end
Rendering a Sprite Using a Hash
link
Using ordinal positioning can get a little unruly given so many properties you have control over.
You can represent a sprite as a Hash
:
def tick args
args.outputs.sprites << {
x: 640 - 50,
y: 360 - 50,
w: 100,
h: 100,
path: 'sprites/square-blue.png',
angle: 0,
a: 255,
r: 255,
g: 255,
b: 255,
# source_ properties have origin of bottom left
source_x: 0,
source_y: 0,
source_w: -1,
source_h: -1,
# tile_ properties have origin of top left
tile_x: 0,
tile_y: 0,
tile_w: -1,
tile_h: -1,
flip_vertically: false,
flip_horizontally: false,
angle_anchor_x: 0.5,
angle_anchor_y: 1.0,
blendmode_enum: 1
# labels anchor/alignment (default is nil)
anchor_x: 0.5,
anchor_y: 0.5
}
end
The blendmode_enum
value can be set to 0
(no blending), 1
(alpha blending), 2
(additive blending), 3
(modulo blending), 4
(multiply blending).
How To Render A Label link
args.outputs.labels
is used to render labels.
Labels are how you display text. This code will go directly inside of the def tick args
method.
NOTE: Rendering using an Array
is "quick and dirty". It's generally recommended that you render using Hashes
long term.
Here is the minimum code:
def tick args
# X Y TEXT
args.outputs.labels << [640, 360, "I am a black label."]
end
A Colored Label link
def tick args
# A colored label
# X Y TEXT, RED GREEN BLUE ALPHA
args.outputs.labels << [640, 360, "I am a redish label.", 255, 128, 128, 255]
end
Extended Label Properties link
def tick args
# A colored label
# X Y TEXT SIZE ALIGNMENT RED GREEN BLUE ALPHA FONT FILE
args.outputs.labels << [
640, # X
360, # Y
"Hello world", # TEXT
0, # SIZE_ENUM
1, # ALIGNMENT_ENUM
0, # RED
0, # GREEN
0, # BLUE
255, # ALPHA
"fonts/coolfont.ttf" # FONT
]
end
A SIZE_ENUM
of 0
represents "default size". A negative
value will decrease the label size. A positive
value will increase the label's size.
An ALIGNMENT_ENUM
of 0
represents "left aligned". 1
represents "center aligned". 2
represents "right aligned".
Rendering A Label As A Hash
link
You can add additional metadata about your game within a label, which requires you to use a `Hash` instead.
If you use a Hash
to render a label, you can set the label's size using either SIZE_ENUM
or SIZE_PX
. If both options are provided, SIZE_PX
will be used.
def tick args
args.outputs.labels << {
x: 200,
y: 550,
text: "dragonruby",
# size specification can be either size_enum or size_px
size_enum: 2,
size_px: 22,
alignment_enum: 1,
r: 155,
g: 50,
b: 50,
a: 255,
font: "fonts/manaspc.ttf",
vertical_alignment_enum: 0, # 0 is bottom, 1 is middle, 2 is top
anchor_x: 0.5,
anchor_y: 0.5
# You can add any properties you like (this will be ignored/won't cause errors)
game_data_one: "Something",
game_data_two: {
value_1: "value",
value_2: "value two",
a_number: 15
}
}
end
Getting The Size Of A Piece Of Text link
You can get the render size of any string using args.gtk.calcstringbox
.
def tick args
# TEXT SIZE_ENUM FONT
w, h = args.gtk.calcstringbox("some string", 0, "font.ttf")
# NOTE: The SIZE_ENUM and FONT are optional arguments.
# Render a label showing the w and h of the text:
args.outputs.labels << [
10,
710,
# This string uses Ruby's string interpolation literal: #{}
"'some string' has width: #{w}, and height: #{h}."
]
end
Rendering Labels With New Line Characters And Wrapping link
You can use a strategy like the following to create multiple labels from a String.
def tick args
long_string = "Lorem ipsum dolor sit amet, consectetur adipiscing elitteger dolor velit, ultricies vitae libero vel, aliquam imperdiet enim."
max_character_length = 30
long_strings_split = args.string.wrapped_lines long_string, max_character_length
args.outputs.labels << long_strings_split.map_with_index do |s, i|
{ x: 10, y: 600 - (i * 20), text: s }
end
end
How To Play A Sound link
Sounds that end .wav
will play once:
def tick args
# Play a sound every second
if (args.state.tick_count % 60) == 0
args.outputs.sounds << 'something.wav'
end
end
Sounds that end .ogg
is considered background music and will loop:
def tick args
# Start a sound loop at the beginning of the game
if args.state.tick_count == 0
args.outputs.sounds << 'background_music.ogg'
end
end
If you want to play a .ogg
once as if it were a sound effect, you can do:
def tick args
# Play a sound every second
if (args.state.tick_count % 60) == 0
args.gtk.queue_sound 'some-ogg.ogg'
end
end
Using args.state
To Store Your Game State link
args.state
is a open data structure that allows you to define properties that are arbitrarily nested. You don't need to define any kind of class
.
To initialize your game state, use the ||=
operator. Any value on the right side of ||=
will only be assigned _once_.
To assign a value every frame, just use the =
operator, but _make sure_ you've initialized a default value.
def tick args
# initialize your game state ONCE
args.state.player.x ||= 0
args.state.player.y ||= 0
args.state.player.hp ||= 100
# increment the x position of the character by one every frame
args.state.player.x += 1
# Render a sprite with a label above the sprite
args.outputs.sprites << [
args.state.player.x,
args.state.player.y,
32, 32,
"player.png"
]
args.outputs.labels << [
args.state.player.x,
args.state.player.y - 50,
args.state.player.hp
]
end
Accessing files link
DragonRuby uses a sandboxed filesystem which will automatically read from and write to a location appropriate for your platform so you don't have to worry about theses details in your code. You can just use gtk.read_file
, gtk.write_file
, and gtk.append_file
with a relative path and the engine will take care of the rest.
The data directories that will be written to in a production build are:
- Windows:
C:\Users\[username]\AppData\Roaming\[devtitle]\[gametitle]
- MacOS:
$HOME/Library/Application Support/[gametitle]
- Linux:
$HOME/.local/share/[gametitle]
- HTML5: The data will be written to the browser's IndexedDB.
The values in square brackets are the values you set in your app/metadata/game_metadata.txt
file.
When reading files, the engine will first look in the game's data directory and then in the game directory itself. This means that if you write a file to the data directory that already exists in your game directory, the file in the data directory will be used instead of the one that is in your game.
When running a development build you will directly write to your game directory (and thus overwrite existing files). This can be useful for built-in development tools like level editors.
For more details on the implementation of the sandboxed filesystem, see Ryan C. Gordon's PhysicsFS documentation: https://icculus.org/physfs/
IMPORTANT: File access functions are sandoxed and assume that the dragonruby
binary lives alongside the game you are building. Do not expect file access functions to return correct values if you are attempting to run the dragonruby
binary from a shared location. It's recommended that the directory structure contained in the zip is not altered and games are built using that starter template.
Troubleshoot Performance link
- If you're using
Array
s for your primitives (args.outputs.sprites << []
), useHash
instead (args.outputs.sprites << { x: ... }
). - If you're using
Entity
for your primitives (args.outputs.sprites << args.state.new_entity
), useStrictEntity
instead (args.outputs.sprites << args.state.new_entity_strict
). - Use
.each
instead of.map
if you don't care about the return value. - When concatenating primitives to outputs, do them in bulk. Instead of:
args.state.bullets.each do |bullet|
args.outputs.sprites << bullet.sprite
end
do
args.outputs.sprites << args.state.bullets.map do |b|
b.sprite
end
- Use
args.outputs.static_
variant for things that don't change often (take a look at the Basic Gorillas sample app and Dueling Starships sample app to see howstatic_
is leveraged. - Consider using a
render_target
if you're doing some form of a camera that moves a lot of primitives (take a look at the Render Target sample apps for more info). - Avoid deleting or adding to an array during iteration. Instead of:
args.state.fx_queue |fx|
fx.count_down ||= 255
fx.countdown -= 5
if fx.countdown < 0
args.state.fx_queue.delete fx
end
end
Do:
args.state.fx_queue |fx|
fx.count_down ||= 255
fx.countdown -= 5
end
args.state.fx_queue.reject! { |fx| fx.countdown < 0 }
Outputs (args.outputs
) link
Outputs is how you render primitives to the screen. The minimal setup for rendering something to the screen is via a tick
method defined in mygame/app/main.rb
def tick args
args.outputs.solids << { x: 0, y: 0, w: 100, h: 100 }
args.outputs.sprites << { x: 100, y: 100, w: 100, h: 100, path: "sprites/square/blue.png" }
args.outputs.labels << { x: 200, y: 200, text: "Hello World" }
args.outputs.borders << { x: 0, y: 0, w: 100, h: 100 }
args.outputs.lines << { x: 300, y: 300, x2: 400, y2: 400 }
end
Render Order link
Primitives are rendered first-in, first-out. The rendering order (sorted by bottom-most to top-most):
solids
sprites
primitives
: Accepts all render primitives. Useful when you want to bypass the default rendering orders for rendering (eg. rendering solids on top of sprites).labels
lines
borders
debug
: Accepts all render primitives. Use this to render primitives for debugging (production builds of your game will not render this layer).
solids
link
Add primitives to this collection to render a solid to the screen.
Rendering a solid using an Array link
Creates a solid black rectangle located at 100, 100. 160 pixels wide and 90 pixels tall.
def tick args
# X Y WIDTH HEIGHT
args.outputs.solids << [100, 100, 160, 90]
end
Rendering a solid using an Array with colors and alpha link
The value for the color and alpha is a number between 0
and 255
. The alpha property is optional and will be set to 255
if not specified.
Creates a green solid rectangle with an opacity of 50%.
def tick args
# X Y WIDTH HEIGHT RED GREEN BLUE ALPHA
args.outputs.solids << [100, 100, 160, 90, 0, 255, 0, 128]
end
Rendering a solid using a Hash link
If you want a more readable invocation. You can use the following hash to create a solid. Any parameters that are not specified will be given a default value. The keys of the hash can be provided in any order.
def tick args
args.outputs.solids << {
x: 0,
y: 0,
w: 100,
h: 100,
r: 0,
g: 255,
b: 0,
a: 255,
anchor_x: 0,
anchor_y: 0,
blendmode_enum: 1
}
end
Rendering a solid using a Class link
You can also create a class with solid properties and render it as a primitive. ALL properties must be on the class. *Additionally*, a method called primitive_marker
must be defined on the class.
Here is an example:
# Create type with ALL solid properties AND primitive_marker
class Solid
attr_accessor :x, :y, :w, :h, :r, :g, :b, :a, :anchor_x, :anchor_y, :blendmode_enum
def primitive_marker
:solid # or :border
end
end
# Inherit from type
class Square < Solid
# constructor
def initialize x, y, size
self.x = x
self.y = y
self.w = size
self.h = size
end
end
def tick args
# render solid/border
args.outputs.solids << Square.new(10, 10, 32)
end
borders
link
Add primitives to this collection to render an unfilled solid to the screen. Take a look at the documentation for Outputs#solids.
The only difference between the two primitives is where they are added.
Instead of using args.outputs.solids
:
def tick args
# X Y WIDTH HEIGHT
args.outputs.solids << [100, 100, 160, 90]
end
You have to use args.outputs.borders
:
def tick args
# X Y WIDTH HEIGHT
args.outputs.borders << [100, 100, 160, 90]
end
sprites
link
Add primitives to this collection to render a sprite to the screen.
Rendering a sprite using an Array link
Creates a sprite of a white circle located at 100, 100. 160 pixels wide and 90 pixels tall.
def tick args
# X Y WIDTH HEIGHT PATH
args.outputs.sprites << [100, 100, 160, 90, "sprites/circle/white.png"]
end
Rendering a sprite using a Hash link
If you want a more readable (and faster) invocation, you can use the following hash to create a sprite. Any parameters that are not specified will be given a default value. The keys of the hash can be provided in any order.
def tick args
args.outputs.sprites << {
x: 0,
y: 0,
w: 100,
h: 100,
path: "sprites/circle/white.png",
angle: 0,
a: 255,
r: 0,
g: 255,
b: 0
}
end
Here are all the properties that you can set on a sprite. The only required ones are x
, y
, w
, h
, and path
.
Required properties
x
: X position of the sprite. Note: the botton left corner of the sprite is used for positioning (this can be changed usinganchor_x
, andanchor_y
).y
: Y position of the sprite. Note: The origin 0,0 is at the bottom left corner. Settingy
to a higher value will move the sprite upwards.w
: The render width.h
: The render height.path
: The path of the sprite relative to the game folder.
Anchors and Rotations
flip_horizonally
: This value can be eithertrue
orfalse
and controls if the sprite will be flipped horizontally (default value is false).flip_vertically
: This value can be eithertrue
orfalse
and controls if the sprite will be flipped horizontally (default value is false).anchor_x
: Used to determine anchor point of the sprite's X position (relative to the render width).anchor_y
: Used to determine anchor point of the sprite's Y position (relative to the render height).angle
: Rotation of the sprite in degrees (default value is 0). Rotation occurs around the center of the sprite. The point of rotation can be changed usingangle_anchor_x
andangle_anchor_y
.angle_anchor_x
: Controls the point of rotation for the sprite (relative to the render width).angle_anchor_y
: Controls the point of rotation for the sprite (relative to the render height).
Here's an example of rendering a 80x80 pixel sprite in the center of the screen:
def tick args
args.outputs.sprites << {
x: 640 - 40, # the logical center of the screen horizontally is 640, minus half the width of the sprite
y: 360 - 40, # the logical center of the screen vertically is 360, minus half the height of the sprite
w: 80,
h: 80,
path: "sprites/square/blue.png"
}
end
Instead of computing the offset, you can use anchor_x
, and anchor_y
to center the sprite. The following is equivalent to the code above:
def tick args
args.outputs.sprites << {
x: 640,
y: 360,
w: 80,
h: 80,
path: "sprites/square/blue.png",
anchor_x: 0.5, # position horizontally at 0.5 of the sprite's width
anchor_y: 0.5 # position vertically at 0.5 of the sprite's height
}
end
Cropping Properties
tile_(x|y|w|h)
: Defines the crop area to use for a sprite. The origin fortile_
properties uses the TOP LEFT as its origin (useful for cropping tiles from a sprite sheet).source_(x|y|w|h)
: Defines the crop area to use for a sprite. The origin fortile_
properties uses the BOTTOM LEFT as its origin.
See the sample apps under ./samples/03_rendering_sprites
for examples of how to use this properties non-trivially.
Blending Options
a
: Alpha/transparency of the sprite from 0 to 255 (default value is 255).r
: Level of red saturation for the sprite (default value is 255). Example: Setting the value to zero will remove all red coloration from the sprite.g
: Level of green saturation for the sprite (default value is 255).b
: Level of blue saturation for the sprite (default value is 255).blendmode_enum
: Valid options are0
: no blending,1
: default/alpha blending,2
: addative blending,3
: modulo blending,4
: multiply blending.
The following sample apps show how blendmode_enum
can be leveraged to create coloring and lighting effects:
./samples/07_advanced_rendering/11_blend_modes
./samples/07_advanced_rendering/13_lighting
Triagles (Indie, Pro Feature)
Sprites can be rendered as triangles at the Indie and Pro License Tiers. To rendering using triangles, instead of providing a w
, h
property, provide x2
, y2
, x3
, y3
. This applies for positioning and cropping.
Here is an example:
def tick args
args.outputs.sprites << {
x: 0,
y: 0,
x2: 80,
y2: 0,
x3: 0,
y3: 80,
source_x: 0,
source_y: 0,
source_x2: 80,
source_y2: 0,
source_x3: 0,
source_y3: 80,
path: "sprites/square/blue.png"
}
end
For more example of rendering using triangles see:
./samples/07_advanced_rendering/14_triangles
./samples/07_advanced_rendering/15_triangles_trapezoid
./samples/07_advanced_rendering/16_matrix_and_triangles_2d
./samples/07_advanced_rendering/16_matrix_and_triangles_3d
./samples/07_advanced_rendering/16_matrix_cubeworld
Rendering a sprite using a Class link
You can also create a class with solid/border properties and render it as a primitive. ALL properties must be on the class. *Additionally*, a method called primitive_marker
must be defined on the class.
Here is an example:
# Create type with ALL sprite properties AND primitive_marker
class Sprite
attr_accessor :x, :y, :w, :h, :path, :angle, :a, :r, :g, :b, :tile_x,
:tile_y, :tile_w, :tile_h, :flip_horizontally,
:flip_vertically, :angle_anchor_x, :angle_anchor_y, :id,
:angle_x, :angle_y, :z,
:source_x, :source_y, :source_w, :source_h, :blendmode_enum,
:source_x2, :source_y2, :source_x3, :source_y3, :x2, :y2, :x3, :y3,
:anchor_x, :anchor_y
def primitive_marker
:sprite
end
end
# Inherit from type
class Circle < Sprite
# constructor
def initialize x, y, size, path
self.x = x
self.y = y
self.w = size
self.h = size
self.path = path
end
def serialize
{x:self.x, y:self.y, w:self.w, h:self.h, path:self.path}
end
def inspect
serialize.to_s
end
def to_s
serialize.to_s
end
end
def tick args
# render circle sprite
args.outputs.sprites << Circle.new(10, 10, 32,"sprites/circle/white.png")
end
attr_sprite
link
The attr_sprite
class macro adds all properties needed to render a sprite to a class. This removes the need to manually define all sprites properties that DragonRuby offers for rendering.
Instead of manually defining the properties, you can represent a sprite using the attr_sprite
class macro:
class BlueSquare
# invoke the helper function at the class level for
# anything you want to represent as a sprite
attr_sprite
def initialize(x: 0, y: 0, w: 0, h: 0k
@x = x
@y = y
@w = w
@h = h
@path = 'sprites/square-blue.png'
end
end
def tick args
args.outputs.sprites << BlueSquare.new(x: 640 - 50,
y: 360 - 50,
w: 50,
h: 50)
end
labels
link
Add primitives to this collection to render a label.
Rendering a label using an Array link
Labels represented as Arrays/Tuples:
def tick args
# X Y TEXT SIZE_ENUM
args.outputs.labels << [175 + 150, 610 - 50, "Smaller label.", 0]
end
Here are all the properties that you can set with a label represented as an Array. It's recommended to move over to using Hashes once you've specified a lot of properties.
def tick args
args.outputs.labels << [
640, # X
360, # Y
"Hello world", # TEXT
0, # SIZE_ENUM
1, # ALIGNMENT_ENUM
0, # RED
0, # GREEN
0, # BLUE
255, # ALPHA
"fonts/coolfont.ttf" # FONT
]
end
d
Rendering a label using a Hash link
def tick args
args.outputs.labels << {
x: 200,
y: 550,
text: "dragonruby",
size_enum: 2,
alignment_enum: 1, # 0 = left, 1 = center, 2 = right
r: 155,
g: 50,
b: 50,
a: 255,
font: "fonts/manaspc.ttf",
vertical_alignment_enum: 0 # 0 = bottom, 1 = center, 2 = top
}
end
Screenshots
link
Add a hash to this collection to take a screenshot and save as png file. The keys of the hash can be provided in any order.
def tick args
args.outputs.screenshots << {
x: 0, y: 0, w: 100, h: 100, # Which portion of the screen should be captured
path: 'screenshot.png', # Output path of PNG file (inside game directory)
r: 255, g: 255, b: 255, a: 0 # Optional chroma key
}
end
Chroma key (Making a color transparent) link
By specifying the r, g, b and a keys of the hash you change the transparency of a color in the resulting PNG file. This can be useful if you want to create files with transparent background like spritesheets. The transparency of the color specified by r
, g
, b
will be set to the transparency specified by a
.
The example above sets the color white (255, 255, 255) as transparent.
Inputs (args.inputs
) link
Access using input using args.inputs
.
last_active
link
This function returns the last active input which will be set to either :keyboard
, :mouse
, or :controller
. The function is helpful when you need to present on screen instructions based on the input the player chose to play with.
def tick args
if args.inputs.last_active == :controller
args.outputs.labels << { x: 60, y: 60, text: "Use the D-Pad to move around." }
else
args.outputs.labels << { x: 60, y: 60, text: "Use the arrow keys to move around." }
end
end
:mouse
, or :controller
. The function is helpful when you need to present on screen instructions based on the input the player chose to play with.
locale
link
Returns the ISO 639-1 two-letter langauge code based on OS preferences. Refer to the following link for locale strings: https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes).
Defaults to "en" if locale can't be retrieved (args.inputs.locale_raw
will be nil in this case).
up
link
Returns true
if: the up
arrow or w
key is pressed or held on the keyboard
; or if up
is pressed or held on controller_one
; or if the left_analog
on controller_one
is tilted upwards.
down
link
Returns true
if: the down
arrow or s
key is pressed or held on the keyboard
; or if down
is pressed or held on controller_one
; or if the left_analog
on controller_one
is tilted downwards.
left
link
Returns true
if: the left
arrow or a
key is pressed or held on the keyboard
; or if left
is pressed or held on controller_one
; or if the left_analog
on controller_one
is tilted to the left.
right
link
Returns true
if: the right
arrow or d
key is pressed or held on the keyboard
; or if right
is pressed or held on controller_one
; or if the left_analog
on controller_one
is tilted to the right.
left_right
link
Returns -1
(left), 0
(neutral), or +1
(right) depending on results of args.inputs.left
and args.inputs.right
.
args.state.player[:x] += args.inputs.left_right * args.state.speed
up_down
link
Returns -1
(down), 0
(neutral), or +1
(up) depending on results of args.inputs.down
and args.inputs.up
.
args.state.player[:y] += args.inputs.up_down * args.state.speed
text
link
Returns a string that represents the last key that was pressed on the keyboard.
Mouse (args.inputs.mouse
) link
Represents the user's mouse.
has_focus
link
Return's true if the game has mouse focus.
x
link
Returns the current x
location of the mouse.
y
link
Returns the current y
location of the mouse.
inside_rect? rect
link
Return. args.inputs.mouse.inside_rect?
takes in any primitive that responds to x, y, w, h
:
inside_circle? center_point, radius
link
Returns true
if the mouse is inside of a specified circle. args.inputs.mouse.inside_circle?
takes in any primitive that responds to x, y
(which represents the circle's center), and takes in a radius
:
moved
link
Returns true
if the mouse has moved on the current frame.
button_left
link
Returns true
if the left mouse button is down.
button_middle
link
Returns true
if the middle mouse button is down.
button_right
link
Returns true
if the right mouse button is down.
button_bits
link
Returns a bitmask for all buttons on the mouse: 1
for a button in the down
state, 0
for a button in the up
state.
wheel
link
Represents the mouse wheel. Returns nil
if no mouse wheel actions occurred. Otherwise args.inputs.mouse.wheel
will return a Hash
with x
, and y
(representing movement on each axis).
click
OR down
, previous_click
, up
link
The properties args.inputs.mouse.(click|down|previous_click|up)
each return nil
if the mouse button event didn't occur. And return an Entity that has an x
, y
properties along with helper functions to determine collision: inside_rect?
, inside_circle
. This value will be true if any of the mouse's buttons caused these events. To scope to a specific button use .button_left
, .button_middle
, .button_right
, or .button_bits
.
Touch link
The following touch apis are available on touch devices (iOS, Android, Mobile Web, Surface).
args.inputs.touch
link
Returns a Hash
representing all touch points on a touch device.
args.inputs.finger_left
link
Returns a Hash
with x
and y
denoting a touch point that is on the left side of the screen.
args.inputs.finger_right
link
Returns a Hash
with x
and y
denoting a touch point that is on the right side of the screen.
Controller (args.inputs.controller_(one-four)
) link
Represents controllers connected to the usb ports.
active
link
Returns true if any of the controller's buttons were used.
up
link
Returns true
if up
is pressed or held on the directional or left analog.
down
link
Returns true
if down
is pressed or held on the directional or left analog.
left
link
Returns true
if left
is pressed or held on the directional or left analog.
right
link
Returns true
if right
is pressed or held on the directional or left analog.
left_right
link
Returns -1
(left), 0
(neutral), or +1
(right) depending on results of args.inputs.controller_(one-four).left
and args.inputs.controller_(one-four).right
.
up_down
link
Returns -1
(down), 0
(neutral), or +1
(up) depending on results of args.inputs.controller_(one-four).up
and args.inputs.controller_(one-four).down
.
(left|right)_analog_x_raw
link
Returns the raw integer value for the analog's horizontal movement (-32,000 to +32,000
).
(left|right)_analog_y_raw
link
Returns the raw integer value for the analog's vertical movement (-32,000 to +32,000
).
(left|right)_analog_x_perc
link
Returns a number between -1
and 1
which represents the percentage the analog is moved horizontally as a ratio of the maximum horizontal movement.
(left|right)_analog_y_perc
link
Returns a number between -1
and 1
which represents the percentage the analog is moved vertically as a ratio of the maximum vertical movement.
directional_up
link
Returns true
if up
is pressed or held on the directional.
directional_down
link
Returns true
if down
is pressed or held on the directional.
directional_left
link
Returns true
if left
is pressed or held on the directional.
directional_right
link
Returns true
if right
is pressed or held on the directional.
(a|b|x|y|l1|r1|l2|r2|l3|r3|start|select)
link
Returns true
if the specific button is pressed or held.
truthy_keys
link
Returns a collection of Symbol
s that represent all keys that are in the pressed or held state.
key_down
link
Returns true
if the specific button was pressed on this frame. args.inputs.controller_(one-four).key_down.BUTTON
will only be true on the frame it was pressed.
key_held
link
Returns true
if the specific button is being held. args.inputs.controller_(one-four).key_held.BUTTON
will be true for all frames after key_down
(until released).
key_up
link
Returns true
if the specific button was released. args.inputs.controller_(one-four).key_up.BUTTON
will be true only on the frame the button was released.
Keyboard (args.inputs.keyboard
) link
Represents the user's keyboard.
active
link
Returns Kernel.tick_count
(args.state.tick_count
) if any keys on the keyboard were pressed.
has_focus
link
Returns true
if the game has keyboard focus.
up
link
Returns true
if up
or w
is pressed or held on the keyboard.
down
link
Returns true
if down
or s
is pressed or held on the keyboard.
left
link
Returns true
if left
or a
is pressed or held on the keyboard.
right
link
Returns true
if right
or d
is pressed or held on the keyboard.
left_right
link
Returns -1
(left), 0
(neutral), or +1
(right) depending on results of args.inputs.keyboard.left
and args.inputs.keyboard.right
.
up_down
link
Returns -1
(left), 0
(neutral), or +1
(right) depending on results of args.inputs.keyboard.up
and args.inputs.keyboard.up
.
keyboard properties link
The following properties represent keys on the keyboard and are available on args.inputs.keyboard.KEY
, args.inputs.keyboard.key_down.KEY
, args.inputs.keyboard.key_held.KEY
, and args.inputs.keyboard.key_up.KEY
:
alt
meta
control
shift
ctrl_KEY
(dynamic method, egargs.inputs.keyboard.ctrl_a
)exclamation_point
zero
-nine
backspace
delete
escape
enter
tab
(open|close)_round_brace
(open|close)_curly_brace
(open|close)_square_brace
colon
semicolon
equal_sign
hyphen
space
dollar_sign
double_quotation_mark
single_quotation_mark
backtick
tilde
period
comma
pipe
underscore
a
-z
shift
control
alt
meta
left
right
up
down
pageup
pagedown
plus
at
forward_slash
back_slash
asterisk
less_than
greater_than
carat
ampersand
superscript_two
circumflex
question_mark
section_sign
ordinal_indicator
raw_key
(unique numeric identifier for key)left_right
up_down
directional_vector
truthy_keys
(array ofSymbols
)
char
link
Method is available under inputs.key_down
, inputs.key_held
, and inputs.key_up
. Take note that
args.inputs.keyboard.key_held.char
will only return the ascii value of the last key that was held. Use args.inputs.keyboard.key_held.truthy_keys
to get an Array
of Symbols
representing all keys being held.
To get a picture of all key states args.inputs.keyboard.keys
returns a Hash
with the following keys: :down
, :held
, :down_or_held
, :up
.
NOTE: args.inputs.keyboard.key_down.char
will be set in line with key repeat behavior of your OS.
This is a demonstration of the behavior (see ./samples/02_input_basics/01_keyboard
for a more detailed example):
def tick args
# uncomment the line below to see the value changes at a slower rate
# $gtk.slowmo! 30
keyboard = args.inputs.keyboard
args.outputs.labels << { x: 30,
y: 720,
text: "use the J key to test" }
args.outputs.labels << { x: 30,
y: 720 - 30,
text: "key_down.char: #{keyboard.key_down.char.inspect}" }
args.outputs.labels << { x: 30,
y: 720 - 60,
text: "key_down.j: #{keyboard.key_down.j}" }
args.outputs.labels << { x: 30,
y: 720 - 30,
text: "key_held.char: #{keyboard.key_held.char.inspect}" }
args.outputs.labels << { x: 30,
y: 720 - 60,
text: "key_held.j: #{keyboard.key_held.j}" }
args.outputs.labels << { x: 30,
y: 720 - 30,
text: "key_up.char: #{keyboard.key_up.char.inspect}" }
args.outputs.labels << { x: 30,
y: 720 - 60,
text: "key_up.j: #{keyboard.key_up.j}" }
end
keys
link
Returns a Hash
with all keys on the keyboard in their respective state. The Hash
contains the following keys
:down
:held
:down_or_held
:up
Runtime
(args.gtk
) link
The GTK::Runtime
class is the core of DragonRuby. It is globally accessible via $gtk
or inside of the tick
method through args
.
def tick args
args.gtk # accessible like this
$gtk # or like this
end
Class Macros link
The following class macros are available within DragonRuby.
attr
link
The attr
class macro is an alias to attr_accessor
.
Instead of:
class Player
attr_accessor :hp, :armor
end
You can do:
class Player
attr :hp, :armor
end
attr_gtk
link
As the size/complexity of your game increases. You may want to create classes to organize everything. The attr_gtk
class macro adds DragonRuby's environment methods (such as args.state
, args.inputs
, args.outputs
, args.audio
, etc) to your class so you don't have to pass args
around everwhere.
Instead of:
class Game
def tick args
defaults args
calc args
render args
end
def defaults args
args.state.space_pressed_at ||= 0
end
def calc args
if args.inputs.keyboard.key_down.space
args.state.space_pressed_at = args.state.tick_count
end
end
def render args
if args.state.space_pressed_at == 0
args.outputs.labels << { x: 100, y: 100,
text: "press space" }
else
args.outputs.labels << { x: 100, y: 100,
text: "space was pressed at: #{args.state.space_pressed_at}" }
end
end
end
def tick args
$game ||= Game.new
$game.tick args
end
You can do:
class Game
attr_gtk # attr_gtk class macro
def tick
defaults
calc
render
end
def defaults
state.space_pressed_at ||= 0
end
def calc
if inputs.keyboard.key_down.space
state.space_pressed_at = state.tick_count
end
end
def render
if state.space_pressed_at == 0
outputs.labels << { x: 100, y: 100,
text: "press space" }
else
outputs.labels << { x: 100, y: 100,
text: "space was pressed at: #{state.space_pressed_at}" }
end
end
end
def tick args
$game ||= Game.new
$game.args = args # set args property on game
$game.tick # call tick without passing in args
end
$game = nil
Indie and Pro Functions link
The following functions are only available at the Indie and Pro License tiers.
get_pixels
link
Given a file_path
to a sprite, this function returns a one dimensional array of hexadecimal values representing the ARGB of each pixel in a sprite.
See the following sample app for a full demonstration of how to use this function: ./samples/07_advanced_rendering/06_pixel_arrays_from_file
dlopen
link
Loads a precompiled C Extension into your game.
See the sample apps at ./samples/12_c_extensions
for detailed walkthroughs of creating C extensions.
Environment and Utility Functions link
The following functions will help in interacting with the OS and rendering pipeline.
calcstringbox
link
Returns the render width and render height as a tuple for a piece of text. The parameters this method takes are:
text
: the text you want to get the width and height of.size_enum
: number representing the render size for the text. This parameter is optional and defaults to0
which represents a baseline font size in units specific to DragonRuby (a negative value denotes a size smaller than what would be comfortable to read on a handheld device postive values above0
represent larger font sizes).font
: path to a font file that the width and height will be based off of. This field is optional and defaults to the DragonRuby's default font.
def tick args
text = "a piece of text"
size_enum = 5 # "large font size"
# path is relative to your game directory (eg mygame/fonts/courier-new.ttf)
font = "fonts/courier-new.ttf"
# get the render width and height
string_w, string_h = args.gtk.calcstringbox text, size_enum, font
# render the label
args.outputs.labels << {
x: 100,
y: 100,
text: text,
size_enum: size_enum,
font: font
}
# render a border around the label based on the results from calcstringbox
args.outputs.borders << {
x: 100,
y: 100,
w: string_w,
h: string_h,
r: 0,
g: 0,
b: 0
}
end
request_quit
link
Call this function to exit your game. You will be given one additional tick if you need to perform any housekeeping before that game closes.
def tick args
# exit the game after 600 frames (10 seconds)
if args.state.tick_count == 600
args.gtk.request_quit
end
end
quit_requested?
link
This function will return true
if the game is about to exit (either from the user closing the game or if request_quit
was invoked).
set_window_fullscreen
link
This function takes in a single boolean parameter. true
to make the game fullscreen, false
to return the game back to windowed mode.
def tick args
# make the game full screen after 600 frames (10 seconds)
if args.state.tick_count == 600
args.gtk.set_window_fullscreen true
end
# return the game to windowed mode after 20 seconds
if args.state.tick_count == 1200
args.gtk.set_window_fullscreen false
end
end
window_fullscreen?
link
Returns true if the window is currently in fullscreen mode.
set_window_scale
link
This function takes in a float value and uses that to resize the game window to a percentage of 1280x720 (or 720x1280 in portrait mode). The valid scale options are 0.1, 0.25, 0.5, 0.75, 1.25, 1.5, 2.0, 2.5, 3.0, and 4.0. The float value you pass in will be floored to the nearest valid scale option.
platform?
link
You can ask DragonRuby which platform your game is currently being run on. This can be useful if you want to perform different pieces of logic based on where the game is running.
The raw platform string value is available via args.gtk.platform
which takes in a symbol
representing the platform's categorization/mapping.
You can see all available platform categorizations via the args.gtk.platform_mappings
function.
Here's an example of how to use args.gtk.platform? category_symbol
:
def tick args
label_style = { x: 640, y: 360, anchor_x: 0.5, anchor_y: 0.5 }
if args.gtk.platform? :macos
args.outputs.labels << { text: "I am running on MacOS.", **label_style }
elsif args.gtk.platform? :win
args.outputs.labels << { text: "I am running on Windows.", **label_style }
elsif args.gtk.platform? :linux
args.outputs.labels << { text: "I am running on Linux.", **label_style }
elsif args.gtk.platform? :web
args.outputs.labels << { text: "I am running on a web page.", **label_style }
elsif args.gtk.platform? :android
args.outputs.labels << { text: "I am running on Android.", **label_style }
elsif args.gtk.platform? :ios
args.outputs.labels << { text: "I am running on iOS.", **label_style }
elsif args.gtk.platform? :touch
args.outputs.labels << { text: "I am running on a device that supports touch (either iOS/Android native or mobile web).", **label_style }
elsif args.gtk.platform? :steam
args.outputs.labels << { text: "I am running via steam (covers both desktop and steamdeck).", **label_style }
elsif args.gtk.platform? :steam_deck
args.outputs.labels << { text: "I am running via steam on the Steam Deck (not steam desktop).", **label_style }
elsif args.gtk.platform? :steam_desktop
args.outputs.labels << { text: "I am running via steam on desktop (not steam deck).", **label_style }
end
end
production?
link
Returns true if the game is being run in a released/shipped state.
If you want to simulate a production build. Add an empty file called DRAGONRUBY_PRODUCTION_BUILD
inside of the metadata
folder. This will turn of all logging and all creation of temp files used for development purposes.
platform_mappings
link
These are the current platform categorizations (args.gtk.platform_mappings
):
{
"Mac OS X" => [:desktop, :macos, :osx, :mac, :macosx], # may also include :steam and :steam_desktop run via steam
"Windows" => [:desktop, :windows, :win], # may also include :steam and :steam_desktop run via steam
"Linux" => [:desktop, :linux, :nix], # may also include :steam and :steam_desktop run via steam
"Emscripten" => [:web, :wasm, :html, :emscripten], # may also include :touch if running in the web browser on mobile
"iOS" => [:mobile, :ios, :touch],
"Android" => [:mobile, :android, :touch],
"Steam Deck" => [:steamdeck, :steam_deck, :steam],
}
Given the mappings above, args.gtk.platform? :desktop
would return true
if the game is running on a player's computer irrespective of OS (MacOS, Linux, and Windows are all categorized as :desktop
platforms).
open_url
link
Given a uri represented as a string. This fuction will open the uri in the user's default browser.
def tick args
# open a url after 600 frames (10 seconds)
if args.state.tick_count == 600
args.gtk.open_url "http://dragonruby.org"
end
end
system
link
Given an OS dependent cli command represented as a string, this function executes the command and puts
the results to the DragonRuby Console (returns nil
).
def tick args
# execute ls on the current directory in 10 seconds
if args.state.tick_count == 600
args.gtk.system "ls ."
end
end
exec
link
Given an OS dependent cli command represented as a string, this function executes the command and returns a string
representing the results.
def tick args
# execute ls on the current directory in 10 seconds
if args.state.tick_count == 600
results = args.gtk.exec "ls ."
puts "The results of the command are:"
puts results
end
end
show_cursor
link
Shows the mouse cursor.
hide_cursor
link
Hides the mouse cursor.
cursor_shown?
link
Returns true
if the mouse cursor is visible.
set_mouse_grab
link
Takes in a numeric parameter representing the mouse grab mode.
0
: Ungrabs the mouse.1
: Grabs the mouse.2
: Hides the cursor, grabs the mouse and puts it in relative position mode accessible viaargs.inputs.mouse.relative_(x|y)
.
set_system_cursor
link
Takes in a string value of "arrow"
, "ibeam"
, "wait"
, or "hand"
and sets the mouse curosor to the corresponding system cursor (if available on the OS).
set_cursor
link
Replaces the mouse cursor with a sprite. Takes in a path
to the sprite, and optionally an x
and y
value representing the realtive positioning the sprite will have to the mouse cursor.
def tick args
if args.state.tick_count == 0
# assumes a sprite of size 80x80 and centers the sprite
# relative to the cursor position.
args.gtk.set_cursor "sprites/square/blue.png", 40, 40
end
end
File IO Functions link
The following functions give you the ability to interact with the file system.
IMPORTANT: File access functions are sandoxed and assume that the dragonruby
binary lives alongside the game you are building. Do not expect these functions to return correct values if you are attempting to run the dragonruby
binary from a shared location. It's recommended that the directory structure contained in the zip is not altered and games are built using that starter template.
list_files
link
This function takes in one parameter. The parameter is the directory path and assumes the the game directory is the root. The method returns an Array
of String
representing all files within the directory. Use stat_file
to determine whether a specific path is a file or a directory.
stat_file
link
This function takes in one parameter. The parameter is the file path and assumes the the game directory is the root. The method returns nil
if the file doesn't exist otherwise it returns a Hash
with the following information:
# {
# path: String,
# file_size: Int,
# mod_time: Int,
# create_time: Int,
# access_time: Int,
# readonly: Boolean,
# file_type: Symbol (:regular, :directory, :symlink, :other),
# }
def tick args
if args.inputs.mouse.click
args.gtk.write_file "last-mouse-click.txt", "Mouse was clicked at #{args.state.tick_count}."
end
file_info = args.gtk.stat_file "last-mouse-click.txt"
if file_info
args.outputs.labels << {
x: 30,
y: 30.from_top,
text: file_info.to_s,
size_enum: -3
}
else
args.outputs.labels << {
x: 30,
y: 30.from_top,
text: "file does not exist, click to create file",
size_enum: -3
}
end
end
read_file
link
Given a file path, a string will be returned representing the contents of the file. nil
will be returned if the file does not exist. You can use stat_file
to get additional information of a file.
write_file
link
This function takes in two parameters. The first parameter is the file path and assumes the the game directory is the root. The second parameter is the string that will be written. The method **overwrites** whatever is currently in the file. Use append_file
to append to the file as opposed to overwriting.
def tick args
if args.inputs.mouse.click
args.gtk.write_file "last-mouse-click.txt", "Mouse was clicked at #{args.state.tick_count}."
end
end
append_file
link
This function takes in two parameters. The first parameter is the file path and assumes the the game directory is the root. The second parameter is the string that will be written. The method appends to whatever is currently in the file (a new file is created if one does not alread exist). Use write_file
to overwrite the file's contents as opposed to appending.
def tick args
if args.inputs.mouse.click
args.gtk.write_file "last-mouse-click.txt", "Mouse was clicked at #{args.state.tick_count}."
end
end
delete_file
link
This function takes in a single parameters. The parameter is the file path that should be deleted. This function will raise an exception if the path requesting to be deleted does not exist.
Notes:
- Use
delete_if_exist
to only delete the file if it exists. - Use
stat_file
to determine if a path exists. - Use
list_files
to determine if a directory is empty. - You cannot delete files outside of your sandboxed game environment.
Here is a list of reasons an exception could be raised:
- If the path is not found. - If the path is still open (for reading or writing). - If the path is not a file or directory. - If the path is a circular symlink. - If you do not have permissions to delete the path. - If the directory attempting to be deleted is not empty.
def tick args
if args.inputs.mouse.click
args.gtk.write_file "last-mouse-click.txt", "Mouse was clicked at #{args.state.tick_count}."
end
end
delete_file_if_exist
link
Has the same behavior as delete_file
except this function does not throw an exception.
XML and JSON link
The following functions help with parsing xml and json.
parse_json
link
Given a json string, this function returns a hash representing the json data.
hash = args.gtk.parse_json '{ "name": "John Doe", "aliases": ["JD"] }'
# structure of hash: { "name"=>"John Doe", "aliases"=>["JD"] }
parse_json_file
link
Same behavior as parse_json_file
except a file path is read for the json string.
parse_xml
link
Given xml data as a string, this function will return a hash that represents the xml data in the following recursive structure:
type: :element,
name: "Person",
children: [...]
parse_xml_file
link
Function has the same behavior as parse_xml
except that the parameter must be a file path that contains xml contents.
Network IO Functions link
The following functions help with interacting with the network.
http_get
link
Returns an object that represents an http response which will eventually have a value. This http_get method is invoked asynchronously. Check for completion before attempting to read results.
def tick args
# perform an http get and print the response when available
args.state.result ||= args.gtk.http_get "https://httpbin.org/html"
if args.state.result && args.state.result[:complete] && !args.state.printed
if args.state.result[:http_response_code] == 200
puts "The response was successful. The body is:"
puts args.state.result[:response_data]
else
puts "The response failed. Status code:"
puts args.state.result[:http_response_code]
end
# set a flag denoting that the response has been printed
args.state.printed = true
# show the console
args.gtk.show_console
end
end
http_post
link
Returns an object that represents an http response which will eventually have a value. This http_post method is invoked asynchronously. Check for completion before attempting to read results.
- First parameter: The url to send the request to.
- Second parameter: Hash that represents form fields to send.
- Third parameter: Headers. Note: Content-Type must be form encoded flavor. If you are unsure of what to pass in, set the content type to application/x-www-form-urlencoded
def tick args
# perform an http get and print the response when available
args.state.form_fields ||= { "userId" => "1701999309" }
args.state.result ||= args.gtk.http_post "http://httpbin.org/post",
args.state.form_fields,
["Content-Type: application/x-www-form-urlencoded"]
if args.state.result && args.state.result[:complete] && !args.state.printed
if args.state.result[:http_response_code] == 200
puts "The response was successful. The body is:"
puts args.state.result[:response_data]
else
puts "The response failed. Status code:"
puts args.state.result[:http_response_code]
end
# set a flag denoting that the response has been printed
args.state.printed = true
# show the console
args.gtk.show_console
end
end
http_post_body
link
Returns an object that represents an http response which will eventually have a value. This http_post_body method is invoked asynchronously. Check for completion before attempting to read results.
- First parameter: The url to send the request to.
- Second parameter: String that represents the body that will be sent
- Third parameter: Headers. Be sure to populate the Content-Type that matches the data you are sending.
def tick args
# perform an http get and print the response when available
args.state.json ||= "{ "userId": "#{Time.now.to_i}"}"
args.state.result ||= args.gtk.http_post_body "http://httpbin.org/post",
args.state.json,
["Content-Type: application/json", "Content-Length: #{args.state.json.length}"]
if args.state.result && args.state.result[:complete] && !args.state.printed
if args.state.result[:http_response_code] == 200
puts "The response was successful. The body is:"
puts args.state.result[:response_data]
else
puts "The response failed. Status code:"
puts args.state.result[:http_response_code]
end
# set a flag denoting that the response has been printed
args.state.printed = true
# show the console
args.gtk.show_console
end
end
start_server!
link
Starts a in-game http server that can be process http requests. When your game is running in development mode. A dev server is started at http://localhost:9001
You can start an in-game http server in production via:
def tick args
# server explicitly enabled in production
args.gtk.start_server! port: 9001, enable_in_prod: true
end
Here's how you would responde to http requests:
def tick args
# server explicitly enabled in production
args.gtk.start_server! port: 9001, enable_in_prod: true
# loop through pending requests and respond to them
args.inputs.http_requests.each do |request|
puts "#{request}"
request.respond 200, "ok"
end
end
Developer Support Functions link
The following functions help support the development process. It is not recommended to use this functions in "production" game logic.
version
link
Returns a string representing the version of DragonRuby you are running.
version_pro?
link
Returns true
if the version of DragonRuby is NOT Standard Edition.
reset
link
Resets DragonRuby's internal state as if it were just started. args.state.tick_count
is set to 0
and args.state
is cleared of any values. This function is helpful when you are developing your game and want to reset everything as if the game just booted up.
def tick args
end
# reset the game if this file is hotloaded/required
# (removes the need to press "r" when I file is updated)
$gtk.reset
Resetting iVars (advanced)
NOTE: args.gtk.reset
does not reset global variables or instance of classes you have have constructed. If you want to also reset global variables or instances of classes when $gtk.reset is called. Define a reset
method. Here's an example:
class Game
def initialize
puts "Game initialize called"
end
end
def tick args
$game ||= Game.new
if args.state.tick_count == 0
puts "tick_count is 0"
end
# if r is pressed on the keyboard, reset the game
if args.inputs.keyboard.key_down.r
args.gtk.reset
end
end
# custom reset function
def reset
puts "Custom reset function was called."
$game = nil
end
seed
and RNG (advanced)
Optionally, $gtk.reset
(args.gtk.reset
) can take in a named parameter for RNG called seed:
. Passing in seed:
will reset RNG so that rand
returns a repeatable set of random numbers. This seed
value is initialized with the start time of your game ($gtk.started_at
). Having this option is is helpful for replays and unit tests.
Don't worry about this capability if you aren't using DragonRuby's unit testing, or replay capabilities.
Here is the behavior of $gtk.reset
when given a seed:
- RNG is seeded initially with the
Time
value of the launch of your game (retrievable via$gtk.started_at
). - Calling $gtk.reset will reset your game and re-initialize your RNG with this initial seed value.
- Calling $gtk.reset with a
:seed
parameter will update the seed value for the current and subsequent resets. - You can get the value used to seed RNG via
$gtk.seed
. - You can set your RNG seed back to its original value by using
$gtk.started_at
.
def tick args
if args.state.tick_count == 0
puts rand
puts rand
puts rand
puts rand
end
end
puts "Started at (RNG seed inital value)"
puts $gtk.started_at # Time as an integer that your game was started at
puts "Seed value that will be used on reset"
puts $gtk.seed # current value that RNG was seeded with
# reset the game and use the last seed to reset RNG
$gtk.reset
# === OR ===
# sets the seed value to predefined value
# subsequent resets will use the new predefined value
# $gtk.reset seed: 100
# (or shorthand)
# $gtk.reset 100
# sets the seed back to its original value
# $gtk.reset seed: $gtk.started_at
If you want to set RNG without resetting your game state, you can use $gtk.set_rng VALUE
.
reset_next_tick
link
Has the same behavior as reset
except the reset occurs before tick
is executed again. reset
resets the environment immediately (while the tick
method is inflight). It's recommended that reset
should be called outside of the tick method (invoked when a file is saved/hotloaded), and reset_next_tick
be used inside of the tick
method so you don't accidentally blow away state the your game depends on to complete the current tick
without exceptions.
def tick args
# reset the game if "r" is pressed on the keyboard
if args.inputs.keyboard.key_down.r
args.gtk.reset_next_tick # use reset_next_tick instead of reset
end
end
# reset the game if this file is hotloaded/required
# (removes the need to press "r" when I file is updated)
$gtk.reset
reset_sprite
link
Sprites when loaded are cached. Given a string parameter, this method invalidates the cache record of a sprite so that updates on from the disk can be loaded.
reset_sprites
link
Sprites when loaded are cached. This method invalidates the cache record of all sprites so that updates on from the disk can be loaded. This function is automatically called when args.gtk.reset
($gtk.reset
) is invoked.
calcspritebox
link
Given a path to a sprite, this method returns the width
and height
of a sprite as a tuple.
NOTE: This method should be used for development purposes only and is expensive to call every frame. Do not use this method to set the size of sprite when rendering (hard code those values since you know what they are beforehand).
current_framerate
link
Returns a float value representing the framerate of your game. This is an approximation/moving average of your framerate and should eventually settle to 60fps.
def tick args
# render a label to the screen that shows the current framerate
# formatted as a floating point number with two decimal places
args.outputs.labels << { x: 30, y: 30.from_top, text: "#{args.gtk.current_framerate.to_sf}" }
end
framerate_diagnostics_primitives
link
Returns a set of primitives that can be rendered to the screen which provide more detailed information about the speed of your simulation (framerate, draw call count, mouse position, etc).
def tick args
args.outputs.primitives << args.gtk.framerate_diagnostics_primitives
end
warn_array_primitives!
link
This function helps you audit your game of usages of array-based primitives. While array-based primitives are simple to create and use, they are slower to process than Hash
or Class
based primitives.
def tick args
# enable array based primitives warnings
args.gtk.warn_array_primitives!
# array-based primitive elsewhere in code
# an log message will be posted giving the location of the array
# based primitive usage
args.outputs.sprites << [100, 100, 200, 200, "sprites/square/blue.png"]
# instead of using array based primitives, migrate to hashes as needed
args.outputs.sprites << {
x: 100,
y: 100,
w: 200,
h: 200, path:
"sprites/square/blue.png"
}
end
benchmark
link
You can use this function to compare the relative performance of blocks of code.
def tick args
# press r to run benchmark
if args.inputs.keyboard.key_down.r
args.gtk.console.show
args.gtk.benchmark iterations: 1000, # number of iterations
# label for experiment
using_numeric_map: -> () {
# experiment body
v = 100.map_with_index do |i|
i * 100
end
},
# label for experiment
using_numeric_times: -> () {
# experiment body
v = []
100.times do |i|
v << i * 100
end
}
end
end
notify!
link
Given a string, this function will present a message at the bottom of your game. This method is only invoked in dev mode and is useful for debugging.
An optional parameter of duration (number value representing ticks) can also be passed in. The default value if 300
ticks (5 seconds).
def tick args
if args.inputs.mouse.click
args.gtk.notify! "Mouse was clicked!"
end
if args.inputs.keyboard.key_down.r
# optional duration parameter
args.gtk.notify! "R key was pressed!", 600 # present message for 10 seconds/600 frames
end
end
notify_extended!
link
Has similar behavior as notify! except you have additional options to show messages in a production environment.
def tick args
if args.inputs.mouse.click
args.gtk.notify_extended! message: "message",
duration: 300,
env: :prod
end
end
slowmo!
link
Given a numeric value representing the factor of 60fps. This function will bring your simulation loop down to slower rate. This method is intended to be used for debugging purposes.
def tick args
# set your simulation speed to (15 fps): args.gtk.slowmo! 4
# set your simulation speed to (1 fps): args.gtk.slowmo! 60
# set your simulation speed to (30 fps):
args.gtk.slowmo! 2
end
Remove this line from your tick method will automatically set your simulation speed back to 60 fps.
show_console
link
Shows the DragonRuby console. Useful when debugging/customizing an in-game dev workflow.
hide_console
link
Shows the DragonRuby console. Useful when debugging/customizing an in-game dev workflow.
enable_console
link
Enables the DragonRuby Console so that it can be presented by pressing the tilde key (the key next to the number 1 key).
disable_console
link
Disables the DragonRuby Console so that it won't show up even if you press the tilde key or call args.gtk.show_console
.
start_recording
link
Resets the game to tick 0
and starts recording gameplay. Useful for visual regression tests/verification.
stop_recording
link
Function takes in a destination file for the currently recording gameplay. This file can be used to replay a recording.
cancel_recording
link
Function cancels a gameplay recording session and discards the replay.
start_replay
link
Given a file that represents a recording, this method will run the recording against the current codebase.
You can start a replay from the command line also:
# first argument: the game directory
# --replay switch is the file path relative to the game directory
# --speed switch is optional. a value of 4 will run the replay and game at 4x speed
# cli command example is in the context of Linux and Mac, for Windows the binary would be ./dragonruby.exe
./dragonruby ./mygame --replay ./replay.txt --speed 4
stop_replay
link
Function stops a replay that is currently executing.
get_base_dir
link
Returns the path to the location of the dragonruby binary. In production mode, this value will be the same as the value returned by get_game_dir
. Function should only be used for debugging/development workflows.
get_game_dir
link
Returns the location within sandbox storage that the game is running. When developing your game, this value will be your mygame
directory. In production, it'll return a value that is OS specific (eg the Roaming directory on Windows or the Application Support directory on Mac).
Invocations of ~(write|append)_file will write to this sandboxed directory.
get_game_dir_url
link
Returns a url encoded string representing the sandbox location for game data.
open_game_dir
link
Opens the game directory in the OS's file explorer. This should be used for debugging purposes only.
write_file_root
link
Given a file path and contents, the contents will be written to a directory outside of the game directory. This method should be used for development purposes only. In production this method will write to the same sandboxed location as write_file
.
append_file_root
link
Has the same behavior as write_file_root
except that it appends the contents as opposed to overwriting them.
argv
link
Returns a string representing the command line arguments passed to the DragonRuby binary. This should be used for development/debugging purposes only.
cli_arguments
link
Returns a Hash
for command line arguments in the format of --switch value
(two hyphens preceding the switch flag with the value seperated by a space). This should be used for development/debugging purposes only.
download_stb_rb(_raw)
link
These two functions can help facilitate the integration of external code files. OSS contributors are encouraged to create libraries that all fit in one file (lowering the barrier to entry for adoption).
Examples:
def tick args
end
# option 1:
# source code will be downloaded from the specified GitHub url, and saved locally with a
# predefined folder convension.
$gtk.download_stb_rb "https://github.com/xenobrain/ruby_vectormath/blob/main/vectormath_2d.rb"
# option 2:
# source code will be downloaded from the specified GitHub username, repository, and file.
# code will be saved locally with a predefined folder convension.
$gtk.download_stb_rb "xenobrain", "ruby_vectormath", "vectormath_2d.rb"
# option 3:
# source code will be downloaded from a direct/raw url and saved to a direct/raw local path.
$gtk.download_stb_rb_raw "https://raw.githubusercontent.com/xenobrain/ruby_vectormath/main/vectormath_2d.rb",
"lib/xenobrain/ruby_vectionmath/vectormath_2d.rb"
reload_history
link
Returns a Hash
representing the code files that have be loaded for your game along with timings for the events. This should be used for development/debugging purposes only.
reload_history_pending
link
Returns a Hash
for files that have been queued for reload, but haven't been processed yet. This should be used for development/debugging purposes only.
reload_if_needed
link
Given a file name, this function will queue the file for reload if it's been modified. An optional second parameter can be passed in to signify if the file should be forced loaded regardless of modified time (true
means to force load, false
means to load only if the file has been modified). This function should be used for development/debugging purposes only.
State (args.state
) link
Store your game state inside of this state
. Properties with arbitrary nesting is allowed and a backing Entity will be created on your behalf.
def tick args
args.state.player.x ||= 0
args.state.player.y ||= 0
end
entity_id
link
Entities automatically receive an entity_id
of type Fixnum
.
entity_type
link
Entities can have an entity_type
which is represented as a Symbol
.
created_at
link
Entities have created_at
set to args.state.tick_count
when they are created.
created_at_elapsed
link
Returns the elapsed number of ticks since creation.
global_created_at
link
Entities have global_created_at
set to Kernel.global_tick_count
when they are created.
global_created_at_elapsed
link
Returns the elapsed number of global ticks since creation.
as_hash
link
Entity cast to a Hash
so you can update values as if you were updating a Hash
.
new_entity
link
Creates a new Entity with a type
, and initial properties. An option block can be passed to change the newly created entity:
def tick args
args.state.player ||= args.state.new_entity :player, x: 0, y: 0 do |e|
e.max_hp = 100
e.hp = e.max_hp * rand
end
end
new_entity_strict
link
Creates a new Strict Entity. While Entities created via args.state.new_entity
can have new properties added later on, Entities created using args.state.new_entity_strict
must define all properties that are allowed during its initialization. Attempting to add new properties after initialization will result in an exception.
args.state.tick_count
link
Returns the current tick of the game. args.state.tick_count
is 0
when the game is first started or if the game is reset via $gtk.reset
.
Geometry (args.geometry
) link
The Geometry module
contains methods for calculations that are frequently used in game development.
The following functions of Geometry
are mixed into Hash
, Array
, and DragonRuby's Entity
class:
intersect_rect?
inside_rect?
scale_rect
angle_to
angle_from
point_inside_circle?
center_inside_rect
center_inside_rect_x
center_inside_rect_y
anchor_rect
rect_center_point
You can invoke the functions above using either the mixin variant or the module variant. Example:
def tick args
# define to rectangles
rect_1 = { x: 0, y: 0, w: 100, h: 100 }
rect_2 = { x: 50, y: 50, w: 100, h: 100 }
# mixin variant
# call geometry method function from instance of a Hash class
puts rect_1.intersect_rect?(rect_2)
# OR
# module variants
puts args.geometry.intersect_rect?(rect_1, rect_2)
puts Geometry.intersect_rect?(rect_1, rect_2)
end
intersect_rect?
link
Invocation variants:
instance.intersect_rect?(other, tolerance)
args.geometry.intersect_rect?(rect_1, rect_2, tolerance)
args.inputs.mouse.intersect_rect?(other, tolerance)
Given two rectangle primitives this function will return true
or false
depending on if the two rectangles intersect or not. An optional final parameter can be passed in representing the tolerence
of overlap needed to be considered a true intersection. The default value of tolerance
is 0.1
which keeps the function from returning true if only the edges of the rectangles overlap.
:anchor_x
, and anchor_y
is taken into consideration if the objects respond to these methods.
Here is an example where one rectangle is stationary, and another rectangle is controlled using directional input. The rectangles change color from blue to read if they intersect.
def tick args
# define a rectangle in state and position it
# at the center of the screen with a color of blue
args.state.box_1 ||= {
x: 640 - 20,
y: 360 - 20,
w: 40,
h: 40,
r: 0,
g: 0,
b: 255
}
# create another rectangle in state and position it
# at the far left center
args.state.box_2 ||= {
x: 0,
y: 360 - 20,
w: 40,
h: 40,
r: 0,
g: 0,
b: 255
}
# take the directional input and use that to move the second rectangle around
# increase or decrease the x value based on if left or right is held
args.state.box_2.x += args.inputs.left_right * 5
# increase or decrease the y value based on if up or down is held
args.state.box_2.y += args.inputs.up_down * 5
# change the colors of the rectangles based on whether they
# intersect or not
if args.state.box_1.intersect_rect? args.state.box_2
args.state.box_1.r = 255
args.state.box_1.g = 0
args.state.box_1.b = 0
args.state.box_2.r = 255
args.state.box_2.g = 0
args.state.box_2.b = 0
else
args.state.box_1.r = 0
args.state.box_1.g = 0
args.state.box_1.b = 255
args.state.box_2.r = 0
args.state.box_2.g = 0
args.state.box_2.b = 255
end
# render the rectangles as border primitives on the screen
args.outputs.borders << args.state.box_1
args.outputs.borders << args.state.box_2
end
inside_rect?
link
Invocation variants:
instance.inside_rect?(other)
args.geometry.inside_rect?(rect_1, rect_2)
Given two rectangle primitives this function will return true
or false
depending on if the first rectangle (or self
) is inside of the second rectangle.
Here is an example where one rectangle is stationary, and another rectangle is controlled using directional input. The rectangles change color from blue to read if the movable rectangle is entirely inside the stationary rectangle.
:anchor_x
, and anchor_y
is taken into consideration if the objects respond to these methods.
def tick args
# define a rectangle in state and position it
# at the center of the screen with a color of blue
args.state.box_1 ||= {
x: 640 - 40,
y: 360 - 40,
w: 80,
h: 80,
r: 0,
g: 0,
b: 255
}
# create another rectangle in state and position it
# at the far left center
args.state.box_2 ||= {
x: 0,
y: 360 - 10,
w: 20,
h: 20,
r: 0,
g: 0,
b: 255
}
# take the directional input and use that to move the second rectangle around
# increase or decrease the x value based on if left or right is held
args.state.box_2.x += args.inputs.left_right * 5
# increase or decrease the y value based on if up or down is held
args.state.box_2.y += args.inputs.up_down * 5
# change the colors of the rectangles based on whether they
# intersect or not
if args.state.box_2.inside_rect? args.state.box_1
args.state.box_1.r = 255
args.state.box_1.g = 0
args.state.box_1.b = 0
args.state.box_2.r = 255
args.state.box_2.g = 0
args.state.box_2.b = 0
else
args.state.box_1.r = 0
args.state.box_1.g = 0
args.state.box_1.b = 255
args.state.box_2.r = 0
args.state.box_2.g = 0
args.state.box_2.b = 255
end
# render the rectangles as border primitives on the screen
args.outputs.borders << args.state.box_1
args.outputs.borders << args.state.box_2
end
scale_rect
link
Given a Rectangle
this function returns a new rectangle with a scaled size.
ratio
: the ratio by which to scale the rect. A ratio of 2 will double the dimensions of the rect while a ratio of 0.5 will halve its dimensions.anchor_x
andanchor_y
specify the point within the rect from which to resize it. Setting both to 0 will affect the width and height of the rect, leaving x and y unchanged. Setting both to 0.5 will scale all sides of the rect proportionally from the center.
def tick args
# a rect at the center of the screen
args.state.rect_1 ||= { x: 640 - 20, y: 360 - 20, w: 40, h: 40 }
# render the rect
args.outputs.borders << args.state.rect_1
# the rect half the size with the x and y position unchanged
args.outputs.borders << args.state.rect_1.scale_rect(0.5)
# the rect double the size, repositioned in the center given anchor optional arguments
args.outputs.borders << args.state.rect_1.scale_rect(2, 0.5, 0.5)
end
scale_rect_extended
link
The behavior is similar to scale_rect
except that you can independently control the scale of each axis. The parameters are all named:
percentage_x
: percentage to change the width (default value of 1.0)percentage_y
: percentage to change the height (default value of 1.0)anchor_x
: anchor repositioning of x (default value of 0.0)anchor_y
: anchor repositioning of y (default value of 0.0)
def tick args
baseline_rect = { x: 640 - 20, y: 360 - 20, w: 40, h: 40 }
args.state.rect_1 ||= baseline_rect
args.state.rect_2 ||= baseline_rect.scale_rect_extended(percentage_x: 2,
percentage_y: 0.5,
anchor_x: 0.5,
anchor_y: 1.0)
args.outputs.borders << args.state.rect_1
args.outputs.borders << args.state.rect_2
end
anchor_rect
link
Returns a new rect that is anchored by an anchor_x
and anchor_y
value. The width and height of the rectangle is taken into consideration when determining the anchor position:
def tick args
args.state.rect ||= {
x: 640,
y: 360,
w: 100,
h: 100
}
# rect's center: 640 + 50, 360 + 50
args.outputs.borders << args.state.rect.anchor_rect(0, 0)
# rect's center: 640, 360
args.outputs.borders << args.state.rect.anchor_rect(0.5, 0.5)
# rect's center: 640, 360
args.outputs.borders << args.state.rect.anchor_rect(0.5, 0)
end
angle_from
link
Invocation variants:
args.geometry.angle_from start_point, end_point
start_point.angle_from end_point
Returns an angle in degrees from the end_point
to the start_point
(if you want the value in radians, you can call .to_radians
on the value returned):
def tick args
rect_1 ||= {
x: 0,
y: 0,
}
rect_2 ||= {
x: 100,
y: 100,
}
angle = rect_1.angle_from rect_2 # returns 225 degrees
angle_radians = angle.to_radians
args.outputs.labels << { x: 30, y: 30.from_top, text: "#{angle}, #{angle_radians}" }
angle = args.geometry.angle_from rect_1, rect_2 # returns 225 degrees
angle_radians = angle.to_radians
args.outputs.labels << { x: 30, y: 60.from_top, text: "#{angle}, #{angle_radians}" }
end
angle_to
link
Invocation variants:
args.geometry.angle_to start_point, end_point
start_point.angle_to end_point
Returns an angle in degrees to the end_point
from the start_point
(if you want the value in radians, you can call .to_radians
on the value returned):
def tick args
rect_1 ||= {
x: 0,
y: 0,
}
rect_2 ||= {
x: 100,
y: 100,
}
angle = rect_1.angle_to rect_2 # returns 45 degrees
angle_radians = angle.to_radians
args.outputs.labels << { x: 30, y: 30.from_top, text: "#{angle}, #{angle_radians}" }
angle = args.geometry.angle_to rect_1, rect_2 # returns 45 degrees
angle_radians = angle.to_radians
args.outputs.labels << { x: 30, y: 60.from_top, text: "#{angle}, #{angle_radians}" }
end
distance
link
Returns the distance between two points;
def tick args
rect_1 ||= {
x: 0,
y: 0,
}
rect_2 ||= {
x: 100,
y: 100,
}
distance = args.geometry.distance rect_1, rect_2
args.outputs.labels << {
x: 30,
y: 30.from_top,
text: "#{distance}"
}
args.outputs.lines << {
x: rect_1.x,
y: rect_1.y,
x2: rect_2.x,
y2: rect_2.y
}
end
point_inside_circle?
link
Invocation variants:
point_1.point_inside_circle? circle_center, circle_radius
args.geometry.point_inside_circle? point_1, circle_center, circle_radius
Returns true
if a point is inside of a circle defined as a center point and radius.
def tick args
# define circle center
args.state.circle_center ||= {
x: 640,
y: 360
}
# define circle radius
args.state.circle_radius ||= 100
# define point
args.state.point_1 ||= {
x: 100,
y: 100
}
# allow point to be moved using keyboard
args.state.point_1.x += args.inputs.left_right * 5
args.state.point_1.y += args.inputs.up_down * 5
# determine if point is inside of circle
intersection = args.geometry.point_inside_circle? args.state.point_1,
args.state.circle_center,
args.state.circle_radius
# render point as a square
args.outputs.sprites << {
x: args.state.point_1.x - 20,
y: args.state.point_1.y - 20,
w: 40,
h: 40,
path: "sprites/square/blue.png"
}
# if there is an intersection, render a red circle
# otherwise render a blue circle
if intersection
args.outputs.sprites << {
x: args.state.circle_center.x - args.state.circle_radius,
y: args.state.circle_center.y - args.state.circle_radius,
w: args.state.circle_radius * 2,
h: args.state.circle_radius * 2,
path: "sprites/circle/red.png",
a: 128
}
else
args.outputs.sprites << {
x: args.state.circle_center.x - args.state.circle_radius,
y: args.state.circle_center.y - args.state.circle_radius,
w: args.state.circle_radius * 2,
h: args.state.circle_radius * 2,
path: "sprites/circle/blue.png",
a: 128
}
end
end
center_inside_rect
link
Invocation variants:
target_rect.center_inside_rect reference_rect
args.geometry.center_inside_rect target_rect, reference_rect
Given a target rect and a reference rect, the target rect is centered inside the reference rect (a new rect is returned).
def tick args
rect_1 = {
x: 0,
y: 0,
w: 100,
h: 100
}
rect_2 = {
x: 640 - 100,
y: 360 - 100,
w: 200,
h: 200
}
centered_rect = args.geometry.center_inside_rect rect_1, rect_2
# OR
# centered_rect = rect_1.center_inside_rect rect_2
args.outputs.solids << rect_1.merge(r: 255)
args.outputs.solids << rect_2.merge(b: 255)
args.outputs.solids << centered_rect.merge(g: 255)
end
ray_test
link
Given a point and a line, ray_test
returns one of the following symbols based on the location of the point relative to the line: :left
, :right
, :on
def tick args
# create a point based off of the mouse location
point = {
x: args.inputs.mouse.x,
y: args.inputs.mouse.y
}
# draw a line from the bottom left to the top right
line = {
x: 0,
y: 0,
x2: 1280,
y2: 720
}
# perform ray_test on point and line
ray = args.geometry.ray_test point, line
# output the results of ray test at mouse location
args.outputs.labels << {
x: point.x,
y: point.y + 25,
text: "#{ray}",
alignment_enum: 1,
vertical_alignment_enum: 1,
}
# render line
args.outputs.lines << line
# render point
args.outputs.solids << {
x: point.x - 5,
y: point.y - 5,
w: 10,
h: 10
}
end
line_rise_run
link
Given a line, this function returns a Hash with x
and y
keys representing a normalized representation of the rise and run of the line.
def tick args
# draw a line from the bottom left to the top right
line = {
x: 0,
y: 0,
x2: 1280,
y2: 720
}
# get rise and run of line
rise_run = args.geometry.line_rise_run line
# output the rise and run of line
args.outputs.labels << {
x: 640,
y: 360,
text: "#{rise_run}",
alignment_enum: 1,
vertical_alignment_enum: 1,
}
# render the line
args.outputs.lines << line
end
rotate_point
link
Given a point and an angle in degrees, a new point is returned that is rotated around the origin by the degrees amount. An optional third argument can be provided to rotate the angle around a point other than the origin.
def tick args
args.state.rotate_amount ||= 0
args.state.rotate_amount += 1
if args.state.rotate_amount >= 360
args.state.rotate_amount = 0
end
point_1 = {
x: 100,
y: 100
}
# rotate point around 0, 0
rotated_point_1 = args.geometry.rotate_point point_1,
args.state.rotate_amount
args.outputs.solids << {
x: rotated_point_1.x - 5,
y: rotated_point_1.y - 5,
w: 10,
h: 10
}
point_2 = {
x: 640 + 100,
y: 360 + 100
}
# rotate point around center screen
rotated_point_2 = args.geometry.rotate_point point_2,
args.state.rotate_amount,
x: 640, y: 360
args.outputs.solids << {
x: rotated_point_2.x - 5,
y: rotated_point_2.y - 5,
w: 10,
h: 10
}
end
find_intersect_rect
link
Given a rect and a collection of rects, find_intersect_rect
returns the first rect that intersects with the the first parameter.
:anchor_x
, and anchor_y
is taken into consideration if the objects respond to these methods.
If you find yourself doing this:
collision = args.state.terrain.find { |t| t.intersect_rect? args.state.player }
Consider using find_intersect_rect
instead (it's more descriptive and faster):
collision = args.geometry.find_intersect_rect args.state.player, args.state.terrain
find_all_intersect_rect
link
Given a rect and a collection of rects, find_all_intersect_rect
returns all rects that intersects with the the first parameter.
:anchor_x
, and anchor_y
is taken into consideration if the objects respond to these methods.
If you find yourself doing this:
collisions = args.state.terrain.find_all { |t| t.intersect_rect? args.state.player }
Consider using find_all_intersect_rect
instead (it's more descriptive and faster):
collisions = args.geometry.find_all_intersect_rect args.state.player, args.state.terrain
find_intersect_rect_quad_tree
link
This is a faster collision algorithm for determining if a rectangle intersects any rectangle in an array. In order to use find_intersect_rect_quad_tree
, you must first generate a quad tree data structure using create_quad_tree
. Use this function if find_intersect_rect
isn't fast enough.
def tick args
# create a player
args.state.player ||= {
x: 640 - 10,
y: 360 - 10,
w: 20,
h: 20
}
# allow control of player movement using arrow keys
args.state.player.x += args.inputs.left_right * 5
args.state.player.y += args.inputs.up_down * 5
# generate 40 random rectangles
args.state.boxes ||= 40.map do
{
x: 1180 * rand + 50,
y: 620 * rand + 50,
w: 100,
h: 100
}
end
# generate a quad tree based off of rectangles.
# the quad tree should only be generated once for
# a given array of rectangles. if the rectangles
# change, then the quad tree must be regenerated
args.state.quad_tree ||= args.geometry.quad_tree_create args.state.boxes
# use quad tree and find_intersect_rect_quad_tree to determine collision with player
collision = args.geometry.find_intersect_rect_quad_tree args.state.player,
args.state.quad_tree
# if there is a collision render a red box
if collision
args.outputs.solids << collision.merge(r: 255)
end
# render player as green
args.outputs.solids << args.state.player.merge(g: 255)
# render boxes as borders
args.outputs.borders << args.state.boxes
end
find_all_intersect_rect_quad_tree
link
This is a faster collision algorithm for determining if a rectangle intersects other rectangles in an array. In order to use find_all_intersect_rect_quad_tree
, you must first generate a quad tree data structure using create_quad_tree
. Use this function if find_all_intersect_rect
isn't fast enough.
def tick args
# create a player
args.state.player ||= {
x: 640 - 10,
y: 360 - 10,
w: 20,
h: 20
}
# allow control of player movement using arrow keys
args.state.player.x += args.inputs.left_right * 5
args.state.player.y += args.inputs.up_down * 5
# generate 40 random rectangles
args.state.boxes ||= 40.map do
{
x: 1180 * rand + 50,
y: 620 * rand + 50,
w: 100,
h: 100
}
end
# generate a quad tree based off of rectangles.
# the quad tree should only be generated once for
# a given array of rectangles. if the rectangles
# change, then the quad tree must be regenerated
args.state.quad_tree ||= args.geometry.quad_tree_create args.state.boxes
# use quad tree and find_intersect_rect_quad_tree to determine collision with player
collisions = args.geometry.find_all_intersect_rect_quad_tree args.state.player,
args.state.quad_tree
# if there is a collision render a red box
args.outputs.solids << collisions.map { |c| c.merge(r: 255) }
# render player as green
args.outputs.solids << args.state.player.merge(g: 255)
# render boxes as borders
args.outputs.borders << args.state.boxes
end
create_quad_tree
link
Generates a quad tree from an array of rectangles. See find_intersect_rect_quad_tree
for usage.
Audio (args.audio
) link
Hash that contains audio sources that are playing.
Sounds that don't specify looping: true
will be removed automatically from the hash after the playback ends. Looping sounds or sounds that should stop early must be removed manually.
When you assign a hash to an audio output, a :length
key will be added to the hash on the following tick. This will tell you the duration of the audio file in seconds (float).
volume
link
You can globally control the volume for all audio using args.audio.volume
. Example:
def tick args
if args.inputs.down
args.audio.volume -= 0.01
elsif args.inputs.up
args.audio.volume += 0.01
end
end
One-Time Sounds link
Here's how to play audio one-time (does not loop).
def tick args
# play a one-time non-looping sound every second
if (args.state.tick_count % 60) == 0
args.audio[:coin] = { input: "sounds/coin.wav" }
# OR
args.outputs.sounds << "sounds/coin.wav"
end
end
Looping Audio link
Here's how to play audio that loops (eg background music), and how to stop the sound.
def tick args
if args.state.tick_count == 0
args.audio[:bg_music] = { input: "sounds/bg-music.ogg", looping: true }
end
# stop sound if space key is pressed
if args.inputs.keyboard.key_down.space
args.audio[:bg_music] = nil
# OR
args.audio.delete :bg_music
end
end
Setting Additional Audio Properties link
Here are additional properties that can be set.
def tick args
# The values below (except for input of course) are the default values that apply if you don't
# specify the value in the hash.
args.audio[:my_audio] ||= {
input: 'sound/boom.wav', # file path relative to mygame directory
gain: 1.0, # Volume (float value 0.0 to 1.0)
pitch: 1.0, # Pitch of the sound (1.0 = original pitch)
paused: false, # Set to true to pause the sound at the current playback position
looping: true, # Set to true to loop the sound/music until you stop it
foobar: :baz, # additional keys/values can be safely added to help with context/game logic (ie metadata)
x: 0.0, y: 0.0, z: 0.0 # Relative position to the listener, x, y, z from -1.0 to 1.0
}
end
IMPORTANT: Please take note that gain
and pitch
must be given float
values (eg gain: 1.0
, not gain: 1
or game: 0
).
Advanced Audio Manipulation (Crossfade) link
Take a look at the Audio Mixer sample app for a non-trival example of how to use these properties. The sample app is located within the DragonRuby zip file at ./samples/07_advanced_audio/01_audio_mixer
.
Here's an example of crossfading two bg music tracks.
def tick args
# start bg-1.ogg at the start
if args.state.tick_count == 0
args.audio[:bg_music] = { input: "sounds/bg-1.ogg", looping: true, gain: 0.0 }
end
# if space is pressed cross fade to new bg music
if args.inputs.keyboard.key_down.space
# get the current bg music and create a new audio entry that represents the crossfade
current_bg_music = args.audio[:bg_music]
# cross fade audio entry
args.audio[:bg_music_fade] = {
input: current_bg_music[:input],
looping: true,
gain: current_bg_music[:gain],
pitch: current_bg_music[:pitch],
paused: false,
playtime: current_bg_music[:playtime]
}
# replace the current playing background music (toggling between bg-1.ogg and bg-2.ogg)
# set the gain/volume to 0.0 (this will be increased to 1.0 accross ticks)
new_background_music = { looping: true, gain: 0.0 }
# determine track to play (swap between bg-1 and bg-2)
new_background_music[:input] = if current_bg_music.input == "sounds/bg-1.ogg"
"sounds/bg-2.ogg"
else
"sounds/bg-2.ogg"
end
# bg music audio entry
args.audio[:bg_music] = new_background_music
end
# process cross fade (happens every tick)
# increase the volume of bg_music every tick until it's at 100%
if args.audio[:bg_music] && args.audio[:bg_music].gain < 1.0
# increase the gain 1% every tick until we are at 100%
args.audio[:bg_music].gain += 0.01
# clamp value to 1.0 max value
args.audio[:bg_music].gain = 1.0 if args.audio[:bg_music].gain > 1.0
end
# decrease the volume of cross fade bg music until it's 0.0, then delete it
if args.audio[:bg_music_fade] && args.audio[:bg_music_fade].gain > 0.0
# decrease by 1% every frame
args.audio[:bg_music_fade].gain -= 0.01
# delete audio when it's at 0%
if args.audio[:bg_music_fade].gain <= 0.0
args.audio[:bg_music_fade] = nil
end
end
end
Audio encoding trouble shooting link
If audio doesn't seem to be working, try re-encoding it via ffmpeg
:
# re-encode ogg
ffmpeg -i ./mygame/sounds/SOUND.ogg -ac 2 -b:a 160k -ar 44100 -acodec libvorbis ./mygame/sounds/SOUND-converted.ogg
# convert wav to ogg
ffmpeg -i ./mygame/sounds/SOUND.wav -ac 2 -b:a 160k -ar 44100 -acodec libvorbis ./mygame/sounds/SOUND-converted.ogg
Audio synthesis link
Instead of a path to an audio file you can specify an array [channels, sample_rate, sound_source]
for input
to procedurally generate sound. You do this by providing an array of float values between -1.0 and 1.0 that describe the waveform you want to play.
channels
is the number of channels: 1 = mono, 2 = stereosample_rate
is the number of values per seconds you will provide to describe the audio wavesound_source
The source of your sound. See below
Sound source link
A sound source can be one of two things:
- A
Proc
object that is called on demand to generate the next samples to play. Every call should generate enough samples for at least 0.1 to 0.5 seconds to get continuous playback without audio skips. The audio will continue playing endlessly until removed, so thelooping
option will have no effect. - An array of sample values that will be played back once. This is useful for procedurally generated one-off SFX.
looping
will work as expected
When you specify 2 for channels
, then the generated sample array will be played back in an interleaved manner. The first element is the first sample for the left channel, the second element is the first sample for the right channel, the third element is the second sample for the left channel etc.
Example: link
def tick args
sample_rate = 48000
generate_sine_wave = lambda do
frequency = 440.0 # A5
samples_per_period = (sample_rate / frequency).ceil
one_period = samples_per_period.map_with_index { |i|
Math.sin((2 * Math::PI) * (i / samples_per_period))
}
one_period * frequency # Generate 1 second worth of sound
end
args.audio[:my_audio] ||= {
input: [1, sample_rate, generate_sine_wave]
}
end
Easing (args.easing
) link
A set of functions that allow you to determine the current progression of an easing function.
ease
link
This function will give you a float value between 0
and 1
that represents a percentage. You need to give the funcation a start_tick
, current_tick
, duration, and easing definitions
.
This YouTube video is a fantastic introduction to easing functions: https://www.youtube.com/watch?v=mr5xkf6zSzk
Examples link
This example shows how to fade in a label at frame 60 over two seconds (120 ticks). The :identity
definition implies a linear fade: f(x) -> x
.
def tick args
fade_in_at = 60
current_tick = args.state.tick_count
duration = 120
percentage = args.easing.ease fade_in_at,
current_tick,
duration,
:identity
alpha = 255 * percentage
args.outputs.labels << { x: 640,
y: 320, text: "#{percentage.to_sf}",
alignment_enum: 1,
a: alpha }
end
This example will move a box at a linear speed from 0 to 1280.
def tick args
start_time = 10
duration = 60
current_progress = args.easing.ease start_time,
args.state.tick_count,
duration,
:identity
args.outputs.solids << { x: 1280 * current_progress, y: 360, w: 10, h: 10 }
end
Easing Definitions link
There are a number of easing definitions availble to you:
:identity
The easing definition for :identity
is f(x) = x
. For example, if start_tick
is 0
, current_tick
is 50
, and duration
is 100
, then args.easing.ease 0, 50, 100, :identity
will return 0.5
(since tick 50
is half way between 0
and 100
).
:flip
The easing definition for :flip
is f(x) = 1 - x
. For example, if start_tick
is 0
, current_tick
is 10
, and duration
is 100
, then args.easing.ease 0, 10, 100, :flip
will return 0.9
(since tick 10
means 100% - 10%).
:quad
, :cube
, :quart
, :quint
These are the power easing definitions. :quad
is f(x) = x * x
(x
squared), :cube
is f(x) = x * x * x
(x
cubed), etc.
The power easing definitions represent Smooth Start easing (the percentage changes slow at first and speeds up at the end).
Example
Here is an example of Smooth Start (the percentage changes slow at first and speeds up at the end).
def tick args
start_tick = 60
current_tick = args.state.tick_count
duration = 120
percentage = args.easing.ease start_tick,
current_tick,
duration,
:quad
start_x = 100
end_x = 1180
distance_x = end_x - start_x
final_x = start_x + (distance_x * percentage)
start_y = 100
end_y = 620
distance_y = end_y - start_y
final_y = start_y + (distance_y * percentage)
args.outputs.labels << { x: final_x,
y: final_y,
text: "#{percentage.to_sf}",
alignment_enum: 1 }
end
Combining Easing Definitions
The base easing definitions can be combined to create common easing functions.
Example
Here is an example of Smooth Stop (the percentage changes fast at first and slows down at the end).
def tick args
start_tick = 60
current_tick = args.state.tick_count
duration = 120
# :flip, :quad, :flip is Smooth Stop
percentage = args.easing.ease start_tick,
current_tick,
duration,
:flip, :quad, :flip
start_x = 100
end_x = 1180
distance_x = end_x - start_x
final_x = start_x + (distance_x * percentage)
start_y = 100
end_y = 620
distance_y = end_y - start_y
final_y = start_y + (distance_y * percentage)
args.outputs.labels << { x: final_x,
y: final_y,
text: "#{percentage.to_sf}",
alignment_enum: 1 }
end
Custom Easing Functions
You can define your own easing functions by passing in a lambda
as a definition
or extending the Easing
module.
Example - Using Lambdas
This easing function goes from 0
to 1
for the first half of the ease, then 1
to 0
for the second half of the ease.
def tick args
fade_in_at = 60
current_tick = args.state.tick_count
duration = 600
easing_lambda = lambda do |percentage, start_tick, duration|
fx = percentage
if fx < 0.5
fx = percentage * 2
else
fx = 1 - (percentage - 0.5) * 2
end
fx
end
percentage = args.easing.ease fade_in_at,
current_tick,
duration,
easing_lambda
alpha = 255 * percentage
args.outputs.labels << { x: 640,
y: 320,
a: alpha,
text: "#{percentage.to_sf}",
alignment_enum: 1 }
end
Example - Extending Easing Definitions
If you don't want to create a lambda, you can register an easing definition like so:
# 1. Extend the Easing module
module Easing
def self.saw_tooth x
if x < 0.5
x * 2
else
1 - (x - 0.5) * 2
end
end
end
def tick args
fade_in_at = 60
current_tick = args.state.tick_count
duration = 600
# 2. Reference easing definition by name
percentage = args.easing.ease fade_in_at,
current_tick,
duration,
:saw_tooth
alpha = 255 * percentage
args.outputs.labels << { x: 640,
y: 320,
a: alpha,
text: "#{percentage.to_sf}",
alignment_enum: 1 }
end
Pixel Arrays (args.pixel_arrays
) link
A PixelArray
object with a width, height and an Array of pixels which are hexadecimal color values in ABGR format.
You can create a pixel array like this:
w = 200
h = 100
args.pixel_array(:my_pixel_array).w = w
args.pixel_array(:my_pixel_array).h = h
You'll also need to fill the pixels with values, if they are nil
, the array will render with the checkerboard texture. You can use #00000000 to fill with transparent pixels if desired.
args.pixel_array(:my_pixel_array).pixels.fill #FF00FF00, 0, w * h
Note: To convert from rgb hex (like skyblue #87CEEB) to abgr hex, you split it in pairs pair (eg 87
CE
EB
) and reverse the order (eg EB
CE
87
) add join them again: #EBCE87
. Then add the alpha component in front ie: FF
for full opacity: #FFEBCE87
.
You can draw it by using the symbol for :path
args.outputs.sprites << { x: 500, y: 300, w: 200, h: 100, path: :my_pixel_array) }
If you want access a specific x, y position, you can do it like this for a bottom-left coordinate system:
x = 150
y = 33
args.pixel_array(:my_pixel_array).pixels[(height - y) * width + x] = 0xFFFFFFFF
Related Sample Apps link
- Animation using pixel arrays:
./samples/07_advanced_rendering/06_pixel_arrays
- Load a pixel array from a png:
./samples/07_advanced_rendering/06_pixel_arrays_from_file/
CVars (args.cvars
) link
Hash contains metadata pulled from the files under the ./metadata
directory. To get the keys that are available type $args.cvars.keys
in the Console. Here is an example of how to retrieve the game version number:
def tick args
args.outputs.labels << {
x: 640,
y: 360,
text: args.cvars["game_metadata.version"].value.to_s
}
end
Each CVar has the following properties value
, name
, description
, type
, locked
.
Layout (args.layout
) link
Layout provides apis for placing primitives on a virtual grid that's within the "safe area" accross all platforms. This virtual grid is useful for rendering static controls (buttons, menu items, configuration screens, etc).
For reference implementations, take a look at the following sample apps:
./samples/07_advanced_rendering/18_layouts
./samples/07_advanced_rendering_hd/04_layouts_and_portrait_mode
./samples/99_genre_rpg_turn_based/turn_based_battle
The following example creates two menu items and updates a label with the button that was clicked:
def tick args
# render debug_primitives of args.layout for help with placement
# args.outputs.primitives << args.layout.debug_primitives
# capture the location for a label centered at the top
args.state.label_rect ||= args.layout.rect(row: 0, col: 10, w: 4, h: 1)
# state variable to hold the current click status
args.state.label_message ||= "click a menu item"
# capture the location of two menu items positioned in the center
# with a cell width of 4 and cell height of 2
args.state.menu_item_1_rect ||= args.layout.rect(row: 1, col: 10, w: 4, h: 2)
args.state.menu_item_2_rect ||= args.layout.rect(row: 3, col: 10, w: 4, h: 2)
# render the label at the center of the label_rect
args.outputs.labels << args.state.label_rect.center.merge(text: args.state.label_message,
anchor_x: 0.5,
anchor_y: 0.5)
# render menu items
args.outputs.sprites << args.state.menu_item_1_rect.merge(path: :solid,
r: 100,
g: 100,
b: 200)
args.outputs.labels << args.state.menu_item_1_rect.center.merge(text: "item 1",
r: 255,
g: 255,
b: 255,
anchor_x: 0.5,
anchor_y: 0.5)
args.outputs.sprites << args.state.menu_item_2_rect.merge(path: :solid,
r: 100,
g: 100,
b: 200)
args.outputs.labels << args.state.menu_item_2_rect.center.merge(text: "item 2",
r: 255,
g: 255,
b: 255,
anchor_x: 0.5,
anchor_y: 0.5)
# if click occurs, then determine which menu item was clicked
if args.inputs.mouse.click
if args.inputs.mouse.intersect_rect?(args.state.menu_item_1_rect)
args.state.label_message = "menu item 1 clicked"
elsif args.inputs.mouse.intersect_rect?(args.state.menu_item_2_rect)
args.state.label_message = "menu item 2 clicked"
else
args.state.label_message = "click a menu item"
end
end
end
rect
link
Given a row:
, col:
, w:
, h:
, returns a Hash
with properties x
, y
, w
, h
, and center
(which contains a Hash
with x
, y
). The virtual grid is 12 rows by 24 columns (or 24 columns by 12 rows in portrait mode).
debug_primitives
link
Function returns an array of primities that can be rendered to the screen to help you place items within the grid.
Example:
def tick args
args.outputs.primitives << args.layout.debug_primitives
end
Array
link
The Array class has been extend to provide methods that will help in common game development tasks. Array is one of the most powerful classes in Ruby and a very fundamental component of Game Toolkit.
map_2d
link
Assuming the array is an array of arrays, Given a block, each 2D array index invoked against the block. A 2D array is a common way to store data/layout for a stage.
repl do
stage = [
[:enemy, :empty, :player],
[:empty, :empty, :empty],
[:enemy, :empty, :enemy],
]
occupied_tiles = stage.map_2d do |row, col, tile|
if tile == :empty
nil
else
[row, col, tile]
end
end.reject_nil
puts "Stage:"
puts stage
puts "Occupied Tiles"
puts occupied_tiles
end
include_any?
link
Given a collection of items, the function will return true
if any of self
's items exists in the collection of items passed in:
any_intersect_rect?
link
Assuming the array contains objects that respond to left
, right
, top
, bottom
, this method returns true
if any of the elements within the array intersect the object being passed in. You are given an optional parameter called tolerance
which informs how close to the other rectangles the elements need to be for it to be considered intersecting.
The default tolerance is set to 0.1
, which means that the primitives are not considered intersecting unless they are overlapping by more than 0.1
.
repl do
# Here is a player class that has position and implement
# the ~attr_rect~ contract.
class Player
attr_rect
attr_accessor :x, :y, :w, :h
def initialize x, y, w, h
@x = x
@y = y
@w = w
@h = h
end
def serialize
{ x: @x, y: @y, w: @w, h: @h }
end
def inspect
"#{serialize}"
end
def to_s
"#{serialize}"
end
end
# Here is a definition of two walls.
walls = [
[10, 10, 10, 10],
{ x: 20, y: 20, w: 10, h: 10 },
]
# Display the walls.
puts "Walls."
puts walls
puts ""
# Check any_intersect_rect? on player
player = Player.new 30, 20, 10, 10
puts "Is Player #{player} touching wall?"
puts (walls.any_intersect_rect? player)
# => false
# The value is false because of the default tolerance is 0.1.
# The overlap of the player rect and any of the wall rects is
# less than 0.1 (for those that intersect).
puts ""
player = Player.new 9, 10, 10, 10
puts "Is Player #{player} touching wall?"
puts (walls.any_intersect_rect? player)
# => true
puts ""
end
map
link
The function given a block returns a new Enumerable
of values.
Example of using Array#map
in conjunction with args.state
and args.outputs.sprites
to render sprites to the screen.
def tick args
# define the colors of the rainbow in ~args.state~
# as an ~Array~ of ~Hash~es with :order and :name.
# :order will be used to determine render location
# and :name will be used to determine sprite path.
args.state.rainbow_colors ||= [
{ order: 0, name: :red },
{ order: 1, name: :orange },
{ order: 2, name: :yellow },
{ order: 3, name: :green },
{ order: 4, name: :blue },
{ order: 5, name: :indigo },
{ order: 6, name: :violet },
]
# render sprites diagonally to the screen
# with a width and height of 50.
args.outputs
.sprites << args.state
.rainbow_colors
.map do |color| # <-- ~Array#map~ usage
[
color[:order] * 50,
color[:order] * 50,
50,
50,
"sprites/square-#{color[:name]}.png"
]
end
end
each
link
The function, given a block, invokes the block for each item in the Array
. Array#each
is synonymous to foreach constructs in other languages.
Example of using Array#each
in conjunction with args.state
and args.outputs.sprites
to render sprites to the screen:
def tick args
# define the colors of the rainbow in ~args.state~
# as an ~Array~ of ~Hash~es with :order and :name.
# :order will be used to determine render location
# and :name will be used to determine sprite path.
args.state.rainbow_colors ||= [
{ order: 0, name: :red },
{ order: 1, name: :orange },
{ order: 2, name: :yellow },
{ order: 3, name: :green },
{ order: 4, name: :blue },
{ order: 5, name: :indigo },
{ order: 6, name: :violet },
]
# render sprites diagonally to the screen
# with a width and height of 50.
args.state
.rainbow_colors
.map do |color| # <-- ~Array#each~ usage
args.outputs.sprites << [
color[:order] * 50,
color[:order] * 50,
50,
50,
"sprites/square-#{color[:name]}.png"
]
end
end
reject_nil
link
Returns an Enumerable
rejecting items that are nil
, this is an alias for Array#compact
:
repl do
a = [1, nil, 4, false, :a]
puts a.reject_nil
# => [1, 4, false, :a]
puts a.compact
# => [1, 4, false, :a]
end
reject_false
link
Returns an `Enumerable` rejecting items that are `nil` or `false`.
repl do
a = [1, nil, 4, false, :a]
puts a.reject_false
# => [1, 4, :a]
end
product
link
Returns all combinations of values between two arrays.
Here are some examples of using product
. Paste the following code at the bottom of main.rb and save the file to see the results:
repl do
a = [0, 1]
puts a.product
# => [[0, 0], [0, 1], [1, 0], [1, 1]]
end
repl do
a = [ 0, 1]
b = [:a, :b]
puts a.product b
# => [[0, :a], [0, :b], [1, :a], [1, :b]]
end
Numeric
link
The Numeric
class has been extend to provide methods that will help in common game development tasks.
frame_index
link
This function is helpful for determining the index of frame-by-frame sprite animation. The numeric value self
represents the moment the animation started.
frame_index
takes three additional parameters:
- How many frames exist in the sprite animation.
- How long to hold each animation for.
- Whether the animation should repeat.
frame_index
will return nil
if the time for the animation is out of bounds of the parameter specification.
Example using variables:
def tick args
start_looping_at = 0
number_of_sprites = 6
number_of_frames_to_show_each_sprite = 4
does_sprite_loop = true
sprite_index =
start_looping_at.frame_index number_of_sprites,
number_of_frames_to_show_each_sprite,
does_sprite_loop
sprite_index ||= 0
args.outputs.sprites << [
640 - 50,
360 - 50,
100,
100,
"sprites/dragon-#{sprite_index}.png"
]
end
Example using named parameters. The named parameters version allows you to also specify a repeat_index
which is useful if your animation has starting frames that shouldn't be considered when looped:
def tick args
start_looping_at = 0
sprite_index =
start_looping_at.frame_index count: 6,
hold_for: 4,
repeat: true,
repeat_index: 0,
tick_count_override: args.state.tick_count
sprite_index ||= 0
args.outputs.sprites << [
640 - 50,
360 - 50,
100,
100,
"sprites/dragon-#{sprite_index}.png"
]
end
The named parameter variant of frame_index
is also available on Numeric
:
def tick args
sprite_index =
Numeric.frame_index start_at: 0,
count: 6,
hold_for: 4,
repeat: true,
repeat_index: 0,
tick_count_override: args.state.tick_count
sprite_index ||= 0
args.outputs.sprites << [
640 - 50,
360 - 50,
100,
100,
"sprites/dragon-#{sprite_index}.png"
]
end
elapsed_time
link
For a given number, the elapsed frames since that number is returned. `Kernel.tick_count` is used to determine how many frames have elapsed. An optional numeric argument can be passed in which will be used instead of `Kernel.tick_count`.
Here is an example of how elapsed_time can be used.
def tick args
args.state.last_click_at ||= 0
# record when a mouse click occurs
if args.inputs.mouse.click
args.state.last_click_at = args.state.tick_count
end
# Use Numeric#elapsed_time to determine how long it's been
if args.state.last_click_at.elapsed_time > 120
args.outputs.labels << [10, 710, "It has been over 2 seconds since the mouse was clicked."]
end
end
And here is an example where the override parameter is passed in:
def tick args
args.state.last_click_at ||= 0
# create a state variable that tracks time at half the speed of args.state.tick_count
args.state.simulation_tick = args.state.tick_count.idiv 2
# record when a mouse click occurs
if args.inputs.mouse.click
args.state.last_click_at = args.state.simulation_tick
end
# Use Numeric#elapsed_time to determine how long it's been
if (args.state.last_click_at.elapsed_time args.state.simulation_tick) > 120
args.outputs.labels << [10, 710, "It has been over 4 seconds since the mouse was clicked."]
end
end
elapsed?
link
Returns true if Numeric#elapsed_time
is greater than the number. An optional parameter can be passed into elapsed?
which is added to the number before evaluating whether elapsed?
is true.
Example usage (no optional parameter):
def tick args
args.state.box_queue ||= []
if args.state.box_queue.empty?
args.state.box_queue << { name: :red,
destroy_at: args.state.tick_count + 60 }
args.state.box_queue << { name: :green,
destroy_at: args.state.tick_count + 60 }
args.state.box_queue << { name: :blue,
destroy_at: args.state.tick_count + 120 }
end
boxes_to_destroy = args.state
.box_queue
.find_all { |b| b[:destroy_at].elapsed? }
if !boxes_to_destroy.empty?
puts "boxes to destroy count: #{boxes_to_destroy.length}"
end
boxes_to_destroy.each { |b| puts "box #{b} was elapsed? on #{args.state.tick_count}." }
args.state.box_queue -= boxes_to_destroy
end
Example usage (with optional parameter):
def tick args
args.state.box_queue ||= []
if args.state.box_queue.empty?
args.state.box_queue << { name: :red,
create_at: args.state.tick_count + 120,
lifespan: 60 }
args.state.box_queue << { name: :green,
create_at: args.state.tick_count + 120,
lifespan: 60 }
args.state.box_queue << { name: :blue,
create_at: args.state.tick_count + 120,
lifespan: 120 }
end
# lifespan is passed in as a parameter to ~elapsed?~
boxes_to_destroy = args.state
.box_queue
.find_all { |b| b[:create_at].elapsed? b[:lifespan] }
if !boxes_to_destroy.empty?
puts "boxes to destroy count: #{boxes_to_destroy.length}"
end
boxes_to_destroy.each { |b| puts "box #{b} was elapsed? on #{args.state.tick_count}." }
args.state.box_queue -= boxes_to_destroy
end
new?
link
Returns true if Numeric#elapsed_time == 0
. Essentially communicating that number is equal to the current frame.
Example usage:
def tick args
args.state.box_queue ||= []
if args.state.box_queue.empty?
args.state.box_queue << { name: :red,
create_at: args.state.tick_count + 60 }
end
boxes_to_spawn_this_frame = args.state
.box_queue
.find_all { |b| b[:create_at].new? }
boxes_to_spawn_this_frame.each { |b| puts "box #{b} was new? on #{args.state.tick_count}." }
args.state.box_queue -= boxes_to_spawn_this_frame
end
Kernel
link
Kernel in the DragonRuby Runtime has patches for how standard out is handled and also contains a unit of time in games called a tick.
tick_count
link
Returns the current tick of the game. This value is reset if you call $gtk.reset.
global_tick_count
link
Returns the current tick of the application from the point it was started. This value is never reset.
Grid (args.grid
) link
Returns the virtual grid for the game.
name
link
Returns either :origin_bottom_left
or :origin_center
.
bottom
link
Returns the y
value that represents the bottom of the grid.
top
link
Returns the y
value that represents the top of the grid.
left
link
Returns the x
value that represents the left of the grid.
right
link
Returns the x
value that represents the right of the grid.
rect
link
Returns a rectangle Primitive that represents the grid.
origin_bottom_left!
link
Change the grids coordinate system to 0, 0 at the bottom left corner.
origin_center!
link
Change the grids coordinate system to 0, 0 at the center of the screen.
orientation
link
Returns either :portrait
or :landscape
. The orientation of your game is set within ./mygame/metadata/game_metadata.txt
.
w
link
Returns the grid's width (value is 1280 if orientation :landscape
, and 720 if orientation is :portrait
).
h
link
Returns the grid's width (value is 720 if orientation :landscape
, and 1280 if orientation is :portrait
).
Grid HD Properties link
The following properties are available to Pro license holders. Setting hd=true
and hd=true
in ./mygame/metadata/game_metadata.txt
will enable All Screen Mode.
Please review the sample app located at ./samples/07_advanced_rendering_hd/03_allscreen_properties
.
When All Screen mode is enabled, you can render outside of the 1280x720 safe area. The 1280x720 logical canvas will be centered within the screen and scaled to one of the following closest/bess-fit resolutions.
- 720p: 1280x720
- HD+: 1600x900
- 1080p: 1920x1080
- 1440p: 2560x1440
- 1880p: 3200x1800
- 4k: 3840x2160
- 5k: 6400x2880
Regardless of the rendering resolution, your logical canvas will always be 1280x720 and all hd_*
values will be at this same logical scale.
hd_left
link
Returns the position of the left edge of the screen at the logical scale of 1280x720. For example, if the window's width is 1290x720, then hd_left
will be -5.
hd_right
link
Returns the position of the right edge of the screen at the logical scale of 1280x720. For example, if the window's width is 1290x720, then hd_right
will be 1285.
hd_bottom
link
Returns the position of the bottom edge of the screen at the logical scale of 1280x720. For example, if the window's width is 1280x730, then hd_bottom
will be -5.
hd_top
link
Returns the position of the top edge of the screen at the logical scale of 1280x720. For example, if the window's width is 1280x730, then hd_top
will be 725.
hd_offset_x
link
Returns the number of pixels that the 1280x720 canvas is offset from the left so that it's centered in the screen.
hd_offset_y
link
Returns the number of pixels that the 1280x720 canvas is offset from the bottom so that it's centered in the screen.
window_width
link
Returns the true width of the window. High DPI settings are not taken into consideration.
window_height
link
Returns the true height of the window. High DPI settings are not taken into consideration.
native_width
link
Returns the true width of the window. High DPI settings (macOS, iOS, Android) are taken into consideration.
native_height
link
Returns the true height of the window. High DPI settings (macOS, iOS, Android) are taken into consideration.
native_scale
link
Returns a decimal value representing the rendering scale of the game.
- 720p: 1.0
- HD+: 1.25
- 1080p, Full HD: 1.5
- Full HD+: 1.75
- 1440p: 2.0
- 1880p: 2.5
- 4k: 3.0
- 5k: 4.0
native_scale_enum
link
Returns an integer value representing the rendering scale of the game.
- 720p: 100
- HD+: 125
- 1080p, Full HD: 150
- Full HD+: 175
- 1440p: 200
- 1880p: 250
- 4k: 300
- 5k: 400
The enum value is taken into consideration when rendering a sprite through texture atlases.
Given the following code:
def tick args
args.outputs.sprites << { x: 0, y: 0, w: 100, h: 100, path: "sprites/player.png" }
end
The sprite path of sprites/player.png
will be replaced according to the following naming conventions (fallback to a lower resolution is automatically handled if a sprite with naming convention isn't found):
- 720p:
sprites/player.png
(100x100) - HD+:
sprites/[email protected]
(125x125) - 1080p:
sprites/[email protected]
(150x150) - 1440p:
sprites/[email protected]
(200x200) - 1880p:
sprites/[email protected]
(250x250) - 4k:
sprites/[email protected]
(300x300) - 5k:
sprites/[email protected]
(400x400)