diff options
| author | Amir Rajan <[email protected]> | 2021-04-10 03:51:14 -0500 |
|---|---|---|
| committer | Amir Rajan <[email protected]> | 2021-04-10 03:51:14 -0500 |
| commit | 00e85147c9a1dd35a0857f361e5833a1c25f4a0a (patch) | |
| tree | 52534d292ca3fa5733584eb926116228cb3551af /docs/docs.html | |
| parent | a2d92c2bf09bcdc494f1391af69b707cca281a16 (diff) | |
| download | dragonruby-game-toolkit-contrib-00e85147c9a1dd35a0857f361e5833a1c25f4a0a.tar.gz dragonruby-game-toolkit-contrib-00e85147c9a1dd35a0857f361e5833a1c25f4a0a.zip | |
Synced from DRGTK 2.10.
Diffstat (limited to 'docs/docs.html')
| -rw-r--r-- | docs/docs.html | 17551 |
1 files changed, 15840 insertions, 1711 deletions
diff --git a/docs/docs.html b/docs/docs.html index 8fafcb1..39d0086 100644 --- a/docs/docs.html +++ b/docs/docs.html @@ -1,208 +1,56 @@ <html> <head> <title>DragonRuby Game Toolkit Documentation</title> - <link href="docs.css?ver=1605252394" rel="stylesheet" type="text/css" media="all"> + <link href="docs.css?ver=1617643886" rel="stylesheet" type="text/css" media="all"> </head> <body> <div id='toc'> <h1>Table Of Contents</h1> <ul> -<li><a href='#--dragonruby-game-toolkit-live-docs'>DragonRuby Game Toolkit Live Docs</a></li> -<li><a href='#--hello-world'>Hello World</a></li> -<li><a href='#--join-the-discord-and-subscribe-to-the-news-letter'>Join the Discord and Subscribe to the News Letter</a></li> -<li><a href='#--watch-some-intro-videos'>Watch Some Intro Videos</a></li> -<li><a href='#--getting-started-tutorial'>Getting Started Tutorial</a></li> -<li><a href='#--important--go-through-all-of-the-sample-apps!-study-them-thoroughly!!-no-really,-you-should-definitely-do-this!'>IMPORTANT: Go through all of the sample apps! Study them thoroughly!! No really, you should definitely do this!</a></li> -<li><a href='#--deploying-to-itch.io'>Deploying To Itch.io</a></li> -<li><a href='#--deploying-to-mobile-devices'>Deploying To Mobile Devices</a></li> -<li><a href='#--dragonruby's-philosophy'>DragonRuby's Philosophy</a></li> -<li><a href='#--frequently-asked-questions,-comments,-and-concerns'>Frequently Asked Questions, Comments, and Concerns</a></li> -<li><a href='#--cheatsheet--how-to-determine-what-frame-you-are-on'>CHEATSHEET: How To Determine What Frame You Are On</a></li> -<li><a href='#--cheatsheet--how-to-get-current-framerate'>CHEATSHEET: How To Get Current Framerate</a></li> -<li><a href='#--cheatsheet--how-to-render-a-sprite-using-an-array'>CHEATSHEET: How To Render A Sprite Using An Array</a></li> -<li><a href='#--cheatsheet--more-sprite-properties-as-an-array'>CHEATSHEET: More Sprite Properties As An Array</a></li> -<li><a href='#--cheatsheet--different-sprite-representations'>CHEATSHEET: Different Sprite Representations</a></li> -<li><a href='#--cheatsheet--how-to-render-a-label'>CHEATSHEET: How To Render A Label</a></li> -<li><a href='#--cheatsheet--a-colored-label'>CHEATSHEET: A Colored Label</a></li> -<li><a href='#--cheatsheet--extended-label-properties'>CHEATSHEET: Extended Label Properties</a></li> -<li><a href='#--cheatsheet--rendering-a-label-as-a--hash-'>CHEATSHEET: Rendering A Label As A <code>Hash</code></a></li> -<li><a href='#--cheatsheet--getting-the-size-of-a-piece-of-text'>CHEATSHEET: Getting The Size Of A Piece Of Text</a></li> -<li><a href='#--cheatsheet--how-to-play-a-sound'>CHEATSHEET: How To Play A Sound</a></li> -<li><a href='#--cheatsheet--using--args.state--to-store-your-game-state'>CHEATSHEET: Using <code>args.state</code> To Store Your Game State</a></li> -<li><a href='#--cheatsheet--troubleshoot-performance'>CHEATSHEET: Troubleshoot Performance</a></li> -<li><a href='#--docs---gtk--runtime-'>DOCS: <code>GTK::Runtime</code></a></li> -<li><a href='#--docs---gtk--runtime#write_file-'>DOCS: <code>GTK::Runtime#write_file</code></a></li> -<li><a href='#--docs---gtk--runtime#reset-'>DOCS: <code>GTK::Runtime#reset</code></a></li> -<li><a href='#--docs---gtk--runtime#calcstringbox-'>DOCS: <code>GTK::Runtime#calcstringbox</code></a></li> -<li><a href='#--docs---array-'>DOCS: <code>Array</code></a></li> -<li><a href='#--docs---array#map-'>DOCS: <code>Array#map</code></a></li> -<li><a href='#--docs---array#each-'>DOCS: <code>Array#each</code></a></li> -<li><a href='#--docs---array#reject_nil-'>DOCS: <code>Array#reject_nil</code></a></li> -<li><a href='#--docs---array#reject_false-'>DOCS: <code>Array#reject_false</code></a></li> -<li><a href='#--docs---array#product-'>DOCS: <code>Array#product</code></a></li> -<li><a href='#--docs---array#map_2d-'>DOCS: <code>Array#map_2d</code></a></li> -<li><a href='#--docs---array#include_any?-'>DOCS: <code>Array#include_any?</code></a></li> -<li><a href='#--docs---array#any_intersect_rect?-'>DOCS: <code>Array#any_intersect_rect?</code></a></li> -<li><a href='#--docs---gtk--outputs-'>DOCS: <code>GTK::Outputs</code></a></li> -<li><a href='#--docs---gtk--outputs#solids-'>DOCS: <code>GTK::Outputs#solids</code></a></li> -<li><a href='#--docs---gtk--outputs#borders-'>DOCS: <code>GTK::Outputs#borders</code></a></li> -<li><a href='#--docs---gtk--outputs#screenshots-'>DOCS: <code>GTK::Outputs#screenshots</code></a></li> -<li><a href='#--docs---gtk--mouse-'>DOCS: <code>GTK::Mouse</code></a></li> -<li><a href='#--docs---gtk--mousepoint-'>DOCS: <code>GTK::MousePoint</code></a></li> -<li><a href='#--docs---gtk--openentity-'>DOCS: <code>GTK::OpenEntity</code></a></li> -<li><a href='#--docs---gtk--openentity#as_hash-'>DOCS: <code>GTK::OpenEntity#as_hash</code></a></li> -<li><a href='#--docs---numeric#frame_index-'>DOCS: <code>Numeric#frame_index</code></a></li> -<li><a href='#--docs---numeric#elapsed_time-'>DOCS: <code>Numeric#elapsed_time</code></a></li> -<li><a href='#--docs---numeric#elapsed?-'>DOCS: <code>Numeric#elapsed?</code></a></li> -<li><a href='#--docs---numeric#created?-'>DOCS: <code>Numeric#created?</code></a></li> -<li><a href='#--docs---kernel-'>DOCS: <code>Kernel</code></a></li> -<li><a href='#--docs---kernel--tick_count-'>DOCS: <code>Kernel::tick_count</code></a></li> -<li><a href='#--docs---kernel--global_tick_count-'>DOCS: <code>Kernel::global_tick_count</code></a></li> -<li><a href='#--open-source'>Open Source</a></li> -<li><a href='#--learn-ruby-optional---beginner-ruby-primer---automation.rb'>Learn Ruby Optional - Beginner Ruby Primer - automation.rb</a></li> -<li><a href='#--learn-ruby-optional---beginner-ruby-primer---main.rb'>Learn Ruby Optional - Beginner Ruby Primer - main.rb</a></li> -<li><a href='#--learn-ruby-optional---intermediate-ruby-primer---printing.txt'>Learn Ruby Optional - Intermediate Ruby Primer - printing.txt</a></li> -<li><a href='#--learn-ruby-optional---intermediate-ruby-primer---strings.txt'>Learn Ruby Optional - Intermediate Ruby Primer - strings.txt</a></li> -<li><a href='#--learn-ruby-optional---intermediate-ruby-primer---numbers.txt'>Learn Ruby Optional - Intermediate Ruby Primer - numbers.txt</a></li> -<li><a href='#--learn-ruby-optional---intermediate-ruby-primer---booleans.txt'>Learn Ruby Optional - Intermediate Ruby Primer - booleans.txt</a></li> -<li><a href='#--learn-ruby-optional---intermediate-ruby-primer---conditionals.txt'>Learn Ruby Optional - Intermediate Ruby Primer - conditionals.txt</a></li> -<li><a href='#--learn-ruby-optional---intermediate-ruby-primer---looping.txt'>Learn Ruby Optional - Intermediate Ruby Primer - looping.txt</a></li> -<li><a href='#--learn-ruby-optional---intermediate-ruby-primer---functions.txt'>Learn Ruby Optional - Intermediate Ruby Primer - functions.txt</a></li> -<li><a href='#--learn-ruby-optional---intermediate-ruby-primer---arrays.txt'>Learn Ruby Optional - Intermediate Ruby Primer - arrays.txt</a></li> -<li><a href='#--learn-ruby-optional---intermediate-ruby-primer---main.rb'>Learn Ruby Optional - Intermediate Ruby Primer - main.rb</a></li> -<li><a href='#--rendering-basics---labels---main.rb'>Rendering Basics - Labels - main.rb</a></li> -<li><a href='#--rendering-basics---lines---main.rb'>Rendering Basics - Lines - main.rb</a></li> -<li><a href='#--rendering-basics---solids-borders---main.rb'>Rendering Basics - Solids Borders - main.rb</a></li> -<li><a href='#--rendering-basics---sprites---main.rb'>Rendering Basics - Sprites - main.rb</a></li> -<li><a href='#--rendering-basics---sounds---main.rb'>Rendering Basics - Sounds - main.rb</a></li> -<li><a href='#--rendering-basics---audio-mixer---main.rb'>Rendering Basics - Audio Mixer - main.rb</a></li> -<li><a href='#--rendering-basics---sound-synthesis---main.rb'>Rendering Basics - Sound Synthesis - main.rb</a></li> -<li><a href='#--input-basics---keyboard---main.rb'>Input Basics - Keyboard - main.rb</a></li> -<li><a href='#--input-basics---mouse---main.rb'>Input Basics - Mouse - main.rb</a></li> -<li><a href='#--input-basics---mouse-point-to-rect---main.rb'>Input Basics - Mouse Point To Rect - main.rb</a></li> -<li><a href='#--input-basics---mouse-rect-to-rect---main.rb'>Input Basics - Mouse Rect To Rect - main.rb</a></li> -<li><a href='#--input-basics---controller---main.rb'>Input Basics - Controller - main.rb</a></li> -<li><a href='#--input-basics---touch---main.rb'>Input Basics - Touch - main.rb</a></li> -<li><a href='#--rendering-sprites---animation-using-separate-pngs---main.rb'>Rendering Sprites - Animation Using Separate Pngs - main.rb</a></li> -<li><a href='#--rendering-sprites---animation-using-sprite-sheet---main.rb'>Rendering Sprites - Animation Using Sprite Sheet - main.rb</a></li> -<li><a href='#--rendering-sprites---animation-states---main.rb'>Rendering Sprites - Animation States - main.rb</a></li> -<li><a href='#--rendering-sprites---color-and-rotation---main.rb'>Rendering Sprites - Color And Rotation - main.rb</a></li> -<li><a href='#--physics-and-collisions---simple---main.rb'>Physics And Collisions - Simple - main.rb</a></li> -<li><a href='#--physics-and-collisions---moving-objects---main.rb'>Physics And Collisions - Moving Objects - main.rb</a></li> -<li><a href='#--physics-and-collisions---entities---main.rb'>Physics And Collisions - Entities - main.rb</a></li> -<li><a href='#--physics-and-collisions---box-collision---main.rb'>Physics And Collisions - Box Collision - main.rb</a></li> -<li><a href='#--physics-and-collisions---box-collision-2---main.rb'>Physics And Collisions - Box Collision 2 - main.rb</a></li> -<li><a href='#--physics-and-collisions---jump-physics---main.rb'>Physics And Collisions - Jump Physics - main.rb</a></li> -<li><a href='#--mouse---mouse-click---main.rb'>Mouse - Mouse Click - main.rb</a></li> -<li><a href='#--mouse---mouse-move---main.rb'>Mouse - Mouse Move - main.rb</a></li> -<li><a href='#--mouse---mouse-move-paint-app---main.rb'>Mouse - Mouse Move Paint App - main.rb</a></li> -<li><a href='#--mouse---coordinate-systems---main.rb'>Mouse - Coordinate Systems - main.rb</a></li> -<li><a href='#--save-load---save-load-game---main.rb'>Save Load - Save Load Game - main.rb</a></li> -<li><a href='#--advanced-rendering---simple-render-targets---main.rb'>Advanced Rendering - Simple Render Targets - main.rb</a></li> -<li><a href='#--advanced-rendering---render-targets-with-tile-manipulation---main.rb'>Advanced Rendering - Render Targets With Tile Manipulation - main.rb</a></li> -<li><a href='#--advanced-rendering---render-target-viewports---main.rb'>Advanced Rendering - Render Target Viewports - main.rb</a></li> -<li><a href='#--advanced-rendering---render-primitive-hierarchies---main.rb'>Advanced Rendering - Render Primitive Hierarchies - main.rb</a></li> -<li><a href='#--advanced-rendering---render-primitives-as-hash---main.rb'>Advanced Rendering - Render Primitives As Hash - main.rb</a></li> -<li><a href='#--advanced-rendering---pixel-arrays---main.rb'>Advanced Rendering - Pixel Arrays - main.rb</a></li> -<li><a href='#--tweening-lerping-easing-functions---easing-functions---main.rb'>Tweening Lerping Easing Functions - Easing Functions - main.rb</a></li> -<li><a href='#--tweening-lerping-easing-functions---cubic-bezier---main.rb'>Tweening Lerping Easing Functions - Cubic Bezier - main.rb</a></li> -<li><a href='#--tweening-lerping-easing-functions---easing-using-spline---main.rb'>Tweening Lerping Easing Functions - Easing Using Spline - main.rb</a></li> -<li><a href='#--tweening-lerping-easing-functions---parametric-enemy-movement---main.rb'>Tweening Lerping Easing Functions - Parametric Enemy Movement - main.rb</a></li> -<li><a href='#--performance---sprites-as-hash---main.rb'>Performance - Sprites As Hash - main.rb</a></li> -<li><a href='#--performance---sprites-as-entities---main.rb'>Performance - Sprites As Entities - main.rb</a></li> -<li><a href='#--performance---sprites-as-strict-entities---main.rb'>Performance - Sprites As Strict Entities - main.rb</a></li> -<li><a href='#--performance---sprites-as-classes---main.rb'>Performance - Sprites As Classes - main.rb</a></li> -<li><a href='#--performance---static-sprites-as-classes---main.rb'>Performance - Static Sprites As Classes - main.rb</a></li> -<li><a href='#--performance---static-sprites-as-classes-with-custom-drawing---main.rb'>Performance - Static Sprites As Classes With Custom Drawing - main.rb</a></li> -<li><a href='#--performance---collision-limits---main.rb'>Performance - Collision Limits - main.rb</a></li> -<li><a href='#--advanced-debugging---trace-debugging---main.rb'>Advanced Debugging - Trace Debugging - main.rb</a></li> -<li><a href='#--advanced-debugging---trace-debugging-classes---main.rb'>Advanced Debugging - Trace Debugging Classes - main.rb</a></li> -<li><a href='#--advanced-debugging---unit-tests---exception_raising_tests.rb'>Advanced Debugging - Unit Tests - exception_raising_tests.rb</a></li> -<li><a href='#--advanced-debugging---unit-tests---gen_docs.rb'>Advanced Debugging - Unit Tests - gen_docs.rb</a></li> -<li><a href='#--advanced-debugging---unit-tests---geometry_tests.rb'>Advanced Debugging - Unit Tests - geometry_tests.rb</a></li> -<li><a href='#--advanced-debugging---unit-tests---http_tests.rb'>Advanced Debugging - Unit Tests - http_tests.rb</a></li> -<li><a href='#--advanced-debugging---unit-tests---object_to_primitive_tests.rb'>Advanced Debugging - Unit Tests - object_to_primitive_tests.rb</a></li> -<li><a href='#--advanced-debugging---unit-tests---parsing_tests.rb'>Advanced Debugging - Unit Tests - parsing_tests.rb</a></li> -<li><a href='#--advanced-debugging---unit-tests---serialize_deserialize_tests.rb'>Advanced Debugging - Unit Tests - serialize_deserialize_tests.rb</a></li> -<li><a href='#--advanced-debugging---unit-tests---state_serialization_experimental_tests.rb'>Advanced Debugging - Unit Tests - state_serialization_experimental_tests.rb</a></li> -<li><a href='#--http---retrieve-images---main.rb'>Http - Retrieve Images - main.rb</a></li> -<li><a href='#--12-c-extensions---basics---main.rb'>12 C Extensions - Basics - main.rb</a></li> -<li><a href='#--12-c-extensions---intermediate---main.rb'>12 C Extensions - Intermediate - main.rb</a></li> -<li><a href='#--12-c-extensions---native-pixel-arrays---main.rb'>12 C Extensions - Native Pixel Arrays - main.rb</a></li> -<li><a href='#--3d---3d-cube---main.rb'>3d - 3d Cube - main.rb</a></li> -<li><a href='#--arcade---dueling-starships---main.rb'>Arcade - Dueling Starships - main.rb</a></li> -<li><a href='#--arcade/flappy-dragon/credits.txt'>arcade/flappy dragon/credits.txt</a></li> -<li><a href='#--arcade/flappy-dragon/main.rb'>arcade/flappy dragon/main.rb</a></li> -<li><a href='#--arcade---pong---main.rb'>Arcade - Pong - main.rb</a></li> -<li><a href='#--arcade---snakemoji---main.rb'>Arcade - Snakemoji - main.rb</a></li> -<li><a href='#--arcade---solar-system---main.rb'>Arcade - Solar System - main.rb</a></li> -<li><a href='#--crafting---craft-game-starting-point---main.rb'>Crafting - Craft Game Starting Point - main.rb</a></li> -<li><a href='#--dev-tools---add-buttons-to-console---main.rb'>Dev Tools - Add Buttons To Console - main.rb</a></li> -<li><a href='#--dev-tools---animation-creator-starting-point---main.rb'>Dev Tools - Animation Creator Starting Point - main.rb</a></li> -<li><a href='#--dev-tools---tile-editor-starting-point---main.rb'>Dev Tools - Tile Editor Starting Point - main.rb</a></li> -<li><a href='#--lowrez---resolution-64x64---lowrez.rb'>Lowrez - Resolution 64x64 - lowrez.rb</a></li> -<li><a href='#--lowrez---resolution-64x64---main.rb'>Lowrez - Resolution 64x64 - main.rb</a></li> -<li><a href='#--platformer---clepto-frog---main.rb'>Platformer - Clepto Frog - main.rb</a></li> -<li><a href='#--platformer---clepto-frog---map.rb'>Platformer - Clepto Frog - map.rb</a></li> -<li><a href='#--platformer---gorillas-basic---credits.txt'>Platformer - Gorillas Basic - credits.txt</a></li> -<li><a href='#--platformer---gorillas-basic---main.rb'>Platformer - Gorillas Basic - main.rb</a></li> -<li><a href='#--platformer---gorillas-basic---tests.rb'>Platformer - Gorillas Basic - tests.rb</a></li> -<li><a href='#--platformer---gorillas-basic---tests---building_generation_tests.rb'>Platformer - Gorillas Basic - Tests - building_generation_tests.rb</a></li> -<li><a href='#--platformer---the-little-probe---main.rb'>Platformer - The Little Probe - main.rb</a></li> -<li><a href='#--platformer---the-little-probe---data---level.txt'>Platformer - The Little Probe - Data - level.txt</a></li> -<li><a href='#--platformer---the-little-probe---data---level_lava.txt'>Platformer - The Little Probe - Data - level_lava.txt</a></li> -<li><a href='#--rpg-narrative---choose-your-own-adventure---decision.rb'>Rpg Narrative - Choose Your Own Adventure - decision.rb</a></li> -<li><a href='#--rpg-narrative---choose-your-own-adventure---main.rb'>Rpg Narrative - Choose Your Own Adventure - main.rb</a></li> -<li><a href='#--rpg-narrative---return-of-serenity---lowrez_simulator.rb'>Rpg Narrative - Return Of Serenity - lowrez_simulator.rb</a></li> -<li><a href='#--rpg-narrative---return-of-serenity---main.rb'>Rpg Narrative - Return Of Serenity - main.rb</a></li> -<li><a href='#--rpg-narrative---return-of-serenity---require.rb'>Rpg Narrative - Return Of Serenity - require.rb</a></li> -<li><a href='#--rpg-narrative---return-of-serenity---storyline.rb'>Rpg Narrative - Return Of Serenity - storyline.rb</a></li> -<li><a href='#--rpg-narrative---return-of-serenity---storyline_anka.rb'>Rpg Narrative - Return Of Serenity - storyline_anka.rb</a></li> -<li><a href='#--rpg-narrative---return-of-serenity---storyline_blinking_light.rb'>Rpg Narrative - Return Of Serenity - storyline_blinking_light.rb</a></li> -<li><a href='#--rpg-narrative---return-of-serenity---storyline_day_one.rb'>Rpg Narrative - Return Of Serenity - storyline_day_one.rb</a></li> -<li><a href='#--rpg-narrative---return-of-serenity---storyline_final_decision.rb'>Rpg Narrative - Return Of Serenity - storyline_final_decision.rb</a></li> -<li><a href='#--rpg-narrative---return-of-serenity---storyline_final_message.rb'>Rpg Narrative - Return Of Serenity - storyline_final_message.rb</a></li> -<li><a href='#--rpg-narrative---return-of-serenity---storyline_serenity_alive.rb'>Rpg Narrative - Return Of Serenity - storyline_serenity_alive.rb</a></li> -<li><a href='#--rpg-narrative---return-of-serenity---storyline_serenity_bio.rb'>Rpg Narrative - Return Of Serenity - storyline_serenity_bio.rb</a></li> -<li><a href='#--rpg-narrative---return-of-serenity---storyline_serenity_introduction.rb'>Rpg Narrative - Return Of Serenity - storyline_serenity_introduction.rb</a></li> -<li><a href='#--rpg-narrative---return-of-serenity---storyline_speed_of_light.rb'>Rpg Narrative - Return Of Serenity - storyline_speed_of_light.rb</a></li> -<li><a href='#--rpg-roguelike---roguelike-line-of-sight---constants.rb'>Rpg Roguelike - Roguelike Line Of Sight - constants.rb</a></li> -<li><a href='#--rpg-roguelike---roguelike-line-of-sight---legend.rb'>Rpg Roguelike - Roguelike Line Of Sight - legend.rb</a></li> -<li><a href='#--rpg-roguelike---roguelike-line-of-sight---main.rb'>Rpg Roguelike - Roguelike Line Of Sight - main.rb</a></li> -<li><a href='#--rpg-roguelike---roguelike-line-of-sight---sprite_lookup.rb'>Rpg Roguelike - Roguelike Line Of Sight - sprite_lookup.rb</a></li> -<li><a href='#--rpg-roguelike---roguelike-starting-point---main.rb'>Rpg Roguelike - Roguelike Starting Point - main.rb</a></li> -<li><a href='#--rpg-tactical---hexagonal-grid---main.rb'>Rpg Tactical - Hexagonal Grid - main.rb</a></li> -<li><a href='#--rpg-tactical---isometric-grid---main.rb'>Rpg Tactical - Isometric Grid - main.rb</a></li> -<li><a href='#--rpg-topdown---topdown-starting-point---main.rb'>Rpg Topdown - Topdown Starting Point - main.rb</a></li> -<li><a href='#--args.rb'>args.rb</a></li> -<li><a href='#--assert.rb'>assert.rb</a></li> -<li><a href='#--attr_gtk.rb'>attr_gtk.rb</a></li> -<li><a href='#--attr_sprite.rb'>attr_sprite.rb</a></li> -<li><a href='#--console.rb'>console.rb</a></li> -<li><a href='#--console_color.rb'>console_color.rb</a></li> -<li><a href='#--console_font_style.rb'>console_font_style.rb</a></li> -<li><a href='#--console_menu.rb'>console_menu.rb</a></li> -<li><a href='#--console_prompt.rb'>console_prompt.rb</a></li> -<li><a href='#--controller.rb'>controller.rb</a></li> -<li><a href='#--controller/config.rb'>controller/config.rb</a></li> -<li><a href='#--controller/keys.rb'>controller/keys.rb</a></li> -<li><a href='#--directional_input_helper_methods.rb'>directional_input_helper_methods.rb</a></li> -<li><a href='#--easing.rb'>easing.rb</a></li> -<li><a href='#--geometry.rb'>geometry.rb</a></li> -<li><a href='#--grid.rb'>grid.rb</a></li> -<li><a href='#--inputs.rb'>inputs.rb</a></li> -<li><a href='#--ios_wizard.rb'>ios_wizard.rb</a></li> -<li><a href='#--itch_wizard.rb'>itch_wizard.rb</a></li> -<li><a href='#--layout.rb'>layout.rb</a></li> -<li><a href='#--log.rb'>log.rb</a></li> -<li><a href='#--numeric.rb'>numeric.rb</a></li> -<li><a href='#--runtime/draw.rb'>runtime/draw.rb</a></li> -<li><a href='#--runtime/framerate_diagnostics.rb'>runtime/framerate_diagnostics.rb</a></li> -<li><a href='#--string.rb'>string.rb</a></li> -<li><a href='#--tests.rb'>tests.rb</a></li> -<li><a href='#--trace.rb'>trace.rb</a></li> -<li><a href='#--wizards.rb'>wizards.rb</a></li> -</ul> </div> +<li><a class='header-1' href='#--dragonruby-game-toolkit-live-docs'>DragonRuby Game Toolkit Live Docs</a></li> +<li><a class='header-1' href='#--hello-world'>Hello World</a></li> +<li><a class='header-1' href='#--join-the-discord-and-subscribe-to-the-news-letter'>Join the Discord and Subscribe to the News Letter</a></li> +<li><a class='header-1' href='#--watch-some-intro-videos'>Watch Some Intro Videos</a></li> +<li><a class='header-1' href='#--getting-started-tutorial'>Getting Started Tutorial</a></li> +<ul><li><a class='header-2' href='#---introduction'>Introduction</a></li></ul><ul><li><a class='header-2' href='#---prerequisites'>Prerequisites</a></li></ul><ul><li><a class='header-2' href='#---the-game-loop'>The Game Loop</a></li></ul><ul><li><a class='header-2' href='#---breakdown-of-the--tick--method'>Breakdown Of The <code>tick</code> Method</a></li></ul><ul><li><a class='header-2' href='#---rendering-a-sprite'>Rendering A Sprite</a></li></ul><ul><li><a class='header-2' href='#---coordinate-system-and-virtual-canvas'>Coordinate System and Virtual Canvas</a></li></ul><ul><li><a class='header-2' href='#---game-state'>Game State</a></li></ul><ul><li><a class='header-2' href='#---there-is-no-delta-time'>There Is No Delta Time</a></li></ul><ul><li><a class='header-2' href='#---handling-user-input'>Handling User Input</a></li></ul><ul><li><a class='header-2' href='#---coding-on-a-raspberry-pi'>Coding On A Raspberry Pi</a></li></ul><ul><li><a class='header-2' href='#---conclusion'>Conclusion</a></li></ul><li><a class='header-1' href='#--deploying-to-itch.io'>Deploying To Itch.io</a></li> +<ul><li><a class='header-2' href='#---creating-your-game-landing-page'>Creating Your Game Landing Page</a></li></ul><ul><li><a class='header-2' href='#---update-your-game's-metadata'>Update Your Game's Metadata</a></li></ul><ul><li><a class='header-2' href='#---building-your-game-for-distribution'>Building Your Game For Distribution</a></li></ul><li><a class='header-1' href='#--deploying-to-mobile-devices'>Deploying To Mobile Devices</a></li> +<li><a class='header-1' href='#--dragonruby's-philosophy'>DragonRuby's Philosophy</a></li> +<ul><li><a class='header-2' href='#---challenge-the-status-quo'>Challenge The Status Quo</a></li></ul><ul><li><a class='header-2' href='#---continuity-of-design'>Continuity of Design</a></li></ul><ul><li><a class='header-2' href='#---release-early-and-often'>Release Early and Often</a></li></ul><ul><li><a class='header-2' href='#---sustainable-and-ethical-monetization'>Sustainable And Ethical Monetization</a></li></ul><ul><li><a class='header-2' href='#---sustainable-and-ethical-open-source'>Sustainable And Ethical Open Source</a></li></ul><ul><li><a class='header-2' href='#---people-over-entities'>People Over Entities</a></li></ul><ul><li><a class='header-2' href='#---building-a-game-should-be-fun-and-bring-happiness'>Building A Game Should Be Fun And Bring Happiness</a></li></ul><ul><li><a class='header-2' href='#---real-world-application-drives-features'>Real World Application Drives Features</a></li></ul><li><a class='header-1' href='#--frequently-asked-questions,-comments,-and-concerns'>Frequently Asked Questions, Comments, and Concerns</a></li> +<ul><li><a class='header-2' href='#---frequently-asked-questions'>Frequently Asked Questions</a></li></ul><ul><ul><li><a class='header-3' href='#----what-is-dragonruby-llp?'>What is DragonRuby LLP?</a></li></ul></ul><ul><ul><li><a class='header-3' href='#----what-is-dragonruby?'>What is DragonRuby?</a></li></ul></ul><ul><ul><li><a class='header-3' href='#----how-is-dragonruby-different-than-mri?'>How is DragonRuby different than MRI?</a></li></ul></ul><ul><li><a class='header-2' href='#---frequent-comments-about-ruby-as-a-language-choice'>Frequent Comments About Ruby as a Language Choice</a></li></ul><ul><ul><li><a class='header-3' href='#----but-ruby-is-dead.'>But Ruby is dead.</a></li></ul></ul><ul><ul><li><a class='header-3' href='#----but-ruby-is-slow.'>But Ruby is slow.</a></li></ul></ul><ul><ul><li><a class='header-3' href='#----dynamic-languages-are-slow.'>Dynamic languages are slow.</a></li></ul></ul><ul><li><a class='header-2' href='#---frequent-concerns'>Frequent Concerns</a></li></ul><ul><ul><li><a class='header-3' href='#----dragonruby-is-not-open-source.-that's-not-right.'>DragonRuby is not open source. That's not right.</a></li></ul></ul><ul><ul><li><a class='header-3' href='#----dragonruby-is-for-pay.-you-should-offer-a-free-version.'>DragonRuby is for pay. You should offer a free version.</a></li></ul></ul><ul><ul><li><a class='header-3' href='#----but-still,-you-should-offer-a-free-version.-so-i-can-try-it-out-and-see-if-i-like-it.'>But still, you should offer a free version. So I can try it out and see if I like it.</a></li></ul></ul><ul><ul><li><a class='header-3' href='#----i-still-think-you-should-do-a-free-version.-think-of-all-people-who-would-give-it-a-shot.'>I still think you should do a free version. Think of all people who would give it a shot.</a></li></ul></ul><ul><ul><li><a class='header-3' href='#----what-if-i-build-something-with-dragonruby,-but-dragonruby-llp-becomes-insolvent.'>What if I build something with DragonRuby, but DragonRuby LLP becomes insolvent.</a></li></ul></ul><li><a class='header-1' href='#--recipies-'>RECIPIES:</a></li> +<ul><li><a class='header-2' href='#---how-to-determine-what-frame-you-are-on'>How To Determine What Frame You Are On</a></li></ul><ul><li><a class='header-2' href='#---how-to-get-current-framerate'>How To Get Current Framerate</a></li></ul><ul><li><a class='header-2' href='#---how-to-render-a-sprite-using-an-array'>How To Render A Sprite Using An Array</a></li></ul><ul><li><a class='header-2' href='#---more-sprite-properties-as-an-array'>More Sprite Properties As An Array</a></li></ul><ul><li><a class='header-2' href='#---different-sprite-representations'>Different Sprite Representations</a></li></ul><ul><li><a class='header-2' href='#---how-to-render-a-label'>How To Render A Label</a></li></ul><ul><li><a class='header-2' href='#---a-colored-label'>A Colored Label</a></li></ul><ul><li><a class='header-2' href='#---extended-label-properties'>Extended Label Properties</a></li></ul><ul><li><a class='header-2' href='#---rendering-a-label-as-a--hash-'>Rendering A Label As A <code>Hash</code></a></li></ul><ul><li><a class='header-2' href='#---getting-the-size-of-a-piece-of-text'>Getting The Size Of A Piece Of Text</a></li></ul><ul><li><a class='header-2' href='#---how-to-play-a-sound'>How To Play A Sound</a></li></ul><ul><li><a class='header-2' href='#---using--args.state--to-store-your-game-state'>Using <code>args.state</code> To Store Your Game State</a></li></ul><ul><li><a class='header-2' href='#---troubleshoot-performance'>Troubleshoot Performance</a></li></ul><li><a class='header-1' href='#--docs---gtk--runtime-'>DOCS: <code>GTK::Runtime</code></a></li> +<li><a class='header-1' href='#--summary---def-tick-args;-end;-'>SUMMARY: <code>def tick args; end;</code></a></li> +<ul><li><a class='header-2' href='#----args.state-'><code>args.state</code></a></li></ul><ul><ul><li><a class='header-3' href='#-----.-.entity_id-'><code>.*.entity_id</code></a></li></ul></ul><ul><ul><li><a class='header-3' href='#-----.-.entity_type-'><code>.*.entity_type</code></a></li></ul></ul><ul><ul><li><a class='header-3' href='#-----.-.created_at-'><code>.*.created_at</code></a></li></ul></ul><ul><ul><li><a class='header-3' href='#-----.-.created_at_elapsed-'><code>.*.created_at_elapsed</code></a></li></ul></ul><ul><ul><li><a class='header-3' href='#-----.-.global_created_at-'><code>.*.global_created_at</code></a></li></ul></ul><ul><ul><li><a class='header-3' href='#-----.-.global_created_at_elapsed-'><code>.*.global_created_at_elapsed</code></a></li></ul></ul><ul><ul><li><a class='header-3' href='#-----.-.as_hash-'><code>.*.as_hash</code></a></li></ul></ul><ul><ul><li><a class='header-3' href='#-----.new_entity-'><code>.new_entity</code></a></li></ul></ul><ul><ul><li><a class='header-3' href='#-----.new_entity_strict-'><code>.new_entity_strict</code></a></li></ul></ul><ul><ul><li><a class='header-3' href='#-----.tick_count-'><code>.tick_count</code></a></li></ul></ul><ul><li><a class='header-2' href='#----args.inputs-'><code>args.inputs</code></a></li></ul><ul><ul><li><a class='header-3' href='#-----.up-'><code>.up</code></a></li></ul></ul><ul><ul><li><a class='header-3' href='#-----.down-'><code>.down</code></a></li></ul></ul><ul><ul><li><a class='header-3' href='#-----.left-'><code>.left</code></a></li></ul></ul><ul><ul><li><a class='header-3' href='#-----.right-'><code>.right</code></a></li></ul></ul><ul><ul><li><a class='header-3' href='#-----.left_right-'><code>.left_right</code></a></li></ul></ul><ul><ul><li><a class='header-3' href='#-----.up_down-'><code>.up_down</code></a></li></ul></ul><ul><ul><li><a class='header-3' href='#-----.text--or--.history-'><code>.text</code> OR <code>.history</code></a></li></ul></ul><ul><ul><li><a class='header-3' href='#-----.mouse-'><code>.mouse</code></a></li></ul></ul><ul><ul><li><a class='header-3' href='#-----.controller_one-,--.controller_two-'><code>.controller_one</code>, <code>.controller_two</code></a></li></ul></ul><ul><ul><li><a class='header-3' href='#-----.keyboard-'><code>.keyboard</code></a></li></ul></ul><ul><li><a class='header-2' href='#----args.outputs-'><code>args.outputs</code></a></li></ul><ul><ul><li><a class='header-3' href='#-----.background_color-'><code>.background_color</code></a></li></ul></ul><ul><ul><li><a class='header-3' href='#-----.sounds-'><code>.sounds</code></a></li></ul></ul><ul><ul><li><a class='header-3' href='#-----.solids-'><code>.solids</code></a></li></ul></ul><ul><ul><li><a class='header-3' href='#-----.static_solids-'><code>.static_solids</code></a></li></ul></ul><ul><ul><li><a class='header-3' href='#-----.sprites-,--.static_sprites-'><code>.sprites</code>, <code>.static_sprites</code></a></li></ul></ul><ul><ul><li><a class='header-3' href='#-----.primitives-,--.static_primitives-'><code>.primitives</code>, <code>.static_primitives</code></a></li></ul></ul><ul><ul><li><a class='header-3' href='#-----.labels-,--.static_labels-'><code>.labels</code>, <code>.static_labels</code></a></li></ul></ul><ul><ul><li><a class='header-3' href='#-----.lines-,--.static_lines-'><code>.lines</code>, <code>.static_lines</code></a></li></ul></ul><ul><ul><li><a class='header-3' href='#-----.borders-,--.static_borders-'><code>.borders</code>, <code>.static_borders</code></a></li></ul></ul><ul><ul><li><a class='header-3' href='#-----.debug-,--.static_debug-'><code>.debug</code>, <code>.static_debug</code></a></li></ul></ul><ul><li><a class='header-2' href='#----args.geometry-'><code>args.geometry</code></a></li></ul><ul><ul><li><a class='header-3' href='#-----.inside_rect?-rect_1,-rect_2-'><code>.inside_rect? rect_1, rect_2</code></a></li></ul></ul><ul><ul><li><a class='header-3' href='#-----.intersect_rect?-rect_2,-rect_2-'><code>.intersect_rect? rect_2, rect_2</code></a></li></ul></ul><ul><ul><li><a class='header-3' href='#-----.scale_rect-rect,-x_percentage,-y_percentage-'><code>.scale_rect rect, x_percentage, y_percentage</code></a></li></ul></ul><ul><ul><li><a class='header-3' href='#-----.angle_to-start_point,-end_point-'><code>.angle_to start_point, end_point</code></a></li></ul></ul><ul><ul><li><a class='header-3' href='#-----.angle_from-start_point,-end_point-'><code>.angle_from start_point, end_point</code></a></li></ul></ul><ul><ul><li><a class='header-3' href='#-----.point_inside_circle?-point,-circle_center_point,-radius-'><code>.point_inside_circle? point, circle_center_point, radius</code></a></li></ul></ul><ul><ul><li><a class='header-3' href='#-----.center_inside_rect-rect,-other_rect-'><code>.center_inside_rect rect, other_rect</code></a></li></ul></ul><ul><ul><li><a class='header-3' href='#-----.center_inside_rect_x-rect,-other_rect-'><code>.center_inside_rect_x rect, other_rect</code></a></li></ul></ul><ul><ul><li><a class='header-3' href='#-----.center_inside_rect_y-rect,-other_rect-'><code>.center_inside_rect_y rect, other_rect</code></a></li></ul></ul><ul><ul><li><a class='header-3' href='#-----.anchor_rect-rect,-anchor_x,-anchor_y-'><code>.anchor_rect rect, anchor_x, anchor_y</code></a></li></ul></ul><ul><ul><li><a class='header-3' href='#-----.shift_line-line,-x,-y-'><code>.shift_line line, x, y</code></a></li></ul></ul><ul><ul><li><a class='header-3' href='#-----.line_y_intercept-line-'><code>.line_y_intercept line</code></a></li></ul></ul><ul><ul><li><a class='header-3' href='#-----.angle_between_lines-line_one,-line_two,-replace_infinity--'><code>.angle_between_lines line_one, line_two, replace_infinity:</code></a></li></ul></ul><ul><ul><li><a class='header-3' href='#-----.line_slope-line,-replace_infinity--'><code>.line_slope line, replace_infinity:</code></a></li></ul></ul><ul><ul><li><a class='header-3' href='#-----.line_rise_run-'><code>.line_rise_run</code></a></li></ul></ul><ul><ul><li><a class='header-3' href='#-----.ray_test-point,-line-'><code>.ray_test point, line</code></a></li></ul></ul><ul><ul><li><a class='header-3' href='#-----.line_rect-line-'><code>.line_rect line</code></a></li></ul></ul><ul><ul><li><a class='header-3' href='#-----.line_intersect-line_one,-line_two-'><code>.line_intersect line_one, line_two</code></a></li></ul></ul><ul><ul><li><a class='header-3' href='#-----.distance-point_one,-point_two-'><code>.distance point_one, point_two</code></a></li></ul></ul><ul><ul><li><a class='header-3' href='#-----.cubic_bezier-t,-a,-b,-c,-d-'><code>.cubic_bezier t, a, b, c, d</code></a></li></ul></ul><ul><li><a class='header-2' href='#----args.easing-'><code>args.easing</code></a></li></ul><ul><ul><li><a class='header-3' href='#-----.ease-start_tick,-current_tick,-duration,-easing_functions-'><code>.ease start_tick, current_tick, duration, easing_functions</code></a></li></ul></ul><ul><ul><li><a class='header-3' href='#-----.ease_spline-start_tick,-current_tick,-duration,-spline-'><code>.ease_spline start_tick, current_tick, duration, spline</code></a></li></ul></ul><ul><li><a class='header-2' href='#----args.string-'><code>args.string</code></a></li></ul><ul><ul><li><a class='header-3' href='#-----.wrapped_lines-string,-max_character_length-'><code>.wrapped_lines string, max_character_length</code></a></li></ul></ul><ul><li><a class='header-2' href='#----args.grid-'><code>args.grid</code></a></li></ul><ul><ul><li><a class='header-3' href='#-----.name-'><code>.name</code></a></li></ul></ul><ul><ul><li><a class='header-3' href='#-----.bottom-'><code>.bottom</code></a></li></ul></ul><ul><ul><li><a class='header-3' href='#-----.top-'><code>.top</code></a></li></ul></ul><ul><ul><li><a class='header-3' href='#-----.left-'><code>.left</code></a></li></ul></ul><ul><ul><li><a class='header-3' href='#-----.right-'><code>.right</code></a></li></ul></ul><ul><ul><li><a class='header-3' href='#-----.rect-'><code>.rect</code></a></li></ul></ul><ul><ul><li><a class='header-3' href='#-----.origin_bottom_left!-'><code>.origin_bottom_left!</code></a></li></ul></ul><ul><ul><li><a class='header-3' href='#-----.origin_center!-'><code>.origin_center!</code></a></li></ul></ul><ul><ul><li><a class='header-3' href='#-----.w-'><code>.w</code></a></li></ul></ul><ul><ul><li><a class='header-3' href='#-----.h-'><code>.h</code></a></li></ul></ul><ul><li><a class='header-2' href='#----args.gtk-'><code>args.gtk</code></a></li></ul><ul><ul><li><a class='header-3' href='#-----.argv-'><code>.argv</code></a></li></ul></ul><ul><ul><li><a class='header-3' href='#-----.platform-'><code>.platform</code></a></li></ul></ul><ul><ul><li><a class='header-3' href='#-----.request_quit-'><code>.request_quit</code></a></li></ul></ul><ul><ul><li><a class='header-3' href='#-----.write_file-path,-contents-'><code>.write_file path, contents</code></a></li></ul></ul><ul><ul><li><a class='header-3' href='#-----.write_file_root-'><code>.write_file_root</code></a></li></ul></ul><ul><ul><li><a class='header-3' href='#-----.append_file-path,-contents-'><code>.append_file path, contents</code></a></li></ul></ul><ul><ul><li><a class='header-3' href='#-----.append_file_root-path,-contents-'><code>.append_file_root path, contents</code></a></li></ul></ul><ul><ul><li><a class='header-3' href='#-----.read_file-path-'><code>.read_file path</code></a></li></ul></ul><ul><ul><li><a class='header-3' href='#-----.parse_xml-string,-parse_xml_file-path-'><code>.parse_xml string, parse_xml_file path</code></a></li></ul></ul><ul><ul><li><a class='header-3' href='#-----.parse_json-string,-parse_json_file-path-'><code>.parse_json string, parse_json_file path</code></a></li></ul></ul><ul><ul><li><a class='header-3' href='#-----.http_get-url,-extra_headers-=-{}-'><code>.http_get url, extra_headers = {}</code></a></li></ul></ul><ul><ul><li><a class='header-3' href='#-----.http_post-url,-form_fields-=-{},-extra_headers-=-{}-'><code>.http_post url, form_fields = {}, extra_headers = {}</code></a></li></ul></ul><ul><ul><li><a class='header-3' href='#-----.reset-'><code>.reset</code></a></li></ul></ul><ul><ul><li><a class='header-3' href='#-----.stop_music-'><code>.stop_music</code></a></li></ul></ul><ul><ul><li><a class='header-3' href='#-----.calcstringbox-str,-size_enum,-font-'><code>.calcstringbox str, size_enum, font</code></a></li></ul></ul><ul><ul><li><a class='header-3' href='#-----.slowmo!-factor-'><code>.slowmo! factor</code></a></li></ul></ul><ul><ul><li><a class='header-3' href='#-----.notify!-string-'><code>.notify! string</code></a></li></ul></ul><ul><ul><li><a class='header-3' href='#-----.system-'><code>.system</code></a></li></ul></ul><ul><ul><li><a class='header-3' href='#-----.exec-'><code>.exec</code></a></li></ul></ul><ul><ul><li><a class='header-3' href='#-----.save_state-'><code>.save_state</code></a></li></ul></ul><ul><ul><li><a class='header-3' href='#-----.load_state-'><code>.load_state</code></a></li></ul></ul><ul><ul><li><a class='header-3' href='#-----.serialize_state-file,-state-'><code>.serialize_state file, state</code></a></li></ul></ul><ul><ul><li><a class='header-3' href='#-----.deserialize_state-file-'><code>.deserialize_state file</code></a></li></ul></ul><ul><ul><li><a class='header-3' href='#-----.reset_sprite-path-'><code>.reset_sprite path</code></a></li></ul></ul><ul><ul><li><a class='header-3' href='#-----.show_cursor-'><code>.show_cursor</code></a></li></ul></ul><ul><ul><li><a class='header-3' href='#-----.hide_cursor-'><code>.hide_cursor</code></a></li></ul></ul><ul><ul><li><a class='header-3' href='#-----.cursor_shown?-'><code>.cursor_shown?</code></a></li></ul></ul><ul><ul><li><a class='header-3' href='#-----.set_window_fullscreen-enabled-'><code>.set_window_fullscreen enabled</code></a></li></ul></ul><ul><ul><li><a class='header-3' href='#-----.openurl-url-'><code>.openurl url</code></a></li></ul></ul><ul><ul><li><a class='header-3' href='#-----.get_base_dir-'><code>.get_base_dir</code></a></li></ul></ul><ul><ul><li><a class='header-3' href='#-----.get_game_dir-'><code>.get_game_dir</code></a></li></ul></ul><li><a class='header-1' href='#--docs---gtk--runtime#reset-'>DOCS: <code>GTK::Runtime#reset</code></a></li> +<li><a class='header-1' href='#--docs---gtk--runtime#calcstringbox-'>DOCS: <code>GTK::Runtime#calcstringbox</code></a></li> +<li><a class='header-1' href='#--docs---gtk--runtime#write_file-'>DOCS: <code>GTK::Runtime#write_file</code></a></li> +<li><a class='header-1' href='#--docs---array-'>DOCS: <code>Array</code></a></li> +<li><a class='header-1' href='#--docs---array#map-'>DOCS: <code>Array#map</code></a></li> +<li><a class='header-1' href='#--docs---array#each-'>DOCS: <code>Array#each</code></a></li> +<li><a class='header-1' href='#--docs---array#reject_nil-'>DOCS: <code>Array#reject_nil</code></a></li> +<li><a class='header-1' href='#--docs---array#reject_false-'>DOCS: <code>Array#reject_false</code></a></li> +<li><a class='header-1' href='#--docs---array#product-'>DOCS: <code>Array#product</code></a></li> +<li><a class='header-1' href='#--docs---array#map_2d-'>DOCS: <code>Array#map_2d</code></a></li> +<li><a class='header-1' href='#--docs---array#include_any?-'>DOCS: <code>Array#include_any?</code></a></li> +<li><a class='header-1' href='#--docs---array#any_intersect_rect?-'>DOCS: <code>Array#any_intersect_rect?</code></a></li> +<li><a class='header-1' href='#--docs---gtk--args#audio-'>DOCS: <code>GTK::Args#audio</code></a></li> +<ul><li><a class='header-2' href='#---audio-synthesis-(pro-only)'>Audio synthesis (Pro only)</a></li></ul><ul><ul><li><a class='header-3' href='#----sound-source'>Sound source</a></li></ul></ul><ul><ul><li><a class='header-3' href='#----example-'>Example:</a></li></ul></ul><li><a class='header-1' href='#--docs---gtk--outputs-'>DOCS: <code>GTK::Outputs</code></a></li> +<li><a class='header-1' href='#--docs---gtk--outputs#solids-'>DOCS: <code>GTK::Outputs#solids</code></a></li> +<ul><li><a class='header-2' href='#---rendering-a-solid-using-an-array'>Rendering a solid using an Array</a></li></ul><ul><li><a class='header-2' href='#---rendering-a-solid-using-an-array-with-colors-and-alpha'>Rendering a solid using an Array with colors and alpha</a></li></ul><ul><li><a class='header-2' href='#---rendering-a-solid-using-a-hash'>Rendering a solid using a Hash</a></li></ul><ul><li><a class='header-2' href='#---rendering-a-solid-using-a-class'>Rendering a solid using a Class</a></li></ul><li><a class='header-1' href='#--docs---gtk--outputs#borders-'>DOCS: <code>GTK::Outputs#borders</code></a></li> +<li><a class='header-1' href='#--docs---gtk--outputs#screenshots-'>DOCS: <code>GTK::Outputs#screenshots</code></a></li> +<ul><li><a class='header-2' href='#---chroma-key-(making-a-color-transparent)'>Chroma key (Making a color transparent)</a></li></ul><li><a class='header-1' href='#--docs---gtk--mouse-'>DOCS: <code>GTK::Mouse</code></a></li> +<li><a class='header-1' href='#--docs---gtk--mousepoint-'>DOCS: <code>GTK::MousePoint</code></a></li> +<li><a class='header-1' href='#--docs---gtk--openentity-'>DOCS: <code>GTK::OpenEntity</code></a></li> +<li><a class='header-1' href='#--docs---gtk--openentity#as_hash-'>DOCS: <code>GTK::OpenEntity#as_hash</code></a></li> +<li><a class='header-1' href='#--docs---numeric#frame_index-'>DOCS: <code>Numeric#frame_index</code></a></li> +<li><a class='header-1' href='#--docs---numeric#elapsed_time-'>DOCS: <code>Numeric#elapsed_time</code></a></li> +<li><a class='header-1' href='#--docs---numeric#elapsed?-'>DOCS: <code>Numeric#elapsed?</code></a></li> +<li><a class='header-1' href='#--docs---numeric#created?-'>DOCS: <code>Numeric#created?</code></a></li> +<li><a class='header-1' href='#--docs---kernel-'>DOCS: <code>Kernel</code></a></li> +<li><a class='header-1' href='#--docs---kernel--tick_count-'>DOCS: <code>Kernel::tick_count</code></a></li> +<li><a class='header-1' href='#--docs---kernel--global_tick_count-'>DOCS: <code>Kernel::global_tick_count</code></a></li> +<li><a class='header-1' href='#--docs---geometry-'>DOCS: <code>Geometry</code></a></li> +<li><a class='header-1' href='#--docs---gtk--geometry#scale_rect-'>DOCS: <code>GTK::Geometry#scale_rect</code></a></li> +<li><a class='header-1' href='#--source-code'>Source Code</a></li> +<ul><li><a class='header-2' href='#---samples'>Samples</a></li></ul><ul><ul><li><a class='header-3' href='#----learn-ruby-optional---beginner-ruby-primer---automation.rb'>Learn Ruby Optional - Beginner Ruby Primer - automation.rb</a></li></ul></ul><ul><ul><li><a class='header-3' href='#----learn-ruby-optional---beginner-ruby-primer---main.rb'>Learn Ruby Optional - Beginner Ruby Primer - main.rb</a></li></ul></ul><ul><ul><li><a class='header-3' href='#----learn-ruby-optional---intermediate-ruby-primer---printing.txt'>Learn Ruby Optional - Intermediate Ruby Primer - printing.txt</a></li></ul></ul><ul><ul><li><a class='header-3' href='#----learn-ruby-optional---intermediate-ruby-primer---strings.txt'>Learn Ruby Optional - Intermediate Ruby Primer - strings.txt</a></li></ul></ul><ul><ul><li><a class='header-3' href='#----learn-ruby-optional---intermediate-ruby-primer---numbers.txt'>Learn Ruby Optional - Intermediate Ruby Primer - numbers.txt</a></li></ul></ul><ul><ul><li><a class='header-3' href='#----learn-ruby-optional---intermediate-ruby-primer---booleans.txt'>Learn Ruby Optional - Intermediate Ruby Primer - booleans.txt</a></li></ul></ul><ul><ul><li><a class='header-3' href='#----learn-ruby-optional---intermediate-ruby-primer---conditionals.txt'>Learn Ruby Optional - Intermediate Ruby Primer - conditionals.txt</a></li></ul></ul><ul><ul><li><a class='header-3' href='#----learn-ruby-optional---intermediate-ruby-primer---looping.txt'>Learn Ruby Optional - Intermediate Ruby Primer - looping.txt</a></li></ul></ul><ul><ul><li><a class='header-3' href='#----learn-ruby-optional---intermediate-ruby-primer---functions.txt'>Learn Ruby Optional - Intermediate Ruby Primer - functions.txt</a></li></ul></ul><ul><ul><li><a class='header-3' href='#----learn-ruby-optional---intermediate-ruby-primer---arrays.txt'>Learn Ruby Optional - Intermediate Ruby Primer - arrays.txt</a></li></ul></ul><ul><ul><li><a class='header-3' href='#----learn-ruby-optional---intermediate-ruby-primer---main.rb'>Learn Ruby Optional - Intermediate Ruby Primer - main.rb</a></li></ul></ul><ul><ul><li><a class='header-3' href='#----rendering-basics---labels---main.rb'>Rendering Basics - Labels - main.rb</a></li></ul></ul><ul><ul><li><a class='header-3' href='#----rendering-basics---lines---main.rb'>Rendering Basics - Lines - main.rb</a></li></ul></ul><ul><ul><li><a class='header-3' href='#----rendering-basics---solids-borders---main.rb'>Rendering Basics - Solids Borders - main.rb</a></li></ul></ul><ul><ul><li><a class='header-3' href='#----rendering-basics---sprites---main.rb'>Rendering Basics - Sprites - main.rb</a></li></ul></ul><ul><ul><li><a class='header-3' href='#----rendering-basics---sounds---main.rb'>Rendering Basics - Sounds - main.rb</a></li></ul></ul><ul><ul><li><a class='header-3' href='#----rendering-basics---audio-mixer---main.rb'>Rendering Basics - Audio Mixer - main.rb</a></li></ul></ul><ul><ul><li><a class='header-3' href='#----rendering-basics---sound-synthesis---main.rb'>Rendering Basics - Sound Synthesis - main.rb</a></li></ul></ul><ul><ul><li><a class='header-3' href='#----input-basics---keyboard---main.rb'>Input Basics - Keyboard - main.rb</a></li></ul></ul><ul><ul><li><a class='header-3' href='#----input-basics---mouse---main.rb'>Input Basics - Mouse - main.rb</a></li></ul></ul><ul><ul><li><a class='header-3' href='#----input-basics---mouse-point-to-rect---main.rb'>Input Basics - Mouse Point To Rect - main.rb</a></li></ul></ul><ul><ul><li><a class='header-3' href='#----input-basics---mouse-rect-to-rect---main.rb'>Input Basics - Mouse Rect To Rect - main.rb</a></li></ul></ul><ul><ul><li><a class='header-3' href='#----input-basics---controller---main.rb'>Input Basics - Controller - main.rb</a></li></ul></ul><ul><ul><li><a class='header-3' href='#----input-basics---touch---main.rb'>Input Basics - Touch - main.rb</a></li></ul></ul><ul><ul><li><a class='header-3' href='#----rendering-sprites---animation-using-separate-pngs---main.rb'>Rendering Sprites - Animation Using Separate Pngs - main.rb</a></li></ul></ul><ul><ul><li><a class='header-3' href='#----rendering-sprites---animation-using-sprite-sheet---main.rb'>Rendering Sprites - Animation Using Sprite Sheet - main.rb</a></li></ul></ul><ul><ul><li><a class='header-3' href='#----rendering-sprites---animation-states---main.rb'>Rendering Sprites - Animation States - main.rb</a></li></ul></ul><ul><ul><li><a class='header-3' href='#----rendering-sprites---color-and-rotation---main.rb'>Rendering Sprites - Color And Rotation - main.rb</a></li></ul></ul><ul><ul><li><a class='header-3' href='#----physics-and-collisions---simple---main.rb'>Physics And Collisions - Simple - main.rb</a></li></ul></ul><ul><ul><li><a class='header-3' href='#----physics-and-collisions---moving-objects---main.rb'>Physics And Collisions - Moving Objects - main.rb</a></li></ul></ul><ul><ul><li><a class='header-3' href='#----physics-and-collisions---entities---main.rb'>Physics And Collisions - Entities - main.rb</a></li></ul></ul><ul><ul><li><a class='header-3' href='#----physics-and-collisions---box-collision---main.rb'>Physics And Collisions - Box Collision - main.rb</a></li></ul></ul><ul><ul><li><a class='header-3' href='#----physics-and-collisions---box-collision-2---main.rb'>Physics And Collisions - Box Collision 2 - main.rb</a></li></ul></ul><ul><ul><li><a class='header-3' href='#----physics-and-collisions---box-collision-3---main.rb'>Physics And Collisions - Box Collision 3 - main.rb</a></li></ul></ul><ul><ul><li><a class='header-3' href='#----physics-and-collisions---jump-physics---main.rb'>Physics And Collisions - Jump Physics - main.rb</a></li></ul></ul><ul><ul><li><a class='header-3' href='#----physics-and-collisions---bouncing-on-collision---ball.rb'>Physics And Collisions - Bouncing On Collision - ball.rb</a></li></ul></ul><ul><ul><li><a class='header-3' href='#----physics-and-collisions---bouncing-on-collision---block.rb'>Physics And Collisions - Bouncing On Collision - block.rb</a></li></ul></ul><ul><ul><li><a class='header-3' href='#----physics-and-collisions---bouncing-on-collision---cannon.rb'>Physics And Collisions - Bouncing On Collision - cannon.rb</a></li></ul></ul><ul><ul><li><a class='header-3' href='#----physics-and-collisions---bouncing-on-collision---main.rb'>Physics And Collisions - Bouncing On Collision - main.rb</a></li></ul></ul><ul><ul><li><a class='header-3' href='#----physics-and-collisions---bouncing-on-collision---peg.rb'>Physics And Collisions - Bouncing On Collision - peg.rb</a></li></ul></ul><ul><ul><li><a class='header-3' href='#----physics-and-collisions---bouncing-on-collision---vector2d.rb'>Physics And Collisions - Bouncing On Collision - vector2d.rb</a></li></ul></ul><ul><ul><li><a class='header-3' href='#----physics-and-collisions---arbitrary-collision---ball.rb'>Physics And Collisions - Arbitrary Collision - ball.rb</a></li></ul></ul><ul><ul><li><a class='header-3' href='#----physics-and-collisions---arbitrary-collision---blocks.rb'>Physics And Collisions - Arbitrary Collision - blocks.rb</a></li></ul></ul><ul><ul><li><a class='header-3' href='#----physics-and-collisions---arbitrary-collision---linear_collider.rb'>Physics And Collisions - Arbitrary Collision - linear_collider.rb</a></li></ul></ul><ul><ul><li><a class='header-3' href='#----physics-and-collisions---arbitrary-collision---main.rb'>Physics And Collisions - Arbitrary Collision - main.rb</a></li></ul></ul><ul><ul><li><a class='header-3' href='#----physics-and-collisions---arbitrary-collision---paddle.rb'>Physics And Collisions - Arbitrary Collision - paddle.rb</a></li></ul></ul><ul><ul><li><a class='header-3' href='#----physics-and-collisions---arbitrary-collision---rectangle.rb'>Physics And Collisions - Arbitrary Collision - rectangle.rb</a></li></ul></ul><ul><ul><li><a class='header-3' href='#----physics-and-collisions---arbitrary-collision---square_collider.rb'>Physics And Collisions - Arbitrary Collision - square_collider.rb</a></li></ul></ul><ul><ul><li><a class='header-3' href='#----physics-and-collisions---arbitrary-collision---vector2d.rb'>Physics And Collisions - Arbitrary Collision - vector2d.rb</a></li></ul></ul><ul><ul><li><a class='header-3' href='#----physics-and-collisions---collision-with-object-removal---ball.rb'>Physics And Collisions - Collision With Object Removal - ball.rb</a></li></ul></ul><ul><ul><li><a class='header-3' href='#----physics-and-collisions---collision-with-object-removal---linear_collider.rb'>Physics And Collisions - Collision With Object Removal - linear_collider.rb</a></li></ul></ul><ul><ul><li><a class='header-3' href='#----physics-and-collisions---collision-with-object-removal---main.rb'>Physics And Collisions - Collision With Object Removal - main.rb</a></li></ul></ul><ul><ul><li><a class='header-3' href='#----physics-and-collisions---collision-with-object-removal---paddle.rb'>Physics And Collisions - Collision With Object Removal - paddle.rb</a></li></ul></ul><ul><ul><li><a class='header-3' href='#----physics-and-collisions---collision-with-object-removal---tests.rb'>Physics And Collisions - Collision With Object Removal - tests.rb</a></li></ul></ul><ul><ul><li><a class='header-3' href='#----physics-and-collisions---collision-with-object-removal---vector2d.rb'>Physics And Collisions - Collision With Object Removal - vector2d.rb</a></li></ul></ul><ul><ul><li><a class='header-3' href='#----mouse---mouse-click---main.rb'>Mouse - Mouse Click - main.rb</a></li></ul></ul><ul><ul><li><a class='header-3' href='#----mouse---mouse-move---main.rb'>Mouse - Mouse Move - main.rb</a></li></ul></ul><ul><ul><li><a class='header-3' href='#----mouse---mouse-move-paint-app---main.rb'>Mouse - Mouse Move Paint App - main.rb</a></li></ul></ul><ul><ul><li><a class='header-3' href='#----mouse---coordinate-systems---main.rb'>Mouse - Coordinate Systems - main.rb</a></li></ul></ul><ul><ul><li><a class='header-3' href='#----save-load---save-load-game---main.rb'>Save Load - Save Load Game - main.rb</a></li></ul></ul><ul><ul><li><a class='header-3' href='#----advanced-rendering---simple-render-targets---main.rb'>Advanced Rendering - Simple Render Targets - main.rb</a></li></ul></ul><ul><ul><li><a class='header-3' href='#----advanced-rendering---render-targets-with-tile-manipulation---main.rb'>Advanced Rendering - Render Targets With Tile Manipulation - main.rb</a></li></ul></ul><ul><ul><li><a class='header-3' href='#----advanced-rendering---render-target-viewports---main.rb'>Advanced Rendering - Render Target Viewports - main.rb</a></li></ul></ul><ul><ul><li><a class='header-3' href='#----advanced-rendering---render-primitive-hierarchies---main.rb'>Advanced Rendering - Render Primitive Hierarchies - main.rb</a></li></ul></ul><ul><ul><li><a class='header-3' href='#----advanced-rendering---render-primitives-as-hash---main.rb'>Advanced Rendering - Render Primitives As Hash - main.rb</a></li></ul></ul><ul><ul><li><a class='header-3' href='#----advanced-rendering---pixel-arrays---main.rb'>Advanced Rendering - Pixel Arrays - main.rb</a></li></ul></ul><ul><ul><li><a class='header-3' href='#----advanced-rendering---splitscreen-camera---main.rb'>Advanced Rendering - Splitscreen Camera - main.rb</a></li></ul></ul><ul><ul><li><a class='header-3' href='#----advanced-rendering---z-targeting-camera---main.rb'>Advanced Rendering - Z Targeting Camera - main.rb</a></li></ul></ul><ul><ul><li><a class='header-3' href='#----tweening-lerping-easing-functions---easing-functions---main.rb'>Tweening Lerping Easing Functions - Easing Functions - main.rb</a></li></ul></ul><ul><ul><li><a class='header-3' href='#----tweening-lerping-easing-functions---cubic-bezier---main.rb'>Tweening Lerping Easing Functions - Cubic Bezier - main.rb</a></li></ul></ul><ul><ul><li><a class='header-3' href='#----tweening-lerping-easing-functions---easing-using-spline---main.rb'>Tweening Lerping Easing Functions - Easing Using Spline - main.rb</a></li></ul></ul><ul><ul><li><a class='header-3' href='#----tweening-lerping-easing-functions---parametric-enemy-movement---main.rb'>Tweening Lerping Easing Functions - Parametric Enemy Movement - main.rb</a></li></ul></ul><ul><ul><li><a class='header-3' href='#----performance---sprites-as-hash---main.rb'>Performance - Sprites As Hash - main.rb</a></li></ul></ul><ul><ul><li><a class='header-3' href='#----performance---sprites-as-entities---main.rb'>Performance - Sprites As Entities - main.rb</a></li></ul></ul><ul><ul><li><a class='header-3' href='#----performance---sprites-as-strict-entities---main.rb'>Performance - Sprites As Strict Entities - main.rb</a></li></ul></ul><ul><ul><li><a class='header-3' href='#----performance---sprites-as-classes---main.rb'>Performance - Sprites As Classes - main.rb</a></li></ul></ul><ul><ul><li><a class='header-3' href='#----performance---static-sprites-as-classes---main.rb'>Performance - Static Sprites As Classes - main.rb</a></li></ul></ul><ul><ul><li><a class='header-3' href='#----performance---static-sprites-as-classes-with-custom-drawing---main.rb'>Performance - Static Sprites As Classes With Custom Drawing - main.rb</a></li></ul></ul><ul><ul><li><a class='header-3' href='#----performance---collision-limits---main.rb'>Performance - Collision Limits - main.rb</a></li></ul></ul><ul><ul><li><a class='header-3' href='#----advanced-debugging---trace-debugging---main.rb'>Advanced Debugging - Trace Debugging - main.rb</a></li></ul></ul><ul><ul><li><a class='header-3' href='#----advanced-debugging---trace-debugging-classes---main.rb'>Advanced Debugging - Trace Debugging Classes - main.rb</a></li></ul></ul><ul><ul><li><a class='header-3' href='#----advanced-debugging---unit-tests---exception_raising_tests.rb'>Advanced Debugging - Unit Tests - exception_raising_tests.rb</a></li></ul></ul><ul><ul><li><a class='header-3' href='#----advanced-debugging---unit-tests---fn_tests.rb'>Advanced Debugging - Unit Tests - fn_tests.rb</a></li></ul></ul><ul><ul><li><a class='header-3' href='#----advanced-debugging---unit-tests---gen_docs.rb'>Advanced Debugging - Unit Tests - gen_docs.rb</a></li></ul></ul><ul><ul><li><a class='header-3' href='#----advanced-debugging---unit-tests---geometry_tests.rb'>Advanced Debugging - Unit Tests - geometry_tests.rb</a></li></ul></ul><ul><ul><li><a class='header-3' href='#----advanced-debugging---unit-tests---http_tests.rb'>Advanced Debugging - Unit Tests - http_tests.rb</a></li></ul></ul><ul><ul><li><a class='header-3' href='#----advanced-debugging---unit-tests---nil_coercion_tests.rb'>Advanced Debugging - Unit Tests - nil_coercion_tests.rb</a></li></ul></ul><ul><ul><li><a class='header-3' href='#----advanced-debugging---unit-tests---object_to_primitive_tests.rb'>Advanced Debugging - Unit Tests - object_to_primitive_tests.rb</a></li></ul></ul><ul><ul><li><a class='header-3' href='#----advanced-debugging---unit-tests---parsing_tests.rb'>Advanced Debugging - Unit Tests - parsing_tests.rb</a></li></ul></ul><ul><ul><li><a class='header-3' href='#----advanced-debugging---unit-tests---require_tests.rb'>Advanced Debugging - Unit Tests - require_tests.rb</a></li></ul></ul><ul><ul><li><a class='header-3' href='#----advanced-debugging---unit-tests---serialize_deserialize_tests.rb'>Advanced Debugging - Unit Tests - serialize_deserialize_tests.rb</a></li></ul></ul><ul><ul><li><a class='header-3' href='#----advanced-debugging---unit-tests---state_serialization_experimental_tests.rb'>Advanced Debugging - Unit Tests - state_serialization_experimental_tests.rb</a></li></ul></ul><ul><ul><li><a class='header-3' href='#----advanced-debugging---unit-tests---suggest_autocompletion_tests.rb'>Advanced Debugging - Unit Tests - suggest_autocompletion_tests.rb</a></li></ul></ul><ul><ul><li><a class='header-3' href='#----http---retrieve-images---main.rb'>Http - Retrieve Images - main.rb</a></li></ul></ul><ul><ul><li><a class='header-3' href='#----http---web-server---main.rb'>Http - Web Server - main.rb</a></li></ul></ul><ul><ul><li><a class='header-3' href='#----c-extensions---basics---main.rb'>C Extensions - Basics - main.rb</a></li></ul></ul><ul><ul><li><a class='header-3' href='#----c-extensions---intermediate---main.rb'>C Extensions - Intermediate - main.rb</a></li></ul></ul><ul><ul><li><a class='header-3' href='#----c-extensions---native-pixel-arrays---main.rb'>C Extensions - Native Pixel Arrays - main.rb</a></li></ul></ul><ul><ul><li><a class='header-3' href='#----path-finding-algorithms---breadth-first-search---main.rb'>Path Finding Algorithms - Breadth First Search - main.rb</a></li></ul></ul><ul><ul><li><a class='header-3' href='#----path-finding-algorithms---detailed-breadth-first-search---main.rb'>Path Finding Algorithms - Detailed Breadth First Search - main.rb</a></li></ul></ul><ul><ul><li><a class='header-3' href='#----path-finding-algorithms---breadcrumbs---main.rb'>Path Finding Algorithms - Breadcrumbs - main.rb</a></li></ul></ul><ul><ul><li><a class='header-3' href='#----path-finding-algorithms---early-exit---main.rb'>Path Finding Algorithms - Early Exit - main.rb</a></li></ul></ul><ul><ul><li><a class='header-3' href='#----path-finding-algorithms---dijkstra---main.rb'>Path Finding Algorithms - Dijkstra - main.rb</a></li></ul></ul><ul><ul><li><a class='header-3' href='#----path-finding-algorithms---heuristic---main.rb'>Path Finding Algorithms - Heuristic - main.rb</a></li></ul></ul><ul><ul><li><a class='header-3' href='#----path-finding-algorithms---heuristic-with-walls---main.rb'>Path Finding Algorithms - Heuristic With Walls - main.rb</a></li></ul></ul><ul><ul><li><a class='header-3' href='#----path-finding-algorithms---a-star---main.rb'>Path Finding Algorithms - A Star - main.rb</a></li></ul></ul><ul><ul><li><a class='header-3' href='#----path-finding-algorithms---tower-defense---main.rb'>Path Finding Algorithms - Tower Defense - main.rb</a></li></ul></ul><ul><ul><li><a class='header-3' href='#----3d---3d-cube---main.rb'>3d - 3d Cube - main.rb</a></li></ul></ul><ul><ul><li><a class='header-3' href='#----3d---wireframe---main.rb'>3d - Wireframe - main.rb</a></li></ul></ul><ul><ul><li><a class='header-3' href='#----3d---wireframe---data---what-is-this.txt'>3d - Wireframe - Data - what-is-this.txt</a></li></ul></ul><ul><ul><li><a class='header-3' href='#----arcade---bullet-hell---main.rb'>Arcade - Bullet Hell - main.rb</a></li></ul></ul><ul><ul><li><a class='header-3' href='#----arcade---dueling-starships---main.rb'>Arcade - Dueling Starships - main.rb</a></li></ul></ul><ul><ul><li><a class='header-3' href='#----arcade/flappy-dragon/credits.txt'>arcade/flappy dragon/credits.txt</a></li></ul></ul><ul><ul><li><a class='header-3' href='#----arcade/flappy-dragon/main.rb'>arcade/flappy dragon/main.rb</a></li></ul></ul><ul><ul><li><a class='header-3' href='#----arcade---pong---main.rb'>Arcade - Pong - main.rb</a></li></ul></ul><ul><ul><li><a class='header-3' href='#----arcade---snakemoji---main.rb'>Arcade - Snakemoji - main.rb</a></li></ul></ul><ul><ul><li><a class='header-3' href='#----arcade---solar-system---main.rb'>Arcade - Solar System - main.rb</a></li></ul></ul><ul><ul><li><a class='header-3' href='#----arcade---sound-golf---main.rb'>Arcade - Sound Golf - main.rb</a></li></ul></ul><ul><ul><li><a class='header-3' href='#----arcade---twinstick---main.rb'>Arcade - Twinstick - main.rb</a></li></ul></ul><ul><ul><li><a class='header-3' href='#----crafting---craft-game-starting-point---main.rb'>Crafting - Craft Game Starting Point - main.rb</a></li></ul></ul><ul><ul><li><a class='header-3' href='#----dev-tools---add-buttons-to-console---main.rb'>Dev Tools - Add Buttons To Console - main.rb</a></li></ul></ul><ul><ul><li><a class='header-3' href='#----dev-tools---animation-creator-starting-point---main.rb'>Dev Tools - Animation Creator Starting Point - main.rb</a></li></ul></ul><ul><ul><li><a class='header-3' href='#----dev-tools---tile-editor-starting-point---main.rb'>Dev Tools - Tile Editor Starting Point - main.rb</a></li></ul></ul><ul><ul><li><a class='header-3' href='#----lowrez---nokia-3310---main.rb'>Lowrez - Nokia 3310 - main.rb</a></li></ul></ul><ul><ul><li><a class='header-3' href='#----lowrez---nokia-3310---nokia.rb'>Lowrez - Nokia 3310 - nokia.rb</a></li></ul></ul><ul><ul><li><a class='header-3' href='#----lowrez---resolution-64x64---lowrez.rb'>Lowrez - Resolution 64x64 - lowrez.rb</a></li></ul></ul><ul><ul><li><a class='header-3' href='#----lowrez---resolution-64x64---main.rb'>Lowrez - Resolution 64x64 - main.rb</a></li></ul></ul><ul><ul><li><a class='header-3' href='#----platformer---clepto-frog---main.rb'>Platformer - Clepto Frog - main.rb</a></li></ul></ul><ul><ul><li><a class='header-3' href='#----platformer---clepto-frog---map.rb'>Platformer - Clepto Frog - map.rb</a></li></ul></ul><ul><ul><li><a class='header-3' href='#----platformer---gorillas-basic---credits.txt'>Platformer - Gorillas Basic - credits.txt</a></li></ul></ul><ul><ul><li><a class='header-3' href='#----platformer---gorillas-basic---main.rb'>Platformer - Gorillas Basic - main.rb</a></li></ul></ul><ul><ul><li><a class='header-3' href='#----platformer---gorillas-basic---tests.rb'>Platformer - Gorillas Basic - tests.rb</a></li></ul></ul><ul><ul><li><a class='header-3' href='#----platformer---gorillas-basic---tests---building_generation_tests.rb'>Platformer - Gorillas Basic - Tests - building_generation_tests.rb</a></li></ul></ul><ul><ul><li><a class='header-3' href='#----platformer---the-little-probe---main.rb'>Platformer - The Little Probe - main.rb</a></li></ul></ul><ul><ul><li><a class='header-3' href='#----platformer---the-little-probe---data---level.txt'>Platformer - The Little Probe - Data - level.txt</a></li></ul></ul><ul><ul><li><a class='header-3' href='#----platformer---the-little-probe---data---level_lava.txt'>Platformer - The Little Probe - Data - level_lava.txt</a></li></ul></ul><ul><ul><li><a class='header-3' href='#----rpg-narrative---choose-your-own-adventure---decision.rb'>Rpg Narrative - Choose Your Own Adventure - decision.rb</a></li></ul></ul><ul><ul><li><a class='header-3' href='#----rpg-narrative---choose-your-own-adventure---main.rb'>Rpg Narrative - Choose Your Own Adventure - main.rb</a></li></ul></ul><ul><ul><li><a class='header-3' href='#----rpg-narrative---return-of-serenity---lowrez_simulator.rb'>Rpg Narrative - Return Of Serenity - lowrez_simulator.rb</a></li></ul></ul><ul><ul><li><a class='header-3' href='#----rpg-narrative---return-of-serenity---main.rb'>Rpg Narrative - Return Of Serenity - main.rb</a></li></ul></ul><ul><ul><li><a class='header-3' href='#----rpg-narrative---return-of-serenity---require.rb'>Rpg Narrative - Return Of Serenity - require.rb</a></li></ul></ul><ul><ul><li><a class='header-3' href='#----rpg-narrative---return-of-serenity---storyline.rb'>Rpg Narrative - Return Of Serenity - storyline.rb</a></li></ul></ul><ul><ul><li><a class='header-3' href='#----rpg-narrative---return-of-serenity---storyline_anka.rb'>Rpg Narrative - Return Of Serenity - storyline_anka.rb</a></li></ul></ul><ul><ul><li><a class='header-3' href='#----rpg-narrative---return-of-serenity---storyline_blinking_light.rb'>Rpg Narrative - Return Of Serenity - storyline_blinking_light.rb</a></li></ul></ul><ul><ul><li><a class='header-3' href='#----rpg-narrative---return-of-serenity---storyline_day_one.rb'>Rpg Narrative - Return Of Serenity - storyline_day_one.rb</a></li></ul></ul><ul><ul><li><a class='header-3' href='#----rpg-narrative---return-of-serenity---storyline_final_decision.rb'>Rpg Narrative - Return Of Serenity - storyline_final_decision.rb</a></li></ul></ul><ul><ul><li><a class='header-3' href='#----rpg-narrative---return-of-serenity---storyline_final_message.rb'>Rpg Narrative - Return Of Serenity - storyline_final_message.rb</a></li></ul></ul><ul><ul><li><a class='header-3' href='#----rpg-narrative---return-of-serenity---storyline_serenity_alive.rb'>Rpg Narrative - Return Of Serenity - storyline_serenity_alive.rb</a></li></ul></ul><ul><ul><li><a class='header-3' href='#----rpg-narrative---return-of-serenity---storyline_serenity_bio.rb'>Rpg Narrative - Return Of Serenity - storyline_serenity_bio.rb</a></li></ul></ul><ul><ul><li><a class='header-3' href='#----rpg-narrative---return-of-serenity---storyline_serenity_introduction.rb'>Rpg Narrative - Return Of Serenity - storyline_serenity_introduction.rb</a></li></ul></ul><ul><ul><li><a class='header-3' href='#----rpg-narrative---return-of-serenity---storyline_speed_of_light.rb'>Rpg Narrative - Return Of Serenity - storyline_speed_of_light.rb</a></li></ul></ul><ul><ul><li><a class='header-3' href='#----rpg-roguelike---roguelike-starting-point---constants.rb'>Rpg Roguelike - Roguelike Starting Point - constants.rb</a></li></ul></ul><ul><ul><li><a class='header-3' href='#----rpg-roguelike---roguelike-starting-point---legend.rb'>Rpg Roguelike - Roguelike Starting Point - legend.rb</a></li></ul></ul><ul><ul><li><a class='header-3' href='#----rpg-roguelike---roguelike-starting-point---main.rb'>Rpg Roguelike - Roguelike Starting Point - main.rb</a></li></ul></ul><ul><ul><li><a class='header-3' href='#----rpg-roguelike---roguelike-starting-point---sprite_lookup.rb'>Rpg Roguelike - Roguelike Starting Point - sprite_lookup.rb</a></li></ul></ul><ul><ul><li><a class='header-3' href='#----rpg-roguelike---roguelike-line-of-sight---main.rb'>Rpg Roguelike - Roguelike Line Of Sight - main.rb</a></li></ul></ul><ul><ul><li><a class='header-3' href='#----rpg-tactical---hexagonal-grid---main.rb'>Rpg Tactical - Hexagonal Grid - main.rb</a></li></ul></ul><ul><ul><li><a class='header-3' href='#----rpg-tactical---isometric-grid---main.rb'>Rpg Tactical - Isometric Grid - main.rb</a></li></ul></ul><ul><ul><li><a class='header-3' href='#----rpg-topdown---topdown-starting-point---main.rb'>Rpg Topdown - Topdown Starting Point - main.rb</a></li></ul></ul><ul><li><a class='header-2' href='#---oss'>OSS</a></li></ul><ul><ul><li><a class='header-3' href='#----args.rb'>args.rb</a></li></ul></ul><ul><ul><li><a class='header-3' href='#----assert.rb'>assert.rb</a></li></ul></ul><ul><ul><li><a class='header-3' href='#----attr_gtk.rb'>attr_gtk.rb</a></li></ul></ul><ul><ul><li><a class='header-3' href='#----attr_sprite.rb'>attr_sprite.rb</a></li></ul></ul><ul><ul><li><a class='header-3' href='#----console.rb'>console.rb</a></li></ul></ul><ul><ul><li><a class='header-3' href='#----console_color.rb'>console_color.rb</a></li></ul></ul><ul><ul><li><a class='header-3' href='#----console_font_style.rb'>console_font_style.rb</a></li></ul></ul><ul><ul><li><a class='header-3' href='#----console_menu.rb'>console_menu.rb</a></li></ul></ul><ul><ul><li><a class='header-3' href='#----console_prompt.rb'>console_prompt.rb</a></li></ul></ul><ul><ul><li><a class='header-3' href='#----controller.rb'>controller.rb</a></li></ul></ul><ul><ul><li><a class='header-3' href='#----controller/config.rb'>controller/config.rb</a></li></ul></ul><ul><ul><li><a class='header-3' href='#----controller/keys.rb'>controller/keys.rb</a></li></ul></ul><ul><ul><li><a class='header-3' href='#----directional_input_helper_methods.rb'>directional_input_helper_methods.rb</a></li></ul></ul><ul><ul><li><a class='header-3' href='#----easing.rb'>easing.rb</a></li></ul></ul><ul><ul><li><a class='header-3' href='#----entity.rb'>entity.rb</a></li></ul></ul><ul><ul><li><a class='header-3' href='#----geometry.rb'>geometry.rb</a></li></ul></ul><ul><ul><li><a class='header-3' href='#----grid.rb'>grid.rb</a></li></ul></ul><ul><ul><li><a class='header-3' href='#----inputs.rb'>inputs.rb</a></li></ul></ul><ul><ul><li><a class='header-3' href='#----ios_wizard.rb'>ios_wizard.rb</a></li></ul></ul><ul><ul><li><a class='header-3' href='#----itch_wizard.rb'>itch_wizard.rb</a></li></ul></ul><ul><ul><li><a class='header-3' href='#----layout.rb'>layout.rb</a></li></ul></ul><ul><ul><li><a class='header-3' href='#----log.rb'>log.rb</a></li></ul></ul><ul><ul><li><a class='header-3' href='#----numeric.rb'>numeric.rb</a></li></ul></ul><ul><ul><li><a class='header-3' href='#----remote_hotload_client.rb'>remote_hotload_client.rb</a></li></ul></ul><ul><ul><li><a class='header-3' href='#----runtime/autocomplete.rb'>runtime/autocomplete.rb</a></li></ul></ul><ul><ul><li><a class='header-3' href='#----runtime/draw.rb'>runtime/draw.rb</a></li></ul></ul><ul><ul><li><a class='header-3' href='#----runtime/framerate.rb'>runtime/framerate.rb</a></li></ul></ul><ul><ul><li><a class='header-3' href='#----runtime/framerate_diagnostics.rb'>runtime/framerate_diagnostics.rb</a></li></ul></ul><ul><ul><li><a class='header-3' href='#----runtime/hotload.rb'>runtime/hotload.rb</a></li></ul></ul><ul><ul><li><a class='header-3' href='#----string.rb'>string.rb</a></li></ul></ul><ul><ul><li><a class='header-3' href='#----tests.rb'>tests.rb</a></li></ul></ul><ul><ul><li><a class='header-3' href='#----trace.rb'>trace.rb</a></li></ul></ul><ul><ul><li><a class='header-3' href='#----wizards.rb'>wizards.rb</a></li></ul></ul></ul> </div> <div id='content'> <h1 id='--dragonruby-game-toolkit-live-docs'>DragonRuby Game Toolkit Live Docs</h1> <p> @@ -211,8 +59,8 @@ The information contained here is all available via the DragonRuby Console. You <p> To search docs you can type <code>docs_search "SEARCH TERM"</code> or if you want to get fancy you can provide a <code>lambda</code> to filter documentation: </p> -<pre>docs_search { |entry| (entry.include? "Array") && (!entry.include? "Enumerable") } -</pre> +<pre><code class="language-ruby">docs_search { |entry| (entry.include? "Array") && (!entry.include? "Enumerable") } +</code></pre> <p> <img src='docs_search.gif'></img> </p> @@ -262,7 +110,7 @@ You may also want to try this free course provided at <a href='http://dragonruby <p> 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: <a href='https://en.wikipedia.org/wiki/Ryan_C._Gordon'>https://en.wikipedia.org/wiki/Ryan_C._Gordon</a>). </p> -<h2>Introduction</h2> +<h2 id='---introduction'>Introduction</h2> <p> Welcome! </p> @@ -278,14 +126,14 @@ Did you not know that? Did you think you couldn't write a game because you're a <p> 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. </p> -<h2>Prerequisites</h2> +<h2 id='---prerequisites'>Prerequisites</h2> <p> 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. </p> <p> Do you know what an if statement is? A for-loop? An array? That's all you'll need to start. </p> -<h2>The Game Loop</h2> +<h2 id='---the-game-loop'>The Game Loop</h2> <p> Ok, here are few rules with regards to game development with GTK: </p> @@ -300,18 +148,18 @@ That's an entire video game in one run-on sentence. <p> 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. </p> -<pre>def tick args +<pre><code class="language-ruby">def tick args args.outputs.labels << [580, 400, 'Hello World!'] end -</pre> +</code></pre> <p> Now run <code>dragonruby</code> ...did you get a window with "Hello World!" written in it? Good, you're officially a game developer! </p> -<h2>Breakdown Of The <code>tick</code> Method</h2> +<h2 id='---breakdown-of-the--tick--method'>Breakdown Of The <code>tick</code> Method</h2> <p> <code>mygame/app/main.rb</code>, 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. </p> -<pre># This "def"ines a function, named "tick," which takes a single argument +<pre><code class="language-ruby"># 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, @@ -328,11 +176,11 @@ def tick args # args.outputs.labels) args.outputs.labels << [580, 400, 'Hello World!'] end -</pre> +</code></pre> <p> Once your <code>tick</code> 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. </p> -<h2>Rendering A Sprite</h2> +<h2 id='---rendering-a-sprite'>Rendering A Sprite</h2> <p> Now let's spice this up a little. </p> @@ -342,18 +190,18 @@ We're going to add some graphics. Each 2D image in DragonRuby is called a "sprit <p> There's a "dragonruby.png" file included, just to get you started. Let's have it draw every frame with our text: </p> -<pre>def tick args +<pre><code class="language-ruby">def tick args args.outputs.labels << [580, 400, 'Hello World!'] args.outputs.sprites << [576, 100, 128, 101, 'dragonruby.png'] end -</pre> +</code></pre> <p> (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.) </p> <p> That <code>.sprites</code> 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. </p> -<h2>Coordinate System and Virtual Canvas</h2> +<h2 id='---coordinate-system-and-virtual-canvas'>Coordinate System and Virtual Canvas</h2> <p> 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. </p> @@ -363,17 +211,17 @@ Also: your game screen is _always_ 1280x720 pixels. If you resize the window, we <p> Ok, now we have an image on the screen, let's animate it: </p> -<pre>def tick args +<pre><code class="language-ruby">def tick args args.state.rotation ||= 0 args.outputs.labels << [580, 400, 'Hello World!' ] args.outputs.sprites << [576, 100, 128, 101, 'dragonruby.png', args.state.rotation] args.state.rotation -= 1 end -</pre> +</code></pre> <p> Now you can see that this function is getting called a lot! </p> -<h2>Game State</h2> +<h2 id='---game-state'>Game State</h2> <p> Here's a fun Ruby thing: <code>args.state.rotation ||= 0</code> 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. </p> @@ -383,18 +231,18 @@ Here's a fun Ruby thing: <code>args.state.rotation ||= 0</code> is shorthand for <p> 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. </p> -<h2>There Is No Delta Time</h2> +<h2 id='---there-is-no-delta-time'>There Is No Delta Time</h2> <p> 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. </p> <p> 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. </p> -<h2>Handling User Input</h2> +<h2 id='---handling-user-input'>Handling User Input</h2> <p> Now, let's move that image around. </p> -<pre>def tick args +<pre><code class="language-ruby">def tick args args.state.rotation ||= 0 args.state.x ||= 576 args.state.y ||= 100 @@ -414,11 +262,11 @@ Now, let's move that image around. args.state.rotation -= 1 end -</pre> +</code></pre> <p> Everywhere you click your mouse, the image moves there. We set a default location for it with <code>args.state.x ||= 576</code>, 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. </p> -<h2>Coding On A Raspberry Pi</h2> +<h2 id='---coding-on-a-raspberry-pi'>Coding On A Raspberry Pi</h2> <p> 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. </p> @@ -428,8 +276,8 @@ If you're running DragonRuby Game Toolkit on a Raspberry Pi, or trying to run a <p> You're probably running a desktop environment: menus, apps, web browsers, etc. This is okay! Launch the terminal app and type: </p> -<pre>do raspi-config -</pre> +<pre><code class="language-ruby">do raspi-config +</code></pre> <p> 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. </p> @@ -439,51 +287,15 @@ If you're _still_ having problems and have a Raspberry Pi 2 or better, go back t <p> 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. </p> -<h2>Conclusion</h2> +<h2 id='---conclusion'>Conclusion</h2> <p> 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 <code>args.output</code> anymore. </p> -<h1 id='--important--go-through-all-of-the-sample-apps!-study-them-thoroughly!!-no-really,-you-should-definitely-do-this!'>IMPORTANT: Go through all of the sample apps! Study them thoroughly!! No really, you should definitely do this!</h1> -<p> -Now that you've completed the Hello World tutorial. Head over to the `samples` directory. It is very very important that you study the sample apps thoroughly! Go through them in order. Here is a short description of each sample app. -</p> -<h2>Guided Samples</h2> -<ol> -<li> <code>samples/00_learn_ruby_optional</code>: This directory contains sample apps that will help you learn the language.</li> -<li> <code>samples/01_rendering_basics</code>: This set of samples will show you how to render basic primitives such as <code>labels</code>, <code>solids</code>, <code>borders</code>, <code>lines</code>, <code>sprites</code>, and how to play <code>sounds</code>.</li> -<li> <code>samples/02_input_basics</code>: This set of samples show you how to accept input from the <code>mouse</code>, <code>keyboard</code>, and <code>controllers</code>.</li> -<li> <code>samples/03_rendering_sprites</code>: This set of samples shows you all the different ways to render sprites (including how to use a sprite sheet).</li> -<li> <code>samples/04_physics_and_collision</code>: This set of samples shows how to do various types of collisions and physics.</li> -<li> <code>samples/05_mouse</code>: This set of samples show more advanced usages of the mouse.</li> -<li> <code>samples/06_save_load</code>: This set of samples show how to save and load game data.</li> -<li> <code>samples/07_advanced_rendering</code>: This set of samples show how to programmatically render sprites using render targets.</li> -<li> <code>samples/08_tweening_lerping_easing_functions</code>: This set of samples show how to perform animations.</li> -<li> <code>samples/09_performance</code>: This set of samples show how to handle performance issues when a large number of sprites on the screen.</li> -<li> <code>samples/10_advanced_debugging</code>: This set of samples show how advanced debugging techniques and testing techniques.</li> -<li> <code>samples/11_http</code>: This set of samples show how use http.</li> -</ol> -<h2>Sample Games</h2> -<p> -There are samples that contain the path <code>samples/99_*</code>. The sample apps that are prefixed with <code>99_</code> show non-trivial implemementations for a real game: -</p> -<ol> -<li> 3D Cube: Shows how to do faux 3D in DragonRuby.</li> -<li> Dueling Starships: A two player top-down versus game where each player controls a ship.</li> -<li> Flappy Dragon: DragonRuby's clone of Flappy Bird.</li> -<li> Pong: A simple implementation of the game Pong.</li> -<li> Snakemoji: The classic game of Snake but with all of the code written using emojis (sometimes you just have to have a little fun).</li> -<li> Solar System: A simulation of our solar system.</li> -<li> Crafting Starting Point: A starting point for those that want to build a crafting game.</li> -<li> Dev Tools: A set of sample apps that show how you can extend DragonRuby's Console, starting point for a tile editor, and a starting point for a paint app.</li> -<li> LOWREZ: Sample apps that show how to render at different resolutions.</li> -<li> RPG: Various sample apps that show how to create narrative, topdown, tactical grid-based, and roguelike RPGs.</li> -<li> Platformers: Various sample apps that show how to create different kinds of physics/collision based platformers.</li> -</ol> <h1 id='--deploying-to-itch.io'>Deploying To Itch.io</h1> <p> 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! </p> -<h2>Creating Your Game Landing Page</h2> +<h2 id='---creating-your-game-landing-page'>Creating Your Game Landing Page</h2> <p> Log into Itch.io and go to <a href='https://itch.io/game/new'>https://itch.io/game/new</a>. </p> @@ -498,28 +310,28 @@ Log into Itch.io and go to <a href='https://itch.io/game/new'>https://itch.io/ga <p> You can fill out all the other options later. </p> -<h2>Update Your Game's Metadata</h2> +<h2 id='---update-your-game's-metadata'>Update Your Game's Metadata</h2> <p> Point your text editor at mygame/metadata/game_metadata.txt and make it look like this: </p> <p> NOTE: Remove the <code>#</code> at the beginning of each line. </p> -<pre>devid=bob +<pre><code class="language-ruby">devid=bob devtitle=Bob The Game Developer gameid=mygame gametitle=My Game version=0.1 -</pre> +</code></pre> <p> The <code>devid</code> property is the username you use to log into Itch.io. The <code>devtitle</code> is your name or company name (it can contain spaces). The <code>gameid</code> is the Project URL value. The <code>gametitle</code> is the name of your game (it can contain spaces). The <code>version</code> can be any <code>major.minor</code> number format. </p> -<h2>Building Your Game For Distribution</h2> +<h2 id='---building-your-game-for-distribution'>Building Your Game For Distribution</h2> <p> Open up the terminal and run this from the command line: </p> -<pre>./dragonruby-publish --only-package mygame -</pre> +<pre><code class="language-ruby">./dragonruby-publish --only-package mygame +</code></pre> <p> (if you're on Windows, don't put the "./" on the front. That's a Mac and Linux thing.) </p> @@ -532,8 +344,8 @@ For the HTML version of your game after you upload it. Check the checkbox labele <p> For subsequent updates you can use an automated deployment to Itch.io: </p> -<pre>./dragonruby-publish mygame -</pre> +<pre><code class="language-ruby">./dragonruby-publish mygame +</code></pre> <p> 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! </p> @@ -550,16 +362,16 @@ To deploy to iOS, you need to have a Mac running MacOS Catalina, an iOS device, <p> To deploy to Android, you need to have an Android emulator/device, and a environment that is able to run Android SDK. <code>dragonruby-publish</code> 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: </p> -<pre>> adb logcat -e mygame # you'll want to run this in a separate terminal +<pre><code class="language-ruby">> adb logcat -e mygame # you'll want to run this in a separate terminal > keytool -genkey -v -keystore mygame.keystore -alias mygame -keyalg RSA -keysize 2048 -validity 10000 > apksigner sign --ks mygame.keystore mygame-android.apk > adb install mygame-android.apk -</pre> +</code></pre> <h1 id='--dragonruby's-philosophy'>DragonRuby's Philosophy</h1> <p> 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 principals. </p> -<h2>Challenge The Status Quo</h2> +<h2 id='---challenge-the-status-quo'>Challenge The Status Quo</h2> <p> 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: </p> @@ -571,51 +383,49 @@ But that's how we've always done it. <p> 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. </p> -<h2>Continuity of Design</h2> +<h2 id='---continuity-of-design'>Continuity of Design</h2> <p> -There is a programming idiom in software called "the pit of success". The term normalizes up front pain as a necessity in the (hopes that the investment will yield dividends "when you become successful"). This results in more "Enterprise TM" code upfront, and makes it more difficult to get started when you are new to programming. +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. </p> <p> -DragonRuby's philosophy is to provide a spectrum across the "make it fast" vs "make it right" spectrum and provide incremental, intuitive transitions between points on that spectrum. This is captured in how render primitives can be represented as tuples/arrays, hashes, open structs/entities, and then finally classes (as opposed to forcing devs to use classes upfront). +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 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 string 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). </p> -<h2>Release Often And Soon</h2> +<h2 id='---release-early-and-often'>Release Early and Often</h2> <p> -The biggest mistake game devs make is spending too much time in isolation building their game. Release something, however small, and release it quickly. +The biggest mistake game devs make is spending too much time in isolation building their game. Release something, however small, and release it soon. </p> <p> 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. </p> <p> -Remember: -</p> -<blockquote> -<p> -Real artists ship. +Real artists ship. Remember that. </p> -</blockquote> -<h2>Sustainable And Ethical Monetization</h2> +<h2 id='---sustainable-and-ethical-monetization'>Sustainable And Ethical Monetization</h2> <p> 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. </p> <p> 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. </p> -<h2>Sustainable And Ethical Open Source</h2> +<p> +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. +</p> +<h2 id='---sustainable-and-ethical-open-source'>Sustainable And Ethical Open Source</h2> <p> 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). </p> <p> 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. </p> -<h2>People Over Entities</h2> +<h2 id='---people-over-entities'>People Over Entities</h2> <p> 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. </p> -<h2>Building A Game Should Be Fun And Bring Happiness</h2> +<h2 id='---building-a-game-should-be-fun-and-bring-happiness'>Building A Game Should Be Fun And Bring Happiness</h2> <p> 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. </p> -<h2>Real World Application Drives Features</h2> +<h2 id='---real-world-application-drives-features'>Real World Application Drives Features</h2> <p> 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. </p> @@ -626,8 +436,8 @@ We want DragonRuby to *actually* help you build the game you want to build (as o <p> Here are questions, comments, and concerns that frequently come up. </p> -<h2>Frequently Asked Questions</h2> -<h3>What is DragonRuby LLP?</h3> +<h2 id='---frequently-asked-questions'>Frequently Asked Questions</h2> +<h3 id='----what-is-dragonruby-llp?'>What is DragonRuby LLP?</h3> <p> DragonRuby LLP is a partnership of four devs who came together with the goal of bringing the aesthetics and joy of Ruby, everywhere possible. </p> @@ -652,7 +462,7 @@ NOTE: We leave the "A DragonRuby LLP Product" off of this one because that just <p> NOTE: Devs who use DragonRuby are "Dragon Riders/Riders of Dragons". That's a bad ass identifier huh? </p> -<h3>What is DragonRuby?</h3> +<h3 id='----what-is-dragonruby?'>What is DragonRuby?</h3> <p> The response to this question requires a few subparts. First we need to clarify some terms. Specifically _language specification_ vs _runtime_. </p> @@ -703,7 +513,7 @@ These levels allow us to stay up to date with open source implementations of Rub <p> 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. </p> -<h3>How is DragonRuby different than MRI?</h3> +<h3 id='----how-is-dragonruby-different-than-mri?'>How is DragonRuby different than MRI?</h3> <p> 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). </p> @@ -718,11 +528,11 @@ You can use DragonRuby's Console within the game to inspect object and execute s <ul> <li>Any code you write in there will be executed when you change the file. You can organize different pieces of code using the <code>repl</code> method:</li> </ul> -<pre>repl do +<pre><code class="language-ruby">repl do puts "hello world" puts 1 + 1 end -</pre> +</code></pre> <ul> <li>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).</li> <li>All <code>puts</code> statements will also be saved to <code>logs/log.txt</code>. So if you want to stay in your editor and not look at the terminal, or the DragonRuby Console, you can <code>tail</code> this file.</li> @@ -730,7 +540,7 @@ end <p> 4. To ignore code in <code>repl.rb</code>, instead of commenting it out, prefix <code>repl</code> with the letter <code>x</code> and it'll be ignored. </p> -<pre>xrepl do # <------- line is prefixed with an "x" +<pre><code class="language-ruby">xrepl do # <------- line is prefixed with an "x" puts "hello world" puts 1 + 1 end @@ -746,9 +556,9 @@ end # use xrepl to "comment out" code xrepl do - puts "This code will not be executed because of the x infront of repl". + puts "This code will not be executed because of the x in front of repl". end -</pre> +</code></pre> <h4>Does DragonRuby support <code>pry</code> or have any other debugging facilities?</h4> <p> <code>pry</code> 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. @@ -763,8 +573,8 @@ You can use DragonRuby's replay capabilities to troubleshoot: <li> Get into the habit of adding debugging facilities within the game itself. You can add drawing primitives to <code>args.outputs.debug</code> that will render on top of your game but will be ignored in a production release.</li> <li> 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.</li> </ol> -<h2>Frequent Comments About Ruby as a Language Choice</h2> -<h3>But Ruby is dead.</h3> +<h2 id='---frequent-comments-about-ruby-as-a-language-choice'>Frequent Comments About Ruby as a Language Choice</h2> +<h3 id='----but-ruby-is-dead.'>But Ruby is dead.</h3> <p> Let's check the official source for the answer to this question: isrubydead.com: <a href='https://isrubydead.com/'>https://isrubydead.com/</a>. </p> @@ -777,11 +587,11 @@ What really matters is _quality/maturity_. Here is the latest (StackOverflow Sur <p> Let's stop making this comment shall we? </p> -<h3>But Ruby is slow.</h3> +<h3 id='----but-ruby-is-slow.'>But Ruby is slow.</h3> <p> 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. </p> -<h3>Dynamic languages are slow.</h3> +<h3 id='----dynamic-languages-are-slow.'>Dynamic languages are slow.</h3> <p> 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_. </p> @@ -791,8 +601,8 @@ Unless you are writing in some form of intermediate representation by hand, your <p> NOTE: If you _are_ hand writing LLVM IR, we are always open to bringing on new partners with such a skill set. Email us ^_^. </p> -<h2>Frequent Concerns</h2> -<h3>DragonRuby is not open source. That's not right.</h3> +<h2 id='---frequent-concerns'>Frequent Concerns</h2> +<h3 id='----dragonruby-is-not-open-source.-that's-not-right.'>DragonRuby is not open source. That's not right.</h3> <p> 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. </p> @@ -808,7 +618,7 @@ If the reason above isn't sufficient, then definitely use something else. <p> All this being said, we do have parts of the engine open sourced on GitHub: <a href='https://github.com/dragonruby/dragonruby-game-toolkit-contrib/'>https://github.com/dragonruby/dragonruby-game-toolkit-contrib/</a> </p> -<h3>DragonRuby is for pay. You should offer a free version.</h3> +<h3 id='----dragonruby-is-for-pay.-you-should-offer-a-free-version.'>DragonRuby is for pay. You should offer a free version.</h3> <p> If you can afford to pay for DragonRuby, you should (and will). We don't go around telling writers 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. </p> @@ -830,44 +640,45 @@ You qualify for a free, unrestricted license to DragonRuby products if any of th <p> Just contact Amir at [email protected] with a short explanation of your current situation and he'll set you up. No questions asked. </p> -<h3>But still, you should offer a free version. So I can try it out and see if I like it.</h3> +<h3 id='----but-still,-you-should-offer-a-free-version.-so-i-can-try-it-out-and-see-if-i-like-it.'>But still, you should offer a free version. So I can try it out and see if I like it.</h3> <p> You can try our [web-based sandbox environment](). But it won't do the runtime justice. Or just come to our [Slack]() or [Discord]() channel 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. </p> <p> Seriously just buy it. Get a refund if you don't like it. We make it stupid easy to do so. </p> -<h3>I still think you should do a free version. Think of all people who would give it a shot.</h3> +<h3 id='----i-still-think-you-should-do-a-free-version.-think-of-all-people-who-would-give-it-a-shot.'>I still think you should do a free version. Think of all people who would give it a shot.</h3> <p> 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). </p> <p> 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. </p> -<h3>What if I build something with DragonRuby, but DragonRuby LLP becomes insolvent.</h3> +<h3 id='----what-if-i-build-something-with-dragonruby,-but-dragonruby-llp-becomes-insolvent.'>What if I build something with DragonRuby, but DragonRuby LLP becomes insolvent.</h3> <p> That won't happen if the development world stop asking for free stuff and non-trivially compensate open source developers. Look, 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 happen. </p> <p> 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. </p> -<h1 id='--cheatsheet--how-to-determine-what-frame-you-are-on'>CHEATSHEET: How To Determine What Frame You Are On</h1> +<h1 id='--recipies-'>RECIPIES:</h1> +<h2 id='---how-to-determine-what-frame-you-are-on'>How To Determine What Frame You Are On</h2> <p> There is a property on <code>state</code> called <code>tick_count</code> that is incremented by DragonRuby every time the <code>tick</code> method is called. The following code renders a label that displays the current <code>tick_count</code>. </p> -<pre>def tick args +<pre><code class="language-ruby">def tick args args.outputs.labels << [10, 670, "#{args.state.tick_count}"] end -</pre> -<h1 id='--cheatsheet--how-to-get-current-framerate'>CHEATSHEET: How To Get Current Framerate</h1> +</code></pre> +<h2 id='---how-to-get-current-framerate'>How To Get Current Framerate</h2> <p> Current framerate is a top level property on the Game Toolkit Runtime and is accessible via <code>args.gtk.current_framerate</code>. </p> -<pre>def tick args +<pre><code class="language-ruby">def tick args args.outputs.labels << [10, 710, "framerate: #{args.gtk.current_framerate.round}"] end -</pre> -<h1 id='--cheatsheet--how-to-render-a-sprite-using-an-array'>CHEATSHEET: How To Render A Sprite Using An Array</h1> +</code></pre> +<h2 id='---how-to-render-a-sprite-using-an-array'>How To Render A Sprite Using An Array</h2> <p> All file paths should use the forward slash <code>/</code> *not* backslash <code></code>. Game Toolkit includes a number of sprites in the <code>sprites</code> folder (everything about your game is located in the <code>mygame</code> directory). </p> @@ -877,7 +688,7 @@ The following code renders a sprite with a <code>width</code> and <code>height</ <p> <code>args.outputs.sprites</code> is used to render a sprite. </p> -<pre>def tick args +<pre><code class="language-ruby">def tick args args.outputs.sprites << [ 640 - 50, # X 360 - 50, # Y @@ -886,12 +697,12 @@ The following code renders a sprite with a <code>width</code> and <code>height</ 'sprites/square-blue.png' # PATH ] end -</pre> -<h1 id='--cheatsheet--more-sprite-properties-as-an-array'>CHEATSHEET: More Sprite Properties As An Array</h1> +</code></pre> +<h2 id='---more-sprite-properties-as-an-array'>More Sprite Properties As An Array</h2> <p> Here are all the properties you can set on a sprite. </p> -<pre>def tick args +<pre><code class="language-ruby">def tick args args.outputs.sprites << [ 100, # X 100, # Y @@ -905,15 +716,15 @@ Here are all the properties you can set on a sprite. 0 # BLUE_SATURATION ] end -</pre> -<h1 id='--cheatsheet--different-sprite-representations'>CHEATSHEET: Different Sprite Representations</h1> +</code></pre> +<h2 id='---different-sprite-representations'>Different Sprite Representations</h2> <p> Using ordinal positioning can get a little unruly given so many properties you have control over. </p> <p> You can represent a sprite as a <code>Hash</code>: </p> -<pre>def tick args +<pre><code class="language-ruby">def tick args args.outputs.sprites << { x: 640 - 50, y: 360 - 50, @@ -935,11 +746,11 @@ You can represent a sprite as a <code>Hash</code>: angle_anchor_y: 1.0 } end -</pre> +</code></pre> <p> You can represent a sprite as an <code>object</code>: </p> -<pre># Create type with ALL sprite properties AND primitive_marker +<pre><code class="language-ruby"># Create type with ALL sprite properties AND primitive_marker class Sprite attr_accessor :x, :y, :w, :h, :path, :angle, :a, :r, :g, :b, :source_x, :source_y, :source_w, :source_h, @@ -968,8 +779,8 @@ def tick args w: 50, h: 50) end -</pre> -<h1 id='--cheatsheet--how-to-render-a-label'>CHEATSHEET: How To Render A Label</h1> +</code></pre> +<h2 id='---how-to-render-a-label'>How To Render A Label</h2> <p> <code>args.outputs.labels</code> is used to render labels. </p> @@ -979,20 +790,20 @@ Labels are how you display text. This code will go directly inside of the <code> <p> Here is the minimum code: </p> -<pre>def tick args +<pre><code class="language-ruby">def tick args # X Y TEXT args.outputs.labels << [640, 360, "I am a black label."] end -</pre> -<h1 id='--cheatsheet--a-colored-label'>CHEATSHEET: A Colored Label</h1> -<pre>def tick args +</code></pre> +<h2 id='---a-colored-label'>A Colored Label</h2> +<pre><code class="language-ruby">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 -</pre> -<h1 id='--cheatsheet--extended-label-properties'>CHEATSHEET: Extended Label Properties</h1> -<pre>def tick args +</code></pre> +<h2 id='---extended-label-properties'>Extended Label Properties</h2> +<pre><code class="language-ruby">def tick args # A colored label # X Y TEXT SIZE ALIGNMENT RED GREEN BLUE ALPHA FONT FILE args.outputs.labels << [ @@ -1008,18 +819,18 @@ end "fonts/coolfont.ttf" # FONT ] end -</pre> +</code></pre> <p> A <code>SIZE_ENUM</code> of <code>0</code> represents "default size". A <code>negative</code> value will decrease the label size. A <code>positive</code> value will increase the label's size. </p> <p> An <code>ALIGNMENT_ENUM</code> of <code>0</code> represents "left aligned". <code>1</code> represents "center aligned". <code>2</code> represents "right aligned". </p> -<h1 id='--cheatsheet--rendering-a-label-as-a--hash-'>CHEATSHEET: Rendering A Label As A <code>Hash</code></h1> +<h2 id='---rendering-a-label-as-a--hash-'>Rendering A Label As A <code>Hash</code></h2> <p> You can add additional metadata about your game within a label, which requires you to use a `Hash` instead. </p> -<pre>def tick args +<pre><code class="language-ruby">def tick args args.outputs.labels << { x: 200, y: 550, @@ -1040,12 +851,12 @@ You can add additional metadata about your game within a label, which requires y } } end -</pre> -<h1 id='--cheatsheet--getting-the-size-of-a-piece-of-text'>CHEATSHEET: Getting The Size Of A Piece Of Text</h1> +</code></pre> +<h2 id='---getting-the-size-of-a-piece-of-text'>Getting The Size Of A Piece Of Text</h2> <p> You can get the render size of any string using <code>args.gtk.calcstringbox</code>. </p> -<pre>def tick args +<pre><code class="language-ruby">def tick args # TEXT SIZE_ENUM FONT w, h = args.gtk.calcstringbox("some string", 0, "font.ttf") @@ -1059,39 +870,39 @@ You can get the render size of any string using <code>args.gtk.calcstringbox</co "'some string' has width: #{w}, and height: #{h}." ] end -</pre> -<h1 id='--cheatsheet--how-to-play-a-sound'>CHEATSHEET: How To Play A Sound</h1> +</code></pre> +<h2 id='---how-to-play-a-sound'>How To Play A Sound</h2> <p> Sounds that end <code>.wav</code> will play once: </p> -<pre>def tick args +<pre><code class="language-ruby">def tick args # Play a sound every second if (args.state.tick_count % 60) == 0 args.outputs.sounds << 'something.wav' end end -</pre> +</code></pre> <p> Sounds that end <code>.ogg</code> is considered background music and will loop: </p> -<pre>def tick args +<pre><code class="language-ruby">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 -</pre> +</code></pre> <p> If you want to play a <code>.ogg</code> once as if it were a sound effect, you can do: </p> -<pre>def tick args +<pre><code class="language-ruby">def tick args # Play a sound every second if (args.state.tick_count % 60) == 0 args.gtk.queue_sound 'some-ogg.ogg' end end -</pre> -<h1 id='--cheatsheet--using--args.state--to-store-your-game-state'>CHEATSHEET: Using <code>args.state</code> To Store Your Game State</h1> +</code></pre> +<h2 id='---using--args.state--to-store-your-game-state'>Using <code>args.state</code> To Store Your Game State</h2> <p> <code>args.state</code> is a open data structure that allows you to define properties that are arbitrarily nested. You don't need to define any kind of <code>class</code>. </p> @@ -1101,7 +912,7 @@ To initialize your game state, use the <code>||=</code> operator. Any value on t <p> To assign a value every frame, just use the <code>=</code> operator, but _make sure_ you've initialized a default value. </p> -<pre>def tick args +<pre><code class="language-ruby">def tick args # initialize your game state ONCE args.state.player.x ||= 0 args.state.player.y ||= 0 @@ -1124,25 +935,25 @@ To assign a value every frame, just use the <code>=</code> operator, but _make s args.state.player.hp ] end -</pre> -<h1 id='--cheatsheet--troubleshoot-performance'>CHEATSHEET: Troubleshoot Performance</h1> +</code></pre> +<h2 id='---troubleshoot-performance'>Troubleshoot Performance</h2> <ol> <li> If you're using <code>Array</code>s for your primitives (<code>args.outputs.sprites << []</code>), use <code>Hash</code> instead (<code>args.outputs.sprites << { x: ... }</code>).</li> <li> If you're using <code>Entity</code> for your primitives (<code>args.outputs.sprites << args.state.new_entity</code>), use <code>StrictEntity</code> instead (<code>args.outputs.sprites << args.state.new_entity_strict</code>).</li> <li> Use <code>.each</code> instead of <code>.map</code> if you don't care about the return value.</li> <li> When concatenating primitives to outputs, do them in bulk. Instead of:</li> </ol> -<pre>args.state.bullets.each do |bullet| +<pre><code class="language-ruby">args.state.bullets.each do |bullet| args.outputs.sprites << bullet.sprite end -</pre> +</code></pre> <p> do </p> -<pre>args.outputs.sprites << args.state.bullets.map do |b| +<pre><code class="language-ruby">args.outputs.sprites << args.state.bullets.map do |b| b.sprite end -</pre> +</code></pre> <p> 5. Use <code>args.outputs.static_</code> variant for things that don't change often (take a look at the Basic Gorillas sample app and Dueling Starships sample app to see how <code>static_</code> is leveraged. </p> @@ -1153,16 +964,741 @@ end <p> The GTK::Runtime class is the core of DragonRuby. It is globally accessible via <code>$gtk</code>. </p> -<h1 id='--docs---gtk--runtime#write_file-'>DOCS: <code>GTK::Runtime#write_file</code></h1> +<h1 id='--summary---def-tick-args;-end;-'>SUMMARY: <code>def tick args; end;</code></h1> <p> -This function takes in two parameters. The first paramter 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 <code>GTK::Runtime#append_file</code> to append to the file as opposed to overwriting. +Most everything you will need to build your game is in the <code>args</code> parameter that is provided to your <code>tick</code> method. Follows is a high level summary of each function that is available from <code>args</code>. </p> -<pre>def tick args - if args.inputs.mouse.click - args.gtk.write_file "last-mouse-click.txt", "Mouse was clicked at #{args.state.tick_count}." +<p> +All the properties below hang off of <code>args</code> and can be accessed in the <code>tick</code> method: +</p> +<pre><code class="language-ruby">def tick args + args.PROPERTY +end +</code></pre> +<h2 id='----args.state-'><code>args.state</code></h2> +<p> +Store your game state inside of this <code>state</code>. Properties with arbitrary nesting is allowed and a backing Entity will be created on your behalf. +</p> +<pre><code class="language-ruby">def tick args + args.state.player.x ||= 0 + args.state.player.y ||= 0 +end +</code></pre> +<h3 id='-----.-.entity_id-'><code>.*.entity_id</code></h3> +<p> +Entities automatically receive an <code>entity_id</code> of type <code>Fixnum</code>. +</p> +<h3 id='-----.-.entity_type-'><code>.*.entity_type</code></h3> +<p> +Entities can have an <code>entity_type</code> which is represented as a <code>Symbol</code>. +</p> +<h3 id='-----.-.created_at-'><code>.*.created_at</code></h3> +<p> +Entities have <code>created_at</code> set to <code>args.state.tick_count</code> when they are created. +</p> +<h3 id='-----.-.created_at_elapsed-'><code>.*.created_at_elapsed</code></h3> +<p> +Returns the elapsed number of ticks since creation. +</p> +<h3 id='-----.-.global_created_at-'><code>.*.global_created_at</code></h3> +<p> +Entities have <code>global_created_at</code> set to <code>Kernel.global_tick_count</code> when they are created. +</p> +<h3 id='-----.-.global_created_at_elapsed-'><code>.*.global_created_at_elapsed</code></h3> +<p> +Returns the elapsed number of global ticks since creation. +</p> +<h3 id='-----.-.as_hash-'><code>.*.as_hash</code></h3> +<p> +Entity cast to a <code>Hash</code> so you can update values as if you were updating a <code>Hash</code>. +</p> +<h3 id='-----.new_entity-'><code>.new_entity</code></h3> +<p> +Creates a new Entity with a <code>type</code>, and initial properties. An option block can be passed to change the newly created entity: +</p> +<pre><code class="language-ruby">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 -</pre> +</code></pre> +<h3 id='-----.new_entity_strict-'><code>.new_entity_strict</code></h3> +<p> +Creates a new Strict Entity. While Entities created via <code>args.state.new_entity</code> can have new properties added later on, Entities created using <code>args.state.new_entity</code> must define all properties that are allowed during its initialization. Attempting to add new properties after initialization will result in an exception. +</p> +<h3 id='-----.tick_count-'><code>.tick_count</code></h3> +<p> +Returns the current tick of the game. <code>args.state.tick_count</code> is <code>0</code> when the game is first started or if the game is reset via <code>$gtk.reset</code>. +</p> +<h2 id='----args.inputs-'><code>args.inputs</code></h2> +<p> +Access using input using <code>args.inputs</code>. +</p> +<h3 id='-----.up-'><code>.up</code></h3> +<p> +Returns <code>true</code> if: the <code>up</code> arrow or <code>w</code> key is pressed or held on the <code>keyboard</code>; or if <code>up</code> is pressed or held on <code>controller_one</code>; or if the <code>left_analog</code> on <code>controller_one</code> is tilted upwards. +</p> +<h3 id='-----.down-'><code>.down</code></h3> +<p> +Returns <code>true</code> if: the <code>down</code> arrow or <code>s</code> key is pressed or held on the <code>keyboard</code>; or if <code>down</code> is pressed or held on <code>controller_one</code>; or if the <code>left_analog</code> on <code>controller_one</code> is tilted downwards. +</p> +<h3 id='-----.left-'><code>.left</code></h3> +<p> +Returns <code>true</code> if: the <code>left</code> arrow or <code>a</code> key is pressed or held on the <code>keyboard</code>; or if <code>left</code> is pressed or held on <code>controller_one</code>; or if the <code>left_analog</code> on <code>controller_one</code> is tilted to the left. +</p> +<h3 id='-----.right-'><code>.right</code></h3> +<p> +Returns <code>true</code> if: the <code>right</code> arrow or <code>d</code> key is pressed or held on the <code>keyboard</code>; or if <code>right</code> is pressed or held on <code>controller_one</code>; or if the <code>left_analog</code> on <code>controller_one</code> is tilted to the right. +</p> +<h3 id='-----.left_right-'><code>.left_right</code></h3> +<p> +Returns <code>-1</code> (left), <code>0</code> (neutral), or <code>+1</code> (right) depending on results of <code>args.inputs.left</code> and <code>args.inputs.right</code>. +</p> +<h3 id='-----.up_down-'><code>.up_down</code></h3> +<p> +Returns <code>-1</code> (down), <code>0</code> (neutral), or <code>+1</code> (right) depending on results of <code>args.inputs.up</code> and <code>args.inputs.down</code>. +</p> +<h3 id='-----.text--or--.history-'><code>.text</code> OR <code>.history</code></h3> +<p> +Returns a string that represents the last key that was pressed on the keyboard. +</p> +<h3 id='-----.mouse-'><code>.mouse</code></h3> +<p> +Represents the user's +</p> +<h4><code>.x</code></h4> +<p> +Returns the current <code>x</code> location of the mouse. +</p> +<h4><code>mouse.y</code></h4> +<p> +Return. +</p> +<h4><code>.inside_rect? rect</code></h4> +<p> +Return. <code>args.inputs.mouse.inside_rect?</code> takes in any primitive that responds to <code>x, y, w, h</code>: +</p> +<h4><code>.inside_circle? center_point, radius</code></h4> +<p> +Returns <code>true</code> if the mouse is inside of a specified circle. <code>args.inputs.mouse.inside_circle?</code> takes in any primitive that responds to <code>x, y</code> (which represents the circle's center), and takes in a <code>radius</code>: +</p> +<h4><code>.moved</code></h4> +<p> +Returns <code>true</code> if the mouse has moved on the current frame. +</p> +<h4><code>.button_left</code></h4> +<p> +Returns <code>true</code> if the left mouse button is down. +</p> +<h4><code>.button_middle</code></h4> +<p> +Returns <code>true</code> if the middle mouse button is down. +</p> +<h4><code>.button_right</code></h4> +<p> +Returns <code>true</code> if the right mouse button is down. +</p> +<h4><code>.button_bits</code></h4> +<p> +Returns a bitmask for all buttons on the mouse: <code>1</code> for a button in the <code>down</code> state, <code>0</code> for a button in the <code>up</code> state. +</p> +<h4><code>mouse.wheel</code></h4> +<p> +Represents the mouse wheel. Returns <code>nil</code> if no mouse wheel actions occurred. ***** <code>.x</code> Returns the negative or positive number if the mouse wheel has changed in the <code>x</code> axis. ***** <code>.y</code> Returns the negative or positive number if the mouse wheel has changed in the <code>y</code> axis. +</p> +<h4><code>.click</code> OR <code>.down</code>, <code>.previous_click</code>, <code>.up</code></h4> +<p> +The properties <code>args.inputs.mouse.(click|down|previous_click|up)</code> each return <code>nil</code> if the mouse button event didn't occur. And return an Entity that has an <code>x</code>, <code>y</code> properties along with helper functions to determine collision: <code>inside_rect?</code>, <code>inside_circle</code>. +</p> +<h3 id='-----.controller_one-,--.controller_two-'><code>.controller_one</code>, <code>.controller_two</code></h3> +<p> +Represents controllers connected to the usb ports. +</p> +<h4>~.up</h4> +<p> +Returns <code>true</code> if <code>up</code> is pressed or held on the directional or left analog. +</p> +<h4>~.down</h4> +<p> +Returns <code>true</code> if <code>down</code> is pressed or held on the directional or left analog. +</p> +<h4>~.left</h4> +<p> +Returns <code>true</code> if <code>left</code> is pressed or held on the directional or left analog. +</p> +<h4>~.right</h4> +<p> +Returns <code>true</code> if <code>right</code> is pressed or held on the directional or left analog. +</p> +<h4>~.left_right</h4> +<p> +Returns <code>-1</code> (left), <code>0</code> (neutral), or <code>+1</code> (right) depending on results of <code>args.inputs.controller_(one|two).left</code> and <code>args.inputs.controller_(one|two).right</code>. +</p> +<h4>~.up_down</h4> +<p> +Returns <code>-1</code> (down), <code>0</code> (neutral), or <code>+1</code> (up) depending on results of <code>args.inputs.controller_(one|two).up</code> and <code>args.inputs.controller_(one|two).down</code>. +</p> +<h4><code>.(left_analog_x_raw|right_analog_x_raw)</code></h4> +<p> +Returns the raw integer value for the analog's horizontal movement (<code>-32,000 to +32,000</code>). +</p> +<h4><code>.left_analog_y_raw|right_analog_y_raw)</code></h4> +<p> +Returns the raw integer value for the analog's vertical movement (<code>-32,000 to +32,000</code>). +</p> +<h4><code>.left_analog_x_perc|right_analog_x_perc)</code></h4> +<p> +Returns a number between <code>-1</code> and <code>1</code> which represents the percentage the analog is moved horizontally as a ratio of the maximum horizontal movement. +</p> +<h4><code>.left_analog_y_perc|right_analog_y_perc)</code></h4> +<p> +Returns a number between <code>-1</code> and <code>1</code> which represents the percentage the analog is moved vertically as a ratio of the maximum vertical movement. +</p> +<h4><code>.directional_up)</code></h4> +<p> +Returns <code>true</code> if <code>up</code> is pressed or held on the directional. +</p> +<h4><code>.directional_down)</code></h4> +<p> +Returns <code>true</code> if <code>down</code> is pressed or held on the directional. +</p> +<h4><code>.directional_left)</code></h4> +<p> +Returns <code>true</code> if <code>left</code> is pressed or held on the directional. +</p> +<h4><code>.directional_right)</code></h4> +<p> +Returns <code>true</code> if <code>right</code> is pressed or held on the directional. +</p> +<h4><code>.(a|b|x|y|l1|r1|l2|r2|l3|r3|start|select)</code></h4> +<p> +Returns <code>true</code> if the specific button is pressed or held. +</p> +<h4><code>.truthy_keys</code></h4> +<p> +Returns a collection of <code>Symbol</code>s that represent all keys that are in the pressed or held state. +</p> +<h4><code>.key_down</code></h4> +<p> +Returns <code>true</code> if the specific button was pressed on this frame. <code>args.inputs.controller_(one|two).key_down.BUTTON</code> will only be true on the frame it was pressed. +</p> +<h4><code>.key_held</code></h4> +<p> +Returns <code>true</code> if the specific button is being held. <code>args.inputs.controller_(one|two).key_held.BUTTON</code> will be true for all frames after <code>key_down</code> (until released). +</p> +<h4><code>.key_up</code></h4> +<p> +Returns <code>true</code> if the specific button was released. <code>args.inputs.controller_(one|two).key_up.BUTTON</code> will be true only on the frame the button was released. +</p> +<h3 id='-----.keyboard-'><code>.keyboard</code></h3> +<p> +Represents the user's keyboard +</p> +<h4><code>.up</code></h4> +<p> +Returns <code>true</code> if <code>up</code> or <code>w</code> is pressed or held on the keyboard. +</p> +<h4><code>.down</code></h4> +<p> +Returns <code>true</code> if <code>down</code> or <code>s</code> is pressed or held on the keyboard. +</p> +<h4><code>.left</code></h4> +<p> +Returns <code>true</code> if <code>left</code> or <code>a</code> is pressed or held on the keyboard. +</p> +<h4><code>.right</code></h4> +<p> +Returns <code>true</code> if <code>right</code> or <code>d</code> is pressed or held on the keyboard. +</p> +<h4><code>.left_right</code></h4> +<p> +Returns <code>-1</code> (left), <code>0</code> (neutral), or <code>+1</code> (right) depending on results of <code>args.inputs.keyboard.left</code> and <code>args.inputs.keyboard.right</code>. +</p> +<h4><code>.up_down</code></h4> +<p> +Returns <code>-1</code> (left), <code>0</code> (neutral), or <code>+1</code> (right) depending on results of <code>args.inputs.keyboard.up</code> and <code>args.inputs.keyboard.up</code>. +</p> +<h4>keyboard properties</h4> +<p> +The following properties represent keys on the keyboard and are available on <code>args.inputs.keyboard.KEY</code>, <code>args.inputs.keyboard.key_down.KEY</code>, <code>args.inputs.keyboard.key_held.KEY</code>, and <code>args.inputs.keyboard.key_up.KEY</code>: +</p> +<ul> +<li><code>alt</code></li> +<li><code>meta</code></li> +<li><code>control</code></li> +<li><code>shift</code></li> +<li><code>ctrl_KEY</code> (dynamic method, eg <code>args.inputs.keyboard.ctrl_a</code>)</li> +<li><code>exclamation_point</code></li> +<li><code>zero</code></li> +<li><code>one</code></li> +<li><code>two</code></li> +<li><code>three</code></li> +<li><code>four</code></li> +<li><code>five</code></li> +<li><code>six</code></li> +<li><code>seven</code></li> +<li><code>eight</code></li> +<li><code>nine</code></li> +<li><code>backspace</code></li> +<li><code>delete</code></li> +<li><code>escape</code></li> +<li><code>enter</code></li> +<li><code>tab</code></li> +<li><code>open_round_brace</code></li> +<li><code>close_round_brace</code></li> +<li><code>open_curly_brace</code></li> +<li><code>close_curly_brace</code></li> +<li><code>open_square_brace</code></li> +<li><code>close_square_brace</code></li> +<li><code>colon</code></li> +<li><code>semicolon</code></li> +<li><code>equal_sign</code></li> +<li><code>hyphen</code></li> +<li><code>space</code></li> +<li><code>dollar_sign</code></li> +<li><code>double_quotation_mark</code></li> +<li><code>single_quotation_mark</code></li> +<li><code>backtick</code></li> +<li><code>tilde</code></li> +<li><code>period</code></li> +<li><code>comma</code></li> +<li><code>pipe</code></li> +<li><code>underscore</code></li> +<li><code>a</code></li> +<li><code>b</code></li> +<li><code>c</code></li> +<li><code>d</code></li> +<li><code>e</code></li> +<li><code>f</code></li> +<li><code>g</code></li> +<li><code>h</code></li> +<li><code>i</code></li> +<li><code>j</code></li> +<li><code>k</code></li> +<li><code>l</code></li> +<li><code>m</code></li> +<li><code>n</code></li> +<li><code>o</code></li> +<li><code>p</code></li> +<li><code>q</code></li> +<li><code>r</code></li> +<li><code>s</code></li> +<li><code>t</code></li> +<li><code>u</code></li> +<li><code>v</code></li> +<li><code>w</code></li> +<li><code>x</code></li> +<li><code>y</code></li> +<li><code>z</code></li> +<li><code>shift</code></li> +<li><code>control</code></li> +<li><code>alt</code></li> +<li><code>meta</code></li> +<li><code>left</code></li> +<li><code>right</code></li> +<li><code>up</code></li> +<li><code>down</code></li> +<li><code>pageup</code></li> +<li><code>pagedown</code></li> +<li><code>char</code></li> +<li><code>plus</code></li> +<li><code>at</code></li> +<li><code>forward_slash</code></li> +<li><code>back_slash</code></li> +<li><code>asterisk</code></li> +<li><code>less_than</code></li> +<li><code>greater_than</code></li> +<li><code>carat</code></li> +<li><code>ampersand</code></li> +<li><code>superscript_two</code></li> +<li><code>circumflex</code></li> +<li><code>question_mark</code></li> +<li><code>section_sign</code></li> +<li><code>ordinal_indicator</code></li> +<li><code>raw_key</code></li> +<li><code>raw_key</code></li> +<li><code>left_right</code></li> +<li><code>up_down</code></li> +<li><code>directional_vector</code></li> +<li><code>truthy_keys</code></li> +</ul> +<h4><code>inputs.keyboard.keys</code></h4> +<p> +Returns a <code>Hash</code> with all keys on the keyboard in their respective state. The <code>Hash</code> contains the following <code>keys</code> +</p> +<ul> +<li><code>:down</code></li> +<li><code>:held</code></li> +<li><code>:down_or_held</code></li> +<li><code>:up</code></li> +</ul> +<h2 id='----args.outputs-'><code>args.outputs</code></h2> +<p> +<code>args.outputs.PROPERTY</code> is how you render to the screen. +</p> +<h3 id='-----.background_color-'><code>.background_color</code></h3> +<p> +Set <code>args.outputs.background_color</code> to an <code>Array</code> with <code>RGB</code> values (eg. <code>[255, 255, 255]</code> for the color white). +</p> +<h3 id='-----.sounds-'><code>.sounds</code></h3> +<p> +Send a file path to this collection to play a sound. The sound file must be under the <code>mygame</code> directory. Example: <code>args.outputs.sounds << "sounds/jump.wav"</code>. +</p> +<h3 id='-----.solids-'><code>.solids</code></h3> +<p> +Send a Primitive to this collection to render a filled in rectangle to the screen. This collection is cleared at the end of every frame. +</p> +<h3 id='-----.static_solids-'><code>.static_solids</code></h3> +<p> +Send a Primitive to this collection to render a filled in rectangle to the screen. This collection is not cleared at the end of every frame. And objects can be mutated by reference. +</p> +<h3 id='-----.sprites-,--.static_sprites-'><code>.sprites</code>, <code>.static_sprites</code></h3> +<p> +Send a Primitive to this collection to render a sprite to the screen. +</p> +<h3 id='-----.primitives-,--.static_primitives-'><code>.primitives</code>, <code>.static_primitives</code></h3> +<p> +Send a Primitive of any type and it'll be rendered. The Primitive must have a <code>primitive_marker</code> that returns <code>:solid</code>, <code>:sprite</code>, <code>:label</code>, <code>:line</code>, <code>:border</code>. +</p> +<h3 id='-----.labels-,--.static_labels-'><code>.labels</code>, <code>.static_labels</code></h3> +<p> +Send a Primitive to this collection to render text to the screen. +</p> +<h3 id='-----.lines-,--.static_lines-'><code>.lines</code>, <code>.static_lines</code></h3> +<p> +Send a Primitive to this collection to render a line to the screen. +</p> +<h3 id='-----.borders-,--.static_borders-'><code>.borders</code>, <code>.static_borders</code></h3> +<p> +Send a Primitive to this collection to render an unfilled rectangle to the screen. +</p> +<h3 id='-----.debug-,--.static_debug-'><code>.debug</code>, <code>.static_debug</code></h3> +<p> +Send any Primitive to this collection which represents things you render to the screen for debugging purposes. Primitives in this collection will not be rendered in a production release of your game. +</p> +<h2 id='----args.geometry-'><code>args.geometry</code></h2> +<p> +This property contains geometric functions. Functions can be invoked via <code>args.geometry.FUNCTION</code>. +</p> +<h3 id='-----.inside_rect?-rect_1,-rect_2-'><code>.inside_rect? rect_1, rect_2</code></h3> +<p> +Returns <code>true</code> if <code>rect_1</code> is inside <code>rect_2</code>. +</p> +<h3 id='-----.intersect_rect?-rect_2,-rect_2-'><code>.intersect_rect? rect_2, rect_2</code></h3> +<p> +Returns <code>true</code> if <code>rect_1</code> intersects <code>rect_2</code>. +</p> +<h3 id='-----.scale_rect-rect,-x_percentage,-y_percentage-'><code>.scale_rect rect, x_percentage, y_percentage</code></h3> +<p> +Returns a new rectangle that is scaled by the percentages provided. +</p> +<h3 id='-----.angle_to-start_point,-end_point-'><code>.angle_to start_point, end_point</code></h3> +<p> +Returns the angle in degrees between two points <code>start_point</code> to <code>end_point</code>. +</p> +<h3 id='-----.angle_from-start_point,-end_point-'><code>.angle_from start_point, end_point</code></h3> +<p> +Returns the angle in degrees between two points <code>start_point</code> from <code>end_point</code>. +</p> +<h3 id='-----.point_inside_circle?-point,-circle_center_point,-radius-'><code>.point_inside_circle? point, circle_center_point, radius</code></h3> +<p> +Returns <code>true</code> if a point is inside a circle defined by its center and radius. +</p> +<h3 id='-----.center_inside_rect-rect,-other_rect-'><code>.center_inside_rect rect, other_rect</code></h3> +<p> +Returns a new rectangle based of off <code>rect</code> that is centered inside of <code>other_rect</code>. +</p> +<h3 id='-----.center_inside_rect_x-rect,-other_rect-'><code>.center_inside_rect_x rect, other_rect</code></h3> +<p> +Returns a new rectangle based of off <code>rect</code> that is centered horizontally inside of <code>other_rect</code>. +</p> +<h3 id='-----.center_inside_rect_y-rect,-other_rect-'><code>.center_inside_rect_y rect, other_rect</code></h3> +<p> +Returns a new rectangle based of off <code>rect</code> that is centered vertically inside of <code>other_rect</code>. +</p> +<h3 id='-----.anchor_rect-rect,-anchor_x,-anchor_y-'><code>.anchor_rect rect, anchor_x, anchor_y</code></h3> +<p> +Returns a new rectangle based of off <code>rect</code> that has been repositioned based on the percentages passed into anchor_x, and anchor_y. +</p> +<h3 id='-----.shift_line-line,-x,-y-'><code>.shift_line line, x, y</code></h3> +<p> +Returns a line that is offset by <code>x</code>, and <code>y</code>. +</p> +<h3 id='-----.line_y_intercept-line-'><code>.line_y_intercept line</code></h3> +<p> +Given a line, the <code>b</code> value is determined for the point slope form equation: <code>y = mx + b</code>. +</p> +<h3 id='-----.angle_between_lines-line_one,-line_two,-replace_infinity--'><code>.angle_between_lines line_one, line_two, replace_infinity:</code></h3> +<p> +Returns the angle between two lines as if they were infinitely long. A numeric value can be passed in for the last parameter which would represent lines that do not intersect. +</p> +<h3 id='-----.line_slope-line,-replace_infinity--'><code>.line_slope line, replace_infinity:</code></h3> +<p> +Given a line, the <code>m</code> value is determined for the point slope form equation: <code>y = mx + b</code>. +</p> +<h3 id='-----.line_rise_run-'><code>.line_rise_run</code></h3> +<p> +Given a line, a <code>Hash</code> is returned that returns the slope as <code>x</code> and <code>y</code> properties with normalized values (the number is between -1 and 1). +</p> +<h3 id='-----.ray_test-point,-line-'><code>.ray_test point, line</code></h3> +<p> +Given a point and a line, <code>:on</code>, <code>:left</code>, or <code>:right</code> which represents the location of the point relative to the line. +</p> +<h3 id='-----.line_rect-line-'><code>.line_rect line</code></h3> +<p> +Returns the bounding rectangle for a line. +</p> +<h3 id='-----.line_intersect-line_one,-line_two-'><code>.line_intersect line_one, line_two</code></h3> +<p> +Returns a point that represents the intersection of the lines. +</p> +<h3 id='-----.distance-point_one,-point_two-'><code>.distance point_one, point_two</code></h3> +<p> +Returns the distance between two points. +</p> +<h3 id='-----.cubic_bezier-t,-a,-b,-c,-d-'><code>.cubic_bezier t, a, b, c, d</code></h3> +<p> +Returns the cubic bezier function for tick_count <code>t</code> with anchors <code>a</code>, <code>b</code>, <code>c</code>, and <code>d</code>. +</p> +<h2 id='----args.easing-'><code>args.easing</code></h2> +<p> +A set of functions that allow you to determine the current progression of an easing function. +</p> +<h3 id='-----.ease-start_tick,-current_tick,-duration,-easing_functions-'><code>.ease start_tick, current_tick, duration, easing_functions</code></h3> +<p> +Given a start, current, duration, and easing function names, <code>ease</code> returns a number between 0 and 1 that represents the progress of an easing function. +</p> +<p> +The built in easing definitions you have access to are <code>:identity</code>, <code>:flip</code>, <code>:quad</code>, <code>:cube</code>, <code>:quart</code>, and <code>:quint</code>. +</p> +<p> +This example will move a box at a linear speed from 0 to 1280. +</p> +<pre><code class="language-ruby">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 +</code></pre> +<h3 id='-----.ease_spline-start_tick,-current_tick,-duration,-spline-'><code>.ease_spline start_tick, current_tick, duration, spline</code></h3> +<p> +Given a start, current, duration, and a multiple bezier values, this function returns a number between 0 and 1 that represents the progress of an easing function. +</p> +<p> +This example will move a box at a linear speed from 0 to 1280 and then back to 0 using two bezier definitions (represented as an array with four values). +</p> +<pre><code class="language-ruby">def tick args + start_time = 10 + duration = 60 + spline = [ + [ 0, 0.25, 0.75, 1.0], + [1.0, 0.75, 0.25, 0] + ] + current_progress = args.easing.ease_spline start_time, + args.state.tick_count, + duration, + spline + args.outputs.solids << { x: 1280 * current_progress, y: 360, w: 10, h: 10 } +end +</code></pre> +<h2 id='----args.string-'><code>args.string</code></h2> +<p> +Useful string functions not included in Ruby core libraries. +</p> +<h3 id='-----.wrapped_lines-string,-max_character_length-'><code>.wrapped_lines string, max_character_length</code></h3> +<p> +This function will return a collection of strings given an input <code>string</code> and <code>max_character_length</code>. The collection of strings returned will split the input string into strings of <code>length <= max_character_length</code>. +</p> +<p> +The following example takes a string with new lines and creates a label for each one. Labels (<code>args.outputs.labels</code>) ignore newline characters <code>\n</code>. +</p> +<pre><code class="language-ruby">def tick args + long_string = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. +teger 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 +</code></pre> +<h2 id='----args.grid-'><code>args.grid</code></h2> +<p> +Returns the virtual grid for the game. +</p> +<h3 id='-----.name-'><code>.name</code></h3> +<p> +Returns either <code>:origin_bottom_left</code> or <code>:origin_center</code>. +</p> +<h3 id='-----.bottom-'><code>.bottom</code></h3> +<p> +Returns the <code>y</code> value that represents the bottom of the grid. +</p> +<h3 id='-----.top-'><code>.top</code></h3> +<p> +Returns the <code>y</code> value that represents the top of the grid. +</p> +<h3 id='-----.left-'><code>.left</code></h3> +<p> +Returns the <code>x</code> value that represents the left of the grid. +</p> +<h3 id='-----.right-'><code>.right</code></h3> +<p> +Returns the <code>x</code> value that represents the right of the grid. +</p> +<h3 id='-----.rect-'><code>.rect</code></h3> +<p> +Returns a rectangle Primitive that represents the grid. +</p> +<h3 id='-----.origin_bottom_left!-'><code>.origin_bottom_left!</code></h3> +<p> +Change the grids coordinate system to 0, 0 at the bottom left corner. +</p> +<h3 id='-----.origin_center!-'><code>.origin_center!</code></h3> +<p> +Change the grids coordinate system to 0, 0 at the center of the screen. +</p> +<h3 id='-----.w-'><code>.w</code></h3> +<p> +Returns the grid's width (always 1280). +</p> +<h3 id='-----.h-'><code>.h</code></h3> +<p> +Returns the grid's height (always 720). +</p> +<h2 id='----args.gtk-'><code>args.gtk</code></h2> +<p> +This represents the DragonRuby Game Toolkit's Runtime Environment and can be accessed via <code>args.gtk.METHOD</code>. +</p> +<h3 id='-----.argv-'><code>.argv</code></h3> +<p> +Returns a <code>String</code> that represents the parameters passed into the <code>./dragonruby</code> binary. +</p> +<h3 id='-----.platform-'><code>.platform</code></h3> +<p> +Returns a <code>String</code> representing the operating system the game is running on. +</p> +<h3 id='-----.request_quit-'><code>.request_quit</code></h3> +<p> +Request that the runtime quit the game. +</p> +<h3 id='-----.write_file-path,-contents-'><code>.write_file path, contents</code></h3> +<p> +Writes/overwrites a file within the game directory + path. +</p> +<h3 id='-----.write_file_root-'><code>.write_file_root</code></h3> +<p> +Writes/overwrites a file within the root dragonruby binary directory + path. +</p> +<h3 id='-----.append_file-path,-contents-'><code>.append_file path, contents</code></h3> +<p> +Append content to a file located at the game directory + path. +</p> +<h3 id='-----.append_file_root-path,-contents-'><code>.append_file_root path, contents</code></h3> +<p> +Append content to a file located at the root dragonruby binary directory + path. +</p> +<h3 id='-----.read_file-path-'><code>.read_file path</code></h3> +<p> +Reads a file from the sandboxed file system. +</p> +<h3 id='-----.parse_xml-string,-parse_xml_file-path-'><code>.parse_xml string, parse_xml_file path</code></h3> +<p> +Returns a <code>Hash</code> for a <code>String</code> that represents XML. +</p> +<h3 id='-----.parse_json-string,-parse_json_file-path-'><code>.parse_json string, parse_json_file path</code></h3> +<p> +Returns a <code>Hash</code> for a <code>String</code> that represents JSON. +</p> +<h3 id='-----.http_get-url,-extra_headers-=-{}-'><code>.http_get url, extra_headers = {}</code></h3> +<p> +Creates an async task to perform an HTTP GET. +</p> +<h3 id='-----.http_post-url,-form_fields-=-{},-extra_headers-=-{}-'><code>.http_post url, form_fields = {}, extra_headers = {}</code></h3> +<p> +Creates an async task to perform an HTTP POST. +</p> +<h3 id='-----.reset-'><code>.reset</code></h3> +<p> +Resets the game by deleting all data in <code>args.state</code> and setting <code>args.state.tick_count</code> back to <code>0</code>. +</p> +<h3 id='-----.stop_music-'><code>.stop_music</code></h3> +<p> +Stops all background music. +</p> +<h3 id='-----.calcstringbox-str,-size_enum,-font-'><code>.calcstringbox str, size_enum, font</code></h3> +<p> +Returns a tuple with width and height of a string being rendered. +</p> +<h3 id='-----.slowmo!-factor-'><code>.slowmo! factor</code></h3> +<p> +Slows the game down by the factor provided. +</p> +<h3 id='-----.notify!-string-'><code>.notify! string</code></h3> +<p> +Renders a toast message at the bottom of the screen. +</p> +<h3 id='-----.system-'><code>.system</code></h3> +<p> +Invokes a shell command and prints the result to the console. +</p> +<h3 id='-----.exec-'><code>.exec</code></h3> +<p> +Invokes a shell command and returns a <code>String</code> that represents the result. +</p> +<h3 id='-----.save_state-'><code>.save_state</code></h3> +<p> +Saves the game state to <code>game_state.txt</code>. +</p> +<h3 id='-----.load_state-'><code>.load_state</code></h3> +<p> +Load <code>args.state</code> from <code>game_state.txt</code>. +</p> +<h3 id='-----.serialize_state-file,-state-'><code>.serialize_state file, state</code></h3> +<p> +Saves entity state to a file. If only one parameter is provided a string is returned for state instead of writing to a file. +</p> +<h3 id='-----.deserialize_state-file-'><code>.deserialize_state file</code></h3> +<p> +Returns entity state from a file or serialization data represented as a <code>String</code>. +</p> +<h3 id='-----.reset_sprite-path-'><code>.reset_sprite path</code></h3> +<p> +Invalids the texture cache of a sprite. +</p> +<h3 id='-----.show_cursor-'><code>.show_cursor</code></h3> +<p> +Shows the mouse cursor. +</p> +<h3 id='-----.hide_cursor-'><code>.hide_cursor</code></h3> +<p> +Hides the mouse cursor. +</p> +<h3 id='-----.cursor_shown?-'><code>.cursor_shown?</code></h3> +<p> +Returns <code>true</code> if the mouse cursor is shown. +</p> +<h3 id='-----.set_window_fullscreen-enabled-'><code>.set_window_fullscreen enabled</code></h3> +<p> +Sets the game to either fullscreen (<code>enabled=true</code>) or windowed (<code>enabled=false)</code>. +</p> +<h3 id='-----.openurl-url-'><code>.openurl url</code></h3> +<p> +Opens a url using the Operating System's default browser. +</p> +<h3 id='-----.get_base_dir-'><code>.get_base_dir</code></h3> +<p> +Returns the full path of the DragonRuby binary directory. +</p> +<h3 id='-----.get_game_dir-'><code>.get_game_dir</code></h3> +<p> +Returns the full path of the game directory in its sandboxed environment. +</p> <h1 id='--docs---gtk--runtime#reset-'>DOCS: <code>GTK::Runtime#reset</code></h1> <p> This function will reset Kernel.tick_count to 0 and will remove all data from args.state. @@ -1171,11 +1707,21 @@ This function will reset Kernel.tick_count to 0 and will remove all data from ar <p> This function returns the width and height of a string. </p> -<pre>def tick args +<pre><code class="language-ruby">def tick args args.state.string_size ||= args.gtk.calcstringbox "Hello World" args.state.string_size_font_size ||= args.gtk.calcstringbox "Hello World" end -</pre> +</code></pre> +<h1 id='--docs---gtk--runtime#write_file-'>DOCS: <code>GTK::Runtime#write_file</code></h1> +<p> +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 <code>GTK::Runtime#append_file</code> to append to the file as opposed to overwriting. +</p> +<pre><code class="language-ruby">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 +</code></pre> <h1 id='--docs---array-'>DOCS: <code>Array</code></h1> <p> 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. @@ -1187,7 +1733,7 @@ The function given a block returns a new <code>Enumerable</code> of values. <p> Example of using <code>Array#map</code> in conjunction with <code>args.state</code> and <code>args.outputs.sprites</code> to render sprites to the screen. </p> -<pre>def tick args +<pre><code class="language-ruby">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 @@ -1217,7 +1763,7 @@ Example of using <code>Array#map</code> in conjunction with <code>args.state</co ] end end -</pre> +</code></pre> <h1 id='--docs---array#each-'>DOCS: <code>Array#each</code></h1> <p> The function, given a block, invokes the block for each item in the <code>Array</code>. <code>Array#each</code> is synonymous to foreach constructs in other languages. @@ -1225,7 +1771,7 @@ The function, given a block, invokes the block for each item in the <code>Array< <p> Example of using <code>Array#each</code> in conjunction with <code>args.state</code> and <code>args.outputs.sprites</code> to render sprites to the screen: </p> -<pre>def tick args +<pre><code class="language-ruby">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 @@ -1254,29 +1800,29 @@ Example of using <code>Array#each</code> in conjunction with <code>args.state</c ] end end -</pre> +</code></pre> <h1 id='--docs---array#reject_nil-'>DOCS: <code>Array#reject_nil</code></h1> <p> Returns an <code>Enumerable</code> rejecting items that are <code>nil</code>, this is an alias for <code>Array#compact</code>: </p> -<pre>repl do +<pre><code class="language-ruby">repl do a = [1, nil, 4, false, :a] puts a.reject_nil # => [1, 4, false, :a] puts a.compact # => [1, 4, false, :a] end -</pre> +</code></pre> <h1 id='--docs---array#reject_false-'>DOCS: <code>Array#reject_false</code></h1> <p> Returns an `Enumerable` rejecting items that are `nil` or `false`. </p> -<pre>repl do +<pre><code class="language-ruby">repl do a = [1, nil, 4, false, :a] puts a.reject_false # => [1, 4, :a] end -</pre> +</code></pre> <h1 id='--docs---array#product-'>DOCS: <code>Array#product</code></h1> <p> Returns all combinations of values between two arrays. @@ -1284,24 +1830,24 @@ Returns all combinations of values between two arrays. <p> Here are some examples of using <code>product</code>. Paste the following code at the bottom of main.rb and save the file to see the results: </p> -<pre>repl do +<pre><code class="language-ruby">repl do a = [0, 1] puts a.product # => [[0, 0], [0, 1], [1, 0], [1, 1]] end -</pre> -<pre>repl do +</code></pre> +<pre><code class="language-ruby">repl do a = [ 0, 1] b = [:a, :b] puts a.product b # => [[0, :a], [0, :b], [1, :a], [1, :b]] end -</pre> +</code></pre> <h1 id='--docs---array#map_2d-'>DOCS: <code>Array#map_2d</code></h1> <p> 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. </p> -<pre>repl do +<pre><code class="language-ruby">repl do stage = [ [:enemy, :empty, :player], [:empty, :empty, :empty], @@ -1322,7 +1868,7 @@ Assuming the array is an array of arrays, Given a block, each 2D array index inv puts "Occupied Tiles" puts occupied_tiles end -</pre> +</code></pre> <h1 id='--docs---array#include_any?-'>DOCS: <code>Array#include_any?</code></h1> <p> Given a collection of items, the function will return <code>true</code> if any of <code>self</code>'s items exists in the collection of items passed in: @@ -1334,7 +1880,7 @@ Assuming the array contains objects that respond to <code>left</code>, <code>rig <p> The default tolerance is set to <code>0.1</code>, which means that the primitives are not considered intersecting unless they are overlapping by more than <code>0.1</code>. </p> -<pre>repl do +<pre><code class="language-ruby">repl do # Here is a player class that has position and implement # the ~attr_rect~ contract. class Player @@ -1388,45 +1934,105 @@ The default tolerance is set to <code>0.1</code>, which means that the primitive # => true puts "" end -</pre> +</code></pre> +<h1 id='--docs---gtk--args#audio-'>DOCS: <code>GTK::Args#audio</code></h1> +<p> +Hash that contains audio sources that are playing. If you want to add a new sound add a hash with keys/values as in the following example: +</p> +<pre><code class="language-ruby">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', # Filename + x: 0.0, y: 0.0, z: 0.0, # Relative position to the listener, x, y, z from -1.0 to 1.0 + gain: 1.0, # Volume (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: false, # Set to true to loop the sound/music until you stop it + } +end +</code></pre> +<p> +Sounds that don't specify <code>looping: true</code> will be removed automatically from the hash after the playback ends. Looping sounds or sounds that should stop early must be removed manually. +</p> +<h2 id='---audio-synthesis-(pro-only)'>Audio synthesis (Pro only)</h2> +<p> +Instead of a path to an audio file you can specify an array <code>[channels, sample_rate, sound_source]</code> for <code>input</code> 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. +</p> +<ul> +<li><code>channels</code> is the number of channels: 1 = mono, 2 = stereo</li> +<li><code>sample_rate</code> is the number of values per seconds you will provide to describe the audio wave</li> +<li><code>sound_source</code> The source of your sound. See below</li> +</ul> +<h3 id='----sound-source'>Sound source</h3> +<p> +A sound source can be one of two things: +</p> +<ul> +<li>A <code>Proc</code> 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 the <code>looping</code> option will have no effect.</li> +<li>An array of sample values that will be played back once. This is useful for procedurally generated one-off SFX. + <code>looping</code> will work as expected</li> +</ul> +<p> +When you specify 2 for <code>channels</code>, 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. +</p> +<h3 id='----example-'>Example:</h3> +<pre><code class="language-ruby">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 +</code></pre> <h1 id='--docs---gtk--outputs-'>DOCS: <code>GTK::Outputs</code></h1> <p> Outputs is how you render primitives to the screen. The minimal setup for rendering something to the screen is via a <code>tick</code> method defined in mygame/app/main.rb </p> -<pre>def tick args +<pre><code class="language-ruby">def tick args # code goes here end -</pre> +</code></pre> <h1 id='--docs---gtk--outputs#solids-'>DOCS: <code>GTK::Outputs#solids</code></h1> <p> Add primitives to this collection to render a solid to the screen. </p> -<h2>Rendering a solid using an Array</h2> +<h2 id='---rendering-a-solid-using-an-array'>Rendering a solid using an Array</h2> <p> Creates a solid black rectangle located at 100, 100. 160 pixels wide and 90 pixels tall. </p> -<pre>def tick args +<pre><code class="language-ruby">def tick args # X Y WIDTH HEIGHT args.outputs.solids << [100, 100, 160, 90] end -</pre> -<h2>Rendering a solid using an Array with colors and alpha</h2> +</code></pre> +<h2 id='---rendering-a-solid-using-an-array-with-colors-and-alpha'>Rendering a solid using an Array with colors and alpha</h2> <p> The value for the color and alpha is an number between <code>0</code> and <code>255</code>. The alpha property is optional and will be set to <code>255</code> if not specified. </p> <p> Creates a green solid rectangle with an opacity of 50%. </p> -<pre>def tick args +<pre><code class="language-ruby">def tick args # X Y WIDTH HEIGHT RED GREEN BLUE ALPHA args.outputs.solids << [100, 100, 160, 90, 0, 255, 0, 128] end -</pre> -<h2>Rendering a solid using a Hash</h2> +</code></pre> +<h2 id='---rendering-a-solid-using-a-hash'>Rendering a solid using a Hash</h2> <p> 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. </p> -<pre>def tick args +<pre><code class="language-ruby">def tick args args.outputs.solids << { x: 0, y: 0, @@ -1438,15 +2044,15 @@ If you want a more readable invocation. You can use the following hash to create a: 255 } end -</pre> -<h2>Rendering a solid using a Class</h2> +</code></pre> +<h2 id='---rendering-a-solid-using-a-class'>Rendering a solid using a Class</h2> <p> You can also create a class with solid/border properties and render it as a primitive. ALL properties must on the class. *Additionally*, a method called <code>primitive_marker</code> must be defined on the class. </p> <p> Here is an example: </p> -<pre># Create type with ALL solid properties AND primitive_marker +<pre><code class="language-ruby"># Create type with ALL solid properties AND primitive_marker class Solid attr_accessor :x, :y, :w, :h, :r, :g, :b, :a @@ -1470,7 +2076,7 @@ def tick args # render solid/border args.outputs.solids << Square.new(10, 10, 32) end -</pre> +</code></pre> <h1 id='--docs---gtk--outputs#borders-'>DOCS: <code>GTK::Outputs#borders</code></h1> <p> Add primitives to this collection to render an unfilled solid to the screen. Take a look at the documentation for Outputs#solids. @@ -1481,32 +2087,32 @@ The only difference between the two primitives is where they are added. <p> Instead of using <code>args.outputs.solids</code>: </p> -<pre>def tick args +<pre><code class="language-ruby">def tick args # X Y WIDTH HEIGHT args.outputs.solids << [100, 100, 160, 90] end -</pre> +</code></pre> <p> You have to use <code>args.outputs.borders</code>: </p> -<pre>def tick args +<pre><code class="language-ruby">def tick args # X Y WIDTH HEIGHT args.outputs.borders << [100, 100, 160, 90] end -</pre> +</code></pre> <h1 id='--docs---gtk--outputs#screenshots-'>DOCS: <code>GTK::Outputs#screenshots</code></h1> <p> 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. </p> -<pre>def tick args +<pre><code class="language-ruby">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 -</pre> -<h2>Chroma key (Making a color transparent)</h2> +</code></pre> +<h2 id='---chroma-key-(making-a-color-transparent)'>Chroma key (Making a color transparent)</h2> <p> 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 <code>r</code>, <code>g</code>, <code>b</code> will be set to the transparency specified by <code>a</code>. </p> @@ -1517,7 +2123,7 @@ The example above sets the color white (255, 255, 255) as transparent. <p> The mouse is accessible via <code>args.inputs.mouse</code>: </p> -<pre>def tick args +<pre><code class="language-ruby">def tick args # Rendering a label that shows the mouse's x and y position (via args.inputs.mouse). args.outputs.labels << [ 10, @@ -1525,7 +2131,7 @@ The mouse is accessible via <code>args.inputs.mouse</code>: "The mouse's position is: #{args.inputs.mouse.x} #{args.inputs.mouse.y}." ] end -</pre> +</code></pre> <p> The mouse has the following properties. </p> @@ -1567,18 +2173,18 @@ The <code>GTK::MousePoint</code> has the following properties. <p> <code>GTK::OpenEntity</code> is accessible within the DragonRuby's top level <code>tick</code> function via the <code>args.state</code> property. </p> -<pre>def tick args +<pre><code class="language-ruby">def tick args args.state.x ||= 100 args.outputs.labels << [10, 710, "value of x is: #{args.state.x}."] end -</pre> +</code></pre> <p> The primary benefit of using <code>args.state</code> as opposed to instance variables is that <code>GTK::OpenEntity</code> allows for arbitrary nesting of properties without the need to create intermediate objects. </p> <p> For example: </p> -<pre>def tick args +<pre><code class="language-ruby">def tick args # intermediate player object does not need to be created args.state.player.x ||= 100 args.state.player.y ||= 100 @@ -1588,7 +2194,7 @@ For example: "player x, y is:#{args.state.player.x}, #{args.state.player.y}." ] end -</pre> +</code></pre> <h1 id='--docs---gtk--openentity#as_hash-'>DOCS: <code>GTK::OpenEntity#as_hash</code></h1> <p> Returns a reference to the <code>GTK::OpenEntity</code> as a <code>Hash</code>. This property is useful when you want to treat <code>args.state</code> as a <code>Hash</code> and invoke methods such as <code>Hash#each</code>. @@ -1596,7 +2202,7 @@ Returns a reference to the <code>GTK::OpenEntity</code> as a <code>Hash</code>. <p> Example: </p> -<pre>def tick args +<pre><code class="language-ruby">def tick args args.state.x ||= 100 args.state.y ||= 100 values = args.state @@ -1611,7 +2217,7 @@ Example: ] end end -</pre> +</code></pre> <h1 id='--docs---numeric#frame_index-'>DOCS: <code>Numeric#frame_index</code></h1> <p> This function is helpful for determining the index of frame-by-frame sprite animation. The numeric value <code>self</code> represents the moment the animation started. @@ -1630,7 +2236,7 @@ This function is helpful for determining the index of frame-by-frame sprite an <p> Example using variables: </p> -<pre>def tick args +<pre><code class="language-ruby">def tick args start_looping_at = 0 number_of_sprites = 6 number_of_frames_to_show_each_sprite = 4 @@ -1651,11 +2257,11 @@ Example using variables: "sprites/dragon-#{sprite_index}.png" ] end -</pre> +</code></pre> <p> Example using named parameters: </p> -<pre>def tick args +<pre><code class="language-ruby">def tick args start_looping_at = 0 sprite_index = @@ -1674,7 +2280,7 @@ Example using named parameters: "sprites/dragon-#{sprite_index}.png" ] end -</pre> +</code></pre> <h1 id='--docs---numeric#elapsed_time-'>DOCS: <code>Numeric#elapsed_time</code></h1> <p> 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`. @@ -1682,7 +2288,7 @@ For a given number, the elapsed frames since that number is returned. `Kernel.ti <p> Here is an example of how elapsed_time can be used. </p> -<pre>def tick args +<pre><code class="language-ruby">def tick args args.state.last_click_at ||= 0 # record when a mouse click occurs @@ -1695,11 +2301,11 @@ Here is an example of how elapsed_time can be used. args.outputs.labels << [10, 710, "It has been over 2 seconds since the mouse was clicked."] end end -</pre> +</code></pre> <p> And here is an example where the override parameter is passed in: </p> -<pre>def tick args +<pre><code class="language-ruby">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 @@ -1715,7 +2321,7 @@ And here is an example where the override parameter is passed in: args.outputs.labels << [10, 710, "It has been over 4 seconds since the mouse was clicked."] end end -</pre> +</code></pre> <h1 id='--docs---numeric#elapsed?-'>DOCS: <code>Numeric#elapsed?</code></h1> <p> Returns true if <code>Numeric#elapsed_time</code> is greater than the number. An optional parameter can be passed into <code>elapsed?</code> which is added to the number before evaluating whether <code>elapsed?</code> is true. @@ -1723,7 +2329,7 @@ Returns true if <code>Numeric#elapsed_time</code> is greater than the number. An <p> Example usage (no optional parameter): </p> -<pre>def tick args +<pre><code class="language-ruby">def tick args args.state.box_queue ||= [] if args.state.box_queue.empty? @@ -1747,11 +2353,11 @@ Example usage (no optional parameter): args.state.box_queue -= boxes_to_destroy end -</pre> +</code></pre> <p> Example usage (with optional parameter): </p> -<pre>def tick args +<pre><code class="language-ruby">def tick args args.state.box_queue ||= [] if args.state.box_queue.empty? @@ -1779,7 +2385,7 @@ Example usage (with optional parameter): args.state.box_queue -= boxes_to_destroy end -</pre> +</code></pre> <h1 id='--docs---numeric#created?-'>DOCS: <code>Numeric#created?</code></h1> <p> Returns true if <code>Numeric#elapsed_time == 0</code>. Essentially communicating that number is equal to the current frame. @@ -1787,7 +2393,7 @@ Returns true if <code>Numeric#elapsed_time == 0</code>. Essentially communicatin <p> Example usage: </p> -<pre>def tick args +<pre><code class="language-ruby">def tick args args.state.box_queue ||= [] if args.state.box_queue.empty? @@ -1803,7 +2409,7 @@ Example usage: args.state.box_queue -= boxes_to_spawn_this_frame end -</pre> +</code></pre> <h1 id='--docs---kernel-'>DOCS: <code>Kernel</code></h1> <p> 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. @@ -1816,12 +2422,41 @@ Returns the current tick of the game. This value is reset if you call $gtk.reset <p> Returns the current tick of the application from the point it was started. This value is never reset. </p> -<h1 id='--open-source'>Open Source</h1> +<h1 id='--docs---geometry-'>DOCS: <code>Geometry</code></h1> <p> -Follows is a source code listing for all files that have been open sourced. This code can be found in the <code>./samples</code> directory and online at <a href='https://github.com/DragonRuby/dragonruby-game-toolkit-contrib/'>https://github.com/DragonRuby/dragonruby-game-toolkit-contrib/</a>. +The Geometry module contains methods for calculations that are frequently used in game development. </p> -<h1 id='--learn-ruby-optional---beginner-ruby-primer---automation.rb'>Learn Ruby Optional - Beginner Ruby Primer - automation.rb</h1> -<pre># ./samples/00_learn_ruby_optional/00_beginner_ruby_primer/app/automation.rb +<h1 id='--docs---gtk--geometry#scale_rect-'>DOCS: <code>GTK::Geometry#scale_rect</code></h1> +<p> +Given an array with 4 elements representing a rect [x, y, w, h], this function returns a scaled rect. It accepts three arguments: +</p> +<p> +<code>ratio</code>: 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. +</p> +<p> +<code>anchor_x</code> and <code>anchor_y</code> 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. +</p> +<p> +The <code>scale_rect</code> method can be applied directly to a sprite or other primitives. See CHEATSHEET: How to Scale a Sprite. +</p> +<pre><code class="language-ruby">def tick args + # x, y, w, h + my_rect = [100, 100, 200, 200] + + # halve the dimensions of the rect: + # ratio, anchor_x, anchor_y + new_rect = my_rect.scale_rect(0.5, 0.5, 0.5) + + puts new_rect # => [150.0, 150.0, 100.0, 100.0] +end +</code></pre> +<h1 id='--source-code'>Source Code</h1> +<p> +Follows is a source code listing for all files that have been open sourced. This code can be found in the <code>./samples</code> directory. +</p> +<h2 id='---samples'>Samples</h2> +<h3 id='----learn-ruby-optional---beginner-ruby-primer---automation.rb'>Learn Ruby Optional - Beginner Ruby Primer - automation.rb</h3> +<pre><code class="language-ruby"># ./samples/00_learn_ruby_optional/00_beginner_ruby_primer/app/automation.rb # ========================================================================== # _ _ ________ __ _ _____ _____ _______ ______ _ _ _ _ _ _ # | | | | ____\ \ / / | | |_ _|/ ____|__ __| ____| \ | | | | | | @@ -1943,9 +2578,9 @@ $gtk.schedule_callback 400 do $gtk.console.eval_the_set_command end -</pre> -<h1 id='--learn-ruby-optional---beginner-ruby-primer---main.rb'>Learn Ruby Optional - Beginner Ruby Primer - main.rb</h1> -<pre># ./samples/00_learn_ruby_optional/00_beginner_ruby_primer/app/main.rb +</code></pre> +<h3 id='----learn-ruby-optional---beginner-ruby-primer---main.rb'>Learn Ruby Optional - Beginner Ruby Primer - main.rb</h3> +<pre><code class="language-ruby"># ./samples/00_learn_ruby_optional/00_beginner_ruby_primer/app/main.rb # ========================================================================== # _ _ ________ __ _ _____ _____ _______ ______ _ _ _ _ _ _ # | | | | ____\ \ / / | | |_ _|/ ____|__ __| ____| \ | | | | | | @@ -2264,9 +2899,9 @@ def outputs $tutorial_outputs end -</pre> -<h1 id='--learn-ruby-optional---intermediate-ruby-primer---printing.txt'>Learn Ruby Optional - Intermediate Ruby Primer - printing.txt</h1> -<pre># ./samples/00_learn_ruby_optional/00_intermediate_ruby_primer/app/01_printing.txt +</code></pre> +<h3 id='----learn-ruby-optional---intermediate-ruby-primer---printing.txt'>Learn Ruby Optional - Intermediate Ruby Primer - printing.txt</h3> +<pre><code class="language-ruby"># ./samples/00_learn_ruby_optional/00_intermediate_ruby_primer/app/01_printing.txt # ==================================================================================== # Commenting Code # ==================================================================================== @@ -2299,9 +2934,9 @@ repl do puts '====' end -</pre> -<h1 id='--learn-ruby-optional---intermediate-ruby-primer---strings.txt'>Learn Ruby Optional - Intermediate Ruby Primer - strings.txt</h1> -<pre># ./samples/00_learn_ruby_optional/00_intermediate_ruby_primer/app/02_strings.txt +</code></pre> +<h3 id='----learn-ruby-optional---intermediate-ruby-primer---strings.txt'>Learn Ruby Optional - Intermediate Ruby Primer - strings.txt</h3> +<pre><code class="language-ruby"># ./samples/00_learn_ruby_optional/00_intermediate_ruby_primer/app/02_strings.txt # ==================================================================================== # Strings # ==================================================================================== @@ -2318,9 +2953,9 @@ repl do puts 'This #{message} is not interpolated because the string uses single quotes.' end -</pre> -<h1 id='--learn-ruby-optional---intermediate-ruby-primer---numbers.txt'>Learn Ruby Optional - Intermediate Ruby Primer - numbers.txt</h1> -<pre># ./samples/00_learn_ruby_optional/00_intermediate_ruby_primer/app/03_numbers.txt +</code></pre> +<h3 id='----learn-ruby-optional---intermediate-ruby-primer---numbers.txt'>Learn Ruby Optional - Intermediate Ruby Primer - numbers.txt</h3> +<pre><code class="language-ruby"># ./samples/00_learn_ruby_optional/00_intermediate_ruby_primer/app/03_numbers.txt # ==================================================================================== # Numerics # ==================================================================================== @@ -2343,9 +2978,9 @@ repl do puts '' end -</pre> -<h1 id='--learn-ruby-optional---intermediate-ruby-primer---booleans.txt'>Learn Ruby Optional - Intermediate Ruby Primer - booleans.txt</h1> -<pre># ./samples/00_learn_ruby_optional/00_intermediate_ruby_primer/app/04_booleans.txt +</code></pre> +<h3 id='----learn-ruby-optional---intermediate-ruby-primer---booleans.txt'>Learn Ruby Optional - Intermediate Ruby Primer - booleans.txt</h3> +<pre><code class="language-ruby"># ./samples/00_learn_ruby_optional/00_intermediate_ruby_primer/app/04_booleans.txt # ==================================================================================== # Booleans # ==================================================================================== @@ -2379,9 +3014,9 @@ repl do end end -</pre> -<h1 id='--learn-ruby-optional---intermediate-ruby-primer---conditionals.txt'>Learn Ruby Optional - Intermediate Ruby Primer - conditionals.txt</h1> -<pre># ./samples/00_learn_ruby_optional/00_intermediate_ruby_primer/app/05_conditionals.txt +</code></pre> +<h3 id='----learn-ruby-optional---intermediate-ruby-primer---conditionals.txt'>Learn Ruby Optional - Intermediate Ruby Primer - conditionals.txt</h3> +<pre><code class="language-ruby"># ./samples/00_learn_ruby_optional/00_intermediate_ruby_primer/app/05_conditionals.txt # ==================================================================================== # Conditionals # ==================================================================================== @@ -2497,9 +3132,9 @@ repl do end end -</pre> -<h1 id='--learn-ruby-optional---intermediate-ruby-primer---looping.txt'>Learn Ruby Optional - Intermediate Ruby Primer - looping.txt</h1> -<pre># ./samples/00_learn_ruby_optional/00_intermediate_ruby_primer/app/06_looping.txt +</code></pre> +<h3 id='----learn-ruby-optional---intermediate-ruby-primer---looping.txt'>Learn Ruby Optional - Intermediate Ruby Primer - looping.txt</h3> +<pre><code class="language-ruby"># ./samples/00_learn_ruby_optional/00_intermediate_ruby_primer/app/06_looping.txt # ==================================================================================== # Looping # ==================================================================================== @@ -2556,9 +3191,9 @@ repl do end end -</pre> -<h1 id='--learn-ruby-optional---intermediate-ruby-primer---functions.txt'>Learn Ruby Optional - Intermediate Ruby Primer - functions.txt</h1> -<pre># ./samples/00_learn_ruby_optional/00_intermediate_ruby_primer/app/07_functions.txt +</code></pre> +<h3 id='----learn-ruby-optional---intermediate-ruby-primer---functions.txt'>Learn Ruby Optional - Intermediate Ruby Primer - functions.txt</h3> +<pre><code class="language-ruby"># ./samples/00_learn_ruby_optional/00_intermediate_ruby_primer/app/07_functions.txt # ==================================================================================== # Functions # ==================================================================================== @@ -2629,9 +3264,9 @@ repl do puts "Passing nil: #{function_with_nil_default_with_local}" end -</pre> -<h1 id='--learn-ruby-optional---intermediate-ruby-primer---arrays.txt'>Learn Ruby Optional - Intermediate Ruby Primer - arrays.txt</h1> -<pre># ./samples/00_learn_ruby_optional/00_intermediate_ruby_primer/app/08_arrays.txt +</code></pre> +<h3 id='----learn-ruby-optional---intermediate-ruby-primer---arrays.txt'>Learn Ruby Optional - Intermediate Ruby Primer - arrays.txt</h3> +<pre><code class="language-ruby"># ./samples/00_learn_ruby_optional/00_intermediate_ruby_primer/app/08_arrays.txt # ==================================================================================== # Arrays # ==================================================================================== @@ -2843,16 +3478,16 @@ end # awesome in Ruby: https://docs.ruby-lang.org/en/2.0.0/Array.html # ==================================================================================== -</pre> -<h1 id='--learn-ruby-optional---intermediate-ruby-primer---main.rb'>Learn Ruby Optional - Intermediate Ruby Primer - main.rb</h1> -<pre># ./samples/00_learn_ruby_optional/00_intermediate_ruby_primer/app/main.rb +</code></pre> +<h3 id='----learn-ruby-optional---intermediate-ruby-primer---main.rb'>Learn Ruby Optional - Intermediate Ruby Primer - main.rb</h3> +<pre><code class="language-ruby"># ./samples/00_learn_ruby_optional/00_intermediate_ruby_primer/app/main.rb def tick args args.outputs.labels << [640, 380, "Open repl.rb in the text editor of your choice and follow the document.", 0, 1] end -</pre> -<h1 id='--rendering-basics---labels---main.rb'>Rendering Basics - Labels - main.rb</h1> -<pre># ./samples/01_rendering_basics/01_labels/app/main.rb +</code></pre> +<h3 id='----rendering-basics---labels---main.rb'>Rendering Basics - Labels - main.rb</h3> +<pre><code class="language-ruby"># ./samples/01_rendering_basics/01_labels/app/main.rb =begin APIs listing that haven't been encountered in a previous sample apps: @@ -2953,9 +3588,9 @@ def tick_instructions args, text, y = 715 args.outputs.debug << [640, y - 25, "(click to dismiss instructions)" , -2, 1, 255, 255, 255].label end -</pre> -<h1 id='--rendering-basics---lines---main.rb'>Rendering Basics - Lines - main.rb</h1> -<pre># ./samples/01_rendering_basics/02_lines/app/main.rb +</code></pre> +<h3 id='----rendering-basics---lines---main.rb'>Rendering Basics - Lines - main.rb</h3> +<pre><code class="language-ruby"># ./samples/01_rendering_basics/02_lines/app/main.rb =begin APIs listing that haven't been encountered in a previous sample apps: @@ -3011,9 +3646,9 @@ def tick_instructions args, text, y = 715 args.outputs.debug << [640, y - 25, "(click to dismiss instructions)" , -2, 1, 255, 255, 255].label end -</pre> -<h1 id='--rendering-basics---solids-borders---main.rb'>Rendering Basics - Solids Borders - main.rb</h1> -<pre># ./samples/01_rendering_basics/03_solids_borders/app/main.rb +</code></pre> +<h3 id='----rendering-basics---solids-borders---main.rb'>Rendering Basics - Solids Borders - main.rb</h3> +<pre><code class="language-ruby"># ./samples/01_rendering_basics/03_solids_borders/app/main.rb =begin APIs listing that haven't been encountered in a previous sample apps: @@ -3081,9 +3716,9 @@ def tick_instructions args, text, y = 715 args.outputs.debug << [640, y - 25, "(click to dismiss instructions)" , -2, 1, 255, 255, 255].label end -</pre> -<h1 id='--rendering-basics---sprites---main.rb'>Rendering Basics - Sprites - main.rb</h1> -<pre># ./samples/01_rendering_basics/04_sprites/app/main.rb +</code></pre> +<h3 id='----rendering-basics---sprites---main.rb'>Rendering Basics - Sprites - main.rb</h3> +<pre><code class="language-ruby"># ./samples/01_rendering_basics/04_sprites/app/main.rb =begin APIs listing that haven't been encountered in a previous sample apps: @@ -3128,9 +3763,9 @@ def tick_instructions args, text, y = 715 args.outputs.debug << [640, y - 25, "(click to dismiss instructions)" , -2, 1, 255, 255, 255].label end -</pre> -<h1 id='--rendering-basics---sounds---main.rb'>Rendering Basics - Sounds - main.rb</h1> -<pre># ./samples/01_rendering_basics/05_sounds/app/main.rb +</code></pre> +<h3 id='----rendering-basics---sounds---main.rb'>Rendering Basics - Sounds - main.rb</h3> +<pre><code class="language-ruby"># ./samples/01_rendering_basics/05_sounds/app/main.rb =begin APIs Listing that haven't been encountered in previous sample apps: @@ -3140,12 +3775,6 @@ end of available notes. Reminders: - - args.grid.(left|right|top|bottom): Pixel value for the boundaries of the virtual - 720 p screen (Dragon Ruby Game Toolkits's virtual resolution is always 1280x720). - - - args.state.new_entity: Used when we want to create a new object, like a sprite or button. - For example, if we want to create a new button, we would declare it as a new entity and - then define its properties. - String interpolation: Uses #{} syntax; everything between the #{ and the } is evaluated as Ruby code, and the placeholder is replaced with its corresponding value or result. @@ -3153,16 +3782,6 @@ end - args.outputs.labels: An array. The values generate a label. The parameters are [X, Y, TEXT, SIZE, ALIGNMENT, RED, GREEN, BLUE, ALPHA, FONT STYLE] For more information about labels, go to mygame/documentation/02-labels.md. - - - reject: Removes elements from a collection if they meet certain requirements. - - - first: Returns the first element of an array. - - - inside_rect: Returns true or false depending on if the point is inside the rect. - - - to_sym: Returns symbol corresponding to string. Will create a symbol if it does - not already exist. - =end # This sample app allows users to test their musical skills by matching the piano sound that plays in each @@ -3170,160 +3789,18 @@ end # Runs all the methods necessary for the game to function properly. def tick args - defaults args - render args - calc args - input_mouse args - tick_instructions args, "Sample app shows how to play sounds. args.outputs.sounds << \"path_to_wav.wav\"" -end - -# Sets default values and creates empty collections -# Initialization happens in the first frame only -def defaults _ - _.state.notes ||= [] - _.state.click_feedbacks ||= [] - _.state.current_level ||= 1 - _.state.times_wrong ||= 0 # when game starts, user hasn't guessed wrong yet -end + args.outputs.labels << [640, 360, "Click anywhere to play a random sound.", 0, 1] + args.state.notes ||= [:C3, :D3, :E3, :F3, :G3, :A3, :B3, :C4] -# Uses a label to display current level, and shows the score -# Creates a button to play the sample note, and displays the available notes that could be a potential match -def render _ - - # grid.w_half positions the label in the horizontal center of the screen. - _.outputs.labels << [_.grid.w_half, _.grid.top.shift_down(40), "Hole #{_.state.current_level} of 9", 0, 1, 0, 0, 0] - - render_score _ # shows score on screen - - if _.state.game_over # if game is over, a "play again" button is shown - # Calculations ensure that Play Again label is displayed in center of border - # Remove calculations from y parameters and see what happens to border and label placement - _.state.play_again_border ||= _.state.with_meta([560, _.grid.h * 3 / 4 - 40, 160, 60], 'again') # array definition, text/title - _.outputs.labels << [_.grid.w_half, _.grid.h * 3 / 4, "Play Again", 0, 1, 0, 0, 0] # outputs label - _.outputs.borders << _.state.play_again_border # outputs border - else # otherwise, if game is not over - # Calculations ensure that label appears in center of border - _.state.play_note_border ||= _.state.with_meta([560, _.grid.h * 3 / 4 - 40, 160, 60], 'play') # array definition, text/title - _.outputs.labels << [_.grid.w_half, _.grid.h * 3 / 4, "Play Note ##{_.state.current_level}", 0, 1, 0, 0, 0] # outputs label - _.outputs.borders << _.state.play_note_border # outputs border - end - - return if _.state.game_over # return if game is over - - _.outputs.labels << [_.grid.w_half, 400, "I think the note is a(n)...", 0, 1, 0, 0, 0] # outputs label - - # Shows all of the available notes that can be potential matches. - available_notes.each_with_index do |note, i| - _.state.notes[i] ||= piano_button(_, note, i + 1) # calls piano_button method on each note (creates label and border) - _.outputs.labels << _.state.notes[i].label # outputs note on screen with a label and a border - _.outputs.borders << _.state.notes[i].border - end - - # Shows whether or not the user is correct by filling the screen with either red or green - _.outputs.solids << _.state.click_feedbacks.map { |c| c.solid } -end - -# Shows the score (number of times the user guesses wrong) onto the screen using labels. -def render_score _ - if _.state.times_wrong == 0 # if the user has guessed wrong zero times, the score is par - _.outputs.labels << [_.grid.w_half, _.grid.top.shift_down(80), "Score: PAR", 0, 1, 0, 0, 0] - else # otherwise, number of times the user has guessed wrong is shown - _.outputs.labels << [_.grid.w_half, _.grid.top.shift_down(80), "Score: +#{_.state.times_wrong}", 0, 1, 0, 0, 0] # shows score using string interpolation - end -end - -# Sets the target note for the level and performs calculations on click_feedbacks. -def calc _ - _.state.target_note ||= available_notes.sample # chooses a note from available_notes collection as target note - _.state.click_feedbacks.each { |c| c.solid[-1] -= 5 } # remove this line and solid color will remain on screen indefinitely - # comment this line out and the solid color will keep flashing on screen instead of being removed from click_feedbacks collection - _.state.click_feedbacks.reject! { |c| c.solid[-1] <= 0 } -end - -# Uses input from the user to play the target note, as well as the other notes that could be a potential match. -def input_mouse _ - return unless _.inputs.mouse.click # return unless the mouse is clicked - - # finds button that was clicked by user - button_clicked = _.outputs.borders.find_all do |b| # go through borders collection to find all borders that meet requirements - _.inputs.mouse.click.point.inside_rect? b # find button border that mouse was clicked inside of - end.reject {|b| !_.state.meta(b)}.first # reject, return first element - - return unless button_clicked # return unless button_clicked as a value (a button was clicked) - - queue_click_feedback _, # calls queue_click_feedback method on the button that was clicked - button_clicked.x, - button_clicked.y, - button_clicked.w, - button_clicked.h, - 150, 100, 200 # sets color of button to shade of purple - - if _.state.meta(button_clicked) == 'play' # if "play note" button is pressed - _.outputs.sounds << "sounds/#{_.state.target_note}.wav" # sound of target note is output - elsif _.state.meta(button_clicked) == 'again' # if "play game again" button is pressed - _.state.target_note = nil # no target note - _.state.current_level = 1 # starts at level 1 again - _.state.times_wrong = 0 # starts off with 0 wrong guesses - _.state.game_over = false # the game is not over (because it has just been restarted) - else # otherwise if neither of those buttons were pressed - _.outputs.sounds << "sounds/#{_.state.meta(button_clicked)}.wav" # sound of clicked note is played - if _.state.meta(button_clicked).to_sym == _.state.target_note # if clicked note is target note - _.state.target_note = nil # target note is emptied - - if _.state.current_level < 9 # if game hasn't reached level 9 - _.state.current_level += 1 # game goes to next level - else # otherwise, if game has reached level 9 - _.state.game_over = true # the game is over - end - - queue_click_feedback _, 0, 0, _.grid.w, _.grid.h, 100, 200, 100 # green shown if user guesses correctly - else # otherwise, if clicked note is not target note - _.state.times_wrong += 1 # increments times user guessed wrong - queue_click_feedback _, 0, 0, _.grid.w, _.grid.h, 200, 100, 100 # red shown is user guesses wrong - end - end -end - -# Creates a collection of all of the available notes as symbols -def available_notes - [:C3, :D3, :E3, :F3, :G3, :A3, :B3, :C4] -end - -# Creates buttons for each note, and sets a label (the note's name) and border for each note's button. -def piano_button _, note, position - _.state.new_entity(:button) do |b| # declares button as new entity - b.label = [460 + 40.mult(position), _.grid.h * 0.4, "#{note}", 0, 1, 0, 0, 0] # label definition - b.border = _.state.with_meta([460 + 40.mult(position) - 20, _.grid.h * 0.4 - 32, 40, 40], note) # border definition, text/title; 20 subtracted so label is in center of border - end -end - -# Color of click feedback changes depending on what button was clicked, and whether the guess is right or wrong -# If a button is clicked, the inside of button is purple (see input_mouse method) -# If correct note is clicked, screen turns green -# If incorrect note is clicked, screen turns red (again, see input_mouse method) -def queue_click_feedback _, x, y, w, h, *color - _.state.click_feedbacks << _.state.new_entity(:click_feedback) do |c| # declares feedback as new entity - c.solid = [x, y, w, h, *color, 255] # sets color + if args.inputs.mouse.click + # Play a sound by adding a string to args.outputs.sounds + args.outputs.sounds << "sounds/#{args.state.notes.sample}.wav" # sound of target note is output end end -def tick_instructions args, text, y = 715 - return if args.state.key_event_occurred - if args.inputs.mouse.click || - args.inputs.keyboard.directional_vector || - args.inputs.keyboard.key_down.enter || - args.inputs.keyboard.key_down.escape - args.state.key_event_occurred = true - end - - args.outputs.debug << [0, y - 50, 1280, 60].solid - args.outputs.debug << [640, y, text, 1, 1, 255, 255, 255].label - args.outputs.debug << [640, y - 25, "(click to dismiss instructions)" , -2, 1, 255, 255, 255].label -end - -</pre> -<h1 id='--rendering-basics---audio-mixer---main.rb'>Rendering Basics - Audio Mixer - main.rb</h1> -<pre># ./samples/01_rendering_basics/06_audio_mixer/app/main.rb +</code></pre> +<h3 id='----rendering-basics---audio-mixer---main.rb'>Rendering Basics - Audio Mixer - main.rb</h3> +<pre><code class="language-ruby"># ./samples/01_rendering_basics/06_audio_mixer/app/main.rb $gtk.reset $boxsize = 30 @@ -3485,369 +3962,606 @@ def tick args render_ui args end -</pre> -<h1 id='--rendering-basics---sound-synthesis---main.rb'>Rendering Basics - Sound Synthesis - main.rb</h1> -<pre># ./samples/01_rendering_basics/07_sound_synthesis/app/main.rb -def tick args - defaults args - render args - input args - process_audio_queue args -end +</code></pre> +<h3 id='----rendering-basics---sound-synthesis---main.rb'>Rendering Basics - Sound Synthesis - main.rb</h3> +<pre><code class="language-ruby"># ./samples/01_rendering_basics/07_sound_synthesis/app/main.rb +begin # region: top level tick methods + def tick args + defaults args + render args + input args + process_audio_queue args + end + + def defaults args + args.state.sine_waves ||= {} + args.state.square_waves ||= {} + args.state.saw_tooth_waves ||= {} + args.state.triangle_waves ||= {} + args.state.audio_queue ||= [] + args.state.buttons ||= [ + (frequency_buttons args), + (sine_wave_note_buttons args), + (bell_buttons args), + (square_wave_note_buttons args), + (saw_tooth_wave_note_buttons args), + (triangle_wave_note_buttons args), + ].flatten + end + + def render args + args.outputs.borders << args.state.buttons.map { |b| b[:border] } + args.outputs.labels << args.state.buttons.map { |b| b[:label] } + args.outputs.labels << args.layout + .rect(row: 0, col: 11.5) + .yield_self { |r| r.merge y: r.y + r.h } + .merge(text: "This is a Pro only feature. Click here to watch the YouTube video if you are on the Standard License.", + alignment_enum: 1) + end -def defaults args - args.state.sine_waves ||= {} - args.state.audio_queue ||= [] - args.state.buttons ||= [ - (frequency_buttons args), - (note_buttons args), - (bell_buttons args) - ].flatten -end -def frequency_buttons args - [ - (button args, - row: 4.0, col: 0, text: "300hz", - frequency: 300, - method_to_call: :play_sine_wave), - (button args, - row: 5.0, col: 0, text: "400hz", - frequency: 400, - method_to_call: :play_sine_wave), - (button args, - row: 6.0, col: 0, text: "500hz", - frequency: 500, - method_to_call: :play_sine_wave), - ] -end + def input args + args.state.buttons.each do |b| + if args.inputs.mouse.click && (args.inputs.mouse.click.inside_rect? b[:rect]) + parameter_string = (b.slice :frequency, :note, :octave).map { |k, v| "#{k}: #{v}" }.join ", " + args.gtk.notify! "#{b[:method_to_call]} #{parameter_string}" + send b[:method_to_call], args, b + end + end -def play_sine_wave args, sender - queue_sine_wave args, - frequency: sender[:frequency], - duration: 1.seconds, - fade_out: true -end + if args.inputs.mouse.click && (args.inputs.mouse.click.inside_rect? (args.layout.rect(row: 0).yield_self { |r| r.merge y: r.y + r.h.half, h: r.h.half })) + args.gtk.openurl 'https://www.youtube.com/watch?v=zEzovM5jT-k&ab_channel=AmirRajan' + end + end + def process_audio_queue args + to_queue = args.state.audio_queue.find_all { |v| v[:queue_at] <= args.tick_count } + args.state.audio_queue -= to_queue + to_queue.each { |a| args.audio[a[:id]] = a } -def note_buttons args - [ - (button args, - row: 1.5, col: 3, text: "C4", - note: :c, octave: 4, method_to_call: :play_note), - (button args, - row: 2.5, col: 3, text: "D4", - note: :d, octave: 4, method_to_call: :play_note), - (button args, - row: 3.5, col: 3, text: "E4", - note: :e, octave: 4, method_to_call: :play_note), - (button args, - row: 4.5, col: 3, text: "F4", - note: :f, octave: 4, method_to_call: :play_note), - (button args, - row: 5.5, col: 3, text: "G4", - note: :g, octave: 4, method_to_call: :play_note), - (button args, - row: 6.5, col: 3, text: "A5", - note: :a, octave: 5, method_to_call: :play_note), - (button args, - row: 7.5, col: 3, text: "B5", - note: :b, octave: 5, method_to_call: :play_note), - (button args, - row: 8.5, col: 3, text: "C5", - note: :c, octave: 5, method_to_call: :play_note), - ] -end + args.audio.find_all { |k, v| v[:decay_rate] } + .each { |k, v| v[:gain] -= v[:decay_rate] } -def play_note args, sender - queue_sine_wave args, - frequency: (frequency_for note: sender[:note], - octave: sender[:octave]), - duration: 1.seconds, - fade_out: true -end + sounds_to_stop = args.audio + .find_all { |k, v| v[:stop_at] && args.state.tick_count >= v[:stop_at] } + .map { |k, v| k } -def bell_buttons args - [ - (button args, - row: 1.5, col: 6, text: "Bell C4", - note: :c, octave: 4, method_to_call: :play_bell), - (button args, - row: 2.5, col: 6, text: "Bell D4", - note: :d, octave: 4, method_to_call: :play_bell), - (button args, - row: 3.5, col: 6, text: "Bell E4", - note: :e, octave: 4, method_to_call: :play_bell), - (button args, - row: 4.5, col: 6, text: "Bell F4", - note: :f, octave: 4, method_to_call: :play_bell), - (button args, - row: 5.5, col: 6, text: "Bell G4", - note: :g, octave: 4, method_to_call: :play_bell), - (button args, - row: 6.5, col: 6, text: "Bell A5", - note: :a, octave: 5, method_to_call: :play_bell), - (button args, - row: 7.5, col: 6, text: "Bell B5", - note: :b, octave: 5, method_to_call: :play_bell), - (button args, - row: 8.5, col: 6, text: "Bell C5", - note: :c, octave: 5, method_to_call: :play_bell), - ] + sounds_to_stop.each { |k| args.audio.delete k } + end end -def play_bell args, sender - queue_bell args, - frequency: (frequency_for note: sender[:note], - octave: sender[:octave]), - duration: 2.seconds, - fade_out: true -end +begin # region: button definitions, ui layout, callback functions + def button args, opts + button_def = opts.merge rect: (args.layout.rect (opts.merge w: 2, h: 1)) -def render args - args.outputs.borders << args.state.buttons.map { |b| b[:border] } - args.outputs.labels << args.state.buttons.map { |b| b[:label] } - args.outputs.labels << args.layout - .rect(row: 0, - col: 11.5) - .yield_self { |r| r.merge y: r.y + r.h } - .merge(text: "This is a Pro only feature. Click here to watch the YouTube video if you are on the Standard License.", - alignment_enum: 1) -end + button_def[:border] = button_def[:rect].merge r: 0, g: 0, b: 0 -def input args - args.state.buttons.each do |b| - if args.inputs.mouse.click.inside_rect? b[:rect] - parameter_string = (b.slice :frequency, :note, :octave).map { |k, v| "#{k}: #{v}" }.join ", " - args.gtk.notify! "#{b[:method_to_call]} #{parameter_string}" - send b[:method_to_call], args, b - end + label_offset_x = 5 + label_offset_y = 30 + + button_def[:label] = button_def[:rect].merge text: opts[:text], + size_enum: -2.5, + x: button_def[:rect].x + label_offset_x, + y: button_def[:rect].y + label_offset_y + + button_def end - if args.inputs.mouse.click.inside_rect? (args.layout.rect(row: 0).yield_self { |r| r.merge y: r.y + r.h.half, h: r.h.half }) - args.gtk.openurl 'https://www.youtube.com/watch?v=zEzovM5jT-k&ab_channel=AmirRajan' + def play_sine_wave args, sender + queue_sine_wave args, + frequency: sender[:frequency], + duration: 1.seconds, + fade_out: true end -end -def process_audio_queue args - to_queue = args.state.audio_queue.find_all { |v| v[:queue_at] <= args.tick_count } - args.state.audio_queue -= to_queue + def play_note args, sender + method_to_call = :queue_sine_wave + method_to_call = :queue_square_wave if sender[:type] == :square + method_to_call = :queue_saw_tooth_wave if sender[:type] == :saw_tooth + method_to_call = :queue_triangle_wave if sender[:type] == :triangle + method_to_call = :queue_bell if sender[:type] == :bell - to_queue.each do |a| - args.audio[a[:id]] = a + send method_to_call, args, + frequency: (frequency_for note: sender[:note], octave: sender[:octave]), + duration: 1.seconds, + fade_out: true end - args.audio.each do |k, v| - if v[:decay_rate] - v[:gain] -= v[:decay_rate] - end + def frequency_buttons args + [ + (button args, + row: 4.0, col: 0, text: "300hz", + frequency: 300, + method_to_call: :play_sine_wave), + (button args, + row: 5.0, col: 0, text: "400hz", + frequency: 400, + method_to_call: :play_sine_wave), + (button args, + row: 6.0, col: 0, text: "500hz", + frequency: 500, + method_to_call: :play_sine_wave), + ] end - sounds_to_stop = args.audio.find_all do |k, v| - v[:stop_at] && args.state.tick_count >= v[:stop_at] + def sine_wave_note_buttons args + [ + (button args, + row: 1.5, col: 2, text: "Sine C4", + note: :c, octave: 4, type: :sine, method_to_call: :play_note), + (button args, + row: 2.5, col: 2, text: "Sine D4", + note: :d, octave: 4, type: :sine, method_to_call: :play_note), + (button args, + row: 3.5, col: 2, text: "Sine E4", + note: :e, octave: 4, type: :sine, method_to_call: :play_note), + (button args, + row: 4.5, col: 2, text: "Sine F4", + note: :f, octave: 4, type: :sine, method_to_call: :play_note), + (button args, + row: 5.5, col: 2, text: "Sine G4", + note: :g, octave: 4, type: :sine, method_to_call: :play_note), + (button args, + row: 6.5, col: 2, text: "Sine A5", + note: :a, octave: 5, type: :sine, method_to_call: :play_note), + (button args, + row: 7.5, col: 2, text: "Sine B5", + note: :b, octave: 5, type: :sine, method_to_call: :play_note), + (button args, + row: 8.5, col: 2, text: "Sine C5", + note: :c, octave: 5, type: :sine, method_to_call: :play_note), + ] end - sounds_to_stop.each do |(k, v)| - args.audio.delete k + def square_wave_note_buttons args + [ + (button args, + row: 1.5, col: 6, text: "Square C4", + note: :c, octave: 4, type: :square, method_to_call: :play_note), + (button args, + row: 2.5, col: 6, text: "Square D4", + note: :d, octave: 4, type: :square, method_to_call: :play_note), + (button args, + row: 3.5, col: 6, text: "Square E4", + note: :e, octave: 4, type: :square, method_to_call: :play_note), + (button args, + row: 4.5, col: 6, text: "Square F4", + note: :f, octave: 4, type: :square, method_to_call: :play_note), + (button args, + row: 5.5, col: 6, text: "Square G4", + note: :g, octave: 4, type: :square, method_to_call: :play_note), + (button args, + row: 6.5, col: 6, text: "Square A5", + note: :a, octave: 5, type: :square, method_to_call: :play_note), + (button args, + row: 7.5, col: 6, text: "Square B5", + note: :b, octave: 5, type: :square, method_to_call: :play_note), + (button args, + row: 8.5, col: 6, text: "Square C5", + note: :c, octave: 5, type: :square, method_to_call: :play_note), + ] end -end - -def graph_sine_wave args, sine_wave, frequency - if args.state.tick_count != args.state.graphed_at - args.outputs.static_lines.clear - args.outputs.static_sprites.clear + def saw_tooth_wave_note_buttons args + [ + (button args, + row: 1.5, col: 8, text: "Saw C4", + note: :c, octave: 4, type: :saw_tooth, method_to_call: :play_note), + (button args, + row: 2.5, col: 8, text: "Saw D4", + note: :d, octave: 4, type: :saw_tooth, method_to_call: :play_note), + (button args, + row: 3.5, col: 8, text: "Saw E4", + note: :e, octave: 4, type: :saw_tooth, method_to_call: :play_note), + (button args, + row: 4.5, col: 8, text: "Saw F4", + note: :f, octave: 4, type: :saw_tooth, method_to_call: :play_note), + (button args, + row: 5.5, col: 8, text: "Saw G4", + note: :g, octave: 4, type: :saw_tooth, method_to_call: :play_note), + (button args, + row: 6.5, col: 8, text: "Saw A5", + note: :a, octave: 5, type: :saw_tooth, method_to_call: :play_note), + (button args, + row: 7.5, col: 8, text: "Saw B5", + note: :b, octave: 5, type: :saw_tooth, method_to_call: :play_note), + (button args, + row: 8.5, col: 8, text: "Saw C5", + note: :c, octave: 5, type: :saw_tooth, method_to_call: :play_note), + ] end - r, g, b = frequency.to_i % 80, frequency.to_i % 128, frequency.to_i % 255 - center_row = args.layout.rect(row: 5, col: 9) - x_scale = 20 - y_scale = 100 - max_points = 20 + def triangle_wave_note_buttons args + [ + (button args, + row: 1.5, col: 10, text: "Triangle C4", + note: :c, octave: 4, type: :triangle, method_to_call: :play_note), + (button args, + row: 2.5, col: 10, text: "Triangle D4", + note: :d, octave: 4, type: :triangle, method_to_call: :play_note), + (button args, + row: 3.5, col: 10, text: "Triangle E4", + note: :e, octave: 4, type: :triangle, method_to_call: :play_note), + (button args, + row: 4.5, col: 10, text: "Triangle F4", + note: :f, octave: 4, type: :triangle, method_to_call: :play_note), + (button args, + row: 5.5, col: 10, text: "Triangle G4", + note: :g, octave: 4, type: :triangle, method_to_call: :play_note), + (button args, + row: 6.5, col: 10, text: "Triangle A5", + note: :a, octave: 5, type: :triangle, method_to_call: :play_note), + (button args, + row: 7.5, col: 10, text: "Triangle B5", + note: :b, octave: 5, type: :triangle, method_to_call: :play_note), + (button args, + row: 8.5, col: 10, text: "Triangle C5", + note: :c, octave: 5, type: :triangle, method_to_call: :play_note), + ] + end - points = sine_wave - if sine_wave.length > max_points - resolution = sine_wave.length.idiv max_points - points = sine_wave.find_all - .with_index { |y, i| i % resolution == 0 } + def bell_buttons args + [ + (button args, + row: 1.5, col: 4, text: "Bell C4", + note: :c, octave: 4, type: :bell, method_to_call: :play_note), + (button args, + row: 2.5, col: 4, text: "Bell D4", + note: :d, octave: 4, type: :bell, method_to_call: :play_note), + (button args, + row: 3.5, col: 4, text: "Bell E4", + note: :e, octave: 4, type: :bell, method_to_call: :play_note), + (button args, + row: 4.5, col: 4, text: "Bell F4", + note: :f, octave: 4, type: :bell, method_to_call: :play_note), + (button args, + row: 5.5, col: 4, text: "Bell G4", + note: :g, octave: 4, type: :bell, method_to_call: :play_note), + (button args, + row: 6.5, col: 4, text: "Bell A5", + note: :a, octave: 5, type: :bell, method_to_call: :play_note), + (button args, + row: 7.5, col: 4, text: "Bell B5", + note: :b, octave: 5, type: :bell, method_to_call: :play_note), + (button args, + row: 8.5, col: 4, text: "Bell C5", + note: :c, octave: 5, type: :bell, method_to_call: :play_note), + ] end +end - args.outputs.static_lines << points.map_with_index do |y, x| - next_y = points[x + 1] +begin # region: wave generation + begin # sine wave + def defaults_sine_wave_for + { frequency: 440, sample_rate: 48000 } + end - if next_y - { - x: center_row.x + (x * x_scale), - y: center_row.y + center_row.h.half + y_scale * y, - x2: center_row.x + ((x + 1) * x_scale), - y2: center_row.y + center_row.h.half + y_scale * next_y, - r: r, - g: g, - b: b - } + def sine_wave_for opts = {} + opts = defaults_sine_wave_for.merge opts + frequency = opts[:frequency] + sample_rate = opts[:sample_rate] + period_size = (sample_rate.fdiv frequency).ceil + period_size.map_with_index do |i| + Math::sin((2.0 * Math::PI) / (sample_rate.to_f / frequency.to_f) * i) + end.to_a end - end - args.outputs.static_sprites << points.map_with_index do |y, x| - { - x: (center_row.x + (x * x_scale)) - 1, - y: (center_row.y + center_row.h.half + y_scale * y) - 1, - w: 2, - h: 2, - path: 'sprites/square-black.png' - } + def defaults_queue_sine_wave + { frequency: 440, duration: 60, gain: 1.0, fade_out: false, queue_in: 0 } + end + + def queue_sine_wave args, opts = {} + opts = defaults_queue_sine_wave.merge opts + frequency = opts[:frequency] + sample_rate = 48000 + + sine_wave = sine_wave_for frequency: frequency, sample_rate: sample_rate + args.state.sine_waves[frequency] ||= sine_wave_for frequency: frequency, sample_rate: sample_rate + + proc = lambda do + generate_audio_data args.state.sine_waves[frequency], sample_rate + end + + audio_state = new_audio_state args, opts + audio_state[:input] = [1, sample_rate, proc] + queue_audio args, audio_state: audio_state, wave: sine_wave + end end - args.state.graphed_at = args.state.tick_count -end + begin # region: square wave + def defaults_square_wave_for + { frequency: 440, sample_rate: 48000 } + end -def defaults_period_sine_wave_for - { frequency: 440, sample_rate: 48000 } -end + def square_wave_for opts = {} + opts = defaults_square_wave_for.merge opts + sine_wave = sine_wave_for opts + sine_wave.map do |v| + if v >= 0 + 1.0 + else + -1.0 + end + end.to_a + end -def sine_wave_for opts = { } - opts = defaults_period_sine_wave_for.merge opts - frequency = opts[:frequency] - sample_rate = opts[:sample_rate] - period_size = (sample_rate.fdiv frequency).ceil - period_size.map_with_index do |i| - Math::sin((2.0 * Math::PI) / (sample_rate.to_f / frequency.to_f) * i) - end.to_a -end + def defaults_queue_square_wave + { frequency: 440, duration: 60, gain: 0.3, fade_out: false, queue_in: 0 } + end -def generate_audio_data sine_wave, sample_rate - sample_size = (sample_rate.fdiv (1000.fdiv 60)).ceil - copy_count = (sample_size.fdiv sine_wave.length).ceil - sine_wave * copy_count -end + def queue_square_wave args, opts = {} + opts = defaults_queue_square_wave.merge opts + frequency = opts[:frequency] + sample_rate = 48000 -def defaults_queue_sine_wave - { frequency: 440, duration: 60, gain: 1.0, fade_out: false, queue_in: 0 } -end + square_wave = square_wave_for frequency: frequency, sample_rate: sample_rate + args.state.square_waves[frequency] ||= square_wave_for frequency: frequency, sample_rate: sample_rate -def queue_sine_wave args, opts = { } - opts = defaults_queue_sine_wave.merge opts - decay_rate = 0 - decay_rate = 1.fdiv(opts[:duration]) * opts[:gain] if opts[:fade_out] - frequency = opts[:frequency] - sample_rate = 48000 + proc = lambda do + generate_audio_data args.state.square_waves[frequency], sample_rate + end - audio_state = { - id: (new_id! args), - frequency: frequency, - sample_rate: 48000, - stop_at: args.tick_count + opts[:queue_in] + opts[:duration], - gain: opts[:gain].to_f, - queue_at: args.state.tick_count + opts[:queue_in], - decay_rate: decay_rate, - pitch: 1.0, - looping: true, - paused: false - } + audio_state = new_audio_state args, opts + audio_state[:input] = [1, sample_rate, proc] + queue_audio args, audio_state: audio_state, wave: square_wave + end + end + + begin # region: saw tooth wave + def defaults_saw_tooth_wave_for + { frequency: 440, sample_rate: 48000 } + end + + def saw_tooth_wave_for opts = {} + opts = defaults_saw_tooth_wave_for.merge opts + sine_wave = sine_wave_for opts + period_size = sine_wave.length + sine_wave.map_with_index do |v, i| + (((i % period_size).fdiv period_size) * 2) - 1 + end + end + + def defaults_queue_saw_tooth_wave + { frequency: 440, duration: 60, gain: 0.3, fade_out: false, queue_in: 0 } + end - sine_wave = sine_wave_for frequency: frequency, sample_rate: sample_rate - args.state.sine_waves[frequency] ||= sine_wave_for frequency: frequency, sample_rate: sample_rate + def queue_saw_tooth_wave args, opts = {} + opts = defaults_queue_saw_tooth_wave.merge opts + frequency = opts[:frequency] + sample_rate = 48000 - proc = lambda do - generate_audio_data args.state.sine_waves[frequency], sample_rate + saw_tooth_wave = saw_tooth_wave_for frequency: frequency, sample_rate: sample_rate + args.state.saw_tooth_waves[frequency] ||= saw_tooth_wave_for frequency: frequency, sample_rate: sample_rate + + proc = lambda do + generate_audio_data args.state.saw_tooth_waves[frequency], sample_rate + end + + audio_state = new_audio_state args, opts + audio_state[:input] = [1, sample_rate, proc] + queue_audio args, audio_state: audio_state, wave: saw_tooth_wave + end end - audio_state[:input] = [1, sample_rate, proc] - graph_sine_wave args, sine_wave, frequency - args.state.audio_queue << audio_state -end + begin # region: triangle wave + def defaults_triangle_wave_for + { frequency: 440, sample_rate: 48000 } + end -def defaults_queue_bell - { frequency: 440, duration: 1.seconds, queue_in: 0 } -end + def triangle_wave_for opts = {} + opts = defaults_saw_tooth_wave_for.merge opts + sine_wave = sine_wave_for opts + period_size = sine_wave.length + sine_wave.map_with_index do |v, i| + ratio = (i.fdiv period_size) + if ratio <= 0.5 + (ratio * 4) - 1 + else + ratio -= 0.5 + 1 - (ratio * 4) + end + end + end -def queue_bell args, opts = {} - (bell_to_sine_waves (defaults_queue_bell.merge opts)).each { |b| queue_sine_wave args, b } -end + def defaults_queue_triangle_wave + { frequency: 440, duration: 60, gain: 1.0, fade_out: false, queue_in: 0 } + end -def bell_harmonics - [ - { frequency_ratio: 0.5, duration_ratio: 1.00 }, - { frequency_ratio: 1.0, duration_ratio: 0.80 }, - { frequency_ratio: 2.0, duration_ratio: 0.60 }, - { frequency_ratio: 3.0, duration_ratio: 0.40 }, - { frequency_ratio: 4.2, duration_ratio: 0.25 }, - { frequency_ratio: 5.4, duration_ratio: 0.20 }, - { frequency_ratio: 6.8, duration_ratio: 0.15 } - ] -end + def queue_triangle_wave args, opts = {} + opts = defaults_queue_triangle_wave.merge opts + frequency = opts[:frequency] + sample_rate = 48000 -def bell_to_sine_waves opts - bell_harmonics.map do |b| - { - frequency: opts[:frequency] * b[:frequency_ratio], - duration: opts[:duration] * b[:duration_ratio], - queue_in: opts[:queue_in], - gain: (1.fdiv bell_harmonics.length), - fade_out: true - } + triangle_wave = triangle_wave_for frequency: frequency, sample_rate: sample_rate + args.state.triangle_waves[frequency] ||= triangle_wave_for frequency: frequency, sample_rate: sample_rate + + proc = lambda do + generate_audio_data args.state.triangle_waves[frequency], sample_rate + end + + audio_state = new_audio_state args, opts + audio_state[:input] = [1, sample_rate, proc] + queue_audio args, audio_state: audio_state, wave: triangle_wave + end end -end -def defaults_frequency_for - { note: :a, octave: 5, sharp: false, flat: false } -end + begin # region: bell + def defaults_queue_bell + { frequency: 440, duration: 1.seconds, queue_in: 0 } + end + + def queue_bell args, opts = {} + (bell_to_sine_waves (defaults_queue_bell.merge opts)).each { |b| queue_sine_wave args, b } + end + + def bell_harmonics + [ + { frequency_ratio: 0.5, duration_ratio: 1.00 }, + { frequency_ratio: 1.0, duration_ratio: 0.80 }, + { frequency_ratio: 2.0, duration_ratio: 0.60 }, + { frequency_ratio: 3.0, duration_ratio: 0.40 }, + { frequency_ratio: 4.2, duration_ratio: 0.25 }, + { frequency_ratio: 5.4, duration_ratio: 0.20 }, + { frequency_ratio: 6.8, duration_ratio: 0.15 } + ] + end -def frequency_for opts = {} - opts = defaults_frequency_for.merge opts - octave_offset_multiplier = opts[:octave] - 5 - note = note_frequencies_octave_5[opts[:note]] - if octave_offset_multiplier < 0 - note = note * 1 / (octave_offset_multiplier.abs + 1) - elsif octave_offset_multiplier > 0 - note = note * (octave_offset_multiplier.abs + 1) / 1 + def defaults_bell_to_sine_waves + { frequency: 440, duration: 1.seconds, queue_in: 0 } + end + + def bell_to_sine_waves opts = {} + opts = defaults_bell_to_sine_waves.merge opts + bell_harmonics.map do |b| + { + frequency: opts[:frequency] * b[:frequency_ratio], + duration: opts[:duration] * b[:duration_ratio], + queue_in: opts[:queue_in], + gain: (1.fdiv bell_harmonics.length), + fade_out: true + } + end + end end - note -end -def note_frequencies_octave_5 - { - a: 440.0, - a_sharp: 466.16, b_flat: 466.16, - b: 493.88, - c: 523.25, - c_sharp: 554.37, d_flat: 587.33, - d: 587.33, - d_sharp: 622.25, e_flat: 659.25, - e: 659.25, - f: 698.25, - f_sharp: 739.99, g_flat: 739.99, - g: 783.99, - g_sharp: 830.61, a_flat: 830.61 - } -end + begin # audio entity construction + def generate_audio_data sine_wave, sample_rate + sample_size = (sample_rate.fdiv (1000.fdiv 60)).ceil + copy_count = (sample_size.fdiv sine_wave.length).ceil + sine_wave * copy_count + end -def new_id! args - args.state.audio_id ||= 0 - args.state.audio_id += 1 -end + def defaults_new_audio_state + { frequency: 440, duration: 60, gain: 1.0, fade_out: false, queue_in: 0 } + end -def button args, opts - button_def = opts.merge rect: (args.layout.rect (opts.merge w: 2, h: 1)) + def new_audio_state args, opts = {} + opts = defaults_new_audio_state.merge opts + decay_rate = 0 + decay_rate = 1.fdiv(opts[:duration]) * opts[:gain] if opts[:fade_out] + frequency = opts[:frequency] + sample_rate = 48000 - button_def[:border] = button_def[:rect].merge r: 0, g: 0, b: 0 + { + id: (new_id! args), + frequency: frequency, + sample_rate: 48000, + stop_at: args.tick_count + opts[:queue_in] + opts[:duration], + gain: opts[:gain].to_f, + queue_at: args.state.tick_count + opts[:queue_in], + decay_rate: decay_rate, + pitch: 1.0, + looping: true, + paused: false + } + end - font_size_enum = args.layout.font_relative_size_enum 0 - label_offset_x = 4 - label_offset_y = button_def[:rect].h.half + button_def[:rect].h.idiv(4) + def queue_audio args, opts = {} + graph_wave args, opts[:wave], opts[:audio_state][:frequency] + args.state.audio_queue << opts[:audio_state] + end - button_def[:label] = button_def[:rect].merge text: opts[:text], - size_enum: font_size_enum, - x: button_def[:rect].x + label_offset_x, - y: button_def[:rect].y + label_offset_y + def new_id! args + args.state.audio_id ||= 0 + args.state.audio_id += 1 + end - button_def + def graph_wave args, wave, frequency + if args.state.tick_count != args.state.graphed_at + args.outputs.static_lines.clear + args.outputs.static_sprites.clear + end + + wave = wave + + r, g, b = frequency.to_i % 85, + frequency.to_i % 170, + frequency.to_i % 255 + + starting_rect = args.layout.rect(row: 5, col: 13) + x_scale = 10 + y_scale = 100 + max_points = 25 + + points = wave + if wave.length > max_points + resolution = wave.length.idiv max_points + points = wave.find_all.with_index { |y, i| (i % resolution == 0) } + end + + args.outputs.static_lines << points.map_with_index do |y, x| + next_y = points[x + 1] + + if next_y + { + x: starting_rect.x + (x * x_scale), + y: starting_rect.y + starting_rect.h.half + y_scale * y, + x2: starting_rect.x + ((x + 1) * x_scale), + y2: starting_rect.y + starting_rect.h.half + y_scale * next_y, + r: r, + g: g, + b: b + } + end + end + + args.outputs.static_sprites << points.map_with_index do |y, x| + { + x: (starting_rect.x + (x * x_scale)) - 2, + y: (starting_rect.y + starting_rect.h.half + y_scale * y) - 2, + w: 4, + h: 4, + path: 'sprites/square-white.png', + r: r, + g: g, + b: b + } + end + + args.state.graphed_at = args.state.tick_count + end + end + + begin # region: musical note mapping + def defaults_frequency_for + { note: :a, octave: 5, sharp: false, flat: false } + end + + def frequency_for opts = {} + opts = defaults_frequency_for.merge opts + octave_offset_multiplier = opts[:octave] - 5 + note = note_frequencies_octave_5[opts[:note]] + if octave_offset_multiplier < 0 + note = note * 1 / (octave_offset_multiplier.abs + 1) + elsif octave_offset_multiplier > 0 + note = note * (octave_offset_multiplier.abs + 1) / 1 + end + note + end + + def note_frequencies_octave_5 + { + a: 440.0, + a_sharp: 466.16, b_flat: 466.16, + b: 493.88, + c: 523.25, + c_sharp: 554.37, d_flat: 587.33, + d: 587.33, + d_sharp: 622.25, e_flat: 659.25, + e: 659.25, + f: 698.25, + f_sharp: 739.99, g_flat: 739.99, + g: 783.99, + g_sharp: 830.61, a_flat: 830.61 + } + end + end end $gtk.reset -</pre> -<h1 id='--input-basics---keyboard---main.rb'>Input Basics - Keyboard - main.rb</h1> -<pre># ./samples/02_input_basics/01_keyboard/app/main.rb +</code></pre> +<h3 id='----input-basics---keyboard---main.rb'>Input Basics - Keyboard - main.rb</h3> +<pre><code class="language-ruby"># ./samples/02_input_basics/01_keyboard/app/main.rb =begin APIs listing that haven't been encountered in a previous sample apps: @@ -4019,9 +4733,9 @@ def tick_instructions args, text, y = 715 args.outputs.debug << [640, y - 25, "(click to dismiss instructions)" , -2, 1, 255, 255, 255].label end -</pre> -<h1 id='--input-basics---mouse---main.rb'>Input Basics - Mouse - main.rb</h1> -<pre># ./samples/02_input_basics/02_mouse/app/main.rb +</code></pre> +<h3 id='----input-basics---mouse---main.rb'>Input Basics - Mouse - main.rb</h3> +<pre><code class="language-ruby"># ./samples/02_input_basics/02_mouse/app/main.rb =begin APIs that haven't been encountered in a previous sample apps: @@ -4110,9 +4824,9 @@ def tick_instructions args, text, y = 715 args.outputs.debug << [640, y - 25, "(click to dismiss instructions)" , -2, 1, 255, 255, 255].label end -</pre> -<h1 id='--input-basics---mouse-point-to-rect---main.rb'>Input Basics - Mouse Point To Rect - main.rb</h1> -<pre># ./samples/02_input_basics/03_mouse_point_to_rect/app/main.rb +</code></pre> +<h3 id='----input-basics---mouse-point-to-rect---main.rb'>Input Basics - Mouse Point To Rect - main.rb</h3> +<pre><code class="language-ruby"># ./samples/02_input_basics/03_mouse_point_to_rect/app/main.rb =begin APIs that haven't been encountered in a previous sample apps: @@ -4204,9 +4918,9 @@ def tick_instructions args, text, y = 715 args.outputs.debug << [640, y - 25, "(click to dismiss instructions)" , -2, 1, 255, 255, 255].label end -</pre> -<h1 id='--input-basics---mouse-rect-to-rect---main.rb'>Input Basics - Mouse Rect To Rect - main.rb</h1> -<pre># ./samples/02_input_basics/04_mouse_rect_to_rect/app/main.rb +</code></pre> +<h3 id='----input-basics---mouse-rect-to-rect---main.rb'>Input Basics - Mouse Rect To Rect - main.rb</h3> +<pre><code class="language-ruby"># ./samples/02_input_basics/04_mouse_rect_to_rect/app/main.rb =begin APIs that haven't been encountered in a previous sample apps: @@ -4305,9 +5019,9 @@ def tick_instructions args, text, y = 715 args.outputs.debug << [640, y - 25, "(click to dismiss instructions)" , -2, 1, 255, 255, 255].label end -</pre> -<h1 id='--input-basics---controller---main.rb'>Input Basics - Controller - main.rb</h1> -<pre># ./samples/02_input_basics/05_controller/app/main.rb +</code></pre> +<h3 id='----input-basics---controller---main.rb'>Input Basics - Controller - main.rb</h3> +<pre><code class="language-ruby"># ./samples/02_input_basics/05_controller/app/main.rb =begin APIs listing that haven't been encountered in previous sample apps: @@ -4435,9 +5149,9 @@ def tick_instructions args, text, y = 715 args.outputs.debug << [640, y - 25, "(click to dismiss instructions)" , -2, 1, 255, 255, 255].label end -</pre> -<h1 id='--input-basics---touch---main.rb'>Input Basics - Touch - main.rb</h1> -<pre># ./samples/02_input_basics/06_touch/app/main.rb +</code></pre> +<h3 id='----input-basics---touch---main.rb'>Input Basics - Touch - main.rb</h3> +<pre><code class="language-ruby"># ./samples/02_input_basics/06_touch/app/main.rb def tick args args.outputs.background_color = [ 0, 0, 0 ] args.outputs.primitives << [640, 700, "Touch your screen.", 5, 1, 255, 255, 255].label @@ -4481,9 +5195,9 @@ def tick args end -</pre> -<h1 id='--rendering-sprites---animation-using-separate-pngs---main.rb'>Rendering Sprites - Animation Using Separate Pngs - main.rb</h1> -<pre># ./samples/03_rendering_sprites/01_animation_using_separate_pngs/app/main.rb +</code></pre> +<h3 id='----rendering-sprites---animation-using-separate-pngs---main.rb'>Rendering Sprites - Animation Using Separate Pngs - main.rb</h3> +<pre><code class="language-ruby"># ./samples/03_rendering_sprites/01_animation_using_separate_pngs/app/main.rb =begin Reminders: @@ -4616,9 +5330,9 @@ def tick_instructions args, text, y = 715 args.outputs.debug << [640, y - 25, "(click to dismiss instructions)" , -2, 1, 255, 255, 255].label end -</pre> -<h1 id='--rendering-sprites---animation-using-sprite-sheet---main.rb'>Rendering Sprites - Animation Using Sprite Sheet - main.rb</h1> -<pre># ./samples/03_rendering_sprites/02_animation_using_sprite_sheet/app/main.rb +</code></pre> +<h3 id='----rendering-sprites---animation-using-sprite-sheet---main.rb'>Rendering Sprites - Animation Using Sprite Sheet - main.rb</h3> +<pre><code class="language-ruby"># ./samples/03_rendering_sprites/02_animation_using_sprite_sheet/app/main.rb def tick args args.state.player.x ||= 100 args.state.player.y ||= 100 @@ -4718,9 +5432,9 @@ def running_sprite args } end -</pre> -<h1 id='--rendering-sprites---animation-states---main.rb'>Rendering Sprites - Animation States - main.rb</h1> -<pre># ./samples/03_rendering_sprites/03_animation_states/app/main.rb +</code></pre> +<h3 id='----rendering-sprites---animation-states---main.rb'>Rendering Sprites - Animation States - main.rb</h3> +<pre><code class="language-ruby"># ./samples/03_rendering_sprites/03_animation_states/app/main.rb class Game attr_gtk @@ -4905,9 +5619,9 @@ end $gtk.reset -</pre> -<h1 id='--rendering-sprites---color-and-rotation---main.rb'>Rendering Sprites - Color And Rotation - main.rb</h1> -<pre># ./samples/03_rendering_sprites/04_color_and_rotation/app/main.rb +</code></pre> +<h3 id='----rendering-sprites---color-and-rotation---main.rb'>Rendering Sprites - Color And Rotation - main.rb</h3> +<pre><code class="language-ruby"># ./samples/03_rendering_sprites/04_color_and_rotation/app/main.rb =begin APIs listing that haven't been encountered in previous sample apps: @@ -5135,9 +5849,9 @@ def source_rect state [0, 0, state.sprite.width, state.sprite.height] end -</pre> -<h1 id='--physics-and-collisions---simple---main.rb'>Physics And Collisions - Simple - main.rb</h1> -<pre># ./samples/04_physics_and_collisions/01_simple/app/main.rb +</code></pre> +<h3 id='----physics-and-collisions---simple---main.rb'>Physics And Collisions - Simple - main.rb</h3> +<pre><code class="language-ruby"># ./samples/04_physics_and_collisions/01_simple/app/main.rb =begin Reminders: @@ -5247,9 +5961,9 @@ def tick_instructions args, text, y = 715 args.outputs.debug << [640, y - 25, "(click to dismiss instructions)" , -2, 1, 255, 255, 255].label end -</pre> -<h1 id='--physics-and-collisions---moving-objects---main.rb'>Physics And Collisions - Moving Objects - main.rb</h1> -<pre># ./samples/04_physics_and_collisions/02_moving_objects/app/main.rb +</code></pre> +<h3 id='----physics-and-collisions---moving-objects---main.rb'>Physics And Collisions - Moving Objects - main.rb</h3> +<pre><code class="language-ruby"># ./samples/04_physics_and_collisions/02_moving_objects/app/main.rb =begin APIs listing that haven't been encountered in previous sample apps: @@ -5551,9 +6265,9 @@ def tick_instructions args, text, y = 715 args.outputs.debug << [640, y - 25, "(click to dismiss instructions)" , -2, 1, 255, 255, 255].label end -</pre> -<h1 id='--physics-and-collisions---entities---main.rb'>Physics And Collisions - Entities - main.rb</h1> -<pre># ./samples/04_physics_and_collisions/03_entities/app/main.rb +</code></pre> +<h3 id='----physics-and-collisions---entities---main.rb'>Physics And Collisions - Entities - main.rb</h3> +<pre><code class="language-ruby"># ./samples/04_physics_and_collisions/03_entities/app/main.rb =begin Reminders: @@ -5706,9 +6420,9 @@ def tick_instructions args, text, y = 715 args.outputs.debug << [640, y - 25, "(click to dismiss instructions)" , -2, 1, 255, 255, 255].label end -</pre> -<h1 id='--physics-and-collisions---box-collision---main.rb'>Physics And Collisions - Box Collision - main.rb</h1> -<pre># ./samples/04_physics_and_collisions/04_box_collision/app/main.rb +</code></pre> +<h3 id='----physics-and-collisions---box-collision---main.rb'>Physics And Collisions - Box Collision - main.rb</h3> +<pre><code class="language-ruby"># ./samples/04_physics_and_collisions/04_box_collision/app/main.rb =begin APIs listing that haven't been encountered in previous sample apps: @@ -6047,9 +6761,9 @@ def tick_instructions args, text, y = 715 args.outputs.debug << [640, y - 25, "(click to dismiss instructions)" , -2, 1, 255, 255, 255].label end -</pre> -<h1 id='--physics-and-collisions---box-collision-2---main.rb'>Physics And Collisions - Box Collision 2 - main.rb</h1> -<pre># ./samples/04_physics_and_collisions/05_box_collision_2/app/main.rb +</code></pre> +<h3 id='----physics-and-collisions---box-collision-2---main.rb'>Physics And Collisions - Box Collision 2 - main.rb</h3> +<pre><code class="language-ruby"># ./samples/04_physics_and_collisions/05_box_collision_2/app/main.rb =begin APIs listing that haven't been encountered in previous sample apps: @@ -6521,9 +7235,268 @@ def tick args $metroidvania_starter.tick end -</pre> -<h1 id='--physics-and-collisions---jump-physics---main.rb'>Physics And Collisions - Jump Physics - main.rb</h1> -<pre># ./samples/04_physics_and_collisions/06_jump_physics/app/main.rb +</code></pre> +<h3 id='----physics-and-collisions---box-collision-3---main.rb'>Physics And Collisions - Box Collision 3 - main.rb</h3> +<pre><code class="language-ruby"># ./samples/04_physics_and_collisions/06_box_collision_3/app/main.rb +class Game + attr_gtk + + def tick + defaults + render + input_edit_map + input_player + calc_player + end + + def defaults + state.gravity = -0.4 + state.drag = 0.15 + state.tile_size = 32 + state.player.size = 16 + state.player.jump_power = 12 + + state.tiles ||= [] + state.player.y ||= 800 + state.player.x ||= 100 + state.player.dy ||= 0 + state.player.dx ||= 0 + state.player.jumped_down_at ||= 0 + state.player.jumped_at ||= 0 + + calc_player_rect if !state.player.rect + end + + def render + outputs.labels << [10, 10.from_top, "tile: click to add a tile, hold X key and click to delete a tile."] + outputs.labels << [10, 35.from_top, "move: use left and right to move, space to jump, down and space to jump down."] + outputs.labels << [10, 55.from_top, " You can jump through or jump down through tiles with a height of 1."] + outputs.background_color = [80, 80, 80] + outputs.sprites << tiles.map(&:sprite) + outputs.sprites << (player.rect.merge path: 'sprites/square/green.png') + + mouse_overlay = { + x: (inputs.mouse.x.ifloor state.tile_size), + y: (inputs.mouse.y.ifloor state.tile_size), + w: state.tile_size, + h: state.tile_size, + a: 100 + } + + mouse_overlay = mouse_overlay.merge r: 255 if state.delete_mode + + if state.mouse_held + outputs.primitives << mouse_overlay.border + else + outputs.primitives << mouse_overlay.solid + end + end + + def input_edit_map + state.mouse_held = true if inputs.mouse.down + state.mouse_held = false if inputs.mouse.up + + if inputs.keyboard.x + state.delete_mode = true + elsif inputs.keyboard.key_up.x + state.delete_mode = false + end + + return unless state.mouse_held + + ordinal = { x: (inputs.mouse.x.idiv state.tile_size), + y: (inputs.mouse.y.idiv state.tile_size) } + + found = find_tile ordinal + if !found && !state.delete_mode + tiles << (state.new_entity :tile, ordinal) + recompute_tiles + elsif found && state.delete_mode + tiles.delete found + recompute_tiles + end + end + + def input_player + player.dx += inputs.left_right + + if inputs.keyboard.key_down.space && inputs.keyboard.down + player.dy = player.jump_power * -1 + player.jumped_at = 0 + player.jumped_down_at = state.tick_count + elsif inputs.keyboard.key_down.space + player.dy = player.jump_power + player.jumped_at = state.tick_count + player.jumped_down_at = 0 + end + end + + def calc_player + calc_player_rect + calc_below + calc_left + calc_right + calc_above + calc_player_dy + calc_player_dx + reset_player if player_off_stage? + end + + def calc_player_rect + player.rect = current_player_rect + player.next_rect = player.rect.merge x: player.x + player.dx, + y: player.y + player.dy + player.prev_rect = player.rect.merge x: player.x - player.dx, + y: player.y - player.dy + end + + def calc_below + return unless player.dy <= 0 + tiles_below = find_tiles { |t| t.rect.top <= player.y } + collision = find_colliding_tile tiles_below, (player.rect.merge y: player.next_rect.y) + return unless collision + if collision.neighbors.b == :none && player.jumped_down_at.elapsed_time < 10 + player.dy = -1 + else + player.y = collision.rect.y + state.tile_size + player.dy = 0 + end + end + + def calc_left + return unless player.dx < 0 + tiles_left = find_tiles { |t| t.rect.right <= player.prev_rect.left } + collision = find_colliding_tile tiles_left, (player.rect.merge x: player.next_rect.x) + return unless collision + player.x = collision.rect.right + player.dx = 0 + end + + def calc_right + return unless player.dx > 0 + tiles_right = find_tiles { |t| t.rect.left >= player.prev_rect.right } + collision = find_colliding_tile tiles_right, (player.rect.merge x: player.next_rect.x) + return unless collision + player.x = collision.rect.left - player.rect.w + player.dx = 0 + end + + def calc_above + return unless player.dy > 0 + tiles_above = find_tiles { |t| t.rect.y >= player.y } + collision = find_colliding_tile tiles_above, (player.rect.merge y: player.next_rect.y) + return unless collision + return if collision.neighbors.t == :none + player.dy = 0 + player.y = collision.rect.bottom - player.rect.h + end + + def calc_player_dx + player.y += player.dy + player.dy += state.gravity + player.dy += player.dy * state.drag ** 2 * -1 + end + + def calc_player_dy + player.dx = player.dx.clamp(-5, 5) + player.dx *= 0.9 + player.x += player.dx + end + + def reset_player + player.x = 100 + player.y = 720 + player.dy = 0 + end + + def recompute_tiles + tiles.each do |t| + t.w = state.tile_size + t.h = state.tile_size + t.neighbors = tile_neighbors t, tiles + + t.rect = [t.x * state.tile_size, + t.y * state.tile_size, + state.tile_size, + state.tile_size].rect.to_hash + + sprite_sub_path = t.neighbors.mask.map { |m| flip_bit m }.join("") + + t.sprite = { + x: t.x * state.tile_size, + y: t.y * state.tile_size, + w: state.tile_size, + h: state.tile_size, + path: "sprites/tile/wall-#{sprite_sub_path}.png" + } + end + end + + def flip_bit bit + return 0 if bit == 1 + return 1 + end + + def player + state.player + end + + def player_off_stage? + player.rect.top < grid.bottom || + player.rect.right < grid.left || + player.rect.left > grid.right + end + + def current_player_rect + { x: player.x, y: player.y, w: player.size, h: player.size } + end + + def tiles + state.tiles + end + + def find_tile ordinal + tiles.find { |t| t.x == ordinal.x && t.y == ordinal.y } + end + + def find_tiles &block + tiles.find_all(&block) + end + + def find_colliding_tile tiles, target + tiles.find { |t| t.rect.intersect_rect? target } + end + + def tile_neighbors tile, other_points + t = find_tile x: tile.x + 0, y: tile.y + 1 + r = find_tile x: tile.x + 1, y: tile.y + 0 + b = find_tile x: tile.x + 0, y: tile.y - 1 + l = find_tile x: tile.x - 1, y: tile.y + 0 + + tile_t, tile_r, tile_b, tile_l = 0 + + tile_t = 1 if t + tile_r = 1 if r + tile_b = 1 if b + tile_l = 1 if l + + state.new_entity :neighbors, mask: [tile_t, tile_r, tile_b, tile_l], + t: t ? :some : :none, + b: b ? :some : :none, + l: l ? :some : :none, + r: r ? :some : :none + end +end + +def tick args + $game ||= Game.new + $game.args = args + $game.tick +end + +</code></pre> +<h3 id='----physics-and-collisions---jump-physics---main.rb'>Physics And Collisions - Jump Physics - main.rb</h3> +<pre><code class="language-ruby"># ./samples/04_physics_and_collisions/07_jump_physics/app/main.rb =begin Reminders: @@ -6721,9 +7694,2575 @@ def tick args $game.tick end -</pre> -<h1 id='--mouse---mouse-click---main.rb'>Mouse - Mouse Click - main.rb</h1> -<pre># ./samples/05_mouse/01_mouse_click/app/main.rb +</code></pre> +<h3 id='----physics-and-collisions---bouncing-on-collision---ball.rb'>Physics And Collisions - Bouncing On Collision - ball.rb</h3> +<pre><code class="language-ruby"># ./samples/04_physics_and_collisions/08_bouncing_on_collision/app/ball.rb +GRAVITY = -0.08 + +class Ball + attr_accessor :velocity, :center, :radius, :collision_enabled + + def initialize args + #Start the ball in the top center + #@x = args.grid.w / 2 + #@y = args.grid.h - 20 + + @velocity = {x: 0, y: 0} + #@width = 20 + #@height = @width + @radius = 20.0 / 2.0 + @center = {x: (args.grid.w / 2), y: (args.grid.h)} + + #@left_wall = (args.state.board_width + args.grid.w / 8) + #@right_wall = @left_wall + args.state.board_width + @left_wall = 0 + @right_wall = $args.grid.right + + @max_velocity = 7 + @collision_enabled = true + end + + #Move the ball according to its velocity + def update args + @center.x += @velocity.x + @center.y += @velocity.y + @velocity.y += GRAVITY + + alpha = 0.2 + if @center.y-@radius <= 0 + @velocity.y = (@velocity.y.abs*0.7).abs + @velocity.x = (@velocity.x.abs*0.9).abs * ((@velocity.x < 0) ? -1 : 1) + + if @velocity.y.abs() < alpha + @velocity.y=0 + end + if @velocity.x.abs() < alpha + @velocity.x=0 + end + end + + if @center.x > args.grid.right+@radius*2 + @center.x = 0-@radius + elsif @center.x< 0-@radius*2 + @center.x = args.grid.right + @radius + end + end + + def wallBounds args + #if @x < @left_wall || @x + @width > @right_wall + #@velocity.x *= -1.1 + #if @velocity.x > @max_velocity + #@velocity.x = @max_velocity + #elsif @velocity.x < @max_velocity * -1 + #@velocity.x = @max_velocity * -1 + #end + #end + #if @y < 0 || @y + @height > args.grid.h + #@velocity.y *= -1.1 + #if @velocity.y > @max_velocity + #@velocity.y = @max_velocity + #elsif @velocity.y < @max_velocity * -1 + #@velocity.y = @max_velocity * -1 + #end + #end + end + + #render the ball to the screen + def draw args + #args.outputs.solids << [@x, @y, @width, @height, 255, 255, 0]; + args.outputs.sprites << [ + @center.x-@radius, + @center.y-@radius, + @radius*2, + @radius*2, + "sprites/circle-white.png", + 0, + 255, + 255, #r + 0, #g + 255 #b + ] + end + end + +</code></pre> +<h3 id='----physics-and-collisions---bouncing-on-collision---block.rb'>Physics And Collisions - Bouncing On Collision - block.rb</h3> +<pre><code class="language-ruby"># ./samples/04_physics_and_collisions/08_bouncing_on_collision/app/block.rb +DEGREES_TO_RADIANS = Math::PI / 180 + +class Block + def initialize(x, y, block_size, rotation) + @x = x + @y = y + @block_size = block_size + @rotation = rotation + + #The repel velocity? + @velocity = {x: 2, y: 0} + + horizontal_offset = (3 * block_size) * Math.cos(rotation * DEGREES_TO_RADIANS) + vertical_offset = block_size * Math.sin(rotation * DEGREES_TO_RADIANS) + + if rotation >= 0 + theta = 90 - rotation + #The line doesn't visually line up exactly with the edge of the sprite, so artificially move it a bit + modifier = 5 + x_offset = modifier * Math.cos(theta * DEGREES_TO_RADIANS) + y_offset = modifier * Math.sin(theta * DEGREES_TO_RADIANS) + @x1 = @x - x_offset + @y1 = @y + y_offset + @x2 = @x1 + horizontal_offset + @y2 = @y1 + (vertical_offset * 3) + + @imaginary_line = [ @x1, @y1, @x2, @y2 ] + else + theta = 90 + rotation + x_offset = @block_size * Math.cos(theta * DEGREES_TO_RADIANS) + y_offset = @block_size * Math.sin(theta * DEGREES_TO_RADIANS) + @x1 = @x + x_offset + @y1 = @y + y_offset + 19 + @x2 = @x1 + horizontal_offset + @y2 = @y1 + (vertical_offset * 3) + + @imaginary_line = [ @x1, @y1, @x2, @y2 ] + end + + end + + def draw args + args.outputs.sprites << [ + @x, + @y, + @block_size*3, + @block_size, + "sprites/square-green.png", + @rotation + ] + + args.outputs.lines << @imaginary_line + args.outputs.solids << @debug_shape + end + + def multiply_matricies + end + + def calc args + if collision? args + collide args + end + end + + #Determine if the ball and block are touching + def collision? args + #The minimum area enclosed by the center of the ball and the 2 corners of the block + #If the area ever drops below this value, we know there is a collision + min_area = ((@block_size * 3) * args.state.ball.radius) / 2 + + #https://www.mathopenref.com/coordtrianglearea.html + ax = @x1 + ay = @y1 + bx = @x2 + by = @y2 + cx = args.state.ball.center.x + cy = args.state.ball.center.y + + current_area = (ax*(by-cy)+bx*(cy-ay)+cx*(ay-by))/2 + + collision = false + if @rotation >= 0 + if (current_area < min_area && + current_area > 0 && + args.state.ball.center.y > @y1 && + args.state.ball.center.x < @x2) + + collision = true + end + else + if (current_area < min_area && + current_area > 0 && + args.state.ball.center.y > @y2 && + args.state.ball.center.x > @x1) + + collision = true + end + end + + return collision + end + + def collide args + #Slope of the block + slope = (@y2 - @y1) / (@x2 - @x1) + + #Create a unit vector and tilt it (@rotation) number of degrees + x = -Math.cos(@rotation * DEGREES_TO_RADIANS) + y = Math.sin(@rotation * DEGREES_TO_RADIANS) + + #Find the vector that is perpendicular to the slope + perpVect = { x: x, y: y } + mag = (perpVect.x**2 + perpVect.y**2)**0.5 # find the magniude of the perpVect + perpVect = {x: perpVect.x/(mag), y: perpVect.y/(mag)} # divide the perpVect by the magniude to make it a unit vector + + previousPosition = { # calculate an ESTIMATE of the previousPosition of the ball + x:args.state.ball.center.x-args.state.ball.velocity.x, + y:args.state.ball.center.y-args.state.ball.velocity.y + } + + velocityMag = (args.state.ball.velocity.x**2 + args.state.ball.velocity.y**2)**0.5 # the current velocity magnitude of the ball + theta_ball = Math.atan2(args.state.ball.velocity.y, args.state.ball.velocity.x) #the angle of the ball's velocity + theta_repel = (180 * DEGREES_TO_RADIANS) - theta_ball + (@rotation * DEGREES_TO_RADIANS) + + fbx = velocityMag * Math.cos(theta_ball) #the x component of the ball's velocity + fby = velocityMag * Math.sin(theta_ball) #the y component of the ball's velocity + + frx = velocityMag * Math.cos(theta_repel) #the x component of the repel's velocity | magnitude is set to twice of fbx + fry = velocityMag * Math.sin(theta_repel) #the y component of the repel's velocity | magnitude is set to twice of fby + + args.state.display_value = velocityMag + fsumx = fbx+frx #sum of x forces + fsumy = fby+fry #sum of y forces + fr = velocityMag #fr is the resulting magnitude + thetaNew = Math.atan2(fsumy, fsumx) #thetaNew is the resulting angle + + xnew = fr*Math.cos(thetaNew) #resulting x velocity + ynew = fr*Math.sin(thetaNew) #resulting y velocity + + dampener = 0.3 + ynew *= dampener * 0.5 + + #If the bounce is very low, that means the ball is rolling and we don't want to dampenen the X velocity + if ynew > -0.1 + xnew *= dampener + end + + #Add the sine component of gravity back in (X component) + gravity_x = 4 * Math.sin(@rotation * DEGREES_TO_RADIANS) + xnew += gravity_x + + args.state.ball.velocity.x = -xnew + args.state.ball.velocity.y = -ynew + + #Set the position of the ball to the previous position so it doesn't warp throught the block + args.state.ball.center.x = previousPosition.x + args.state.ball.center.y = previousPosition.y + end +end + +</code></pre> +<h3 id='----physics-and-collisions---bouncing-on-collision---cannon.rb'>Physics And Collisions - Bouncing On Collision - cannon.rb</h3> +<pre><code class="language-ruby"># ./samples/04_physics_and_collisions/08_bouncing_on_collision/app/cannon.rb +class Cannon + def initialize args + @pointA = {x: args.grid.right/2,y: args.grid.top} + @pointB = {x: args.inputs.mouse.x, y: args.inputs.mouse.y} + end + def update args + activeBall = args.state.ball + @pointB = {x: args.inputs.mouse.x, y: args.inputs.mouse.y} + + if args.inputs.mouse.click + alpha = 0.01 + activeBall.velocity.y = (@pointB.y - @pointA.y) * alpha + activeBall.velocity.x = (@pointB.x - @pointA.x) * alpha + activeBall.center = {x: (args.grid.w / 2), y: (args.grid.h)} + end + end + def render args + args.outputs.lines << [@pointA.x, @pointA.y, @pointB.x, @pointB.y] + end +end + +</code></pre> +<h3 id='----physics-and-collisions---bouncing-on-collision---main.rb'>Physics And Collisions - Bouncing On Collision - main.rb</h3> +<pre><code class="language-ruby"># ./samples/04_physics_and_collisions/08_bouncing_on_collision/app/main.rb +INFINITY= 10**10 + +require 'app/vector2d.rb' +require 'app/peg.rb' +require 'app/block.rb' +require 'app/ball.rb' +require 'app/cannon.rb' + + +#Method to init default values +def defaults args + args.state.pegs ||= [] + args.state.blocks ||= [] + args.state.cannon ||= Cannon.new args + args.state.ball ||= Ball.new args + args.state.horizontal_offset ||= 0 + init_pegs args + init_blocks args + + args.state.display_value ||= "test" +end + +begin :default_methods + def init_pegs args + num_horizontal_pegs = 14 + num_rows = 5 + + return unless args.state.pegs.count < num_rows * num_horizontal_pegs + + block_size = 32 + block_spacing = 50 + total_width = num_horizontal_pegs * (block_size + block_spacing) + starting_offset = (args.grid.w - total_width) / 2 + block_size + + for i in (0...num_rows) + for j in (0...num_horizontal_pegs) + row_offset = 0 + if i % 2 == 0 + row_offset = 20 + else + row_offset = -20 + end + args.state.pegs.append(Peg.new(j * (block_size+block_spacing) + starting_offset + row_offset, (args.grid.h - block_size * 2) - (i * block_size * 2)-90, block_size)) + end + end + + end + + def init_blocks args + return unless args.state.blocks.count < 10 + + #Sprites are rotated in degrees, but the Ruby math functions work on radians + radians_to_degrees = Math::PI / 180 + + block_size = 25 + #Rotation angle (in degrees) of the blocks + rotation = 30 + vertical_offset = block_size * Math.sin(rotation * radians_to_degrees) + horizontal_offset = (3 * block_size) * Math.cos(rotation * radians_to_degrees) + center = args.grid.w / 2 + + for i in (0...5) + #Create a ramp of blocks. Not going to be perfect because of the float to integer conversion and anisotropic to isotropic coversion + args.state.blocks.append(Block.new((center + 100 + (i * horizontal_offset)).to_i, 100 + (vertical_offset * i) + (i * block_size), block_size, rotation)) + args.state.blocks.append(Block.new((center - 100 - (i * horizontal_offset)).to_i, 100 + (vertical_offset * i) + (i * block_size), block_size, -rotation)) + end + end +end + +#Render loop +def render args + args.outputs.borders << args.state.game_area + render_pegs args + render_blocks args + args.state.cannon.render args + args.state.ball.draw args +end + +begin :render_methods + #Draw the pegs in a grid pattern + def render_pegs args + args.state.pegs.each do |peg| + peg.draw args + end + end + + def render_blocks args + args.state.blocks.each do |block| + block.draw args + end + end + +end + +#Calls all methods necessary for performing calculations +def calc args + args.state.pegs.each do |peg| + peg.calc args + end + + args.state.blocks.each do |block| + block.calc args + end + + args.state.ball.update args + args.state.cannon.update args +end + +begin :calc_methods + +end + +def tick args + defaults args + render args + calc args +end + +</code></pre> +<h3 id='----physics-and-collisions---bouncing-on-collision---peg.rb'>Physics And Collisions - Bouncing On Collision - peg.rb</h3> +<pre><code class="language-ruby"># ./samples/04_physics_and_collisions/08_bouncing_on_collision/app/peg.rb +class Peg + def initialize(x, y, block_size) + @x = x # x cordinate of the LEFT side of the peg + @y = y # y cordinate of the RIGHT side of the peg + @block_size = block_size # diameter of the peg + + @radius = @block_size/2.0 # radius of the peg + @center = { # cordinatees of the CENTER of the peg + x: @x+@block_size/2.0, + y: @y+@block_size/2.0 + } + + @r = 255 # color of the peg + @g = 0 + @b = 0 + + @velocity = {x: 2, y: 0} + end + + def draw args + args.outputs.sprites << [ # draw the peg according to the @x, @y, @radius, and the RGB + @x, + @y, + @radius*2.0, + @radius*2.0, + "sprites/circle-white.png", + 0, + 255, + @r, #r + @g, #g + @b #b + ] + end + + + def calc args + if collisionWithBounce? args # if the is a collision with the bouncing ball + collide args + @r = 0 + @b = 0 + @g = 255 + else + end + end + + + # do two circles (the ball and this peg) intersect + def collisionWithBounce? args + squareDistance = ( # the squared distance between the ball's center and this peg's center + (args.state.ball.center.x - @center.x) ** 2.0 + + (args.state.ball.center.y - @center.y) ** 2.0 + ) + radiusSum = ( # the sum of the radius squared of the this peg and the ball + (args.state.ball.radius + @radius) ** 2.0 + ) + # if the squareDistance is less or equal to radiusSum, then there is a radial intersection between the ball and this peg + return (squareDistance <= radiusSum) + end + + # ! The following links explain the getRepelMagnitude function ! + # https://raw.githubusercontent.com/DragonRuby/dragonruby-game-toolkit-physics/master/docs/docImages/LinearCollider_4.png + # https://raw.githubusercontent.com/DragonRuby/dragonruby-game-toolkit-physics/master/docs/docImages/LinearCollider_5.png + # https://github.com/DragonRuby/dragonruby-game-toolkit-physics/blob/master/docs/LinearCollider.md + def getRepelMagnitude (args, fbx, fby, vrx, vry, ballMag) + a = fbx ; b = vrx ; c = fby + d = vry ; e = ballMag + if b**2 + d**2 == 0 + #unexpected + end + + x1 = (-a*b+-c*d + (e**2 * b**2 - b**2 * c**2 + 2*a*b*c*d + e**2 + d**2 - a**2 * d**2)**0.5)/(b**2 + d**2) + x2 = -((a*b + c*d + (e**2 * b**2 - b**2 * c**2 + 2*a*b*c*d + e**2 * d**2 - a**2 * d**2)**0.5)/(b**2 + d**2)) + + err = 0.00001 + o = ((fbx + x1*vrx)**2 + (fby + x1*vry)**2 ) ** 0.5 + p = ((fbx + x2*vrx)**2 + (fby + x2*vry)**2 ) ** 0.5 + r = 0 + + if (ballMag >= o-err and ballMag <= o+err) + r = x1 + elsif (ballMag >= p-err and ballMag <= p+err) + r = x2 + else + #unexpected + end + + if (args.state.ball.center.x > @center.x) + return x2*-1 + end + + return x2 + + #return r + end + + #this sets the new velocity of the ball once it has collided with this peg + def collide args + normalOfRCCollision = [ #this is the normal of the collision in COMPONENT FORM + {x: @center.x, y: @center.y}, #see https://www.google.com/url?sa=i&url=https%3A%2F%2Fwww.mathscard.co.uk%2Fonline%2Fcircle-coordinate-geometry%2F&psig=AOvVaw2GcD-e2-nJR_IUKpw3hO98&ust=1605731315521000&source=images&cd=vfe&ved=0CAIQjRxqFwoTCMjBo7e1iu0CFQAAAAAdAAAAABAD + {x: args.state.ball.center.x, y: args.state.ball.center.y}, + ] + + normalSlope = ( #normalSlope is the slope of normalOfRCCollision + (normalOfRCCollision[1].y - normalOfRCCollision[0].y) / + (normalOfRCCollision[1].x - normalOfRCCollision[0].x) + ) + slope = normalSlope**-1.0 * -1 # slope is the slope of the tangent + # args.state.display_value = slope + pointA = { # pointA and pointB are using the var slope to tangent in COMPONENT FORM + x: args.state.ball.center.x-1, + y: -(slope-args.state.ball.center.y) + } + pointB = { + x: args.state.ball.center.x+1, + y: slope+args.state.ball.center.y + } + + perpVect = {x: pointB.x - pointA.x, y:pointB.y - pointA.y} # perpVect is to be VECTOR of the perpendicular tangent + mag = (perpVect.x**2 + perpVect.y**2)**0.5 # find the magniude of the perpVect + perpVect = {x: perpVect.x/(mag), y: perpVect.y/(mag)} # divide the perpVect by the magniude to make it a unit vector + perpVect = {x: -perpVect.y, y: perpVect.x} # swap the x and y and multiply by -1 to make the vector perpendicular + args.state.display_value = perpVect + if perpVect.y > 0 #ensure perpVect points upward + perpVect = {x: perpVect.x*-1, y: perpVect.y*-1} + end + + previousPosition = { # calculate an ESTIMATE of the previousPosition of the ball + x:args.state.ball.center.x-args.state.ball.velocity.x, + y:args.state.ball.center.y-args.state.ball.velocity.y + } + + yInterc = pointA.y + -slope*pointA.x + if slope == INFINITY # the perpVect presently either points in the correct dirrection or it is 180 degrees off we need to correct this + if previousPosition.x < pointA.x + perpVect = {x: perpVect.x*-1, y: perpVect.y*-1} + yInterc = -INFINITY + end + elsif previousPosition.y < slope*previousPosition.x + yInterc # check if ball is bellow or above the collider to determine if perpVect is - or + + perpVect = {x: perpVect.x*-1, y: perpVect.y*-1} + end + + velocityMag = # the current velocity magnitude of the ball + (args.state.ball.velocity.x**2 + args.state.ball.velocity.y**2)**0.5 + theta_ball= + Math.atan2(args.state.ball.velocity.y,args.state.ball.velocity.x) #the angle of the ball's velocity + theta_repel= + Math.atan2(args.state.ball.center.y,args.state.ball.center.x) #the angle of the repelling force(perpVect) + + fbx = velocityMag * Math.cos(theta_ball) #the x component of the ball's velocity + fby = velocityMag * Math.sin(theta_ball) #the y component of the ball's velocity + repelMag = getRepelMagnitude( # the magniude of the collision vector + args, + fbx, + fby, + perpVect.x, + perpVect.y, + (args.state.ball.velocity.x**2 + args.state.ball.velocity.y**2)**0.5 + ) + frx = repelMag* Math.cos(theta_repel) #the x component of the repel's velocity | magnitude is set to twice of fbx + fry = repelMag* Math.sin(theta_repel) #the y component of the repel's velocity | magnitude is set to twice of fby + + fsumx = fbx+frx # sum of x forces + fsumy = fby+fry # sum of y forces + fr = velocityMag # fr is the resulting magnitude + thetaNew = Math.atan2(fsumy, fsumx) # thetaNew is the resulting angle + xnew = fr*Math.cos(thetaNew) # resulting x velocity + ynew = fr*Math.sin(thetaNew) # resulting y velocity + if (args.state.ball.center.x >= @center.x) # this is necessary for the ball colliding on the right side of the peg + xnew=xnew.abs + end + + args.state.ball.velocity.x = xnew # set the x-velocity to the new velocity + if args.state.ball.center.y > @center.y # if the ball is above the middle of the peg we need to temporarily ignore some of the gravity + args.state.ball.velocity.y = ynew + GRAVITY * 0.01 + else + args.state.ball.velocity.y = ynew - GRAVITY * 0.01 # if the ball is bellow the middle of the peg we need to temporarily increase the power of the gravity + end + + args.state.ball.center.x+= args.state.ball.velocity.x # update the position of the ball so it never looks like the ball is intersecting the circle + args.state.ball.center.y+= args.state.ball.velocity.y + end +end + +</code></pre> +<h3 id='----physics-and-collisions---bouncing-on-collision---vector2d.rb'>Physics And Collisions - Bouncing On Collision - vector2d.rb</h3> +<pre><code class="language-ruby"># ./samples/04_physics_and_collisions/08_bouncing_on_collision/app/vector2d.rb +class Vector2d + attr_accessor :x, :y + + def initialize x=0, y=0 + @x=x + @y=y + end + + #returns a vector multiplied by scalar x + #x [float] scalar + def mult x + r = Vector2d.new(0,0) + r.x=@x*x + r.y=@y*x + r + end + + # vect [Vector2d] vector to copy + def copy vect + Vector2d.new(@x, @y) + end + + #returns a new vector equivalent to this+vect + #vect [Vector2d] vector to add to self + def add vect + Vector2d.new(@x+vect.x,@y+vect.y) + end + + #returns a new vector equivalent to this-vect + #vect [Vector2d] vector to subtract to self + def sub vect + Vector2d.new(@x-vect.c, @y-vect.y) + end + + #return the magnitude of the vector + def mag + ((@x**2)+(@y**2))**0.5 + end + + #returns a new normalize version of the vector + def normalize + Vector2d.new(@x/mag, @y/mag) + end + + #TODO delet? + def distABS vect + (((vect.x-@x)**2+(vect.y-@y)**2)**0.5).abs() + end + end +</code></pre> +<h3 id='----physics-and-collisions---arbitrary-collision---ball.rb'>Physics And Collisions - Arbitrary Collision - ball.rb</h3> +<pre><code class="language-ruby"># ./samples/04_physics_and_collisions/09_arbitrary_collision/app/ball.rb + +class Ball + attr_accessor :velocity, :child, :parent, :number, :leastChain + attr_reader :x, :y, :hypotenuse, :width, :height + + def initialize args, number, leastChain, parent, child + #Start the ball in the top center + @number = number + @leastChain = leastChain + @x = args.grid.w / 2 + @y = args.grid.h - 20 + + @velocity = Vector2d.new(2, -2) + @width = 10 + @height = 10 + + @left_wall = (args.state.board_width + args.grid.w / 8) + @right_wall = @left_wall + args.state.board_width + + @max_velocity = MAX_VELOCITY + + @child = child + @parent = parent + + @past = [{x: @x, y: @y}] + @next = nil + end + + def reassignLeastChain (lc=nil) + if (lc == nil) + lc = @number + end + @leastChain = lc + if (parent != nil) + @parent.reassignLeastChain(lc) + end + + end + + def makeLeader args + if isLeader + return + end + @parent.reassignLeastChain + args.state.ballParents.push(self) + @parent = nil + + end + + def isLeader + return (parent == nil) + end + + def receiveNext (p) + #trace! + if parent != nil + @x = p[:x] + @y = p[:y] + @velocity = p[:velocity] + #puts @x.to_s + "|" + @y.to_s + "|"[email protected]_s + @past.append(p) + if (@past.length >= BALL_DISTANCE) + if (@child != nil) + @child.receiveNext(@past[0]) + @past.shift + end + end + end + end + + #Move the ball according to its velocity + def update args + + if isLeader + wallBounds args + @x += @velocity.x + @y += @velocity.y + @past.append({x: @x, y: @y, velocity: @velocity}) + #puts @past + + if (@past.length >= BALL_DISTANCE) + if (@child != nil) + @child.receiveNext(@past[0]) + @past.shift + end + end + + else + puts "unexpected" + raise "unexpected" + end + end + + def wallBounds args + b= false + if @x < @left_wall + @velocity.x = @velocity.x.abs() * 1 + b=true + elsif @x + @width > @right_wall + @velocity.x = @velocity.x.abs() * -1 + b=true + end + if @y < 0 + @velocity.y = @velocity.y.abs() * 1 + b=true + elsif @y + @height > args.grid.h + @velocity.y = @velocity.y.abs() * -1 + b=true + end + mag = (@velocity.x**2.0 + @velocity.y**2.0)**0.5 + if (b == true && mag < MAX_VELOCITY) + @velocity.x*=1.1; + @velocity.y*=1.1; + end + + end + + #render the ball to the screen + def draw args + + #update args + #args.outputs.solids << [@x, @y, @width, @height, 255, 255, 0]; + #args.outputs.sprits << { + #x: @x, + #y: @y, + #w: @width, + #h: @height, + #path: "sprites/ball10.png" + #} + #args.outputs.sprites <<[@x, @y, @width, @height, "sprites/ball10.png"] + args.outputs.sprites << {x: @x, y: @y, w: @width, h: @height, path:"sprites/ball10.png" } + end + + def getDraw args + #wallBounds args + #update args + #args.outputs.labels << [@x, @y, @number.to_s + "|" + @leastChain.to_s] + return [@x, @y, @width, @height, "sprites/ball10.png"] + end + + def getPoints args + points = [ + {x:@x+@width/2, y: @y}, + {x:@x+@width, y:@y+@height/2}, + {x:@x+@width/2,y:@y+@height}, + {x:@x,y:@y+@height/2} + ] + #psize = 5.0 + #for p in points + #args.outputs.solids << [p.x-psize/2.0, p.y-psize/2.0, psize, psize, 0, 0, 0]; + #end + return points + end + + def serialize + {x: @x, y:@y} + end + + def inspect + serialize.to_s + end + + def to_s + serialize.to_s + end + end + +</code></pre> +<h3 id='----physics-and-collisions---arbitrary-collision---blocks.rb'>Physics And Collisions - Arbitrary Collision - blocks.rb</h3> +<pre><code class="language-ruby"># ./samples/04_physics_and_collisions/09_arbitrary_collision/app/blocks.rb +MAX_COUNT=100 + +def universalUpdateOne args, shape + didHit = false + hitters = [] + #puts shape.to_s + toCollide = nil + for b in args.state.balls + if [b.x, b.y, b.width, b.height].intersect_rect?(shape.bold) + didSquare = false + for s in shape.squareColliders + if (s.collision?(args, b)) + didSquare = true + didHit = true + #s.collide(args, b) + toCollide = s + #hitter = b + hitters.append(b) + end #end if + end #end for + if (didSquare == false) + for c in shape.colliders + #puts args.state.ball.velocity + if c.collision?(args, b.getPoints(args),b) + #c.collide args, b + toCollide = c + didHit = true + hitters.append(b) + end #end if + end #end for + end #end if + end#end if + end#end for + if (didHit) + shape.count=0 + hitters = hitters.uniq + for hitter in hitters + hitter.makeLeader args + #toCollide.collide(args, hitter) + if shape.home == "squares" + args.state.squares.delete(shape) + elsif shape.home == "tshapes" + args.state.tshapes.delete(shape) + else shape.home == "lines" + args.state.lines.delete(shape) + end + end + + #puts "HIT!" + hitter.number + end +end + +def universalUpdate args, shape + #puts shape.home + if (shape.count <= 1) + universalUpdateOne args, shape + return + end + + didHit = false + hitter = nil + for b in args.state.ballParents + if [b.x, b.y, b.width, b.height].intersect_rect?(shape.bold) + didSquare = false + for s in shape.squareColliders + if (s.collision?(args, b)) + didSquare = true + didHit = true + s.collide(args, b) + hitter = b + end + end + if (didSquare == false) + for c in shape.colliders + #puts args.state.ball.velocity + if c.collision?(args, b.getPoints(args),b) + c.collide args, b + didHit = true + hitter = b + end + end + end + end + end + if (didHit) + shape.count=shape.count-1 + shape.damageCount.append([(hitter.leastChain+1 - hitter.number)-1, args.state.tick_count]) + + end + i=0 + while i < shape.damageCount.length + if shape.damageCount[i][0] <= 0 + shape.damageCount.delete_at(i) + i-=1 + elsif shape.damageCount[i][1].elapsed_time > BALL_DISTANCE and shape.damageCount[i][0] > 1 + shape.count-=1 + shape.damageCount[i][0]-=1 + shape.damageCount[i][1] = args.state.tick_count + end + i+=1 + end +end + + +class Square + attr_accessor :count, :x, :y, :home, :bold, :squareColliders, :colliders, :damageCount + def initialize(args, x, y, block_size, orientation, block_offset) + @x = x * block_size + @y = y * block_size + @block_size = block_size + @block_offset = block_offset + @orientation = orientation + @damageCount = [] + @home = 'squares' + + + Kernel.srand() + @r = rand(255) + @g = rand(255) + @b = rand(255) + + @count = rand(MAX_COUNT)+1 + + x_offset = (args.state.board_width + args.grid.w / 8) + @block_offset / 2 + @x_adjusted = @x + x_offset + @y_adjusted = @y + @size_adjusted = @block_size * 2 - @block_offset + + hypotenuse=args.state.ball_hypotenuse + @bold = [(@x_adjusted-hypotenuse/2)-1, (@y_adjusted-hypotenuse/2)-1, @size_adjusted + hypotenuse + 2, @size_adjusted + hypotenuse + 2] + + @points = [ + {x:@x_adjusted, y:@y_adjusted}, + {x:@x_adjusted+@size_adjusted, y:@y_adjusted}, + {x:@x_adjusted+@size_adjusted, y:@y_adjusted+@size_adjusted}, + {x:@x_adjusted, y:@y_adjusted+@size_adjusted} + ] + @squareColliders = [ + SquareCollider.new(@points[0].x,@points[0].y,{x:-1,y:-1}), + SquareCollider.new(@points[1].x-COLLISIONWIDTH,@points[1].y,{x:1,y:-1}), + SquareCollider.new(@points[2].x-COLLISIONWIDTH,@points[2].y-COLLISIONWIDTH,{x:1,y:1}), + SquareCollider.new(@points[3].x,@points[3].y-COLLISIONWIDTH,{x:-1,y:1}), + ] + @colliders = [ + LinearCollider.new(@points[0],@points[1], :neg), + LinearCollider.new(@points[1],@points[2], :neg), + LinearCollider.new(@points[2],@points[3], :pos), + LinearCollider.new(@points[0],@points[3], :pos) + ] + end + + def draw(args) + #Offset the coordinates to the edge of the game area + x_offset = (args.state.board_width + args.grid.w / 8) + @block_offset / 2 + #args.outputs.solids << [@x + x_offset, @y, @block_size * 2 - @block_offset, @block_size * 2 - @block_offset, @r, @g, @b] + args.outputs.solids <<{x: (@x + x_offset), y: (@y), w: (@block_size * 2 - @block_offset), h: (@block_size * 2 - @block_offset), r: @r , g: @g , b: @b } + #args.outputs.solids << @bold.append([255,0,0]) + args.outputs.labels << [@x + x_offset + (@block_size * 2 - @block_offset)/2, (@y) + (@block_size * 2 - @block_offset)/2, @count.to_s] + + end + + def update args + universalUpdate args, self + end +end + +class TShape + attr_accessor :count, :x, :y, :home, :bold, :squareColliders, :colliders, :damageCount + def initialize(args, x, y, block_size, orientation, block_offset) + @x = x * block_size + @y = y * block_size + @block_size = block_size + @block_offset = block_offset + @orientation = orientation + @damageCount = [] + @home = "tshapes" + + Kernel.srand() + @r = rand(255) + @g = rand(255) + @b = rand(255) + + @count = rand(MAX_COUNT)+1 + + + @shapePoints = getShapePoints(args) + minX={x:INFINITY, y:0} + minY={x:0, y:INFINITY} + maxX={x:-INFINITY, y:0} + maxY={x:0, y:-INFINITY} + for p in @shapePoints + if p.x < minX.x + minX = p + end + if p.x > maxX.x + maxX = p + end + if p.y < minY.y + minY = p + end + if p.y > maxY.y + maxY = p + end + end + + + hypotenuse=args.state.ball_hypotenuse + + @bold = [(minX.x-hypotenuse/2)-1, (minY.y-hypotenuse/2)-1, -((minX.x-hypotenuse/2)-1)+(maxX.x + hypotenuse + 2), -((minY.y-hypotenuse/2)-1)+(maxY.y + hypotenuse + 2)] + end + def getShapePoints(args) + points=[] + x_offset = (args.state.board_width + args.grid.w / 8) + (@block_offset / 2) + + if @orientation == :right + #args.outputs.solids << [@x + x_offset, @y, @block_size - @block_offset, @block_size * 3 - @block_offset, @r, @g, @b] + #args.outputs.solids << [@x + x_offset, @y + @block_size, @block_size * 2, @block_size, @r, @g, @b] + points = [ + {x:@x + x_offset, y:@y}, + {x:(@x + x_offset)+(@block_size - @block_offset), y:@y}, + {x:(@x + x_offset)+(@block_size - @block_offset),y:@y + @block_size}, + {x:(@x + x_offset)+ @block_size * 2,y:@y + @block_size}, + {x:(@x + x_offset)+ @block_size * 2,y:@y + @block_size+@block_size}, + {x:(@x + x_offset)+(@block_size - @block_offset),y:@y + @block_size+@block_size}, + {x:(@x + x_offset)+(@block_size - @block_offset), y:@y+ @block_size * 3 - @block_offset}, + {x:@x + x_offset , y:@y+ @block_size * 3 - @block_offset} + ] + @squareColliders = [ + SquareCollider.new(points[0].x,points[0].y,{x:-1,y:-1}), + SquareCollider.new(points[1].x-COLLISIONWIDTH,points[1].y,{x:1,y:-1}), + SquareCollider.new(points[2].x,points[2].y-COLLISIONWIDTH,{x:1,y:-1}), + SquareCollider.new(points[3].x-COLLISIONWIDTH,points[3].y,{x:1,y:-1}), + SquareCollider.new(points[4].x-COLLISIONWIDTH,points[4].y-COLLISIONWIDTH,{x:1,y:1}), + SquareCollider.new(points[5].x,points[5].y,{x:1,y:1}), + SquareCollider.new(points[6].x-COLLISIONWIDTH,points[6].y-COLLISIONWIDTH,{x:1,y:1}), + SquareCollider.new(points[7].x,points[7].y-COLLISIONWIDTH,{x:-1,y:1}), + ] + @colliders = [ + LinearCollider.new(points[0],points[1], :neg), + LinearCollider.new(points[1],points[2], :neg), + LinearCollider.new(points[2],points[3], :neg), + LinearCollider.new(points[3],points[4], :neg), + LinearCollider.new(points[4],points[5], :pos), + LinearCollider.new(points[5],points[6], :neg), + LinearCollider.new(points[6],points[7], :pos), + LinearCollider.new(points[0],points[7], :pos) + ] + elsif @orientation == :up + #args.outputs.solids << [@x + x_offset, @y, @block_size * 3 - @block_offset, @block_size - @block_offset, @r, @g, @b] + #args.outputs.solids << [@x + x_offset + @block_size, @y, @block_size, @block_size * 2, @r, @g, @b] + points = [ + {x:@x + x_offset, y:@y}, + {x:(@x + x_offset)+(@block_size * 3 - @block_offset), y:@y}, + {x:(@x + x_offset)+(@block_size * 3 - @block_offset), y:@y+(@block_size - @block_offset)}, + {x:@x + x_offset + @block_size + @block_size, y:@y+(@block_size - @block_offset)}, + {x:@x + x_offset + @block_size + @block_size, y:@y+@block_size*2}, + {x:@x + x_offset + @block_size, y:@y+@block_size*2}, + {x:@x + x_offset + @block_size, y:@y+(@block_size - @block_offset)}, + {x:@x + x_offset, y:@y+(@block_size - @block_offset)} + ] + @squareColliders = [ + SquareCollider.new(points[0].x,points[0].y,{x:-1,y:-1}), + SquareCollider.new(points[1].x-COLLISIONWIDTH,points[1].y,{x:1,y:-1}), + SquareCollider.new(points[2].x-COLLISIONWIDTH,points[2].y-COLLISIONWIDTH,{x:1,y:1}), + SquareCollider.new(points[3].x,points[3].y,{x:1,y:1}), + SquareCollider.new(points[4].x-COLLISIONWIDTH,points[4].y-COLLISIONWIDTH,{x:1,y:1}), + SquareCollider.new(points[5].x,points[5].y-COLLISIONWIDTH,{x:-1,y:1}), + SquareCollider.new(points[6].x-COLLISIONWIDTH,points[6].y,{x:-1,y:1}), + SquareCollider.new(points[7].x,points[7].y-COLLISIONWIDTH,{x:-1,y:1}), + ] + @colliders = [ + LinearCollider.new(points[0],points[1], :neg), + LinearCollider.new(points[1],points[2], :neg), + LinearCollider.new(points[2],points[3], :pos), + LinearCollider.new(points[3],points[4], :neg), + LinearCollider.new(points[4],points[5], :pos), + LinearCollider.new(points[5],points[6], :neg), + LinearCollider.new(points[6],points[7], :pos), + LinearCollider.new(points[0],points[7], :pos) + ] + elsif @orientation == :left + #args.outputs.solids << [@x + x_offset + @block_size, @y, @block_size - @block_offset, @block_size * 3 - @block_offset, @r, @g, @b] + #args.outputs.solids << [@x + x_offset, @y + @block_size, @block_size * 2 - @block_offset, @block_size - @block_offset, @r, @g, @b] + xh = @x + x_offset + #points = [ + #{x:@x + x_offset, y:@y}, + #{x:(@x + x_offset)+(@block_size - @block_offset), y:@y}, + #{x:(@x + x_offset)+(@block_size - @block_offset),y:@y + @block_size}, + #{x:(@x + x_offset)+ @block_size * 2,y:@y + @block_size}, + #{x:(@x + x_offset)+ @block_size * 2,y:@y + @block_size+@block_size}, + #{x:(@x + x_offset)+(@block_size - @block_offset),y:@y + @block_size+@block_size}, + #{x:(@x + x_offset)+(@block_size - @block_offset), y:@y+ @block_size * 3 - @block_offset}, + #{x:@x + x_offset , y:@y+ @block_size * 3 - @block_offset} + #] + points = [ + {x:@x + x_offset + @block_size, y:@y}, + {x:@x + x_offset + @block_size + (@block_size - @block_offset), y:@y}, + {x:@x + x_offset + @block_size + (@block_size - @block_offset),y:@y+@block_size*3- @block_offset}, + {x:@x + x_offset + @block_size, y:@y+@block_size*3- @block_offset}, + {x:@x + x_offset+@block_size, y:@y+@block_size*2- @block_offset}, + {x:@x + x_offset, y:@y+@block_size*2- @block_offset}, + {x:@x + x_offset, y:@y+@block_size}, + {x:@x + x_offset+@block_size, y:@y+@block_size} + ] + @squareColliders = [ + SquareCollider.new(points[0].x,points[0].y,{x:-1,y:-1}), + SquareCollider.new(points[1].x-COLLISIONWIDTH,points[1].y,{x:1,y:-1}), + SquareCollider.new(points[2].x-COLLISIONWIDTH,points[2].y-COLLISIONWIDTH,{x:1,y:1}), + SquareCollider.new(points[3].x,points[3].y-COLLISIONWIDTH,{x:-1,y:1}), + SquareCollider.new(points[4].x-COLLISIONWIDTH,points[4].y,{x:-1,y:1}), + SquareCollider.new(points[5].x,points[5].y-COLLISIONWIDTH,{x:-1,y:1}), + SquareCollider.new(points[6].x,points[6].y,{x:-1,y:-1}), + SquareCollider.new(points[7].x-COLLISIONWIDTH,points[7].y-COLLISIONWIDTH,{x:-1,y:-1}), + ] + @colliders = [ + LinearCollider.new(points[0],points[1], :neg), + LinearCollider.new(points[1],points[2], :neg), + LinearCollider.new(points[2],points[3], :pos), + LinearCollider.new(points[3],points[4], :neg), + LinearCollider.new(points[4],points[5], :pos), + LinearCollider.new(points[5],points[6], :neg), + LinearCollider.new(points[6],points[7], :neg), + LinearCollider.new(points[0],points[7], :pos) + ] + elsif @orientation == :down + #args.outputs.solids << [@x + x_offset, @y + @block_size, @block_size * 3 - @block_offset, @block_size - @block_offset, @r, @g, @b] + #args.outputs.solids << [@x + x_offset + @block_size, @y, @block_size - @block_offset, @block_size * 2 - @block_offset, @r, @g, @b] + + points = [ + {x:@x + x_offset, y:@y+(@block_size*2)-@block_offset}, + {x:@x + x_offset+ @block_size*3-@block_offset, y:@y+(@block_size*2)-@block_offset}, + {x:@x + x_offset+ @block_size*3-@block_offset, y:@y+(@block_size)}, + {x:@x + x_offset+ @block_size*2-@block_offset, y:@y+(@block_size)}, + {x:@x + x_offset+ @block_size*2-@block_offset, y:@y},# + {x:@x + x_offset+ @block_size, y:@y},# + {x:@x + x_offset + @block_size, y:@y+(@block_size)}, + {x:@x + x_offset, y:@y+(@block_size)} + ] + @squareColliders = [ + SquareCollider.new(points[0].x,points[0].y-COLLISIONWIDTH,{x:-1,y:1}), + SquareCollider.new(points[1].x-COLLISIONWIDTH,points[1].y-COLLISIONWIDTH,{x:1,y:1}), + SquareCollider.new(points[2].x-COLLISIONWIDTH,points[2].y,{x:1,y:-1}), + SquareCollider.new(points[3].x,points[3].y-COLLISIONWIDTH,{x:1,y:-1}), + SquareCollider.new(points[4].x-COLLISIONWIDTH,points[4].y,{x:1,y:-1}), + SquareCollider.new(points[5].x,points[5].y,{x:-1,y:-1}), + SquareCollider.new(points[6].x-COLLISIONWIDTH,points[6].y-COLLISIONWIDTH,{x:-1,y:-1}), + SquareCollider.new(points[7].x,points[7].y,{x:-1,y:-1}), + ] + @colliders = [ + LinearCollider.new(points[0],points[1], :pos), + LinearCollider.new(points[1],points[2], :pos), + LinearCollider.new(points[2],points[3], :neg), + LinearCollider.new(points[3],points[4], :pos), + LinearCollider.new(points[4],points[5], :neg), + LinearCollider.new(points[5],points[6], :pos), + LinearCollider.new(points[6],points[7], :neg), + LinearCollider.new(points[0],points[7], :neg) + ] + end + return points + end + + def draw(args) + #Offset the coordinates to the edge of the game area + x_offset = (args.state.board_width + args.grid.w / 8) + (@block_offset / 2) + + if @orientation == :right + #args.outputs.solids << [@x + x_offset, @y, @block_size - @block_offset, @block_size * 3 - @block_offset, @r, @g, @b] + args.outputs.solids << {x: (@x + x_offset), y: @y, w: @block_size - @block_offset, h: (@block_size * 3 - @block_offset), r: @r , g: @g, b: @b} + #args.outputs.solids << [@x + x_offset, @y + @block_size, @block_size * 2, @block_size, @r, @g, @b] + args.outputs.solids << {x: (@x + x_offset), y: (@y + @block_size), w: (@block_size * 2), h: (@block_size), r: @r , g: @g, b: @b } + elsif @orientation == :up + #args.outputs.solids << [@x + x_offset, @y, @block_size * 3 - @block_offset, @block_size - @block_offset, @r, @g, @b] + args.outputs.solids << {x: (@x + x_offset), y: (@y), w: (@block_size * 3 - @block_offset), h: (@block_size - @block_offset), r: @r , g: @g, b: @b} + #args.outputs.solids << [@x + x_offset + @block_size, @y, @block_size, @block_size * 2, @r, @g, @b] + args.outputs.solids << {x: (@x + x_offset + @block_size), y: (@y), w: (@block_size), h: (@block_size * 2), r: @r , g: @g, b: @b} + elsif @orientation == :left + #args.outputs.solids << [@x + x_offset + @block_size, @y, @block_size - @block_offset, @block_size * 3 - @block_offset, @r, @g, @b] + args.outputs.solids << {x: (@x + x_offset + @block_size), y: (@y), w: (@block_size - @block_offset), h: (@block_size * 3 - @block_offset), r: @r , g: @g, b: @b} + #args.outputs.solids << [@x + x_offset, @y + @block_size, @block_size * 2 - @block_offset, @block_size - @block_offset, @r, @g, @b] + args.outputs.solids << {x: (@x + x_offset), y: (@y + @block_size), w: (@block_size * 2 - @block_offset), h: (@block_size - @block_offset), r: @r , g: @g, b: @b} + elsif @orientation == :down + #args.outputs.solids << [@x + x_offset, @y + @block_size, @block_size * 3 - @block_offset, @block_size - @block_offset, @r, @g, @b] + args.outputs.solids << {x: (@x + x_offset), y: (@y + @block_size), w: (@block_size * 3 - @block_offset), h: (@block_size - @block_offset), r: @r , g: @g, b: @b} + #args.outputs.solids << [@x + x_offset + @block_size, @y, @block_size - @block_offset, @block_size * 2 - @block_offset, @r, @g, @b] + args.outputs.solids << {x: (@x + x_offset + @block_size), y: (@y), w: (@block_size - @block_offset), h: ( @block_size * 2 - @block_offset), r: @r , g: @g, b: @b} + end + + #psize = 5.0 + #for p in @shapePoints + #args.outputs.solids << [p.x-psize/2, p.y-psize/2, psize, psize, 0, 0, 0] + #end + args.outputs.labels << [@x + x_offset + (@block_size * 2 - @block_offset)/2, (@y) + (@block_size * 2 - @block_offset)/2, @count.to_s] + + end + + def updateOne_old args + didHit = false + hitter = nil + toCollide = nil + for b in args.state.balls + if [b.x, b.y, b.width, b.height].intersect_rect?(@bold) + didSquare = false + for s in @squareColliders + if (s.collision?(args, b)) + didSquare = true + didHit = true + #s.collide(args, b) + toCollide = s + hitter = b + break + end + end + if (didSquare == false) + for c in @colliders + #puts args.state.ball.velocity + if c.collision?(args, b.getPoints(args),b) + #c.collide args, b + toCollide = c + didHit = true + hitter = b + break + end + end + end + end + if didHit + break + end + end + if (didHit) + @count=0 + hitter.makeLeader args + #toCollide.collide(args, hitter) + args.state.tshapes.delete(self) + #puts "HIT!" + hitter.number + end + end + + def update_old args + if (@count == 1) + updateOne args + return + end + didHit = false + hitter = nil + for b in args.state.ballParents + if [b.x, b.y, b.width, b.height].intersect_rect?(@bold) + didSquare = false + for s in @squareColliders + if (s.collision?(args, b)) + didSquare = true + didHit=true + s.collide(args, b) + hitter = b + end + end + if (didSquare == false) + for c in @colliders + #puts args.state.ball.velocity + if c.collision?(args, b.getPoints(args), b) + c.collide args, b + didHit=true + hitter = b + end + end + end + end + end + if (didHit) + @count=@count-1 + @damageCount.append([(hitter.leastChain+1 - hitter.number)-1, args.state.tick_count]) + + if (@count == 0) + args.state.tshapes.delete(self) + return + end + end + i=0 + + while i < @damageCount.length + if @damageCount[i][0] <= 0 + @damageCount.delete_at(i) + i-=1 + elsif @damageCount[i][1].elapsed_time > BALL_DISTANCE + @count-=1 + @damageCount[i][0]-=1 + end + if (@count == 0) + args.state.tshapes.delete(self) + return + end + i+=1 + end + end #end update + + def update args + universalUpdate args, self + end + +end + +class Line + attr_accessor :count, :x, :y, :home, :bold, :squareColliders, :colliders, :damageCount + def initialize(args, x, y, block_size, orientation, block_offset) + @x = x * block_size + @y = y * block_size + @block_size = block_size + @block_offset = block_offset + @orientation = orientation + @damageCount = [] + @home = "lines" + + Kernel.srand() + @r = rand(255) + @g = rand(255) + @b = rand(255) + + @count = rand(MAX_COUNT)+1 + + @shapePoints = getShapePoints(args) + minX={x:INFINITY, y:0} + minY={x:0, y:INFINITY} + maxX={x:-INFINITY, y:0} + maxY={x:0, y:-INFINITY} + for p in @shapePoints + if p.x < minX.x + minX = p + end + if p.x > maxX.x + maxX = p + end + if p.y < minY.y + minY = p + end + if p.y > maxY.y + maxY = p + end + end + + + hypotenuse=args.state.ball_hypotenuse + + @bold = [(minX.x-hypotenuse/2)-1, (minY.y-hypotenuse/2)-1, -((minX.x-hypotenuse/2)-1)+(maxX.x + hypotenuse + 2), -((minY.y-hypotenuse/2)-1)+(maxY.y + hypotenuse + 2)] + end + + def getShapePoints(args) + points=[] + x_offset = (args.state.board_width + args.grid.w / 8) + (@block_offset / 2) + + if @orientation == :right + #args.outputs.solids << [@x + x_offset, @y, @block_size * 3 - @block_offset, @block_size - @block_offset, @r, @g, @b] + xa =@x + x_offset + ya =@y + wa =@block_size * 3 - @block_offset + ha =(@block_size - @block_offset) + elsif @orientation == :up + #args.outputs.solids << [@x + x_offset, @y, @block_size - @block_offset, @block_size * 3 - @block_offset, @r, @g, @b] + xa =@x + x_offset + ya =@y + wa =@block_size - @block_offset + ha =@block_size * 3 - @block_offset + + elsif @orientation == :left + #args.outputs.solids << [@x + x_offset, @y, @block_size * 3 - @block_offset, @block_size - @block_offset, @r, @g, @b] + xa =@x + x_offset + ya =@y + wa =@block_size * 3 - @block_offset + ha =@block_size - @block_offset + elsif @orientation == :down + #args.outputs.solids << [@x + x_offset, @y, @block_size - @block_offset, @block_size * 3 - @block_offset, @r, @g, @b] + xa =@x + x_offset + ya =@y + wa =@block_size - @block_offset + ha =@block_size * 3 - @block_offset + end + points = [ + {x: xa, y:ya}, + {x: xa + wa,y:ya}, + {x: xa + wa,y:ya+ha}, + {x: xa, y:ya+ha}, + ] + @squareColliders = [ + SquareCollider.new(points[0].x,points[0].y,{x:-1,y:-1}), + SquareCollider.new(points[1].x-COLLISIONWIDTH,points[1].y,{x:1,y:-1}), + SquareCollider.new(points[2].x-COLLISIONWIDTH,points[2].y-COLLISIONWIDTH,{x:1,y:1}), + SquareCollider.new(points[3].x,points[3].y-COLLISIONWIDTH,{x:-1,y:1}), + ] + @colliders = [ + LinearCollider.new(points[0],points[1], :neg), + LinearCollider.new(points[1],points[2], :neg), + LinearCollider.new(points[2],points[3], :pos), + LinearCollider.new(points[0],points[3], :pos), + ] + return points + end + + def update args + universalUpdate args, self + end + + def draw(args) + x_offset = (args.state.board_width + args.grid.w / 8) + @block_offset / 2 + + if @orientation == :right + args.outputs.solids << [@x + x_offset, @y, @block_size * 3 - @block_offset, @block_size - @block_offset, @r, @g, @b] + elsif @orientation == :up + args.outputs.solids << [@x + x_offset, @y, @block_size - @block_offset, @block_size * 3 - @block_offset, @r, @g, @b] + elsif @orientation == :left + args.outputs.solids << [@x + x_offset, @y, @block_size * 3 - @block_offset, @block_size - @block_offset, @r, @g, @b] + elsif @orientation == :down + args.outputs.solids << [@x + x_offset, @y, @block_size - @block_offset, @block_size * 3 - @block_offset, @r, @g, @b] + end + + args.outputs.labels << [@x + x_offset + (@block_size * 2 - @block_offset)/2, (@y) + (@block_size * 2 - @block_offset)/2, @count.to_s] + + end +end + +</code></pre> +<h3 id='----physics-and-collisions---arbitrary-collision---linear_collider.rb'>Physics And Collisions - Arbitrary Collision - linear_collider.rb</h3> +<pre><code class="language-ruby"># ./samples/04_physics_and_collisions/09_arbitrary_collision/app/linear_collider.rb + +COLLISIONWIDTH=8 + +class LinearCollider + attr_reader :pointA, :pointB + def initialize (pointA, pointB, mode,collisionWidth=COLLISIONWIDTH) + @pointA = pointA + @pointB = pointB + @mode = mode + @collisionWidth = collisionWidth + + if (@pointA.x > @pointB.x) + @pointA, @pointB = @pointB, @pointA + end + + @linearCollider_collision_once = false + end + + def collisionSlope args + if (@[email protected] == 0) + return INFINITY + end + return (@pointB.y - @pointA.y) / (@pointB.x - @pointA.x) + end + + + def collision? (args, points, ball=nil) + + slope = collisionSlope args + result = false + + # calculate a vector with a magnitude of (1/2)collisionWidth and a direction perpendicular to the collision line + vect=nil;mag=nil;vect=nil; + if @mode == :both + vect = {x: @pointB.x - @pointA.x, y:@pointB.y - @pointA.y} + mag = (vect.x**2 + vect.y**2)**0.5 + vect = {y: -1*(vect.x/(mag))*@collisionWidth*0.5, x: (vect.y/(mag))*@collisionWidth*0.5} + else + vect = {x: @pointB.x - @pointA.x, y:@pointB.y - @pointA.y} + mag = (vect.x**2 + vect.y**2)**0.5 + vect = {y: -1*(vect.x/(mag))*@collisionWidth, x: (vect.y/(mag))*@collisionWidth} + end + + rpointA=nil;rpointB=nil;rpointC=nil;rpointD=nil; + if @mode == :pos + rpointA = {x:@pointA.x + vect.x, y:@pointA.y + vect.y} + rpointB = {x:@pointB.x + vect.x, y:@pointB.y + vect.y} + rpointC = {x:@pointB.x, y:@pointB.y} + rpointD = {x:@pointA.x, y:@pointA.y} + elsif @mode == :neg + rpointA = {x:@pointA.x, y:@pointA.y} + rpointB = {x:@pointB.x, y:@pointB.y} + rpointC = {x:@pointB.x - vect.x, y:@pointB.y - vect.y} + rpointD = {x:@pointA.x - vect.x, y:@pointA.y - vect.y} + elsif @mode == :both + rpointA = {x:@pointA.x + vect.x, y:@pointA.y + vect.y} + rpointB = {x:@pointB.x + vect.x, y:@pointB.y + vect.y} + rpointC = {x:@pointB.x - vect.x, y:@pointB.y - vect.y} + rpointD = {x:@pointA.x - vect.x, y:@pointA.y - vect.y} + end + #four point rectangle + + + + if ball != nil + xs = [rpointA.x,rpointB.x,rpointC.x,rpointD.x] + ys = [rpointA.y,rpointB.y,rpointC.y,rpointD.y] + correct = 1 + rect1 = [ball.x, ball.y, ball.width, ball.height] + #$r1 = rect1 + rect2 = [xs.min-correct,ys.min-correct,(xs.max-xs.min)+correct*2,(ys.max-ys.min)+correct*2] + #$r2 = rect2 + if rect1.intersect_rect?(rect2) == false + return false + end + end + + + #area of a triangle + triArea = -> (a,b,c) { ((a.x * (b.y - c.y) + b.x * (c.y - a.y) + c.x * (a.y - b.y))/2.0).abs } + + #if at least on point is in the rectangle then collision? is true - otherwise false + for point in points + #Check whether a given point lies inside a rectangle or not: + #if the sum of the area of traingls, PAB, PBC, PCD, PAD equal the area of the rec, then an intersection has occured + areaRec = triArea.call(rpointA, rpointB, rpointC)+triArea.call(rpointA, rpointC, rpointD) + areaSum = [ + triArea.call(point, rpointA, rpointB),triArea.call(point, rpointB, rpointC), + triArea.call(point, rpointC, rpointD),triArea.call(point, rpointA, rpointD) + ].inject(0){|sum,x| sum + x } + e = 0.0001 #allow for minor error + if areaRec>= areaSum-e and areaRec<= areaSum+e + result = true + #return true + break + end + end + + #args.outputs.lines << [@pointA.x, @pointA.y, @pointB.x, @pointB.y, 000, 000, 000] + #args.outputs.lines << [rpointA.x, rpointA.y, rpointB.x, rpointB.y, 255, 000, 000] + #args.outputs.lines << [rpointC.x, rpointC.y, rpointD.x, rpointD.y, 000, 000, 255] + + + #puts (rpointA.x.to_s + " " + rpointA.y.to_s + " " + rpointB.x.to_s + " "+ rpointB.y.to_s) + return result + end #end collision? + + def getRepelMagnitude (fbx, fby, vrx, vry, ballMag) + a = fbx ; b = vrx ; c = fby + d = vry ; e = ballMag + if b**2 + d**2 == 0 + #unexpected + end + x1 = (-a*b+-c*d + (e**2 * b**2 - b**2 * c**2 + 2*a*b*c*d + e**2 + d**2 - a**2 * d**2)**0.5)/(b**2 + d**2) + x2 = -((a*b + c*d + (e**2 * b**2 - b**2 * c**2 + 2*a*b*c*d + e**2 * d**2 - a**2 * d**2)**0.5)/(b**2 + d**2)) + err = 0.00001 + o = ((fbx + x1*vrx)**2 + (fby + x1*vry)**2 ) ** 0.5 + p = ((fbx + x2*vrx)**2 + (fby + x2*vry)**2 ) ** 0.5 + r = 0 + if (ballMag >= o-err and ballMag <= o+err) + r = x1 + elsif (ballMag >= p-err and ballMag <= p+err) + r = x2 + else + #unexpected + end + return r + end + + def collide args, ball + slope = collisionSlope args + + # perpVect: normal vector perpendicular to collision + perpVect = {x: @pointB.x - @pointA.x, y:@pointB.y - @pointA.y} + mag = (perpVect.x**2 + perpVect.y**2)**0.5 + perpVect = {x: perpVect.x/(mag), y: perpVect.y/(mag)} + perpVect = {x: -perpVect.y, y: perpVect.x} + if perpVect.y > 0 #ensure perpVect points upward + perpVect = {x: perpVect.x*-1, y: perpVect.y*-1} + end + previousPosition = { + x:ball.x-ball.velocity.x, + y:ball.y-ball.velocity.y + } + yInterc = @pointA.y + -slope*@pointA.x + if slope == INFINITY + if previousPosition.x < @pointA.x + perpVect = {x: perpVect.x*-1, y: perpVect.y*-1} + yInterc = -INFINITY + end + elsif previousPosition.y < slope*previousPosition.x + yInterc #check if ball is bellow or above the collider to determine if perpVect is - or + + perpVect = {x: perpVect.x*-1, y: perpVect.y*-1} + end + + velocityMag = (ball.velocity.x**2 + ball.velocity.y**2)**0.5 + theta_ball=Math.atan2(ball.velocity.y,ball.velocity.x) #the angle of the ball's velocity + theta_repel=Math.atan2(perpVect.y,perpVect.x) #the angle of the repelling force(perpVect) + + fbx = velocityMag * Math.cos(theta_ball) #the x component of the ball's velocity + fby = velocityMag * Math.sin(theta_ball) #the y component of the ball's velocity + + #the magnitude of the repelling force + repelMag = getRepelMagnitude(fbx, fby, perpVect.x, perpVect.y, (ball.velocity.x**2 + ball.velocity.y**2)**0.5) + frx = repelMag* Math.cos(theta_repel) #the x component of the repel's velocity | magnitude is set to twice of fbx + fry = repelMag* Math.sin(theta_repel) #the y component of the repel's velocity | magnitude is set to twice of fby + + fsumx = fbx+frx #sum of x forces + fsumy = fby+fry #sum of y forces + fr = velocityMag#fr is the resulting magnitude + thetaNew = Math.atan2(fsumy, fsumx) #thetaNew is the resulting angle + xnew = fr*Math.cos(thetaNew)#resulting x velocity + ynew = fr*Math.sin(thetaNew)#resulting y velocity + if (velocityMag < MAX_VELOCITY) + ball.velocity = Vector2d.new(xnew*1.1, ynew*1.1) + else + ball.velocity = Vector2d.new(xnew, ynew) + end + + end +end + +</code></pre> +<h3 id='----physics-and-collisions---arbitrary-collision---main.rb'>Physics And Collisions - Arbitrary Collision - main.rb</h3> +<pre><code class="language-ruby"># ./samples/04_physics_and_collisions/09_arbitrary_collision/app/main.rb +INFINITY= 10**10 +MAX_VELOCITY = 8.0 +BALL_COUNT = 90 +BALL_DISTANCE = 20 +require 'app/vector2d.rb' +require 'app/blocks.rb' +require 'app/ball.rb' +require 'app/rectangle.rb' +require 'app/linear_collider.rb' +require 'app/square_collider.rb' + + + +#Method to init default values +def defaults args + args.state.board_width ||= args.grid.w / 4 + args.state.board_height ||= args.grid.h + args.state.game_area ||= [(args.state.board_width + args.grid.w / 8), 0, args.state.board_width, args.grid.h] + args.state.balls ||= [] + args.state.num_balls ||= 0 + args.state.ball_created_at ||= args.state.tick_count + args.state.ball_hypotenuse = (10**2 + 10**2)**0.5 + args.state.ballParents ||=nil + + init_blocks args + init_balls args +end + +begin :default_methods + def init_blocks args + block_size = args.state.board_width / 8 + #Space inbetween each block + block_offset = 4 + + args.state.squares ||=[ + Square.new(args, 2, 0, block_size, :right, block_offset), + Square.new(args, 5, 0, block_size, :right, block_offset), + Square.new(args, 6, 7, block_size, :right, block_offset) + ] + + + #Possible orientations are :right, :left, :up, :down + + + args.state.tshapes ||= [ + TShape.new(args, 0, 6, block_size, :left, block_offset), + TShape.new(args, 3, 3, block_size, :down, block_offset), + TShape.new(args, 0, 3, block_size, :right, block_offset), + TShape.new(args, 0, 11, block_size, :up, block_offset) + ] + + args.state.lines ||= [ + Line.new(args,3, 8, block_size, :down, block_offset), + Line.new(args, 7, 3, block_size, :up, block_offset), + Line.new(args, 3, 7, block_size, :right, block_offset) + ] + + #exit() + end + + def init_balls args + return unless args.state.num_balls < BALL_COUNT + + + #only create a new ball every 10 ticks + return unless args.state.ball_created_at.elapsed_time > 10 + + if (args.state.num_balls == 0) + args.state.balls.append(Ball.new(args,args.state.num_balls,BALL_COUNT-1, nil, nil)) + args.state.ballParents = [args.state.balls[0]] + else + args.state.balls.append(Ball.new(args,args.state.num_balls,BALL_COUNT-1, args.state.balls.last, nil) ) + args.state.balls[-2].child = args.state.balls[-1] + end + args.state.ball_created_at = args.state.tick_count + args.state.num_balls += 1 + end +end + +#Render loop +def render args + bgClr = {r:10, g:10, b:200} + bgClr = {r:255-30, g:255-30, b:255-30} + + args.outputs.solids << [0, 0, $args.grid.right, $args.grid.top, bgClr[:r], bgClr[:g], bgClr[:b]]; + args.outputs.borders << args.state.game_area + + render_instructions args + render_shapes args + + render_balls args + + #args.state.rectangle.draw args + + args.outputs.sprites << [$args.grid.right-(args.state.board_width + args.grid.w / 8), 0, $args.grid.right, $args.grid.top, "sprites/square-white-2.png", 0, 255, bgClr[:r], bgClr[:g], bgClr[:b]] + args.outputs.sprites << [0, 0, (args.state.board_width + args.grid.w / 8), $args.grid.top, "sprites/square-white-2.png", 0, 255, bgClr[:r], bgClr[:g], bgClr[:b]] + +end + +begin :render_methods + def render_instructions args + #gtk.current_framerate + args.outputs.labels << [20, $args.grid.top-20, "FPS: " + $gtk.current_framerate.to_s] + if (args.state.balls != nil && args.state.balls[0] != nil) + bx = args.state.balls[0].velocity.x + by = args.state.balls[0].velocity.y + bmg = (bx**2.0 + by**2.0)**0.5 + args.outputs.labels << [20, $args.grid.top-20-20, "V: " + bmg.to_s ] + end + + + end + + def render_shapes args + for s in args.state.squares + s.draw args + end + + for l in args.state.lines + l.draw args + end + + for t in args.state.tshapes + t.draw args + end + + + end + + def render_balls args + #args.state.balls.each do |ball| + #ball.draw args + #end + + args.outputs.sprites << args.state.balls.map do |ball| + ball.getDraw args + end + end +end + +#Calls all methods necessary for performing calculations +def calc args + for b in args.state.ballParents + b.update args + end + + for s in args.state.squares + s.update args + end + + for l in args.state.lines + l.update args + end + + for t in args.state.tshapes + t.update args + end + + + +end + +begin :calc_methods + +end + +def tick args + defaults args + render args + calc args +end + +</code></pre> +<h3 id='----physics-and-collisions---arbitrary-collision---paddle.rb'>Physics And Collisions - Arbitrary Collision - paddle.rb</h3> +<pre><code class="language-ruby"># ./samples/04_physics_and_collisions/09_arbitrary_collision/app/paddle.rb +class Paddle + attr_accessor :enabled + + def initialize () + @x=WIDTH/2 + @y=100 + @width=100 + @height=20 + @speed=10 + + @xyCollision = LinearCollider.new({x: @x,y: @y+@height+5}, {x: @x+@width, y: @y+@height+5}) + @xyCollision2 = LinearCollider.new({x: @x,y: @y}, {x: @x+@width, y: @y}, :pos) + @xyCollision3 = LinearCollider.new({x: @x,y: @y}, {x: @x, y: @y+@height+5}) + @xyCollision4 = LinearCollider.new({x: @x+@width,y: @y}, {x: @x+@width, y: @y+@height+5}, :pos) + + @enabled = true + end + + def update args + @xyCollision.resetPoints({x: @x,y: @y+@height+5}, {x: @x+@width, y: @y+@height+5}) + @xyCollision2.resetPoints({x: @x,y: @y}, {x: @x+@width, y: @y}) + @xyCollision3.resetPoints({x: @x,y: @y}, {x: @x, y: @y+@height+5}) + @xyCollision4.resetPoints({x: @x+@width,y: @y}, {x: @x+@width, y: @y+@height+5}) + + @xyCollision.update args + @xyCollision2.update args + @xyCollision3.update args + @xyCollision4.update args + + args.inputs.keyboard.key_held.left ||= false + args.inputs.keyboard.key_held.right ||= false + + if not (args.inputs.keyboard.key_held.left == args.inputs.keyboard.key_held.right) + if args.inputs.keyboard.key_held.left && @enabled + @x-=@speed + elsif args.inputs.keyboard.key_held.right && @enabled + @x+=@speed + end + end + + xmin =WIDTH/4 + xmax = 3*(WIDTH/4) + @x = (@x+@width > xmax) ? xmax-@width : (@x<xmin) ? xmin : @x; + end + + def render args + args.outputs.solids << [@x,@y,@width,@height,255,0,0]; + end + + def rect + [@x, @y, @width, @height] + end +end + +</code></pre> +<h3 id='----physics-and-collisions---arbitrary-collision---rectangle.rb'>Physics And Collisions - Arbitrary Collision - rectangle.rb</h3> +<pre><code class="language-ruby"># ./samples/04_physics_and_collisions/09_arbitrary_collision/app/rectangle.rb +class Rectangle + def initialize args + + @image = "sprites/roundSquare_white.png" + @width = 160.0 + @height = 80.0 + @x=$args.grid.right/2.0 - @width/2.0 + @y=$args.grid.top/2.0 - @height/2.0 + + @xtmp = @width * (1.0/10.0) + @ytmp = @height * (1.0/10.0) + + #ball0 = args.state.balls[0] + #hypotenuse = (args.state.balls[0].width**2 + args.state.balls[0].height**2)**0.5 + hypotenuse=args.state.ball_hypotenuse + @boldXY = {x:(@x-hypotenuse/2)-1, y:(@y-hypotenuse/2)-1} + @boldWidth = @width + hypotenuse + 2 + @boldHeight = @height + hypotenuse + 2 + @bold = [(@x-hypotenuse/2)-1,(@y-hypotenuse/2)-1,@width + hypotenuse + 2,@height + hypotenuse + 2] + + + @points = [ + {x:@x, y:@y+@ytmp}, + {x:@x+@xtmp, y:@y}, + {x:@x+@width-@xtmp, y:@y}, + {x:@x+@width, y:@y+@ytmp}, + {x:@x+@width, y:@y+@height-@ytmp},# + {x:@x+@width-@xtmp, y:@y+@height}, + {x:@x+@xtmp, y:@y+@height}, + {x:@x, y:@y+@height-@ytmp} + ] + + @colliders = [] + #i = 0 + #while i < @points.length-1 + #@colliders.append(LinearCollider.new(@points[i],@points[i+1],:pos)) + #i+=1 + #end + @colliders.append(LinearCollider.new(@points[0],@points[1], :neg)) + @colliders.append(LinearCollider.new(@points[1],@points[2], :neg)) + @colliders.append(LinearCollider.new(@points[2],@points[3], :neg)) + @colliders.append(LinearCollider.new(@points[3],@points[4], :neg)) + @colliders.append(LinearCollider.new(@points[4],@points[5], :pos)) + @colliders.append(LinearCollider.new(@points[5],@points[6], :pos)) + @colliders.append(LinearCollider.new(@points[6],@points[7], :pos)) + @colliders.append(LinearCollider.new(@points[0],@points[7], :pos)) + + end + + def update args + + for b in args.state.balls + if [b.x, b.y, b.width, b.height].intersect_rect?(@bold) + for c in @colliders + if c.collision?(args, b.getPoints(args),b) + c.collide args, b + end + end + end + end + end + + def draw args + args.outputs.sprites << [ + @x, # X + @y, # Y + @width, # W + @height, # H + @image, # PATH + 0, # ANGLE + 255, # ALPHA + 219, # RED_SATURATION + 112, # GREEN_SATURATION + 147 # BLUE_SATURATION + ] + #args.outputs.sprites << [@x, @y, @width, @height, "sprites/roundSquare_small_black.png"] + end + + def serialize + {x: @x, y:@y} + end + + def inspect + serialize.to_s + end + + def to_s + serialize.to_s + end +end + +</code></pre> +<h3 id='----physics-and-collisions---arbitrary-collision---square_collider.rb'>Physics And Collisions - Arbitrary Collision - square_collider.rb</h3> +<pre><code class="language-ruby"># ./samples/04_physics_and_collisions/09_arbitrary_collision/app/square_collider.rb + +class SquareCollider + def initialize x,y,direction,size=COLLISIONWIDTH + @x = x + @y = y + @size = size + @direction = direction + + end + def collision? args, ball + #args.outputs.solids << [@x, @y, @size, @size, 000, 255, 255] + + + return [@x,@y,@size,@size].intersect_rect?([ball.x,ball.y,ball.width,ball.height]) + end + + def collide args, ball + vmag = (ball.velocity.x**2.0 +ball.velocity.y**2.0)**0.5 + a = ((2.0**0.5)*vmag)/2.0 + if vmag < MAX_VELOCITY + ball.velocity.x = (a) * @direction.x * 1.1 + ball.velocity.y = (a) * @direction.y * 1.1 + else + ball.velocity.x = (a) * @direction.x + ball.velocity.y = (a) * @direction.y + end + + end +end + +</code></pre> +<h3 id='----physics-and-collisions---arbitrary-collision---vector2d.rb'>Physics And Collisions - Arbitrary Collision - vector2d.rb</h3> +<pre><code class="language-ruby"># ./samples/04_physics_and_collisions/09_arbitrary_collision/app/vector2d.rb +class Vector2d + attr_accessor :x, :y + + def initialize x=0, y=0 + @x=x + @y=y + end + + #returns a vector multiplied by scalar x + #x [float] scalar + def mult x + r = Vector2d.new(0,0) + r.x=@x*x + r.y=@y*x + r + end + + # vect [Vector2d] vector to copy + def copy vect + Vector2d.new(@x, @y) + end + + #returns a new vector equivalent to this+vect + #vect [Vector2d] vector to add to self + def add vect + Vector2d.new(@x+vect.x,@y+vect.y) + end + + #returns a new vector equivalent to this-vect + #vect [Vector2d] vector to subtract to self + def sub vect + Vector2d.new(@x-vect.c, @y-vect.y) + end + + #return the magnitude of the vector + def mag + ((@x**2)+(@y**2))**0.5 + end + + #returns a new normalize version of the vector + def normalize + Vector2d.new(@x/mag, @y/mag) + end + + #TODO delet? + def distABS vect + (((vect.x-@x)**2+(vect.y-@y)**2)**0.5).abs() + end + end +</code></pre> +<h3 id='----physics-and-collisions---collision-with-object-removal---ball.rb'>Physics And Collisions - Collision With Object Removal - ball.rb</h3> +<pre><code class="language-ruby"># ./samples/04_physics_and_collisions/10_collision_with_object_removal/app/ball.rb +class Ball + #TODO limit accessors? + attr_accessor :xy, :width, :height, :velocity + + + #@xy [Vector2d] x,y position + #@velocity [Vector2d] velocity of ball + def initialize + @xy = Vector2d.new(WIDTH/2,500) + @velocity = Vector2d.new(4,-4) + @width = 20 + @height = 20 + end + + #move the ball according to its velocity + def update args + end + + #render the ball to the screen + def render args + args.outputs.solids << [@xy.x,@xy.y,@width,@height,255,0,255]; + #args.outputs.labels << [20,HEIGHT-50,"velocity: " [email protected]_s+","[email protected]_s + " magnitude:" + @velocity.mag.to_s] + end + + def rect + [@xy.x,@xy.y,@width,@height] + end + +end + +</code></pre> +<h3 id='----physics-and-collisions---collision-with-object-removal---linear_collider.rb'>Physics And Collisions - Collision With Object Removal - linear_collider.rb</h3> +<pre><code class="language-ruby"># ./samples/04_physics_and_collisions/10_collision_with_object_removal/app/linear_collider.rb +#The LinearCollider (theoretically) produces collisions upon a line segment defined point.y two x,y cordinates + +class LinearCollider + + #start [Array of length 2] start of the line segment as a x,y cordinate + #last [Array of length 2] end of the line segment as a x,y cordinate + + #inorder for the LinearCollider to be functional the line segment must be said to have a thickness + #(as it is unlikly that a colliding object will land exactly on the linesegment) + + #extension defines if the line's thickness extends negatively or positively + #extension :pos extends positively + #extension :neg extends negatively + + #thickness [float] how thick the line should be (should always be atleast as large as the magnitude of the colliding object) + def initialize (pointA, pointB, extension=:neg, thickness=10) + @pointA = pointA + @pointB = pointB + @thickness = thickness + @extension = extension + + @pointAExtended={ + x: @pointA.x + @thickness*(@extension == :neg ? -1 : 1), + y: @pointA.y + @thickness*(@extension == :neg ? -1 : 1) + } + @pointBExtended={ + x: @pointB.x + @thickness*(@extension == :neg ? -1 : 1), + y: @pointB.y + @thickness*(@extension == :neg ? -1 : 1) + } + + end + + def resetPoints(pointA,pointB) + @pointA = pointA + @pointB = pointB + + @pointAExtended={ + x:@pointA.x + @thickness*(@extension == :neg ? -1 : 1), + y:@pointA.y + @thickness*(@extension == :neg ? -1 : 1) + } + @pointBExtended={ + x:@pointB.x + @thickness*(@extension == :neg ? -1 : 1), + y:@pointB.y + @thickness*(@extension == :neg ? -1 : 1) + } + end + + #TODO: Ugly function + def slope (pointA, pointB) + return (pointB.x==pointA.x) ? INFINITY : (pointB.y+-pointA.y)/(pointB.x+-pointA.x) + end + + #TODO: Ugly function + def intercept(pointA, pointB) + if (slope(pointA, pointB) == INFINITY) + -INFINITY + elsif slope(pointA, pointB) == -1*INFINITY + INFINITY + else + pointA.y+-1.0*(slope(pointA, pointB)*pointA.x) + end + end + + def calcY(pointA, pointB, x) + return slope(pointA, pointB)*x + intercept(pointA, pointB) + end + + #test if a collision has occurred + def isCollision? (point) + #INFINITY slop breaks down when trying to determin collision, ergo it requires a special test + if slope(@pointA, @pointB) == INFINITY && + point.x >= [@pointA.x,@pointB.x].min+(@extension == :pos ? -@thickness : 0) && + point.x <= [@pointA.x,@pointB.x].max+(@extension == :neg ? @thickness : 0) && + point.y >= [@pointA.y,@pointB.y].min && point.y <= [@pointA.y,@pointB.y].max + return true + end + + isNegInLine = @extension == :neg && + point.y <= slope(@pointA, @pointB)*point.x+intercept(@pointA,@pointB) && + point.y >= point.x*slope(@pointAExtended, @pointBExtended)+intercept(@pointAExtended,@pointBExtended) + isPosInLine = @extension == :pos && + point.y >= slope(@pointA, @pointB)*point.x+intercept(@pointA,@pointB) && + point.y <= point.x*slope(@pointAExtended, @pointBExtended)+intercept(@pointAExtended,@pointBExtended) + isInBoxBounds = point.x >= [@pointA.x,@pointB.x].min && + point.x <= [@pointA.x,@pointB.x].max && + point.y >= [@pointA.y,@pointB.y].min+(@extension == :neg ? -@thickness : 0) && + point.y <= [@pointA.y,@pointB.y].max+(@extension == :pos ? @thickness : 0) + + return isInBoxBounds && (isNegInLine || isPosInLine) + + end + + def getRepelMagnitude (fbx, fby, vrx, vry, args) + a = fbx ; b = vrx ; c = fby + d = vry ; e = args.state.ball.velocity.mag + + if b**2 + d**2 == 0 + puts "magnitude error" + end + + x1 = (-a*b+-c*d + (e**2 * b**2 - b**2 * c**2 + 2*a*b*c*d + e**2 + d**2 - a**2 * d**2)**0.5)/(b**2 + d**2) + x2 = -((a*b + c*d + (e**2 * b**2 - b**2 * c**2 + 2*a*b*c*d + e**2 * d**2 - a**2 * d**2)**0.5)/(b**2 + d**2)) + return ((a+x1*b)**2 + (c+x1*d)**2 == e**2) ? x1 : x2 + end + + def update args + #each of the four points on the square ball - NOTE simple to extend to a circle + points= [ {x: args.state.ball.xy.x, y: args.state.ball.xy.y}, + {x: args.state.ball.xy.x+args.state.ball.width, y: args.state.ball.xy.y}, + {x: args.state.ball.xy.x, y: args.state.ball.xy.y+args.state.ball.height}, + {x: args.state.ball.xy.x+args.state.ball.width, y: args.state.ball.xy.y + args.state.ball.height} + ] + + #for each point p in points + for point in points + #isCollision.md has more information on this section + #TODO: section can certainly be simplifyed + if isCollision?(point) + u = Vector2d.new(1.0,((slope(@pointA, @pointB)==0) ? INFINITY : -1/slope(@pointA, @pointB))*1.0).normalize #normal perpendicular (to line segment) vector + + #the vector with the repeling force can be u or -u depending of where the ball was coming from in relation to the line segment + previousBallPosition=Vector2d.new(point.x-args.state.ball.velocity.x,point.y-args.state.ball.velocity.y) + choiceA = (u.mult(1)) + choiceB = (u.mult(-1)) + vectorRepel = nil + + if (slope(@pointA, @pointB))!=INFINITY && u.y < 0 + choiceA, choiceB = choiceB, choiceA + end + vectorRepel = (previousBallPosition.y > calcY(@pointA, @pointB, previousBallPosition.x)) ? choiceA : choiceB + + #vectorRepel = (previousBallPosition.y > slope(@pointA, @pointB)*previousBallPosition.x+intercept(@pointA,@pointB)) ? choiceA : choiceB) + if (slope(@pointA, @pointB) == INFINITY) #slope INFINITY breaks down in the above test, ergo it requires a custom test + vectorRepel = (previousBallPosition.x > @pointA.x) ? (u.mult(1)) : (u.mult(-1)) + end + #puts (" " + $t[0].to_s + "," + $t[1].to_s + " " + $t[2].to_s + "," + $t[3].to_s + " " + " " + u.x.to_s + "," + u.y.to_s) + #vectorRepel now has the repeling force + + mag = args.state.ball.velocity.mag + theta_ball=Math.atan2(args.state.ball.velocity.y,args.state.ball.velocity.x) #the angle of the ball's velocity + theta_repel=Math.atan2(vectorRepel.y,vectorRepel.x) #the angle of the repeling force + #puts ("theta:" + theta_ball.to_s + " " + theta_repel.to_s) #theta okay + + fbx = mag * Math.cos(theta_ball) #the x component of the ball's velocity + fby = mag * Math.sin(theta_ball) #the y component of the ball's velocity + + repelMag = getRepelMagnitude(fbx, fby, vectorRepel.x, vectorRepel.y, args) + + frx = repelMag* Math.cos(theta_repel) #the x component of the repel's velocity | magnitude is set to twice of fbx + fry = repelMag* Math.sin(theta_repel) #the y component of the repel's velocity | magnitude is set to twice of fby + + fsumx = fbx+frx #sum of x forces + fsumy = fby+fry #sum of y forces + fr = mag#fr is the resulting magnitude + thetaNew = Math.atan2(fsumy, fsumx) #thetaNew is the resulting angle + xnew = fr*Math.cos(thetaNew) #resulting x velocity + ynew = fr*Math.sin(thetaNew) #resulting y velocity + + args.state.ball.velocity = Vector2d.new(xnew,ynew) + #args.state.ball.xy.add(args.state.ball.velocity) + break #no need to check the other points ? + else + end + end + end #end update + +end + +</code></pre> +<h3 id='----physics-and-collisions---collision-with-object-removal---main.rb'>Physics And Collisions - Collision With Object Removal - main.rb</h3> +<pre><code class="language-ruby"># ./samples/04_physics_and_collisions/10_collision_with_object_removal/app/main.rb +# coding: utf-8 +INFINITY= 10**10 +WIDTH=1280 +HEIGHT=720 + +require 'app/vector2d.rb' +require 'app/paddle.rb' +require 'app/ball.rb' +require 'app/linear_collider.rb' + +#Method to init default values +def defaults args + args.state.game_board ||= [(args.grid.w / 2 - args.grid.w / 4), 0, (args.grid.w / 2), args.grid.h] + args.state.bricks ||= [] + args.state.num_bricks ||= 0 + args.state.game_over_at ||= 0 + args.state.paddle ||= Paddle.new + args.state.ball ||= Ball.new + args.state.westWall ||= LinearCollider.new({x: args.grid.w/4, y: 0}, {x: args.grid.w/4, y: args.grid.h}, :pos) + args.state.eastWall ||= LinearCollider.new({x: 3*args.grid.w*0.25, y: 0}, {x: 3*args.grid.w*0.25, y: args.grid.h}) + args.state.southWall ||= LinearCollider.new({x: 0, y: 0}, {x: args.grid.w, y: 0}) + args.state.northWall ||= LinearCollider.new({x: 0, y:args.grid.h}, {x: args.grid.w, y: args.grid.h}, :pos) + + #args.state.testWall ||= LinearCollider.new({x:0 , y:0},{x:args.grid.w, y:args.grid.h}) +end + +#Render loop +def render args + render_instructions args + render_board args + render_bricks args +end + +begin :render_methods + #Method to display the instructions of the game + def render_instructions args + args.outputs.labels << [225, args.grid.h - 30, "← and → to move the paddle left and right", 0, 1] + end + + def render_board args + args.outputs.borders << args.state.game_board + end + + def render_bricks args + args.outputs.solids << args.state.bricks.map(&:rect) + end +end + +#Calls all methods necessary for performing calculations +def calc args + add_new_bricks args + reset_game args + calc_collision args + win_game args + + args.state.westWall.update args + args.state.eastWall.update args + args.state.southWall.update args + args.state.northWall.update args + args.state.paddle.update args + args.state.ball.update args + + #args.state.testWall.update args + + args.state.paddle.render args + args.state.ball.render args +end + +begin :calc_methods + def add_new_bricks args + return if args.state.num_bricks > 40 + + #Width of the game board is 640px + brick_width = (args.grid.w / 2) / 10 + brick_height = brick_width / 2 + + (4).map_with_index do |y| + #Make a box that is 10 bricks wide and 4 bricks tall + args.state.bricks += (10).map_with_index do |x| + args.state.new_entity(:brick) do |b| + b.x = x * brick_width + (args.grid.w / 2 - args.grid.w / 4) + b.y = args.grid.h - ((y + 1) * brick_height) + b.rect = [b.x + 1, b.y - 1, brick_width - 2, brick_height - 2, 235, 50 * y, 52] + + #Add linear colliders to the brick + b.collider_bottom = LinearCollider.new([(b.x-2), (b.y-5)], [(b.x+brick_width+1), (b.y-5)], :pos, brick_height) + b.collider_right = LinearCollider.new([(b.x+brick_width+1), (b.y-5)], [(b.x+brick_width+1), (b.y+brick_height+1)], :pos) + b.collider_left = LinearCollider.new([(b.x-2), (b.y-5)], [(b.x-2), (b.y+brick_height+1)], :neg) + b.collider_top = LinearCollider.new([(b.x-2), (b.y+brick_height+1)], [(b.x+brick_width+1), (b.y+brick_height+1)], :neg) + + # @xyCollision = LinearCollider.new({x: @x,y: @y+@height}, {x: @x+@width, y: @y+@height}) + # @xyCollision2 = LinearCollider.new({x: @x,y: @y}, {x: @x+@width, y: @y}, :pos) + # @xyCollision3 = LinearCollider.new({x: @x,y: @y}, {x: @x, y: @y+@height}) + # @xyCollision4 = LinearCollider.new({x: @x+@width,y: @y}, {x: @x+@width, y: @y+@height}, :pos) + + b.broken = false + + args.state.num_bricks += 1 + end + end + end + end + + def reset_game args + if args.state.ball.xy.y < 20 && args.state.game_over_at.elapsed_time > 60 + #Freeze the ball + args.state.ball.velocity.x = 0 + args.state.ball.velocity.y = 0 + #Freeze the paddle + args.state.paddle.enabled = false + + args.state.game_over_at = args.state.tick_count + end + + if args.state.game_over_at.elapsed_time < 60 && args.state.tick_count > 60 && args.state.bricks.count != 0 + #Display a "Game over" message + args.outputs.labels << [100, 100, "GAME OVER", 10] + end + + #If 60 frames have passed since the game ended, restart the game + if args.state.game_over_at != 0 && args.state.game_over_at.elapsed_time == 60 + # FIXME: only put value types in state + args.state.ball = Ball.new + + # FIXME: only put value types in state + args.state.paddle = Paddle.new + + args.state.bricks = [] + args.state.num_bricks = 0 + end + end + + def calc_collision args + #Remove the brick if it is hit with the ball + ball = args.state.ball + ball_rect = [ball.xy.x, ball.xy.y, 20, 20] + + #Loop through each brick to see if the ball is colliding with it + args.state.bricks.each do |b| + if b.rect.intersect_rect?(ball_rect) + #Run the linear collider for the brick if there is a collision + b[:collider_bottom].update args + b[:collider_right].update args + b[:collider_left].update args + b[:collider_top].update args + + b.broken = true + end + end + + args.state.bricks = args.state.bricks.reject(&:broken) + end + + def win_game args + if args.state.bricks.count == 0 && args.state.game_over_at.elapsed_time > 60 + #Freeze the ball + args.state.ball.velocity.x = 0 + args.state.ball.velocity.y = 0 + #Freeze the paddle + args.state.paddle.enabled = false + + args.state.game_over_at = args.state.tick_count + end + + if args.state.game_over_at.elapsed_time < 60 && args.state.tick_count > 60 && args.state.bricks.count == 0 + #Display a "Game over" message + args.outputs.labels << [100, 100, "CONGRATULATIONS!", 10] + end + end + +end + +def tick args + defaults args + render args + calc args + + #args.outputs.lines << [0, 0, args.grid.w, args.grid.h] + + #$tc+=1 + #if $tc == 5 + #$train << [args.state.ball.xy.x, args.state.ball.xy.y] + #$tc = 0 + #end + #for t in $train + + #args.outputs.solids << [t[0],t[1],5,5,255,0,0]; + #end +end + +</code></pre> +<h3 id='----physics-and-collisions---collision-with-object-removal---paddle.rb'>Physics And Collisions - Collision With Object Removal - paddle.rb</h3> +<pre><code class="language-ruby"># ./samples/04_physics_and_collisions/10_collision_with_object_removal/app/paddle.rb +class Paddle + attr_accessor :enabled + + def initialize () + @x=WIDTH/2 + @y=100 + @width=100 + @height=20 + @speed=10 + + @xyCollision = LinearCollider.new({x: @x,y: @y+@height+5}, {x: @x+@width, y: @y+@height+5}) + @xyCollision2 = LinearCollider.new({x: @x,y: @y}, {x: @x+@width, y: @y}, :pos) + @xyCollision3 = LinearCollider.new({x: @x,y: @y}, {x: @x, y: @y+@height+5}) + @xyCollision4 = LinearCollider.new({x: @x+@width,y: @y}, {x: @x+@width, y: @y+@height+5}, :pos) + + @enabled = true + end + + def update args + @xyCollision.resetPoints({x: @x,y: @y+@height+5}, {x: @x+@width, y: @y+@height+5}) + @xyCollision2.resetPoints({x: @x,y: @y}, {x: @x+@width, y: @y}) + @xyCollision3.resetPoints({x: @x,y: @y}, {x: @x, y: @y+@height+5}) + @xyCollision4.resetPoints({x: @x+@width,y: @y}, {x: @x+@width, y: @y+@height+5}) + + @xyCollision.update args + @xyCollision2.update args + @xyCollision3.update args + @xyCollision4.update args + + args.inputs.keyboard.key_held.left ||= false + args.inputs.keyboard.key_held.right ||= false + + if not (args.inputs.keyboard.key_held.left == args.inputs.keyboard.key_held.right) + if args.inputs.keyboard.key_held.left && @enabled + @x-=@speed + elsif args.inputs.keyboard.key_held.right && @enabled + @x+=@speed + end + end + + xmin =WIDTH/4 + xmax = 3*(WIDTH/4) + @x = (@x+@width > xmax) ? xmax-@width : (@x<xmin) ? xmin : @x; + end + + def render args + args.outputs.solids << [@x,@y,@width,@height,255,0,0]; + end + + def rect + [@x, @y, @width, @height] + end +end + +</code></pre> +<h3 id='----physics-and-collisions---collision-with-object-removal---tests.rb'>Physics And Collisions - Collision With Object Removal - tests.rb</h3> +<pre><code class="language-ruby"># ./samples/04_physics_and_collisions/10_collision_with_object_removal/app/tests.rb +# For advanced users: +# You can put some quick verification tests here, any method +# that starts with the `test_` will be run when you save this file. + +# Here is an example test and game + +# To run the test: ./dragonruby mygame --eval app/tests.rb --no-tick + +class MySuperHappyFunGame + attr_gtk + + def tick + outputs.solids << [100, 100, 300, 300] + end +end + +def test_universe args, assert + game = MySuperHappyFunGame.new + game.args = args + game.tick + assert.true! args.outputs.solids.length == 1, "failure: a solid was not added after tick" + assert.false! 1 == 2, "failure: some how, 1 equals 2, the world is ending" + puts "test_universe completed successfully" +end + +puts "running tests" +$gtk.reset 100 +$gtk.log_level = :off +$gtk.tests.start + +</code></pre> +<h3 id='----physics-and-collisions---collision-with-object-removal---vector2d.rb'>Physics And Collisions - Collision With Object Removal - vector2d.rb</h3> +<pre><code class="language-ruby"># ./samples/04_physics_and_collisions/10_collision_with_object_removal/app/vector2d.rb + +class Vector2d + attr_accessor :x, :y + + def initialize x=0, y=0 + @x=x + @y=y + end + + #returns a vector multiplied by scalar x + #x [float] scalar + def mult x + r = Vector2d.new(0,0) + r.x=@x*x + r.y=@y*x + r + end + + # vect [Vector2d] vector to copy + def copy vect + Vector2d.new(@x, @y) + end + + #returns a new vector equivalent to this+vect + #vect [Vector2d] vector to add to self + def add vect + Vector2d.new(@x+vect.x,@y+vect.y) + end + + #returns a new vector equivalent to this-vect + #vect [Vector2d] vector to subtract to self + def sub vect + Vector2d.new(@x-vect.c, @y-vect.y) + end + + #return the magnitude of the vector + def mag + ((@x**2)+(@y**2))**0.5 + end + + #returns a new normalize version of the vector + def normalize + Vector2d.new(@x/mag, @y/mag) + end + + #TODO delet? + def distABS vect + (((vect.x-@x)**2+(vect.y-@y)**2)**0.5).abs() + end +end + +</code></pre> +<h3 id='----mouse---mouse-click---main.rb'>Mouse - Mouse Click - main.rb</h3> +<pre><code class="language-ruby"># ./samples/05_mouse/01_mouse_click/app/main.rb =begin APIs listing that haven't been encountered in previous sample apps: @@ -6969,9 +10508,9 @@ def tick_instructions args, text, y = 715 args.outputs.debug << [640, y - 25, "(click to dismiss instructions)" , -2, 1, 255, 255, 255].label end -</pre> -<h1 id='--mouse---mouse-move---main.rb'>Mouse - Mouse Move - main.rb</h1> -<pre># ./samples/05_mouse/02_mouse_move/app/main.rb +</code></pre> +<h3 id='----mouse---mouse-move---main.rb'>Mouse - Mouse Move - main.rb</h3> +<pre><code class="language-ruby"># ./samples/05_mouse/02_mouse_move/app/main.rb =begin Reminders: @@ -7269,9 +10808,9 @@ def tick_instructions args, text, y = 715 args.outputs.debug << [640, y - 25, "(click to dismiss instructions)" , -2, 1, 255, 255, 255].label end -</pre> -<h1 id='--mouse---mouse-move-paint-app---main.rb'>Mouse - Mouse Move Paint App - main.rb</h1> -<pre># ./samples/05_mouse/03_mouse_move_paint_app/app/main.rb +</code></pre> +<h3 id='----mouse---mouse-move-paint-app---main.rb'>Mouse - Mouse Move Paint App - main.rb</h3> +<pre><code class="language-ruby"># ./samples/05_mouse/03_mouse_move_paint_app/app/main.rb =begin APIs listing that haven't been encountered in previous sample apps: @@ -7513,9 +11052,9 @@ def tick_instructions args, text, y = 715 args.outputs.debug << [640, y - 25, "(click to dismiss instructions)" , -2, 1, 255, 255, 255].label end -</pre> -<h1 id='--mouse---coordinate-systems---main.rb'>Mouse - Coordinate Systems - main.rb</h1> -<pre># ./samples/05_mouse/04_coordinate_systems/app/main.rb +</code></pre> +<h3 id='----mouse---coordinate-systems---main.rb'>Mouse - Coordinate Systems - main.rb</h3> +<pre><code class="language-ruby"># ./samples/05_mouse/04_coordinate_systems/app/main.rb =begin APIs listing that haven't been encountered in previous sample apps: @@ -7597,9 +11136,9 @@ def tick_instructions args, text, y = 715 args.outputs.debug << [640, y - 25, "(click to dismiss instructions)" , -2, 1, 255, 255, 255].label end -</pre> -<h1 id='--save-load---save-load-game---main.rb'>Save Load - Save Load Game - main.rb</h1> -<pre># ./samples/06_save_load/01_save_load_game/app/main.rb +</code></pre> +<h3 id='----save-load---save-load-game---main.rb'>Save Load - Save Load Game - main.rb</h3> +<pre><code class="language-ruby"># ./samples/06_save_load/01_save_load_game/app/main.rb =begin APIs listing that haven't been encountered in previous sample apps: @@ -7990,9 +11529,9 @@ def tick args $text_based_rpg.tick end -</pre> -<h1 id='--advanced-rendering---simple-render-targets---main.rb'>Advanced Rendering - Simple Render Targets - main.rb</h1> -<pre># ./samples/07_advanced_rendering/01_simple_render_targets/app/main.rb +</code></pre> +<h3 id='----advanced-rendering---simple-render-targets---main.rb'>Advanced Rendering - Simple Render Targets - main.rb</h3> +<pre><code class="language-ruby"># ./samples/07_advanced_rendering/01_simple_render_targets/app/main.rb def tick args # args.outputs.render_targets are really really powerful. # They essentially allow you to create a sprite programmatically and cache the result. @@ -8046,9 +11585,9 @@ end $gtk.reset -</pre> -<h1 id='--advanced-rendering---render-targets-with-tile-manipulation---main.rb'>Advanced Rendering - Render Targets With Tile Manipulation - main.rb</h1> -<pre># ./samples/07_advanced_rendering/02_render_targets_with_tile_manipulation/app/main.rb +</code></pre> +<h3 id='----advanced-rendering---render-targets-with-tile-manipulation---main.rb'>Advanced Rendering - Render Targets With Tile Manipulation - main.rb</h3> +<pre><code class="language-ruby"># ./samples/07_advanced_rendering/02_render_targets_with_tile_manipulation/app/main.rb # This sample is meant to show you how to do that dripping transition thing # at the start of the original Doom. Most of this file is here to animate # a scene to wipe away; the actual wipe effect is in the last 20 lines or @@ -8145,9 +11684,9 @@ def tick_instructions args, text, y = 715 args.outputs.debug << [640, y - 25, "(click to dismiss instructions)" , -2, 1, 255, 255, 255].label end -</pre> -<h1 id='--advanced-rendering---render-target-viewports---main.rb'>Advanced Rendering - Render Target Viewports - main.rb</h1> -<pre># ./samples/07_advanced_rendering/03_render_target_viewports/app/main.rb +</code></pre> +<h3 id='----advanced-rendering---render-target-viewports---main.rb'>Advanced Rendering - Render Target Viewports - main.rb</h3> +<pre><code class="language-ruby"># ./samples/07_advanced_rendering/03_render_target_viewports/app/main.rb =begin APIs listing that haven't been encountered in previous sample apps: @@ -8617,9 +12156,9 @@ def tick_instructions args, text, y = 715 args.outputs.debug << [640, y - 25, "(click to dismiss instructions)" , -2, 1, 255, 255, 255].label end -</pre> -<h1 id='--advanced-rendering---render-primitive-hierarchies---main.rb'>Advanced Rendering - Render Primitive Hierarchies - main.rb</h1> -<pre># ./samples/07_advanced_rendering/04_render_primitive_hierarchies/app/main.rb +</code></pre> +<h3 id='----advanced-rendering---render-primitive-hierarchies---main.rb'>Advanced Rendering - Render Primitive Hierarchies - main.rb</h3> +<pre><code class="language-ruby"># ./samples/07_advanced_rendering/04_render_primitive_hierarchies/app/main.rb =begin APIs listing that haven't been encountered in previous sample apps: @@ -8793,9 +12332,9 @@ def collection_of_sprites args ] end -</pre> -<h1 id='--advanced-rendering---render-primitives-as-hash---main.rb'>Advanced Rendering - Render Primitives As Hash - main.rb</h1> -<pre># ./samples/07_advanced_rendering/05_render_primitives_as_hash/app/main.rb +</code></pre> +<h3 id='----advanced-rendering---render-primitives-as-hash---main.rb'>Advanced Rendering - Render Primitives As Hash - main.rb</h3> +<pre><code class="language-ruby"># ./samples/07_advanced_rendering/05_render_primitives_as_hash/app/main.rb =begin Reminders: @@ -8988,9 +12527,9 @@ def tick args }.line end -</pre> -<h1 id='--advanced-rendering---pixel-arrays---main.rb'>Advanced Rendering - Pixel Arrays - main.rb</h1> -<pre># ./samples/07_advanced_rendering/06_pixel_arrays/app/main.rb +</code></pre> +<h3 id='----advanced-rendering---pixel-arrays---main.rb'>Advanced Rendering - Pixel Arrays - main.rb</h3> +<pre><code class="language-ruby"># ./samples/07_advanced_rendering/06_pixel_arrays/app/main.rb $gtk.reset def tick args @@ -9033,9 +12572,518 @@ def tick args end -</pre> -<h1 id='--tweening-lerping-easing-functions---easing-functions---main.rb'>Tweening Lerping Easing Functions - Easing Functions - main.rb</h1> -<pre># ./samples/08_tweening_lerping_easing_functions/01_easing_functions/app/main.rb +</code></pre> +<h3 id='----advanced-rendering---splitscreen-camera---main.rb'>Advanced Rendering - Splitscreen Camera - main.rb</h3> +<pre><code class="language-ruby"># ./samples/07_advanced_rendering/07_splitscreen_camera/app/main.rb +class CameraMovement + attr_accessor :state, :inputs, :outputs, :grid + + #============================================================================================== + #Serialize + def serialize + {state: state, inputs: inputs, outputs: outputs, grid: grid } + end + + def inspect + serialize.to_s + end + + def to_s + serialize.to_s + end + + #============================================================================================== + #Tick + def tick + defaults + calc + render + input + end + + #============================================================================================== + #Default functions + def defaults + outputs[:scene].background_color = [0,0,0] + state.trauma ||= 0.0 + state.trauma_power ||= 2 + state.player_cyan ||= new_player_cyan + state.player_magenta ||= new_player_magenta + state.camera_magenta ||= new_camera_magenta + state.camera_cyan ||= new_camera_cyan + state.camera_center ||= new_camera_center + state.room ||= new_room + end + + def default_player x, y, w, h, sprite_path + state.new_entity(:player, + { x: x, + y: y, + dy: 0, + dx: 0, + w: w, + h: h, + damage: 0, + dead: false, + orientation: "down", + max_alpha: 255, + sprite_path: sprite_path}) + end + + def default_floor_tile x, y, w, h, sprite_path + state.new_entity(:room, + { x: x, + y: y, + w: w, + h: h, + sprite_path: sprite_path}) + end + + def default_camera x, y, w, h + state.new_entity(:camera, + { x: x, + y: y, + dx: 0, + dy: 0, + w: w, + h: h}) + end + + def new_player_cyan + default_player(0, 0, 64, 64, + "sprites/player/player_#{state.player_cyan.orientation}_standing.png") + end + + def new_player_magenta + default_player(64, 0, 64, 64, + "sprites/player/player_#{state.player_magenta.orientation}_standing.png") + end + + def new_camera_magenta + default_camera(0,0,720,720) + end + + def new_camera_cyan + default_camera(0,0,720,720) + end + + def new_camera_center + default_camera(0,0,1280,720) + end + + def new_room + default_floor_tile(0,0,1024,1024,'sprites/rooms/camera_room.png') + end + + #============================================================================================== + #Calculation functions + def calc + calc_camera_magenta + calc_camera_cyan + calc_camera_center + calc_player_cyan + calc_player_magenta + calc_trauma_decay + end + + def center_camera_tolerance + return Math.sqrt(((state.player_magenta.x - state.player_cyan.x) ** 2) + + ((state.player_magenta.y - state.player_cyan.y) ** 2)) > 640 + end + + def calc_player_cyan + state.player_cyan.x += state.player_cyan.dx + state.player_cyan.y += state.player_cyan.dy + end + + def calc_player_magenta + state.player_magenta.x += state.player_magenta.dx + state.player_magenta.y += state.player_magenta.dy + end + + def calc_camera_center + timeScale = 1 + midX = (state.player_magenta.x + state.player_cyan.x)/2 + midY = (state.player_magenta.y + state.player_cyan.y)/2 + targetX = midX - state.camera_center.w/2 + targetY = midY - state.camera_center.h/2 + state.camera_center.x += (targetX - state.camera_center.x) * 0.1 * timeScale + state.camera_center.y += (targetY - state.camera_center.y) * 0.1 * timeScale + end + + + def calc_camera_magenta + timeScale = 1 + targetX = state.player_magenta.x + state.player_magenta.w - state.camera_magenta.w/2 + targetY = state.player_magenta.y + state.player_magenta.h - state.camera_magenta.h/2 + state.camera_magenta.x += (targetX - state.camera_magenta.x) * 0.1 * timeScale + state.camera_magenta.y += (targetY - state.camera_magenta.y) * 0.1 * timeScale + end + + def calc_camera_cyan + timeScale = 1 + targetX = state.player_cyan.x + state.player_cyan.w - state.camera_cyan.w/2 + targetY = state.player_cyan.y + state.player_cyan.h - state.camera_cyan.h/2 + state.camera_cyan.x += (targetX - state.camera_cyan.x) * 0.1 * timeScale + state.camera_cyan.y += (targetY - state.camera_cyan.y) * 0.1 * timeScale + end + + def calc_player_quadrant angle + if angle < 45 and angle > -45 and state.player_cyan.x < state.player_magenta.x + return 1 + elsif angle < 45 and angle > -45 and state.player_cyan.x > state.player_magenta.x + return 3 + elsif (angle > 45 or angle < -45) and state.player_cyan.y < state.player_magenta.y + return 2 + elsif (angle > 45 or angle < -45) and state.player_cyan.y > state.player_magenta.y + return 4 + end + end + + def calc_camera_shake + state.trauma + end + + def calc_trauma_decay + state.trauma = state.trauma * 0.9 + end + + def calc_random_float_range(min, max) + rand * (max-min) + min + end + + #============================================================================================== + #Render Functions + def render + render_floor + render_player_cyan + render_player_magenta + if center_camera_tolerance + render_split_camera_scene + else + render_camera_center_scene + end + end + + def render_player_cyan + outputs[:scene].sprites << {x: state.player_cyan.x, + y: state.player_cyan.y, + w: state.player_cyan.w, + h: state.player_cyan.h, + path: "sprites/player/player_#{state.player_cyan.orientation}_standing.png", + r: 0, + g: 255, + b: 255} + end + + def render_player_magenta + outputs[:scene].sprites << {x: state.player_magenta.x, + y: state.player_magenta.y, + w: state.player_magenta.w, + h: state.player_magenta.h, + path: "sprites/player/player_#{state.player_magenta.orientation}_standing.png", + r: 255, + g: 0, + b: 255} + end + + def render_floor + outputs[:scene].sprites << [state.room.x, state.room.y, + state.room.w, state.room.h, + state.room.sprite_path] + end + + def render_camera_center_scene + zoomFactor = 1 + outputs[:scene].width = state.room.w + outputs[:scene].height = state.room.h + + maxAngle = 10.0 + maxOffset = 20.0 + angle = maxAngle * calc_camera_shake * calc_random_float_range(-1,1) + offsetX = 32 - (maxOffset * calc_camera_shake * calc_random_float_range(-1,1)) + offsetY = 32 - (maxOffset * calc_camera_shake * calc_random_float_range(-1,1)) + + outputs.sprites << {x: (-state.camera_center.x - offsetX)/zoomFactor, + y: (-state.camera_center.y - offsetY)/zoomFactor, + w: outputs[:scene].width/zoomFactor, + h: outputs[:scene].height/zoomFactor, + path: :scene, + angle: angle, + source_w: -1, + source_h: -1} + outputs.labels << [128,64,"#{state.trauma.round(1)}",8,2,255,0,255,255] + end + + def render_split_camera_scene + outputs[:scene].width = state.room.w + outputs[:scene].height = state.room.h + render_camera_magenta_scene + render_camera_cyan_scene + + angle = Math.atan((state.player_magenta.y - state.player_cyan.y)/(state.player_magenta.x- state.player_cyan.x)) * 180/Math::PI + output_split_camera angle + + end + + def render_camera_magenta_scene + zoomFactor = 1 + offsetX = 32 + offsetY = 32 + + outputs[:scene_magenta].sprites << {x: (-state.camera_magenta.x*2), + y: (-state.camera_magenta.y), + w: outputs[:scene].width*2, + h: outputs[:scene].height, + path: :scene} + + end + + def render_camera_cyan_scene + zoomFactor = 1 + offsetX = 32 + offsetY = 32 + outputs[:scene_cyan].sprites << {x: (-state.camera_cyan.x*2), + y: (-state.camera_cyan.y), + w: outputs[:scene].width*2, + h: outputs[:scene].height, + path: :scene} + end + + def output_split_camera angle + #TODO: Clean this up! + quadrant = calc_player_quadrant angle + outputs.labels << [128,64,"#{quadrant}",8,2,255,0,255,255] + if quadrant == 1 + set_camera_attributes(w: 640, h: 720, m_x: 640, m_y: 0, c_x: 0, c_y: 0) + + elsif quadrant == 2 + set_camera_attributes(w: 1280, h: 360, m_x: 0, m_y: 360, c_x: 0, c_y: 0) + + elsif quadrant == 3 + set_camera_attributes(w: 640, h: 720, m_x: 0, m_y: 0, c_x: 640, c_y: 0) + + elsif quadrant == 4 + set_camera_attributes(w: 1280, h: 360, m_x: 0, m_y: 0, c_x: 0, c_y: 360) + + end + end + + def set_camera_attributes(w: 0, h: 0, m_x: 0, m_y: 0, c_x: 0, c_y: 0) + state.camera_cyan.w = w + 64 + state.camera_cyan.h = h + 64 + outputs[:scene_cyan].width = (w) * 2 + outputs[:scene_cyan].height = h + + state.camera_magenta.w = w + 64 + state.camera_magenta.h = h + 64 + outputs[:scene_magenta].width = (w) * 2 + outputs[:scene_magenta].height = h + outputs.sprites << {x: m_x, + y: m_y, + w: w, + h: h, + path: :scene_magenta} + outputs.sprites << {x: c_x, + y: c_y, + w: w, + h: h, + path: :scene_cyan} + end + + def add_trauma amount + state.trauma = [state.trauma + amount, 1.0].min + end + + def remove_trauma amount + state.trauma = [state.trauma - amount, 0.0].max + end + #============================================================================================== + #Input functions + def input + input_move_cyan + input_move_magenta + + if inputs.keyboard.key_down.t + add_trauma(0.5) + elsif inputs.keyboard.key_down.y + remove_trauma(0.1) + end + end + + def input_move_cyan + if inputs.keyboard.key_held.up + state.player_cyan.dy = 5 + state.player_cyan.orientation = "up" + elsif inputs.keyboard.key_held.down + state.player_cyan.dy = -5 + state.player_cyan.orientation = "down" + else + state.player_cyan.dy *= 0.8 + end + if inputs.keyboard.key_held.left + state.player_cyan.dx = -5 + state.player_cyan.orientation = "left" + elsif inputs.keyboard.key_held.right + state.player_cyan.dx = 5 + state.player_cyan.orientation = "right" + else + state.player_cyan.dx *= 0.8 + end + + outputs.labels << [128,512,"#{state.player_cyan.x.round()}",8,2,0,255,255,255] + outputs.labels << [128,480,"#{state.player_cyan.y.round()}",8,2,0,255,255,255] + end + + def input_move_magenta + if inputs.keyboard.key_held.w + state.player_magenta.dy = 5 + state.player_magenta.orientation = "up" + elsif inputs.keyboard.key_held.s + state.player_magenta.dy = -5 + state.player_magenta.orientation = "down" + else + state.player_magenta.dy *= 0.8 + end + if inputs.keyboard.key_held.a + state.player_magenta.dx = -5 + state.player_magenta.orientation = "left" + elsif inputs.keyboard.key_held.d + state.player_magenta.dx = 5 + state.player_magenta.orientation = "right" + else + state.player_magenta.dx *= 0.8 + end + + outputs.labels << [128,360,"#{state.player_magenta.x.round()}",8,2,255,0,255,255] + outputs.labels << [128,328,"#{state.player_magenta.y.round()}",8,2,255,0,255,255] + end +end + +$camera_movement = CameraMovement.new + +def tick args + args.outputs.background_color = [0,0,0] + $camera_movement.inputs = args.inputs + $camera_movement.outputs = args.outputs + $camera_movement.state = args.state + $camera_movement.grid = args.grid + $camera_movement.tick +end + +</code></pre> +<h3 id='----advanced-rendering---z-targeting-camera---main.rb'>Advanced Rendering - Z Targeting Camera - main.rb</h3> +<pre><code class="language-ruby"># ./samples/07_advanced_rendering/08_z_targeting_camera/app/main.rb +class Game + attr_gtk + + def tick + defaults + render + input + calc + end + + def defaults + outputs.background_color = [219, 208, 191] + player.x ||= 634 + player.y ||= 153 + player.angle ||= 90 + player.distance ||= arena_radius + target.x ||= 634 + target.y ||= 359 + end + + def render + outputs[:scene].sprites << ([0, 0, 933, 700, 'sprites/arena.png'].center_inside_rect grid.rect) + outputs[:scene].sprites << target_sprite + outputs[:scene].sprites << player_sprite + outputs.sprites << scene + end + + def target_sprite + { + x: target.x, y: target.y, + w: 10, h: 10, + path: 'sprites/square/black.png' + }.anchor_rect 0.5, 0.5 + end + + def input + if inputs.up && player.distance > 30 + player.distance -= 2 + elsif inputs.down && player.distance < 200 + player.distance += 2 + end + + player.angle += inputs.left_right * -1 + end + + def calc + player.x = target.x + ((player.angle * 1).vector_x player.distance) + player.y = target.y + ((player.angle * -1).vector_y player.distance) + end + + def player_sprite + { + x: player.x, + y: player.y, + w: 50, + h: 100, + path: 'sprites/player.png', + angle: (player.angle * -1) + 90 + }.anchor_rect 0.5, 0 + end + + def center_map + { x: 634, y: 359 } + end + + def zoom_factor_single + 2 - ((args.geometry.distance player, center_map).fdiv arena_radius) + end + + def zoom_factor + zoom_factor_single ** 2 + end + + def arena_radius + 206 + end + + def scene + { + x: (640 - player.x) + (640 - (640 * zoom_factor)), + y: (360 - player.y - (75 * zoom_factor)) + (320 - (320 * zoom_factor)), + w: 1280 * zoom_factor, + h: 720 * zoom_factor, + path: :scene, + angle: player.angle - 90, + angle_anchor_x: (player.x.fdiv 1280), + angle_anchor_y: (player.y.fdiv 720) + } + end + + def player + state.player + end + + def target + state.target + end +end + +def tick args + $game ||= Game.new + $game.args = args + $game.tick +end + +$gtk.reset + +</code></pre> +<h3 id='----tweening-lerping-easing-functions---easing-functions---main.rb'>Tweening Lerping Easing Functions - Easing Functions - main.rb</h3> +<pre><code class="language-ruby"># ./samples/08_tweening_lerping_easing_functions/01_easing_functions/app/main.rb def tick args # STOP! Watch the following presentation first!!!! # Math for Game Programmers: Fast and Funky 1D Nonlinear Transformations @@ -9169,9 +13217,9 @@ module Easing end end -</pre> -<h1 id='--tweening-lerping-easing-functions---cubic-bezier---main.rb'>Tweening Lerping Easing Functions - Cubic Bezier - main.rb</h1> -<pre># ./samples/08_tweening_lerping_easing_functions/02_cubic_bezier/app/main.rb +</code></pre> +<h3 id='----tweening-lerping-easing-functions---cubic-bezier---main.rb'>Tweening Lerping Easing Functions - Cubic Bezier - main.rb</h3> +<pre><code class="language-ruby"># ./samples/08_tweening_lerping_easing_functions/02_cubic_bezier/app/main.rb def tick args args.outputs.background_color = [33, 33, 33] args.outputs.lines << bezier(100, 100, @@ -9234,9 +13282,9 @@ def pow n, to n ** to end -</pre> -<h1 id='--tweening-lerping-easing-functions---easing-using-spline---main.rb'>Tweening Lerping Easing Functions - Easing Using Spline - main.rb</h1> -<pre># ./samples/08_tweening_lerping_easing_functions/03_easing_using_spline/app/main.rb +</code></pre> +<h3 id='----tweening-lerping-easing-functions---easing-using-spline---main.rb'>Tweening Lerping Easing Functions - Easing Using Spline - main.rb</h3> +<pre><code class="language-ruby"># ./samples/08_tweening_lerping_easing_functions/03_easing_using_spline/app/main.rb def tick args args.state.duration = 10.seconds args.state.spline = [ @@ -9256,9 +13304,9 @@ def tick args "perc: #{"%.2f" % (args.state.simulation_tick / args.state.duration)} t: #{args.state.simulation_tick}"] end -</pre> -<h1 id='--tweening-lerping-easing-functions---parametric-enemy-movement---main.rb'>Tweening Lerping Easing Functions - Parametric Enemy Movement - main.rb</h1> -<pre># ./samples/08_tweening_lerping_easing_functions/04_parametric_enemy_movement/app/main.rb +</code></pre> +<h3 id='----tweening-lerping-easing-functions---parametric-enemy-movement---main.rb'>Tweening Lerping Easing Functions - Parametric Enemy Movement - main.rb</h3> +<pre><code class="language-ruby"># ./samples/08_tweening_lerping_easing_functions/04_parametric_enemy_movement/app/main.rb def new_star args { x: 1280.randomize(:ratio), starting_y: 800, @@ -9473,9 +13521,9 @@ def tick args render args end -</pre> -<h1 id='--performance---sprites-as-hash---main.rb'>Performance - Sprites As Hash - main.rb</h1> -<pre># ./samples/09_performance/01_sprites_as_hash/app/main.rb +</code></pre> +<h3 id='----performance---sprites-as-hash---main.rb'>Performance - Sprites As Hash - main.rb</h3> +<pre><code class="language-ruby"># ./samples/09_performance/01_sprites_as_hash/app/main.rb # Sprites represented as Hashes using the queue ~args.outputs.sprites~ # code up, but are the "slowest" to render. @@ -9541,9 +13589,9 @@ def reset_with count: count $gtk.args.state.star_count = count end -</pre> -<h1 id='--performance---sprites-as-entities---main.rb'>Performance - Sprites As Entities - main.rb</h1> -<pre># ./samples/09_performance/02_sprites_as_entities/app/main.rb +</code></pre> +<h3 id='----performance---sprites-as-entities---main.rb'>Performance - Sprites As Entities - main.rb</h3> +<pre><code class="language-ruby"># ./samples/09_performance/02_sprites_as_entities/app/main.rb # Sprites represented as Entities using the queue ~args.outputs.sprites~ # yields nicer access apis over Hashes, but require a bit more code upfront. # The hash sample has to use star[:s] to get the speed of the star, but @@ -9609,9 +13657,9 @@ def reset_with count: count $gtk.args.state.star_count = count end -</pre> -<h1 id='--performance---sprites-as-strict-entities---main.rb'>Performance - Sprites As Strict Entities - main.rb</h1> -<pre># ./samples/09_performance/03_sprites_as_strict_entities/app/main.rb +</code></pre> +<h3 id='----performance---sprites-as-strict-entities---main.rb'>Performance - Sprites As Strict Entities - main.rb</h3> +<pre><code class="language-ruby"># ./samples/09_performance/03_sprites_as_strict_entities/app/main.rb # Sprites represented as StrictEntities using the queue ~args.outputs.sprites~ # yields apis access similar to Entities, but all properties that can be set on the # entity must be predefined with a default value. Strict entities do not support the @@ -9681,9 +13729,9 @@ def reset_with count: count $gtk.args.state.star_count = count end -</pre> -<h1 id='--performance---sprites-as-classes---main.rb'>Performance - Sprites As Classes - main.rb</h1> -<pre># ./samples/09_performance/04_sprites_as_classes/app/main.rb +</code></pre> +<h3 id='----performance---sprites-as-classes---main.rb'>Performance - Sprites As Classes - main.rb</h3> +<pre><code class="language-ruby"># ./samples/09_performance/04_sprites_as_classes/app/main.rb # Sprites represented as Classes using the queue ~args.outputs.sprites~. # gives you full control of property declaration and method invocation. # They are more performant than OpenEntities and StrictEntities, but more code upfront. @@ -9735,9 +13783,9 @@ def reset_with count: count $gtk.args.state.star_count = count end -</pre> -<h1 id='--performance---static-sprites-as-classes---main.rb'>Performance - Static Sprites As Classes - main.rb</h1> -<pre># ./samples/09_performance/05_static_sprites_as_classes/app/main.rb +</code></pre> +<h3 id='----performance---static-sprites-as-classes---main.rb'>Performance - Static Sprites As Classes - main.rb</h3> +<pre><code class="language-ruby"># ./samples/09_performance/05_static_sprites_as_classes/app/main.rb # Sprites represented as Classes using the queue ~args.outputs.static_sprites~. # bypasses the queue behavior of ~args.outputs.sprites~. All instances are held # by reference. You get better performance, but you are mutating state of held objects @@ -9790,9 +13838,9 @@ def reset_with count: count $gtk.args.state.star_count = count end -</pre> -<h1 id='--performance---static-sprites-as-classes-with-custom-drawing---main.rb'>Performance - Static Sprites As Classes With Custom Drawing - main.rb</h1> -<pre># ./samples/09_performance/06_static_sprites_as_classes_with_custom_drawing/app/main.rb +</code></pre> +<h3 id='----performance---static-sprites-as-classes-with-custom-drawing---main.rb'>Performance - Static Sprites As Classes With Custom Drawing - main.rb</h3> +<pre><code class="language-ruby"># ./samples/09_performance/06_static_sprites_as_classes_with_custom_drawing/app/main.rb # Sprites represented as Classes, with a draw_override method, and using the queue ~args.outputs.static_sprites~. # is the fastest approach. This is comparable to what other game engines set as the default behavior. # There are tradeoffs for all this speed if the creation of a full blown class, and bypassing @@ -9866,9 +13914,9 @@ def reset_with count: count $gtk.args.state.star_count = count end -</pre> -<h1 id='--performance---collision-limits---main.rb'>Performance - Collision Limits - main.rb</h1> -<pre># ./samples/09_performance/07_collision_limits/app/main.rb +</code></pre> +<h3 id='----performance---collision-limits---main.rb'>Performance - Collision Limits - main.rb</h3> +<pre><code class="language-ruby"># ./samples/09_performance/07_collision_limits/app/main.rb =begin Reminders: @@ -9925,9 +13973,9 @@ end # Resets the game. $gtk.reset -</pre> -<h1 id='--advanced-debugging---trace-debugging---main.rb'>Advanced Debugging - Trace Debugging - main.rb</h1> -<pre># ./samples/10_advanced_debugging/01_trace_debugging/app/main.rb +</code></pre> +<h3 id='----advanced-debugging---trace-debugging---main.rb'>Advanced Debugging - Trace Debugging - main.rb</h3> +<pre><code class="language-ruby"># ./samples/10_advanced_debugging/01_trace_debugging/app/main.rb class Game attr_gtk @@ -9982,9 +14030,9 @@ def tick args $game.tick end -</pre> -<h1 id='--advanced-debugging---trace-debugging-classes---main.rb'>Advanced Debugging - Trace Debugging Classes - main.rb</h1> -<pre># ./samples/10_advanced_debugging/02_trace_debugging_classes/app/main.rb +</code></pre> +<h3 id='----advanced-debugging---trace-debugging-classes---main.rb'>Advanced Debugging - Trace Debugging Classes - main.rb</h3> +<pre><code class="language-ruby"># ./samples/10_advanced_debugging/02_trace_debugging_classes/app/main.rb class Foobar def initialize trace! # Trace is added to the constructor. @@ -10008,9 +14056,9 @@ def tick args args.state.foobar.clicky args end -</pre> -<h1 id='--advanced-debugging---unit-tests---exception_raising_tests.rb'>Advanced Debugging - Unit Tests - exception_raising_tests.rb</h1> -<pre># ./samples/10_advanced_debugging/03_unit_tests/exception_raising_tests.rb +</code></pre> +<h3 id='----advanced-debugging---unit-tests---exception_raising_tests.rb'>Advanced Debugging - Unit Tests - exception_raising_tests.rb</h3> +<pre><code class="language-ruby"># ./samples/10_advanced_debugging/03_unit_tests/exception_raising_tests.rb begin :shared class ExceptionalClass def initialize exception_to_throw = nil @@ -10028,20 +14076,202 @@ def test_exception_in_newing_object args, assert end end -puts "running tests" $gtk.reset 100 $gtk.log_level = :off -$gtk.tests.start -</pre> -<h1 id='--advanced-debugging---unit-tests---gen_docs.rb'>Advanced Debugging - Unit Tests - gen_docs.rb</h1> -<pre># ./samples/10_advanced_debugging/03_unit_tests/gen_docs.rb -# sh ./amir-build-and-run.sh --eval samples/99_zz_gtk_unit_tests/gen_docs.rb --no-tick +</code></pre> +<h3 id='----advanced-debugging---unit-tests---fn_tests.rb'>Advanced Debugging - Unit Tests - fn_tests.rb</h3> +<pre><code class="language-ruby"># ./samples/10_advanced_debugging/03_unit_tests/fn_tests.rb +def infinity + 1 / 0 +end + +def neg_infinity + -1 / 0 +end + +def nan + 0.0 / 0 +end + +def test_add args, assert + assert.equal! (args.fn.add), 0 + assert.equal! (args.fn.+), 0 + assert.equal! (args.fn.+ 1, 2, 3), 6 + assert.equal! (args.fn.+ 0), 0 + assert.equal! (args.fn.+ 0, nil), 0 + assert.equal! (args.fn.+ 0, nan), nil + assert.equal! (args.fn.+ 0, nil, infinity), nil + assert.equal! (args.fn.+ [1, 2, 3, [4, 5, 6]]), 21 + assert.equal! (args.fn.+ [nil, [4, 5, 6]]), 15 +end + +def test_sub args, assert + neg_infinity = infinity * -1 + assert.equal! (args.fn.+), 0 + assert.equal! (args.fn.- 1, 2, 3), -4 + assert.equal! (args.fn.- 4), -4 + assert.equal! (args.fn.- 4, nan), nil + assert.equal! (args.fn.- 0, nil), 0 + assert.equal! (args.fn.- 0, nil, infinity), nil + assert.equal! (args.fn.- [0, 1, 2, 3, [4, 5, 6]]), -21 + assert.equal! (args.fn.- [nil, 0, [4, 5, 6]]), -15 +end + +def test_div args, assert + assert.equal! (args.fn.div), 1 + assert.equal! (args.fn./), 1 + assert.equal! (args.fn./ 6, 3), 2 + assert.equal! (args.fn./ 6, infinity), nil + assert.equal! (args.fn./ 6, nan), nil + assert.equal! (args.fn./ infinity), nil + assert.equal! (args.fn./ 0), nil + assert.equal! (args.fn./ 6, [3]), 2 +end + +def test_idiv args, assert + assert.equal! (args.fn.idiv), 1 + assert.equal! (args.fn.idiv 7, 3), 2 + assert.equal! (args.fn.idiv 6, infinity), nil + assert.equal! (args.fn.idiv 6, nan), nil + assert.equal! (args.fn.idiv infinity), nil + assert.equal! (args.fn.idiv 0), nil + assert.equal! (args.fn.idiv 7, [3]), 2 +end + +def test_mul args, assert + assert.equal! (args.fn.mul), 1 + assert.equal! (args.fn.*), 1 + assert.equal! (args.fn.* 7, 3), 21 + assert.equal! (args.fn.* 6, nan), nil + assert.equal! (args.fn.* 6, infinity), nil + assert.equal! (args.fn.* infinity), nil + assert.equal! (args.fn.* 0), 0 + assert.equal! (args.fn.* 7, [3]), 21 +end + +def test_lt args, assert + assert.equal! (args.fn.lt 1), 1 + assert.equal! (args.fn.lt), nil + assert.equal! (args.fn.lt infinity), nil + assert.equal! (args.fn.lt nan), nil + assert.equal! (args.fn.lt 10, 9, 8), 8 + assert.equal! (args.fn.< 10, 9, 8), 8 + assert.equal! (args.fn.< [10, 9, [8]]), 8 + assert.equal! (args.fn.< 10, 10), nil +end + +def test_lte args, assert + assert.equal! (args.fn.lte 1), 1 + assert.equal! (args.fn.lte), nil + assert.equal! (args.fn.lte infinity), nil + assert.equal! (args.fn.lte nan), nil + assert.equal! (args.fn.lte 10, 9, 8), 8 + assert.equal! (args.fn.lte 10, 10), 10 + assert.equal! (args.fn.lte 10, 9, [8]), 8 + assert.equal! (args.fn.<= 10, 9, 8), 8 +end + +def test_gt args, assert + assert.equal! (args.fn.gt 1), 1 + assert.equal! (args.fn.gt), nil + assert.equal! (args.fn.gt infinity), nil + assert.equal! (args.fn.gt nan), nil + assert.equal! (args.fn.gt 8, 9, 10), 10 + assert.equal! (args.fn.gt [8, 9, [10]]), 10 + assert.equal! (args.fn.gt 10, 10), nil + assert.equal! (args.fn.gt 10, 10), nil + assert.equal! (args.fn.gt 10, 9), nil + assert.equal! (args.fn.> 8, 9, 10), 10 +end + +def test_gte args, assert + assert.equal! (args.fn.gte 1), 1 + assert.equal! (args.fn.gte), nil + assert.equal! (args.fn.gte infinity), nil + assert.equal! (args.fn.gte nan), nil + assert.equal! (args.fn.gte 8, 9, 10), 10 + assert.equal! (args.fn.gte 10, 10), 10 + assert.equal! (args.fn.gte 8, 9, [10]), 10 + assert.equal! (args.fn.gte 10, 9), nil + assert.equal! (args.fn.>= 8, 9, 10), 10 +end + + +def test_acopy args, assert + orig = [1, 2, 3] + clone = args.fn.acopy orig + assert.equal! clone, [1, 2, 3] + assert.equal! clone, orig + assert.not_equal! clone.object_id, orig.object_id +end + +def test_aget args, assert + assert.equal! (args.fn.aget [:a, :b, :c], 1), :b + assert.equal! (args.fn.aget [:a, :b, :c], nil), nil + assert.equal! (args.fn.aget nil, 1), nil +end + +def test_alength args, assert + assert.equal! (args.fn.alength [:a, :b, :c]), 3 + assert.equal! (args.fn.alength nil), nil +end + +def test_amap args, assert + inc = lambda { |i| i + 1 } + ary = [1, 2, 3] + assert.equal! (args.fn.amap ary, inc), [2, 3, 4] + assert.equal! (args.fn.amap nil, inc), nil + assert.equal! (args.fn.amap ary, nil), nil + assert.equal! (args.fn.amap ary, inc).class, Array +end + +def test_and args, assert + assert.equal! (args.fn.and 1, 2, 3, 4), 4 + assert.equal! (args.fn.and 1, 2, nil, 4), nil + assert.equal! (args.fn.and), true +end + +def test_or args, assert + assert.equal! (args.fn.or 1, 2, 3, 4), 1 + assert.equal! (args.fn.or 1, 2, nil, 4), 1 + assert.equal! (args.fn.or), nil + assert.equal! (args.fn.or nil, nil, false, 5, 10), 5 +end + +def test_eq_eq args, assert + assert.equal! (args.fn.eq?), true + assert.equal! (args.fn.eq? 1, 0), false + assert.equal! (args.fn.eq? 1, 1, 1), true + assert.equal! (args.fn.== 1, 1, 1), true + assert.equal! (args.fn.== nil, nil), true +end + +def test_apply args, assert + assert.equal! (args.fn.and [nil, nil, nil]), [nil, nil, nil] + assert.equal! (args.fn.apply [nil, nil, nil], args.fn.method(:and)), nil + and_lambda = lambda {|*xs| args.fn.and(*xs)} + assert.equal! (args.fn.apply [nil, nil, nil], and_lambda), nil +end + +def test_areduce args, assert + assert.equal! (args.fn.areduce [1, 2, 3], 0, lambda { |i, a| i + a }), 6 +end + +def test_array_hash args, assert + assert.equal! (args.fn.array_hash :a, 1, :b, 2), { a: 1, b: 2 } + assert.equal! (args.fn.array_hash), { } +end + +</code></pre> +<h3 id='----advanced-debugging---unit-tests---gen_docs.rb'>Advanced Debugging - Unit Tests - gen_docs.rb</h3> +<pre><code class="language-ruby"># ./samples/10_advanced_debugging/03_unit_tests/gen_docs.rb +# ./dragonruby mygame --eval samples/99_zz_gtk_unit_tests/gen_docs.rb --no-tick Kernel.export_docs! -</pre> -<h1 id='--advanced-debugging---unit-tests---geometry_tests.rb'>Advanced Debugging - Unit Tests - geometry_tests.rb</h1> -<pre># ./samples/10_advanced_debugging/03_unit_tests/geometry_tests.rb +</code></pre> +<h3 id='----advanced-debugging---unit-tests---geometry_tests.rb'>Advanced Debugging - Unit Tests - geometry_tests.rb</h3> +<pre><code class="language-ruby"># ./samples/10_advanced_debugging/03_unit_tests/geometry_tests.rb begin :shared def primitive_representations x, y, w, h [ @@ -10154,14 +14384,12 @@ begin :scale_rect end end -puts "running tests" $gtk.reset 100 $gtk.log_level = :off -$gtk.tests.start -</pre> -<h1 id='--advanced-debugging---unit-tests---http_tests.rb'>Advanced Debugging - Unit Tests - http_tests.rb</h1> -<pre># ./samples/10_advanced_debugging/03_unit_tests/http_tests.rb +</code></pre> +<h3 id='----advanced-debugging---unit-tests---http_tests.rb'>Advanced Debugging - Unit Tests - http_tests.rb</h3> +<pre><code class="language-ruby"># ./samples/10_advanced_debugging/03_unit_tests/http_tests.rb def try_assert_or_schedule args, assert if $result[:complete] log_info "Request completed! Verifying." @@ -10182,14 +14410,108 @@ def test_http args, assert try_assert_or_schedule args, assert end -puts "running tests" $gtk.reset 100 $gtk.log_level = :off -$gtk.tests.start -</pre> -<h1 id='--advanced-debugging---unit-tests---object_to_primitive_tests.rb'>Advanced Debugging - Unit Tests - object_to_primitive_tests.rb</h1> -<pre># ./samples/10_advanced_debugging/03_unit_tests/object_to_primitive_tests.rb +</code></pre> +<h3 id='----advanced-debugging---unit-tests---nil_coercion_tests.rb'>Advanced Debugging - Unit Tests - nil_coercion_tests.rb</h3> +<pre><code class="language-ruby"># ./samples/10_advanced_debugging/03_unit_tests/nil_coercion_tests.rb +# numbers +def test_open_entity_add_number args, assert + assert.nil! args.state.i_value + args.state.i_value += 5 + assert.equal! args.state.i_value, 5 + + assert.nil! args.state.f_value + args.state.f_value += 5.5 + assert.equal! args.state.f_value, 5.5 +end + +def test_open_entity_subtract_number args, assert + assert.nil! args.state.i_value + args.state.i_value -= 5 + assert.equal! args.state.i_value, -5 + + assert.nil! args.state.f_value + args.state.f_value -= 5.5 + assert.equal! args.state.f_value, -5.5 +end + +def test_open_entity_multiply_number args, assert + assert.nil! args.state.i_value + args.state.i_value *= 5 + assert.equal! args.state.i_value, 0 + + assert.nil! args.state.f_value + args.state.f_value *= 5.5 + assert.equal! args.state.f_value, 0 +end + +def test_open_entity_divide_number args, assert + assert.nil! args.state.i_value + args.state.i_value /= 5 + assert.equal! args.state.i_value, 0 + + assert.nil! args.state.f_value + args.state.f_value /= 5.5 + assert.equal! args.state.f_value, 0 +end + +# array +def test_open_entity_add_array args, assert + assert.nil! args.state.values + args.state.values += [:a, :b, :c] + assert.equal! args.state.values, [:a, :b, :c] +end + +def test_open_entity_subtract_array args, assert + assert.nil! args.state.values + args.state.values -= [:a, :b, :c] + assert.equal! args.state.values, [] +end + +def test_open_entity_shovel_array args, assert + assert.nil! args.state.values + args.state.values << :a + assert.equal! args.state.values, [:a] +end + +def test_open_entity_enumerate args, assert + assert.nil! args.state.values + args.state.values = args.state.values.map_with_index { |i| i } + assert.equal! args.state.values, [] + + assert.nil! args.state.values_2 + args.state.values_2 = args.state.values_2.map { |i| i } + assert.equal! args.state.values_2, [] + + assert.nil! args.state.values_3 + args.state.values_3 = args.state.values_3.flat_map { |i| i } + assert.equal! args.state.values_3, [] +end + +# hashes +def test_open_entity_indexer args, assert + GTK::Entity.__reset_id__! + assert.nil! args.state.values + args.state.values[:test] = :value + assert.equal! args.state.values.to_s, { entity_id: 1, entity_name: :values, entity_keys_by_ref: {}, test: :value }.to_s +end + +# bug +def test_open_entity_nil_bug args, assert + GTK::Entity.__reset_id__! + args.state.foo.a + args.state.foo.b + @hello[:foobar] + assert.nil! args.state.foo.a, "a was not nil." + # the line below fails + # assert.nil! args.state.foo.b, "b was not nil." +end + +</code></pre> +<h3 id='----advanced-debugging---unit-tests---object_to_primitive_tests.rb'>Advanced Debugging - Unit Tests - object_to_primitive_tests.rb</h3> +<pre><code class="language-ruby"># ./samples/10_advanced_debugging/03_unit_tests/object_to_primitive_tests.rb class PlayerSpriteForTest end @@ -10206,11 +14528,10 @@ end $gtk.reset 100 $gtk.log_level = :off -$gtk.tests.start -</pre> -<h1 id='--advanced-debugging---unit-tests---parsing_tests.rb'>Advanced Debugging - Unit Tests - parsing_tests.rb</h1> -<pre># ./samples/10_advanced_debugging/03_unit_tests/parsing_tests.rb +</code></pre> +<h3 id='----advanced-debugging---unit-tests---parsing_tests.rb'>Advanced Debugging - Unit Tests - parsing_tests.rb</h3> +<pre><code class="language-ruby"># ./samples/10_advanced_debugging/03_unit_tests/parsing_tests.rb def test_parse_json args, assert result = args.gtk.parse_json '{ "name": "John Doe", "aliases": ["JD"] }' assert.equal! result, { "name"=>"John Doe", "aliases"=>["JD"] }, "Parsing JSON failed." @@ -10236,14 +14557,54 @@ S assert.equal! result, expected, "Parsing xml failed." end -puts "running tests" $gtk.reset 100 $gtk.log_level = :off -$gtk.tests.start -</pre> -<h1 id='--advanced-debugging---unit-tests---serialize_deserialize_tests.rb'>Advanced Debugging - Unit Tests - serialize_deserialize_tests.rb</h1> -<pre># ./samples/10_advanced_debugging/03_unit_tests/serialize_deserialize_tests.rb +</code></pre> +<h3 id='----advanced-debugging---unit-tests---require_tests.rb'>Advanced Debugging - Unit Tests - require_tests.rb</h3> +<pre><code class="language-ruby"># ./samples/10_advanced_debugging/03_unit_tests/require_tests.rb +def write_src path, src + $gtk.write_file path, src +end + +write_src 'app/unit_testing_game.rb', <<-S +module UnitTesting + class Game + end +end +S + +write_src 'lib/unit_testing_lib.rb', <<-S +module UnitTesting + class Lib + end +end +S + +write_src 'app/nested/unit_testing_nested.rb', <<-S +module UnitTesting + class Nested + end +end +S + +require 'app/unit_testing_game.rb' +require 'app/nested/unit_testing_nested.rb' +require 'lib/unit_testing_lib.rb' + +def test_require args, assert + UnitTesting::Game.new + UnitTesting::Lib.new + UnitTesting::Nested.new + $gtk.exec 'rm ./mygame/app/unit_testing_game.rb' + $gtk.exec 'rm ./mygame/app/nested/unit_testing_nested.rb' + $gtk.exec 'rm ./mygame/lib/unit_testing_lib.rb' + assert.ok! +end + +</code></pre> +<h3 id='----advanced-debugging---unit-tests---serialize_deserialize_tests.rb'>Advanced Debugging - Unit Tests - serialize_deserialize_tests.rb</h3> +<pre><code class="language-ruby"># ./samples/10_advanced_debugging/03_unit_tests/serialize_deserialize_tests.rb def test_serialize args, assert GTK::Entity.__reset_id__! args.state.player_one = "test" @@ -10360,11 +14721,9 @@ def test_by_reference_state_strict_entities args, assert assert.equal! deserialized_state.strict_entity.one, deserialized_state.strict_entity.two end -$tests.start - -</pre> -<h1 id='--advanced-debugging---unit-tests---state_serialization_experimental_tests.rb'>Advanced Debugging - Unit Tests - state_serialization_experimental_tests.rb</h1> -<pre># ./samples/10_advanced_debugging/03_unit_tests/state_serialization_experimental_tests.rb +</code></pre> +<h3 id='----advanced-debugging---unit-tests---state_serialization_experimental_tests.rb'>Advanced Debugging - Unit Tests - state_serialization_experimental_tests.rb</h3> +<pre><code class="language-ruby"># ./samples/10_advanced_debugging/03_unit_tests/state_serialization_experimental_tests.rb MAX_CODE_GEN_LENGTH = 50 # NOTE: This is experimental/advanced stuff. @@ -10471,11 +14830,52 @@ end $gtk.reset 100 $gtk.log_level = :off -$gtk.tests.start -</pre> -<h1 id='--http---retrieve-images---main.rb'>Http - Retrieve Images - main.rb</h1> -<pre># ./samples/11_http/01_retrieve_images/app/main.rb +</code></pre> +<h3 id='----advanced-debugging---unit-tests---suggest_autocompletion_tests.rb'>Advanced Debugging - Unit Tests - suggest_autocompletion_tests.rb</h3> +<pre><code class="language-ruby"># ./samples/10_advanced_debugging/03_unit_tests/suggest_autocompletion_tests.rb +def default_suggest_autocompletion args + { + index: 4, + text: "args.", + __meta__: { + other_options: [ + { + index: Fixnum, + file: "app/main.rb" + } + ] + } + } +end + +def assert_completion source, *expected + results = suggest_autocompletion text: (source.strip.gsub ":cursor", ""), + index: (source.strip.index ":cursor") + + puts results +end + +def test_args_completion args, assert + $gtk.write_file_root "autocomplete.txt", ($gtk.suggest_autocompletion text: <<-S, index: 128).join("\n") +require 'app/game.rb' + +def tick args + args.gtk.suppress_mailbox = false + $game ||= Game.new + $game.args = args + $game.args. + $game.tick +end +S + + puts "contents:" + puts ($gtk.read_file "autocomplete.txt") +end + +</code></pre> +<h3 id='----http---retrieve-images---main.rb'>Http - Retrieve Images - main.rb</h3> +<pre><code class="language-ruby"># ./samples/11_http/01_retrieve_images/app/main.rb def tick args args.outputs.background_color = [0, 0, 0] @@ -10530,9 +14930,40 @@ def tick args end end -</pre> -<h1 id='--12-c-extensions---basics---main.rb'>12 C Extensions - Basics - main.rb</h1> -<pre># ./samples/12_c_extensions/01_basics/app/main.rb +</code></pre> +<h3 id='----http---web-server---main.rb'>Http - Web Server - main.rb</h3> +<pre><code class="language-ruby"># ./samples/11_http/02_web_server/app/main.rb +def tick args + args.state.port ||= 3000 + # by default the embedded webserver runs on port 9001 (the port number is over 9000) and is disabled in a production build + # to enable the http server in a production build, you need to manually start + # the server up: + args.gtk.start_server! port: args.state.port, enable_in_prod: true + args.outputs.background_color = [0, 0, 0] + args.outputs.labels << [640, 600, "Point your web browser at http://localhost:#{args.state.port}/", 10, 1, 255, 255, 255] + + args.inputs.http_requests.each { |req| + puts("METHOD: #{req.method}"); + puts("URI: #{req.uri}"); + puts("HEADERS:"); + req.headers.each { |k,v| puts(" #{k}: #{v}") } + + if (req.uri == '/') + # headers and body can be nil if you don't care about them. + # If you don't set the Content-Type, it will default to + # "text/html; charset=utf-8". + # Don't set Content-Length; we'll ignore it and calculate it for you + args.state.reqnum += 1 + req.respond 200, "<html><head><title>hello</title></head><body><h1>This #{req.method} was request number #{args.state.reqnum}!</h1></body></html>\n", { 'X-DRGTK-header' => 'Powered by DragonRuby!' } + else + req.reject + end + } +end + +</code></pre> +<h3 id='----c-extensions---basics---main.rb'>C Extensions - Basics - main.rb</h3> +<pre><code class="language-ruby"># ./samples/12_c_extensions/01_basics/app/main.rb $gtk.ffi_misc.gtk_dlopen("ext") include FFI::CExt @@ -10544,9 +14975,9 @@ def tick args end -</pre> -<h1 id='--12-c-extensions---intermediate---main.rb'>12 C Extensions - Intermediate - main.rb</h1> -<pre># ./samples/12_c_extensions/02_intermediate/app/main.rb +</code></pre> +<h3 id='----c-extensions---intermediate---main.rb'>C Extensions - Intermediate - main.rb</h3> +<pre><code class="language-ruby"># ./samples/12_c_extensions/02_intermediate/app/main.rb $gtk.ffi_misc.gtk_dlopen("ext") include FFI::RE @@ -10567,9 +14998,9 @@ def tick args args.outputs.labels << [640, 500, split_words("hello, dragonriders!").join(' '), 5, 1] end -</pre> -<h1 id='--12-c-extensions---native-pixel-arrays---main.rb'>12 C Extensions - Native Pixel Arrays - main.rb</h1> -<pre># ./samples/12_c_extensions/03_native_pixel_arrays/app/main.rb +</code></pre> +<h3 id='----c-extensions---native-pixel-arrays---main.rb'>C Extensions - Native Pixel Arrays - main.rb</h3> +<pre><code class="language-ruby"># ./samples/12_c_extensions/03_native_pixel_arrays/app/main.rb $gtk.ffi_misc.gtk_dlopen("ext") include FFI::CExt @@ -10593,9 +15024,6726 @@ def tick args end -</pre> -<h1 id='--3d---3d-cube---main.rb'>3d - 3d Cube - main.rb</h1> -<pre># ./samples/99_genre_3d/3d_cube/app/main.rb +</code></pre> +<h3 id='----path-finding-algorithms---breadth-first-search---main.rb'>Path Finding Algorithms - Breadth First Search - main.rb</h3> +<pre><code class="language-ruby"># ./samples/13_path_finding_algorithms/01_breadth_first_search/app/main.rb +# A visual demonstration of a breadth first search +# Inspired by https://www.redblobgames.com/pathfinding/a-star/introduction.html + +# An animation that can respond to user input in real time + +# A breadth first search expands in all directions one step at a time +# The frontier is a queue of cells to be expanded from +# The visited hash allows quick lookups of cells that have been expanded from +# The walls hash allows quick lookup of whether a cell is a wall + +# The breadth first search starts by adding the red star to the frontier array +# and marking it as visited +# Each step a cell is removed from the front of the frontier array (queue) +# Unless the neighbor is a wall or visited, it is added to the frontier array +# The neighbor is then marked as visited + +# The frontier is blue +# Visited cells are light brown +# Walls are camo green +# Even when walls are visited, they will maintain their wall color + +# The star can be moved by clicking and dragging +# Walls can be added and removed by clicking and dragging + +class BreadthFirstSearch + attr_gtk + + def initialize(args) + # Variables to edit the size and appearance of the grid + # Freely customizable to user's liking + args.state.grid.width = 30 + args.state.grid.height = 15 + args.state.grid.cell_size = 40 + + # Stores which step of the animation is being rendered + # When the user moves the star or messes with the walls, + # the breadth first search is recalculated up to this step + args.state.anim_steps = 0 + + # At some step the animation will end, + # and further steps won't change anything (the whole grid will be explored) + # This step is roughly the grid's width * height + # When anim_steps equals max_steps no more calculations will occur + # and the slider will be at the end + args.state.max_steps = args.state.grid.width * args.state.grid.height + + # Whether the animation should play or not + # If true, every tick moves anim_steps forward one + # Pressing the stepwise animation buttons will pause the animation + args.state.play = true + + # The location of the star and walls of the grid + # They can be modified to have a different initial grid + # Walls are stored in a hash for quick look up when doing the search + args.state.star = [0, 0] + args.state.walls = { + [3, 3] => true, + [3, 4] => true, + [3, 5] => true, + [3, 6] => true, + [3, 7] => true, + [3, 8] => true, + [3, 9] => true, + [3, 10] => true, + [3, 11] => true, + [4, 3] => true, + [4, 4] => true, + [4, 5] => true, + [4, 6] => true, + [4, 7] => true, + [4, 8] => true, + [4, 9] => true, + [4, 10] => true, + [4, 11] => true, + + [13, 0] => true, + [13, 1] => true, + [13, 2] => true, + [13, 3] => true, + [13, 4] => true, + [13, 5] => true, + [13, 6] => true, + [13, 7] => true, + [13, 8] => true, + [13, 9] => true, + [13, 10] => true, + [14, 0] => true, + [14, 1] => true, + [14, 2] => true, + [14, 3] => true, + [14, 4] => true, + [14, 5] => true, + [14, 6] => true, + [14, 7] => true, + [14, 8] => true, + [14, 9] => true, + [14, 10] => true, + + [21, 8] => true, + [21, 9] => true, + [21, 10] => true, + [21, 11] => true, + [21, 12] => true, + [21, 13] => true, + [21, 14] => true, + [22, 8] => true, + [22, 9] => true, + [22, 10] => true, + [22, 11] => true, + [22, 12] => true, + [22, 13] => true, + [22, 14] => true, + [23, 8] => true, + [23, 9] => true, + [24, 8] => true, + [24, 9] => true, + [25, 8] => true, + [25, 9] => true, + } + + # Variables that are used by the breadth first search + # Storing cells that the search has visited, prevents unnecessary steps + # Expanding the frontier of the search in order makes the search expand + # from the center outward + args.state.visited = {} + args.state.frontier = [] + + + # What the user is currently editing on the grid + # Possible values are: :none, :slider, :star, :remove_wall, :add_wall + + # We store this value, because we want to remember the value even when + # the user's cursor is no longer over what they're interacting with, but + # they are still clicking down on the mouse. + args.state.click_and_drag = :none + + # Store the rects of the buttons that control the animation + # They are here for user customization + # Editing these might require recentering the text inside them + # Those values can be found in the render_button methods + args.state.buttons.left = [450, 600, 50, 50] + args.state.buttons.center = [500, 600, 200, 50] + args.state.buttons.right = [700, 600, 50, 50] + + # The variables below are related to the slider + # They allow the user to customize them + # They also give a central location for the render and input methods to get + # information from + # x & y are the coordinates of the leftmost part of the slider line + args.state.slider.x = 400 + args.state.slider.y = 675 + # This is the width of the line + args.state.slider.w = 360 + # This is the offset for the circle + # Allows the center of the circle to be on the line, + # as opposed to the upper right corner + args.state.slider.offset = 20 + # This is the spacing between each of the notches on the slider + # Notches are places where the circle can rest on the slider line + # There needs to be a notch for each step before the maximum number of steps + args.state.slider.spacing = args.state.slider.w.to_f / args.state.max_steps.to_f + end + + # This method is called every frame/tick + # Every tick, the current state of the search is rendered on the screen, + # User input is processed, and + # The next step in the search is calculated + def tick + render + input + # If animation is playing, and max steps have not been reached + # Move the search a step forward + if state.play && state.anim_steps < state.max_steps + # Variable that tells the program what step to recalculate up to + state.anim_steps += 1 + calc + end + end + + # Draws everything onto the screen + def render + render_buttons + render_slider + + render_background + render_visited + render_frontier + render_walls + render_star + end + + # The methods below subdivide the task of drawing everything to the screen + + # Draws the buttons that control the animation step and state + def render_buttons + render_left_button + render_center_button + render_right_button + end + + # Draws the button which steps the search backward + # Shows the user where to click to move the search backward + def render_left_button + # Draws the gray button, and a black border + # The border separates the buttons visually + outputs.solids << [buttons.left, gray] + outputs.borders << [buttons.left, black] + + # Renders an explanatory label in the center of the button + # Explains to the user what the button does + # If the button size is changed, the label might need to be edited as well + # to keep the label in the center of the button + label_x = buttons.left.x + 20 + label_y = buttons.left.y + 35 + outputs.labels << [label_x, label_y, "<"] + end + + def render_center_button + # Draws the gray button, and a black border + # The border separates the buttons visually + outputs.solids << [buttons.center, gray] + outputs.borders << [buttons.center, black] + + # Renders an explanatory label in the center of the button + # Explains to the user what the button does + # If the button size is changed, the label might need to be edited as well + # to keep the label in the center of the button + label_x = buttons.center.x + 37 + label_y = buttons.center.y + 35 + label_text = state.play ? "Pause Animation" : "Play Animation" + outputs.labels << [label_x, label_y, label_text] + end + + def render_right_button + # Draws the gray button, and a black border + # The border separates the buttons visually + outputs.solids << [buttons.right, gray] + outputs.borders << [buttons.right, black] + + # Renders an explanatory label in the center of the button + # Explains to the user what the button does + label_x = buttons.right.x + 20 + label_y = buttons.right.y + 35 + outputs.labels << [label_x, label_y, ">"] + end + + # Draws the slider so the user can move it and see the progress of the search + def render_slider + # Using primitives hides the line under the white circle of the slider + # Draws the line + outputs.primitives << [slider.x, slider.y, slider.x + slider.w, slider.y].line + # The circle needs to be offset so that the center of the circle + # overlaps the line instead of the upper right corner of the circle + # The circle's x value is also moved based on the current seach step + circle_x = (slider.x - slider.offset) + (state.anim_steps * slider.spacing) + circle_y = (slider.y - slider.offset) + circle_rect = [circle_x, circle_y, 37, 37] + outputs.primitives << [circle_rect, 'circle-white.png'].sprite + end + + # Draws what the grid looks like with nothing on it + def render_background + render_unvisited + render_grid_lines + end + + # Draws a rectangle the size of the entire grid to represent unvisited cells + def render_unvisited + outputs.solids << [scale_up([0, 0, grid.width, grid.height]), unvisited_color] + end + + # Draws grid lines to show the division of the grid into cells + def render_grid_lines + for x in 0..grid.width + outputs.lines << vertical_line(x) + end + + for y in 0..grid.height + outputs.lines << horizontal_line(y) + end + end + + # Easy way to draw vertical lines given an index + def vertical_line column + scale_up([column, 0, column, grid.height]) + end + + # Easy way to draw horizontal lines given an index + def horizontal_line row + scale_up([0, row, grid.width, row]) + end + + # Draws the area that is going to be searched from + # The frontier is the most outward parts of the search + def render_frontier + outputs.solids << state.frontier.map do |cell| + [scale_up([cell.x, cell.y]), frontier_color] + end + end + + # Draws the walls + def render_walls + outputs.solids << state.walls.map do |wall| + [scale_up([wall.x, wall.y]), wall_color] + end + end + + # Renders cells that have been searched in the appropriate color + def render_visited + outputs.solids << state.visited.map do |cell| + [scale_up([cell.x, cell.y]), visited_color] + end + end + + # Renders the star + def render_star + outputs.sprites << [scale_up(state.star), 'star.png'] + end + + # In code, the cells are represented as 1x1 rectangles + # When drawn, the cells are larger than 1x1 rectangles + # This method is used to scale up cells, and lines + # Objects are scaled up according to the grid.cell_size variable + # This allows for easy customization of the visual scale of the grid + def scale_up(cell) + # Prevents the original value of cell from being edited + cell = cell.clone + + # If cell is just an x and y coordinate + if cell.size == 2 + # Add a width and height of 1 + cell << 1 + cell << 1 + end + + # Scale all the values up + cell.map! { |value| value * grid.cell_size } + + # Returns the scaled up cell + cell + end + + # This method processes user input every tick + # This method allows the user to use the buttons, slider, and edit the grid + # There are 2 types of input: + # Button Input + # Click and Drag Input + # + # Button Input is used for the backward step and forward step buttons + # Input is detected by mouse up within the bounds of the rect + # + # Click and Drag Input is used for moving the star, adding walls, + # removing walls, and the slider + # + # When the mouse is down on the star, the click_and_drag variable is set to :star + # While click_and_drag equals :star, the cursor's position is used to calculate the + # appropriate drag behavior + # + # When the mouse goes up click_and_drag is set to :none + # + # A variable has to be used because the star has to continue being edited even + # when the cursor is no longer over the star + # + # Similar things occur for the other Click and Drag inputs + def input + # Checks whether any of the buttons are being clicked + input_buttons + + # The detection and processing of click and drag inputs are separate + # The program has to remember that the user is dragging an object + # even when the mouse is no longer over that object + detect_click_and_drag + process_click_and_drag + end + + # Detects and Process input for each button + def input_buttons + input_left_button + input_center_button + input_next_step_button + end + + # Checks if the previous step button is clicked + # If it is, it pauses the animation and moves the search one step backward + def input_left_button + if left_button_clicked? + state.play = false + state.anim_steps -= 1 + recalculate + end + end + + # Controls the play/pause button + # Inverses whether the animation is playing or not when clicked + def input_center_button + if center_button_clicked? or inputs.keyboard.key_down.space + state.play = !state.play + end + end + + # Checks if the next step button is clicked + # If it is, it pauses the animation and moves the search one step forward + def input_next_step_button + if right_button_clicked? + state.play = false + state.anim_steps += 1 + calc + end + end + + # Determines what the user is editing and stores the value + # Storing the value allows the user to continue the same edit as long as the + # mouse left click is held + def detect_click_and_drag + if inputs.mouse.up + state.click_and_drag = :none + elsif star_clicked? + state.click_and_drag = :star + elsif wall_clicked? + state.click_and_drag = :remove_wall + elsif grid_clicked? + state.click_and_drag = :add_wall + elsif slider_clicked? + state.click_and_drag = :slider + end + end + + # Processes click and drag based on what the user is currently dragging + def process_click_and_drag + if state.click_and_drag == :star + input_star + elsif state.click_and_drag == :remove_wall + input_remove_wall + elsif state.click_and_drag == :add_wall + input_add_wall + elsif state.click_and_drag == :slider + input_slider + end + end + + # Moves the star to the grid closest to the mouse + # Only recalculates the search if the star changes position + # Called whenever the user is editing the star (puts mouse down on star) + def input_star + old_star = state.star.clone + state.star = cell_closest_to_mouse + unless old_star == state.star + recalculate + end + end + + # Removes walls that are under the cursor + def input_remove_wall + # The mouse needs to be inside the grid, because we only want to remove walls + # the cursor is directly over + # Recalculations should only occur when a wall is actually deleted + if mouse_inside_grid? + if state.walls.has_key?(cell_closest_to_mouse) + state.walls.delete(cell_closest_to_mouse) + recalculate + end + end + end + + # Adds walls at cells under the cursor + def input_add_wall + if mouse_inside_grid? + unless state.walls.has_key?(cell_closest_to_mouse) + state.walls[cell_closest_to_mouse] = true + recalculate + end + end + end + + # This method is called when the user is editing the slider + # It pauses the animation and moves the white circle to the closest integer point + # on the slider + # Changes the step of the search to be animated + def input_slider + state.play = false + mouse_x = inputs.mouse.point.x + + # Bounds the mouse_x to the closest x value on the slider line + mouse_x = slider.x if mouse_x < slider.x + mouse_x = slider.x + slider.w if mouse_x > slider.x + slider.w + + # Sets the current search step to the one represented by the mouse x value + # The slider's circle moves due to the render_slider method using anim_steps + state.anim_steps = ((mouse_x - slider.x) / slider.spacing).to_i + + recalculate + end + + # Whenever the user edits the grid, + # The search has to be recalculated upto the current step + # with the current grid as the initial state of the grid + def recalculate + # Resets the search + state.frontier = [] + state.visited = {} + + # Moves the animation forward one step at a time + state.anim_steps.times { calc } + end + + + # This method moves the search forward one step + # When the animation is playing it is called every tick + # And called whenever the current step of the animation needs to be recalculated + + # Moves the search forward one step + # Parameter called_from_tick is true if it is called from the tick method + # It is false when the search is being recalculated after user editing the grid + def calc + + # The setup to the search + # Runs once when the there is no frontier or visited cells + if state.frontier.empty? && state.visited.empty? + state.frontier << state.star + state.visited[state.star] = true + end + + # A step in the search + unless state.frontier.empty? + # Takes the next frontier cell + new_frontier = state.frontier.shift + # For each of its neighbors + adjacent_neighbors(new_frontier).each do |neighbor| + # That have not been visited and are not walls + unless state.visited.has_key?(neighbor) || state.walls.has_key?(neighbor) + # Add them to the frontier and mark them as visited + state.frontier << neighbor + state.visited[neighbor] = true + end + end + end + end + + + # Returns a list of adjacent cells + # Used to determine what the next cells to be added to the frontier are + def adjacent_neighbors(cell) + neighbors = [] + + neighbors << [cell.x, cell.y + 1] unless cell.y == grid.height - 1 + neighbors << [cell.x + 1, cell.y] unless cell.x == grid.width - 1 + neighbors << [cell.x, cell.y - 1] unless cell.y == 0 + neighbors << [cell.x - 1, cell.y] unless cell.x == 0 + + neighbors + end + + # When the user grabs the star and puts their cursor to the far right + # and moves up and down, the star is supposed to move along the grid as well + # Finding the cell closest to the mouse helps with this + def cell_closest_to_mouse + # Closest cell to the mouse + x = (inputs.mouse.point.x / grid.cell_size).to_i + y = (inputs.mouse.point.y / grid.cell_size).to_i + # Bound x and y to the grid + x = grid.width - 1 if x > grid.width - 1 + y = grid.height - 1 if y > grid.height - 1 + # Return closest cell + [x, y] + end + + # These methods detect when the buttons are clicked + def left_button_clicked? + inputs.mouse.up && inputs.mouse.point.inside_rect?(buttons.left) + end + + def center_button_clicked? + inputs.mouse.up && inputs.mouse.point.inside_rect?(buttons.center) + end + + def right_button_clicked? + inputs.mouse.up && inputs.mouse.point.inside_rect?(buttons.right) + end + + # Signal that the user is going to be moving the slider + # Is the mouse down on the circle of the slider? + def slider_clicked? + circle_x = (slider.x - slider.offset) + (state.anim_steps * slider.spacing) + circle_y = (slider.y - slider.offset) + circle_rect = [circle_x, circle_y, 37, 37] + inputs.mouse.down && inputs.mouse.point.inside_rect?(circle_rect) + end + + # Signal that the user is going to be moving the star + def star_clicked? + inputs.mouse.down && inputs.mouse.point.inside_rect?(scale_up(state.star)) + end + + # Signal that the user is going to be removing walls + def wall_clicked? + inputs.mouse.down && mouse_inside_a_wall? + end + + # Signal that the user is going to be adding walls + def grid_clicked? + inputs.mouse.down && mouse_inside_grid? + end + + # Returns whether the mouse is inside of a wall + # Part of the condition that checks whether the user is removing a wall + def mouse_inside_a_wall? + state.walls.each_key do | wall | + return true if inputs.mouse.point.inside_rect?(scale_up([wall.x, wall.y])) + end + + false + end + + # Returns whether the mouse is inside of a grid + # Part of the condition that checks whether the user is adding a wall + def mouse_inside_grid? + inputs.mouse.point.inside_rect?(scale_up([0, 0, grid.width, grid.height])) + end + + + # These methods provide handy aliases to colors + + # Light brown + def unvisited_color + [221, 212, 213] + end + + # Black + def grid_line_color + [255, 255, 255] + end + + # Dark Brown + def visited_color + [204, 191, 179] + end + + # Blue + def frontier_color + [103, 136, 204] + end + + # Camo Green + def wall_color + [134, 134, 120] + end + + # Button Background + def gray + [190, 190, 190] + end + + # Button Outline + def black + [0, 0, 0] + end + + # These methods make the code more concise + def grid + state.grid + end + + def buttons + state.buttons + end + + def slider + state.slider + end +end + +# Method that is called by DragonRuby periodically +# Used for updating animations and calculations +def tick args + + # Pressing r will reset the application + if args.inputs.keyboard.key_down.r + args.gtk.reset + reset + return + end + + # Every tick, new args are passed, and the Breadth First Search tick is called + $breadth_first_search ||= BreadthFirstSearch.new(args) + $breadth_first_search.args = args + $breadth_first_search.tick +end + + +def reset + $breadth_first_search = nil +end + +</code></pre> +<h3 id='----path-finding-algorithms---detailed-breadth-first-search---main.rb'>Path Finding Algorithms - Detailed Breadth First Search - main.rb</h3> +<pre><code class="language-ruby"># ./samples/13_path_finding_algorithms/02_detailed_breadth_first_search/app/main.rb +# A visual demonstration of a breadth first search +# Inspired by https://www.redblobgames.com/pathfinding/a-star/introduction.html + +# An animation that can respond to user input in real time + +# A breadth first search expands in all directions one step at a time +# The frontier is a queue of cells to be expanded from +# The visited hash allows quick lookups of cells that have been expanded from +# The walls hash allows quick lookup of whether a cell is a wall + +# The breadth first search starts by adding the red star to the frontier array +# and marking it as visited +# Each step a cell is removed from the front of the frontier array (queue) +# Unless the neighbor is a wall or visited, it is added to the frontier array +# The neighbor is then marked as visited + +# The frontier is blue +# Visited cells are light brown +# Walls are camo green +# Even when walls are visited, they will maintain their wall color + +# This search numbers the order in which new cells are explored +# The next cell from where the search will continue is highlighted yellow +# And the cells that will be considered for expansion are in semi-transparent green + +# The star can be moved by clicking and dragging +# Walls can be added and removed by clicking and dragging + +class DetailedBreadthFirstSearch + attr_gtk + + def initialize(args) + # Variables to edit the size and appearance of the grid + # Freely customizable to user's liking + args.state.grid.width = 9 + args.state.grid.height = 4 + args.state.grid.cell_size = 90 + + # Stores which step of the animation is being rendered + # When the user moves the star or messes with the walls, + # the breadth first search is recalculated up to this step + args.state.anim_steps = 0 + + # At some step the animation will end, + # and further steps won't change anything (the whole grid will be explored) + # This step is roughly the grid's width * height + # When anim_steps equals max_steps no more calculations will occur + # and the slider will be at the end + args.state.max_steps = args.state.grid.width * args.state.grid.height + + # The location of the star and walls of the grid + # They can be modified to have a different initial grid + # Walls are stored in a hash for quick look up when doing the search + args.state.star = [3, 2] + args.state.walls = {} + + # Variables that are used by the breadth first search + # Storing cells that the search has visited, prevents unnecessary steps + # Expanding the frontier of the search in order makes the search expand + # from the center outward + args.state.visited = {} + args.state.frontier = [] + args.state.cell_numbers = [] + + + + # What the user is currently editing on the grid + # Possible values are: :none, :slider, :star, :remove_wall, :add_wall + + # We store this value, because we want to remember the value even when + # the user's cursor is no longer over what they're interacting with, but + # they are still clicking down on the mouse. + args.state.click_and_drag = :none + + # The x, y, w, h values for the buttons + # Allow easy movement of the buttons location + # A centralized location to get values to detect input and draw the buttons + # Editing these values might mean needing to edit the label offsets + # which can be found in the appropriate render button methods + args.state.buttons.left = [450, 600, 160, 50] + args.state.buttons.right = [610, 600, 160, 50] + + # The variables below are related to the slider + # They allow the user to customize them + # They also give a central location for the render and input methods to get + # information from + # x & y are the coordinates of the leftmost part of the slider line + args.state.slider.x = 400 + args.state.slider.y = 675 + # This is the width of the line + args.state.slider.w = 360 + # This is the offset for the circle + # Allows the center of the circle to be on the line, + # as opposed to the upper right corner + args.state.slider.offset = 20 + # This is the spacing between each of the notches on the slider + # Notches are places where the circle can rest on the slider line + # There needs to be a notch for each step before the maximum number of steps + args.state.slider.spacing = args.state.slider.w.to_f / args.state.max_steps.to_f + end + + # This method is called every frame/tick + # Every tick, the current state of the search is rendered on the screen, + # User input is processed, and + def tick + render + input + end + + # This method is called from tick and renders everything every tick + def render + render_buttons + render_slider + + render_background + render_visited + render_frontier + render_walls + render_star + + render_highlights + render_cell_numbers + end + + # The methods below subdivide the task of drawing everything to the screen + + # Draws the buttons that move the search backward or forward + # These buttons are rendered so the user knows where to click to move the search + def render_buttons + render_left_button + render_right_button + end + + # Renders the button which steps the search backward + # Shows the user where to click to move the search backward + def render_left_button + # Draws the gray button, and a black border + # The border separates the buttons visually + outputs.solids << [buttons.left, gray] + outputs.borders << [buttons.left, black] + + # Renders an explanatory label in the center of the button + # Explains to the user what the button does + label_x = buttons.left.x + 05 + label_y = buttons.left.y + 35 + outputs.labels << [label_x, label_y, "< Step backward"] + end + + # Renders the button which steps the search forward + # Shows the user where to click to move the search forward + def render_right_button + # Draws the gray button, and a black border + # The border separates the buttons visually + outputs.solids << [buttons.right, gray] + outputs.borders << [buttons.right, black] + + # Renders an explanatory label in the center of the button + # Explains to the user what the button does + label_x = buttons.right.x + 10 + label_y = buttons.right.y + 35 + outputs.labels << [label_x, label_y, "Step forward >"] + end + + # Draws the slider so the user can move it and see the progress of the search + def render_slider + # Using primitives hides the line under the white circle of the slider + # Draws the line + outputs.primitives << [slider.x, slider.y, slider.x + slider.w, slider.y].line + # The circle needs to be offset so that the center of the circle + # overlaps the line instead of the upper right corner of the circle + # The circle's x value is also moved based on the current seach step + circle_x = (slider.x - slider.offset) + (state.anim_steps * slider.spacing) + circle_y = (slider.y - slider.offset) + circle_rect = [circle_x, circle_y, 37, 37] + outputs.primitives << [circle_rect, 'circle-white.png'].sprite + end + + # Draws what the grid looks like with nothing on it + # Which is a bunch of unvisited cells + # Drawn first so other things can draw on top of it + def render_background + render_unvisited + + # The grid lines make the cells appear separate + render_grid_lines + end + + # Draws a rectangle the size of the entire grid to represent unvisited cells + # Unvisited cells are the default cell + def render_unvisited + background = [0, 0, grid.width, grid.height] + outputs.solids << [scale_up(background), unvisited_color] + end + + # Draws grid lines to show the division of the grid into cells + def render_grid_lines + for x in 0..grid.width + outputs.lines << [scale_up(vertical_line(x)), grid_line_color] + end + + for y in 0..grid.height + outputs.lines << [scale_up(horizontal_line(y)), grid_line_color] + end + end + + # Easy way to get a vertical line given an index + def vertical_line column + [column, 0, column, grid.height] + end + + # Easy way to get a horizontal line given an index + def horizontal_line row + [0, row, grid.width, row] + end + + # Draws the area that is going to be searched from + # The frontier is the most outward parts of the search + def render_frontier + state.frontier.each do |cell| + outputs.solids << [scale_up(cell), frontier_color] + end + end + + # Draws the walls + def render_walls + state.walls.each_key do |wall| + outputs.solids << [scale_up(wall), wall_color] + end + end + + # Renders cells that have been searched in the appropriate color + def render_visited + state.visited.each_key do |cell| + outputs.solids << [scale_up(cell), visited_color] + end + end + + # Renders the star + def render_star + outputs.sprites << [scale_up(state.star), 'star.png'] + end + + # Cells have a number rendered in them based on when they were explored + # This is based off of their index in the cell_numbers array + # Cells are added to this array the same time they are added to the frontier array + def render_cell_numbers + state.cell_numbers.each_with_index do |cell, index| + # Math that approx centers the number in the cell + label_x = (cell.x * grid.cell_size) + grid.cell_size / 2 - 5 + label_y = (cell.y * grid.cell_size) + (grid.cell_size / 2) + 5 + + outputs.labels << [label_x, label_y, (index + 1).to_s] + end + end + + # The next frontier to be expanded is highlighted yellow + # Its adjacent non-wall neighbors have their border highlighted green + # This is to show the user how the search expands + def render_highlights + return if state.frontier.empty? + + # Highlight the next frontier to be expanded yellow + next_frontier = state.frontier[0] + outputs.solids << [scale_up(next_frontier), highlighter_yellow] + + # Neighbors have a semi-transparent green layer over them + # Unless the neighbor is a wall + adjacent_neighbors(next_frontier).each do |neighbor| + unless state.walls.has_key?(neighbor) + outputs.solids << [scale_up(neighbor), highlighter_green, 70] + end + end + end + + + # Cell Size is used when rendering to allow the grid to be scaled up or down + # Cells in the frontier array and visited hash and walls hash are stored as x & y + # Scaling up cells and lines when rendering allows omitting of width and height + def scale_up(cell) + # Prevents the original value of cell from being edited + cell = cell.clone + + # If cell is just an x and y coordinate + if cell.size == 2 + # Add a width and height of 1 + cell << 1 + cell << 1 + end + + # Scale all the values up + cell.map! { |value| value * grid.cell_size } + + # Returns the scaled up cell + cell + end + + + # This method processes user input every tick + # This method allows the user to use the buttons, slider, and edit the grid + # There are 2 types of input: + # Button Input + # Click and Drag Input + # + # Button Input is used for the backward step and forward step buttons + # Input is detected by mouse up within the bounds of the rect + # + # Click and Drag Input is used for moving the star, adding walls, + # removing walls, and the slider + # + # When the mouse is down on the star, the click_and_drag variable is set to :star + # While click_and_drag equals :star, the cursor's position is used to calculate the + # appropriate drag behavior + # + # When the mouse goes up click_and_drag is set to :none + # + # A variable has to be used because the star has to continue being edited even + # when the cursor is no longer over the star + # + # Similar things occur for the other Click and Drag inputs + def input + # Processes inputs for the buttons + input_buttons + + # Detects which if any click and drag input is occurring + detect_click_and_drag + + # Does the appropriate click and drag input based on the click_and_drag variable + process_click_and_drag + end + + # Detects and Process input for each button + def input_buttons + input_left_button + input_right_button + end + + # Checks if the previous step button is clicked + # If it is, it pauses the animation and moves the search one step backward + def input_left_button + if left_button_clicked? + unless state.anim_steps == 0 + state.anim_steps -= 1 + recalculate + end + end + end + + # Checks if the next step button is clicked + # If it is, it pauses the animation and moves the search one step forward + def input_right_button + if right_button_clicked? + unless state.anim_steps == state.max_steps + state.anim_steps += 1 + # Although normally recalculate would be called here + # because the right button only moves the search forward + # We can just do that + calc + end + end + end + + # Whenever the user edits the grid, + # The search has to be recalculated upto the current step + + def recalculate + # Resets the search + state.frontier = [] + state.visited = {} + state.cell_numbers = [] + + # Moves the animation forward one step at a time + state.anim_steps.times { calc } + end + + + # Determines what the user is clicking and planning on dragging + # Click and drag input is initiated by a click on the appropriate item + # and ended by mouse up + # Storing the value allows the user to continue the same edit as long as the + # mouse left click is held + def detect_click_and_drag + if inputs.mouse.up + state.click_and_drag = :none + elsif star_clicked? + state.click_and_drag = :star + elsif wall_clicked? + state.click_and_drag = :remove_wall + elsif grid_clicked? + state.click_and_drag = :add_wall + elsif slider_clicked? + state.click_and_drag = :slider + end + end + + # Processes input based on what the user is currently dragging + def process_click_and_drag + if state.click_and_drag == :slider + input_slider + elsif state.click_and_drag == :star + input_star + elsif state.click_and_drag == :remove_wall + input_remove_wall + elsif state.click_and_drag == :add_wall + input_add_wall + end + end + + # This method is called when the user is dragging the slider + # It moves the current animation step to the point represented by the slider + def input_slider + mouse_x = inputs.mouse.point.x + + # Bounds the mouse_x to the closest x value on the slider line + mouse_x = slider.x if mouse_x < slider.x + mouse_x = slider.x + slider.w if mouse_x > slider.x + slider.w + + # Sets the current search step to the one represented by the mouse x value + # The slider's circle moves due to the render_slider method using anim_steps + state.anim_steps = ((mouse_x - slider.x) / slider.spacing).to_i + + recalculate + end + + # Moves the star to the grid closest to the mouse + # Only recalculates the search if the star changes position + # Called whenever the user is dragging the star + def input_star + old_star = state.star.clone + state.star = cell_closest_to_mouse + unless old_star == state.star + recalculate + end + end + + # Removes walls that are under the cursor + def input_remove_wall + # The mouse needs to be inside the grid, because we only want to remove walls + # the cursor is directly over + # Recalculations should only occur when a wall is actually deleted + if mouse_inside_grid? + if state.walls.has_key?(cell_closest_to_mouse) + state.walls.delete(cell_closest_to_mouse) + recalculate + end + end + end + + # Adds walls at cells under the cursor + def input_add_wall + # Adds a wall to the hash + # We can use the grid closest to mouse, because the cursor is inside the grid + if mouse_inside_grid? + unless state.walls.has_key?(cell_closest_to_mouse) + state.walls[cell_closest_to_mouse] = true + recalculate + end + end + end + + # This method moves the search forward one step + # When the animation is playing it is called every tick + # And called whenever the current step of the animation needs to be recalculated + + # Moves the search forward one step + # Parameter called_from_tick is true if it is called from the tick method + # It is false when the search is being recalculated after user editing the grid + def calc + # The setup to the search + # Runs once when the there is no frontier or visited cells + if state.frontier.empty? && state.visited.empty? + state.frontier << state.star + state.visited[state.star] = true + end + + # A step in the search + unless state.frontier.empty? + # Takes the next frontier cell + new_frontier = state.frontier.shift + # For each of its neighbors + adjacent_neighbors(new_frontier).each do |neighbor| + # That have not been visited and are not walls + unless state.visited.has_key?(neighbor) || state.walls.has_key?(neighbor) + # Add them to the frontier and mark them as visited + state.frontier << neighbor + state.visited[neighbor] = true + + # Also assign them a frontier number + state.cell_numbers << neighbor + end + end + end + end + + + # Returns a list of adjacent cells + # Used to determine what the next cells to be added to the frontier are + def adjacent_neighbors cell + neighbors = [] + + neighbors << [cell.x, cell.y + 1] unless cell.y == grid.height - 1 + neighbors << [cell.x + 1, cell.y] unless cell.x == grid.width - 1 + neighbors << [cell.x, cell.y - 1] unless cell.y == 0 + neighbors << [cell.x - 1, cell.y] unless cell.x == 0 + + neighbors + end + + # When the user grabs the star and puts their cursor to the far right + # and moves up and down, the star is supposed to move along the grid as well + # Finding the grid closest to the mouse helps with this + def cell_closest_to_mouse + x = (inputs.mouse.point.x / grid.cell_size).to_i + y = (inputs.mouse.point.y / grid.cell_size).to_i + x = grid.width - 1 if x > grid.width - 1 + y = grid.height - 1 if y > grid.height - 1 + [x, y] + end + + + # These methods detect when the buttons are clicked + def left_button_clicked? + (inputs.mouse.up && inputs.mouse.point.inside_rect?(buttons.left)) || inputs.keyboard.key_up.left + end + + def right_button_clicked? + (inputs.mouse.up && inputs.mouse.point.inside_rect?(buttons.right)) || inputs.keyboard.key_up.right + end + + # Signal that the user is going to be moving the slider + def slider_clicked? + circle_x = (slider.x - slider.offset) + (state.anim_steps * slider.spacing) + circle_y = (slider.y - slider.offset) + circle_rect = [circle_x, circle_y, 37, 37] + inputs.mouse.down && inputs.mouse.point.inside_rect?(circle_rect) + end + + # Signal that the user is going to be moving the star + def star_clicked? + inputs.mouse.down && inputs.mouse.point.inside_rect?(scale_up(state.star)) + end + + # Signal that the user is going to be removing walls + def wall_clicked? + inputs.mouse.down && mouse_inside_a_wall? + end + + # Signal that the user is going to be adding walls + def grid_clicked? + inputs.mouse.down && mouse_inside_grid? + end + + # Returns whether the mouse is inside of a wall + # Part of the condition that checks whether the user is removing a wall + def mouse_inside_a_wall? + state.walls.each_key do | wall | + return true if inputs.mouse.point.inside_rect?(scale_up(wall)) + end + + false + end + + # Returns whether the mouse is inside of a grid + # Part of the condition that checks whether the user is adding a wall + def mouse_inside_grid? + inputs.mouse.point.inside_rect?(scale_up([0, 0, grid.width, grid.height])) + end + + # These methods provide handy aliases to colors + + # Light brown + def unvisited_color + [221, 212, 213] + end + + # Black + def grid_line_color + [255, 255, 255] + end + + # Dark Brown + def visited_color + [204, 191, 179] + end + + # Blue + def frontier_color + [103, 136, 204] + end + + # Camo Green + def wall_color + [134, 134, 120] + end + + # Next frontier to be expanded + def highlighter_yellow + [214, 231, 125] + end + + # The neighbors of the next frontier to be expanded + def highlighter_green + [65, 191, 127] + end + + # Button background + def gray + [190, 190, 190] + end + + # Button outline + def black + [0, 0, 0] + end + + # These methods make the code more concise + def grid + state.grid + end + + def buttons + state.buttons + end + + def slider + state.slider + end +end + + +def tick args + # Pressing r resets the program + if args.inputs.keyboard.key_down.r + args.gtk.reset + reset + return + end + + $detailed_breadth_first_search ||= DetailedBreadthFirstSearch.new(args) + $detailed_breadth_first_search.args = args + $detailed_breadth_first_search.tick +end + + +def reset + $detailed_breadth_first_search = nil +end + +</code></pre> +<h3 id='----path-finding-algorithms---breadcrumbs---main.rb'>Path Finding Algorithms - Breadcrumbs - main.rb</h3> +<pre><code class="language-ruby"># ./samples/13_path_finding_algorithms/03_breadcrumbs/app/main.rb +class Breadcrumbs + attr_gtk + + # This method is called every frame/tick + # Every tick, the current state of the search is rendered on the screen, + # User input is processed, and + # The next step in the search is calculated + def tick + defaults + # If the grid has not been searched + if search.came_from.empty? + calc + # Calc Path + end + render + input + end + + def defaults + # Variables to edit the size and appearance of the grid + # Freely customizable to user's liking + grid.width ||= 30 + grid.height ||= 15 + grid.cell_size ||= 40 + grid.rect ||= [0, 0, grid.width, grid.height] + + # The location of the star and walls of the grid + # They can be modified to have a different initial grid + # Walls are stored in a hash for quick look up when doing the search + grid.star ||= [2, 8] + grid.target ||= [10, 5] + grid.walls ||= { + [3, 3] => true, + [3, 4] => true, + [3, 5] => true, + [3, 6] => true, + [3, 7] => true, + [3, 8] => true, + [3, 9] => true, + [3, 10] => true, + [3, 11] => true, + [4, 3] => true, + [4, 4] => true, + [4, 5] => true, + [4, 6] => true, + [4, 7] => true, + [4, 8] => true, + [4, 9] => true, + [4, 10] => true, + [4, 11] => true, + [13, 0] => true, + [13, 1] => true, + [13, 2] => true, + [13, 3] => true, + [13, 4] => true, + [13, 5] => true, + [13, 6] => true, + [13, 7] => true, + [13, 8] => true, + [13, 9] => true, + [13, 10] => true, + [14, 0] => true, + [14, 1] => true, + [14, 2] => true, + [14, 3] => true, + [14, 4] => true, + [14, 5] => true, + [14, 6] => true, + [14, 7] => true, + [14, 8] => true, + [14, 9] => true, + [14, 10] => true, + [21, 8] => true, + [21, 9] => true, + [21, 10] => true, + [21, 11] => true, + [21, 12] => true, + [21, 13] => true, + [21, 14] => true, + [22, 8] => true, + [22, 9] => true, + [22, 10] => true, + [22, 11] => true, + [22, 12] => true, + [22, 13] => true, + [22, 14] => true, + [23, 8] => true, + [23, 9] => true, + [24, 8] => true, + [24, 9] => true, + [25, 8] => true, + [25, 9] => true, + } + + # Variables that are used by the breadth first search + # Storing cells that the search has visited, prevents unnecessary steps + # Expanding the frontier of the search in order makes the search expand + # from the center outward + + # The cells from which the search is to expand + search.frontier ||= [] + # A hash of where each cell was expanded from + # The key is a cell, and the value is the cell it came from + search.came_from ||= {} + # Cells that are part of the path from the target to the star + search.path ||= {} + + # What the user is currently editing on the grid + # We store this value, because we want to remember the value even when + # the user's cursor is no longer over what they're interacting with, but + # they are still clicking down on the mouse. + state.current_input ||= :none + end + + def calc + # Setup the search to start from the star + search.frontier << grid.star + search.came_from[grid.star] = nil + + # Until there are no more cells to expand from + until search.frontier.empty? + # Takes the next frontier cell + new_frontier = search.frontier.shift + # For each of its neighbors + adjacent_neighbors(new_frontier).each do |neighbor| + # That have not been visited and are not walls + unless search.came_from.has_key?(neighbor) || grid.walls.has_key?(neighbor) + # Add them to the frontier and mark them as visited in the first grid + # Unless the target has been visited + # Add the neighbor to the frontier and remember which cell it came from + search.frontier << neighbor + search.came_from[neighbor] = new_frontier + end + end + end + end + + + # Draws everything onto the screen + def render + render_background + # render_heat_map + render_walls + # render_path + # render_labels + render_arrows + render_star + render_target + unless grid.walls.has_key?(grid.target) + render_trail + end + end + + def render_trail(current_cell=grid.target) + return if current_cell == grid.star + parent_cell = search.came_from[current_cell] + if current_cell && parent_cell + outputs.lines << [(current_cell.x + 0.5) * grid.cell_size, (current_cell.y + 0.5) * grid.cell_size, + (parent_cell.x + 0.5) * grid.cell_size, (parent_cell.y + 0.5) * grid.cell_size, purple] + + end + render_trail(parent_cell) + end + + def render_arrows + search.came_from.each do |child, parent| + if parent && child + arrow_cell = [(child.x + parent.x) / 2, (child.y + parent.y) / 2] + if parent.x > child.x # If the parent cell is to the right of the child cell + outputs.sprites << [scale_up(arrow_cell), 'arrow.png', 0] # Point the arrow to the right + elsif parent.x < child.x # If the parent cell is to the right of the child cell + outputs.sprites << [scale_up(arrow_cell), 'arrow.png', 180] # Point the arrow to the right + elsif parent.y > child.y # If the parent cell is to the right of the child cell + outputs.sprites << [scale_up(arrow_cell), 'arrow.png', 90] # Point the arrow to the right + elsif parent.y < child.y # If the parent cell is to the right of the child cell + outputs.sprites << [scale_up(arrow_cell), 'arrow.png', 270] # Point the arrow to the right + end + end + end + end + + # The methods below subdivide the task of drawing everything to the screen + + # Draws what the grid looks like with nothing on it + def render_background + render_unvisited + render_grid_lines + end + + # Draws both grids + def render_unvisited + outputs.solids << [scale_up(grid.rect), unvisited_color] + end + + # Draws grid lines to show the division of the grid into cells + def render_grid_lines + for x in 0..grid.width + outputs.lines << vertical_line(x) + end + + for y in 0..grid.height + outputs.lines << horizontal_line(y) + end + end + + # Easy way to draw vertical lines given an index + def vertical_line column + scale_up([column, 0, column, grid.height]) + end + + # Easy way to draw horizontal lines given an index + def horizontal_line row + scale_up([0, row, grid.width, row]) + end + + # Draws the walls on both grids + def render_walls + grid.walls.each_key do |wall| + outputs.solids << [scale_up(wall), wall_color] + end + end + + # Renders the star on both grids + def render_star + outputs.sprites << [scale_up(grid.star), 'star.png'] + end + + # Renders the target on both grids + def render_target + outputs.sprites << [scale_up(grid.target), 'target.png'] + end + + # Labels the grids + def render_labels + outputs.labels << [200, 625, "Without early exit"] + end + + # Renders the path based off of the search.path hash + def render_path + # If the star and target are disconnected there will only be one path + # The path should not render in that case + unless search.path.size == 1 + search.path.each_key do | cell | + # Renders path on both grids + outputs.solids << [scale_up(cell), path_color] + end + end + end + + # Calculates the path from the target to the star after the search is over + # Relies on the came_from hash + # Fills the search.path hash, which is later rendered on screen + def calc_path + endpoint = grid.target + while endpoint + search.path[endpoint] = true + endpoint = search.came_from[endpoint] + end + end + + # In code, the cells are represented as 1x1 rectangles + # When drawn, the cells are larger than 1x1 rectangles + # This method is used to scale up cells, and lines + # Objects are scaled up according to the grid.cell_size variable + # This allows for easy customization of the visual scale of the grid + def scale_up(cell) + # Prevents the original value of cell from being edited + cell = cell.clone + + # If cell is just an x and y coordinate + if cell.size == 2 + # Add a width and height of 1 + cell << 1 + cell << 1 + end + + # Scale all the values up + cell.map! { |value| value * grid.cell_size } + + # Returns the scaled up cell + cell + end + + # This method processes user input every tick + # Any method with "1" is related to the first grid + # Any method with "2" is related to the second grid + def input + # The program has to remember that the user is dragging an object + # even when the mouse is no longer over that object + # So detecting input and processing input is separate + # detect_input + # process_input + if inputs.mouse.up + state.current_input = :none + elsif star_clicked? + state.current_input = :star + end + + if mouse_inside_grid? + unless grid.target == cell_closest_to_mouse + grid.target = cell_closest_to_mouse + end + if state.current_input == :star + unless grid.star == cell_closest_to_mouse + grid.star = cell_closest_to_mouse + end + end + end + end + + # Determines what the user is editing and stores the value + # Storing the value allows the user to continue the same edit as long as the + # mouse left click is held + def detect_input + # When the mouse is up, nothing is being edited + if inputs.mouse.up + state.current_input = :none + # When the star in the no second grid is clicked + elsif star_clicked? + state.current_input = :star + # When the target in the no second grid is clicked + elsif target_clicked? + state.current_input = :target + # When a wall in the first grid is clicked + elsif wall_clicked? + state.current_input = :remove_wall + # When the first grid is clicked + elsif grid_clicked? + state.current_input = :add_wall + end + end + + # Processes click and drag based on what the user is currently dragging + def process_input + if state.current_input == :star + input_star + elsif state.current_input == :target + input_target + elsif state.current_input == :remove_wall + input_remove_wall + elsif state.current_input == :add_wall + input_add_wall + end + end + + # Moves the star to the cell closest to the mouse in the first grid + # Only resets the search if the star changes position + # Called whenever the user is editing the star (puts mouse down on star) + def input_star + old_star = grid.star.clone + grid.star = cell_closest_to_mouse + unless old_star == grid.star + reset_search + end + end + + # Moves the target to the grid closest to the mouse in the first grid + # Only reset_searchs the search if the target changes position + # Called whenever the user is editing the target (puts mouse down on target) + def input_target + old_target = grid.target.clone + grid.target = cell_closest_to_mouse + unless old_target == grid.target + reset_search + end + end + + # Removes walls in the first grid that are under the cursor + def input_remove_wall + # The mouse needs to be inside the grid, because we only want to remove walls + # the cursor is directly over + # Recalculations should only occur when a wall is actually deleted + if mouse_inside_grid? + if grid.walls.has_key?(cell_closest_to_mouse) + grid.walls.delete(cell_closest_to_mouse) + reset_search + end + end + end + + # Adds a wall in the first grid in the cell the mouse is over + def input_add_wall + if mouse_inside_grid? + unless grid.walls.has_key?(cell_closest_to_mouse) + grid.walls[cell_closest_to_mouse] = true + reset_search + end + end + end + + + # Whenever the user edits the grid, + # The search has to be reset_searchd upto the current step + # with the current grid as the initial state of the grid + def reset_search + # Reset_Searchs the search + search.frontier = [] + search.came_from = {} + search.path = {} + end + + + # Returns a list of adjacent cells + # Used to determine what the next cells to be added to the frontier are + def adjacent_neighbors(cell) + neighbors = [] + + # Gets all the valid neighbors into the array + # From southern neighbor, clockwise + neighbors << [cell.x, cell.y - 1] unless cell.y == 0 + neighbors << [cell.x - 1, cell.y] unless cell.x == 0 + neighbors << [cell.x, cell.y + 1] unless cell.y == grid.height - 1 + neighbors << [cell.x + 1, cell.y] unless cell.x == grid.width - 1 + + # Sorts the neighbors so the rendered path is a zigzag path + # Cells in a diagonal direction are given priority + # Comment this line to see the difference + neighbors = neighbors.sort_by { |neighbor_x, neighbor_y| proximity_to_star(neighbor_x, neighbor_y) } + + neighbors + end + + # Finds the vertical and horizontal distance of a cell from the star + # and returns the larger value + # This method is used to have a zigzag pattern in the rendered path + # A cell that is [5, 5] from the star, + # is explored before over a cell that is [0, 7] away. + # So, if possible, the search tries to go diagonal (zigzag) first + def proximity_to_star(x, y) + distance_x = (grid.star.x - x).abs + distance_y = (grid.star.y - y).abs + + if distance_x > distance_y + return distance_x + else + return distance_y + end + end + + # When the user grabs the star and puts their cursor to the far right + # and moves up and down, the star is supposed to move along the grid as well + # Finding the cell closest to the mouse helps with this + def cell_closest_to_mouse + # Closest cell to the mouse in the first grid + x = (inputs.mouse.point.x / grid.cell_size).to_i + y = (inputs.mouse.point.y / grid.cell_size).to_i + # Bound x and y to the grid + x = grid.width - 1 if x > grid.width - 1 + y = grid.height - 1 if y > grid.height - 1 + # Return closest cell + [x, y] + end + + # Signal that the user is going to be moving the star from the first grid + def star_clicked? + inputs.mouse.down && inputs.mouse.point.inside_rect?(scale_up(grid.star)) + end + + # Signal that the user is going to be moving the target from the first grid + def target_clicked? + inputs.mouse.down && inputs.mouse.point.inside_rect?(scale_up(grid.target)) + end + + # Signal that the user is going to be adding walls from the first grid + def grid_clicked? + inputs.mouse.down && mouse_inside_grid? + end + + # Returns whether the mouse is inside of the first grid + # Part of the condition that checks whether the user is adding a wall + def mouse_inside_grid? + inputs.mouse.point.inside_rect?(scale_up(grid.rect)) + end + + # These methods provide handy aliases to colors + + # Light brown + def unvisited_color + [221, 212, 213] + # [255, 255, 255] + end + + # Camo Green + def wall_color + [134, 134, 120] + end + + # Pastel White + def path_color + [231, 230, 228] + end + + def red + [255, 0, 0] + end + + def purple + [149, 64, 191] + end + + # Makes code more concise + def grid + state.grid + end + + def search + state.search + end +end + +# Method that is called by DragonRuby periodically +# Used for updating animations and calculations +def tick args + + # Pressing r will reset the application + if args.inputs.keyboard.key_down.r + args.gtk.reset + reset + return + end + + # Every tick, new args are passed, and the Breadth First Search tick is called + $breadcrumbs ||= Breadcrumbs.new(args) + $breadcrumbs.args = args + $breadcrumbs.tick +end + + +def reset + $breadcrumbs = nil +end + + # # Representation of how far away visited cells are from the star + # # Replaces the render_visited method + # # Visually demonstrates the effectiveness of early exit for pathfinding + # def render_heat_map + # # THIS CODE NEEDS SOME FIXING DUE TO REFACTORING + # search.came_from.each_key do | cell | + # distance = (grid.star.x - visited_cell.x).abs + (state.star.y - visited_cell.y).abs + # max_distance = grid.width + grid.height + # alpha = 255.to_i * distance.to_i / max_distance.to_i + # outputs.solids << [scale_up(visited_cell), red, alpha] + # # outputs.solids << [early_exit_scale_up(visited_cell), red, alpha] + # end + # end + +</code></pre> +<h3 id='----path-finding-algorithms---early-exit---main.rb'>Path Finding Algorithms - Early Exit - main.rb</h3> +<pre><code class="language-ruby"># ./samples/13_path_finding_algorithms/04_early_exit/app/main.rb +# Comparison of a breadth first search with and without early exit +# Inspired by https://www.redblobgames.com/pathfinding/a-star/introduction.html + +# Demonstrates the exploration difference caused by early exit +# Also demonstrates how breadth first search is used for path generation + +# The left grid is a breadth first search without early exit +# The right grid is a breadth first search with early exit +# The red squares represent how far the search expanded +# The darker the red, the farther the search proceeded +# Comparison of the heat map reveals how much searching can be saved by early exit +# The white path shows path generation via breadth first search +class EarlyExitBreadthFirstSearch + attr_gtk + + # This method is called every frame/tick + # Every tick, the current state of the search is rendered on the screen, + # User input is processed, and + # The next step in the search is calculated + def tick + defaults + # If the grid has not been searched + if state.visited.empty? + # Complete the search + state.max_steps.times { step } + # And calculate the path + calc_path + end + render + input + end + + def defaults + # Variables to edit the size and appearance of the grid + # Freely customizable to user's liking + grid.width ||= 15 + grid.height ||= 15 + grid.cell_size ||= 40 + grid.rect ||= [0, 0, grid.width, grid.height] + + # At some step the animation will end, + # and further steps won't change anything (the whole grid.widthill be explored) + # This step is roughly the grid's width * height + # When anim_steps equals max_steps no more calculations will occur + # and the slider will be at the end + state.max_steps ||= args.state.grid.width * args.state.grid.height + + # The location of the star and walls of the grid + # They can be modified to have a different initial grid + # Walls are stored in a hash for quick look up when doing the search + state.star ||= [2, 8] + state.target ||= [10, 5] + state.walls ||= {} + + # Variables that are used by the breadth first search + # Storing cells that the search has visited, prevents unnecessary steps + # Expanding the frontier of the search in order makes the search expand + # from the center outward + + # Visited cells in the first grid + state.visited ||= {} + # Visited cells in the second grid + state.early_exit_visited ||= {} + # The cells from which the search is to expand + state.frontier ||= [] + # A hash of where each cell was expanded from + # The key is a cell, and the value is the cell it came from + state.came_from ||= {} + # Cells that are part of the path from the target to the star + state.path ||= {} + + # What the user is currently editing on the grid + # We store this value, because we want to remember the value even when + # the user's cursor is no longer over what they're interacting with, but + # they are still clicking down on the mouse. + state.current_input ||= :none + end + + # Draws everything onto the screen + def render + render_background + render_heat_map + render_walls + render_path + render_star + render_target + render_labels + end + + # The methods below subdivide the task of drawing everything to the screen + + # Draws what the grid looks like with nothing on it + def render_background + render_unvisited + render_grid_lines + end + + # Draws both grids + def render_unvisited + outputs.solids << [scale_up(grid.rect), unvisited_color] + outputs.solids << [early_exit_scale_up(grid.rect), unvisited_color] + end + + # Draws grid lines to show the division of the grid into cells + def render_grid_lines + for x in 0..grid.width + outputs.lines << vertical_line(x) + outputs.lines << early_exit_vertical_line(x) + end + + for y in 0..grid.height + outputs.lines << horizontal_line(y) + outputs.lines << early_exit_horizontal_line(y) + end + end + + # Easy way to draw vertical lines given an index + def vertical_line column + scale_up([column, 0, column, grid.height]) + end + + # Easy way to draw horizontal lines given an index + def horizontal_line row + scale_up([0, row, grid.width, row]) + end + + # Easy way to draw vertical lines given an index + def early_exit_vertical_line column + scale_up([column + grid.width + 1, 0, column + grid.width + 1, grid.height]) + end + + # Easy way to draw horizontal lines given an index + def early_exit_horizontal_line row + scale_up([grid.width + 1, row, grid.width + grid.width + 1, row]) + end + + # Draws the walls on both grids + def render_walls + state.walls.each_key do |wall| + outputs.solids << [scale_up(wall), wall_color] + outputs.solids << [early_exit_scale_up(wall), wall_color] + end + end + + # Renders the star on both grids + def render_star + outputs.sprites << [scale_up(state.star), 'star.png'] + outputs.sprites << [early_exit_scale_up(state.star), 'star.png'] + end + + # Renders the target on both grids + def render_target + outputs.sprites << [scale_up(state.target), 'target.png'] + outputs.sprites << [early_exit_scale_up(state.target), 'target.png'] + end + + # Labels the grids + def render_labels + outputs.labels << [200, 625, "Without early exit"] + outputs.labels << [875, 625, "With early exit"] + end + + # Renders the path based off of the state.path hash + def render_path + # If the star and target are disconnected there will only be one path + # The path should not render in that case + unless state.path.size == 1 + state.path.each_key do | cell | + # Renders path on both grids + outputs.solids << [scale_up(cell), path_color] + outputs.solids << [early_exit_scale_up(cell), path_color] + end + end + end + + # Calculates the path from the target to the star after the search is over + # Relies on the came_from hash + # Fills the state.path hash, which is later rendered on screen + def calc_path + endpoint = state.target + while endpoint + state.path[endpoint] = true + endpoint = state.came_from[endpoint] + end + end + + # Representation of how far away visited cells are from the star + # Replaces the render_visited method + # Visually demonstrates the effectiveness of early exit for pathfinding + def render_heat_map + state.visited.each_key do | visited_cell | + distance = (state.star.x - visited_cell.x).abs + (state.star.y - visited_cell.y).abs + max_distance = grid.width + grid.height + alpha = 255.to_i * distance.to_i / max_distance.to_i + outputs.solids << [scale_up(visited_cell), red, alpha] + # outputs.solids << [early_exit_scale_up(visited_cell), red, alpha] + end + + state.early_exit_visited.each_key do | visited_cell | + distance = (state.star.x - visited_cell.x).abs + (state.star.y - visited_cell.y).abs + max_distance = grid.width + grid.height + alpha = 255.to_i * distance.to_i / max_distance.to_i + outputs.solids << [early_exit_scale_up(visited_cell), red, alpha] + end + end + + # Translates the given cell grid.width + 1 to the right and then scales up + # Used to draw cells for the second grid + # This method does not work for lines, + # so separate methods exist for the grid lines + def early_exit_scale_up(cell) + cell_clone = cell.clone + cell_clone.x += grid.width + 1 + scale_up(cell_clone) + end + + # In code, the cells are represented as 1x1 rectangles + # When drawn, the cells are larger than 1x1 rectangles + # This method is used to scale up cells, and lines + # Objects are scaled up according to the grid.cell_size variable + # This allows for easy customization of the visual scale of the grid + def scale_up(cell) + # Prevents the original value of cell from being edited + cell = cell.clone + + # If cell is just an x and y coordinate + if cell.size == 2 + # Add a width and height of 1 + cell << 1 + cell << 1 + end + + # Scale all the values up + cell.map! { |value| value * grid.cell_size } + + # Returns the scaled up cell + cell + end + + # This method processes user input every tick + # Any method with "1" is related to the first grid + # Any method with "2" is related to the second grid + def input + # The program has to remember that the user is dragging an object + # even when the mouse is no longer over that object + # So detecting input and processing input is separate + detect_input + process_input + end + + # Determines what the user is editing and stores the value + # Storing the value allows the user to continue the same edit as long as the + # mouse left click is held + def detect_input + # When the mouse is up, nothing is being edited + if inputs.mouse.up + state.current_input = :none + # When the star in the no second grid is clicked + elsif star_clicked? + state.current_input = :star + # When the star in the second grid is clicked + elsif star2_clicked? + state.current_input = :star2 + # When the target in the no second grid is clicked + elsif target_clicked? + state.current_input = :target + # When the target in the second grid is clicked + elsif target2_clicked? + state.current_input = :target2 + # When a wall in the first grid is clicked + elsif wall_clicked? + state.current_input = :remove_wall + # When a wall in the second grid is clicked + elsif wall2_clicked? + state.current_input = :remove_wall2 + # When the first grid is clicked + elsif grid_clicked? + state.current_input = :add_wall + # When the second grid is clicked + elsif grid2_clicked? + state.current_input = :add_wall2 + end + end + + # Processes click and drag based on what the user is currently dragging + def process_input + if state.current_input == :star + input_star + elsif state.current_input == :star2 + input_star2 + elsif state.current_input == :target + input_target + elsif state.current_input == :target2 + input_target2 + elsif state.current_input == :remove_wall + input_remove_wall + elsif state.current_input == :remove_wall2 + input_remove_wall2 + elsif state.current_input == :add_wall + input_add_wall + elsif state.current_input == :add_wall2 + input_add_wall2 + end + end + + # Moves the star to the cell closest to the mouse in the first grid + # Only resets the search if the star changes position + # Called whenever the user is editing the star (puts mouse down on star) + def input_star + old_star = state.star.clone + state.star = cell_closest_to_mouse + unless old_star == state.star + reset_search + end + end + + # Moves the star to the cell closest to the mouse in the second grid + # Only resets the search if the star changes position + # Called whenever the user is editing the star (puts mouse down on star) + def input_star2 + old_star = state.star.clone + state.star = cell_closest_to_mouse2 + unless old_star == state.star + reset_search + end + end + + # Moves the target to the grid closest to the mouse in the first grid + # Only reset_searchs the search if the target changes position + # Called whenever the user is editing the target (puts mouse down on target) + def input_target + old_target = state.target.clone + state.target = cell_closest_to_mouse + unless old_target == state.target + reset_search + end + end + + # Moves the target to the cell closest to the mouse in the second grid + # Only reset_searchs the search if the target changes position + # Called whenever the user is editing the target (puts mouse down on target) + def input_target2 + old_target = state.target.clone + state.target = cell_closest_to_mouse2 + unless old_target == state.target + reset_search + end + end + + # Removes walls in the first grid that are under the cursor + def input_remove_wall + # The mouse needs to be inside the grid, because we only want to remove walls + # the cursor is directly over + # Recalculations should only occur when a wall is actually deleted + if mouse_inside_grid? + if state.walls.has_key?(cell_closest_to_mouse) + state.walls.delete(cell_closest_to_mouse) + reset_search + end + end + end + + # Removes walls in the second grid that are under the cursor + def input_remove_wall2 + # The mouse needs to be inside the grid, because we only want to remove walls + # the cursor is directly over + # Recalculations should only occur when a wall is actually deleted + if mouse_inside_grid2? + if state.walls.has_key?(cell_closest_to_mouse2) + state.walls.delete(cell_closest_to_mouse2) + reset_search + end + end + end + + # Adds a wall in the first grid in the cell the mouse is over + def input_add_wall + if mouse_inside_grid? + unless state.walls.has_key?(cell_closest_to_mouse) + state.walls[cell_closest_to_mouse] = true + reset_search + end + end + end + + + # Adds a wall in the second grid in the cell the mouse is over + def input_add_wall2 + if mouse_inside_grid2? + unless state.walls.has_key?(cell_closest_to_mouse2) + state.walls[cell_closest_to_mouse2] = true + reset_search + end + end + end + + # Whenever the user edits the grid, + # The search has to be reset_searchd upto the current step + # with the current grid as the initial state of the grid + def reset_search + # Reset_Searchs the search + state.frontier = [] + state.visited = {} + state.early_exit_visited = {} + state.came_from = {} + state.path = {} + end + + # Moves the search forward one step + def step + # The setup to the search + # Runs once when there are no visited cells + if state.visited.empty? + state.visited[state.star] = true + state.early_exit_visited[state.star] = true + state.frontier << state.star + state.came_from[state.star] = nil + end + + # A step in the search + unless state.frontier.empty? + # Takes the next frontier cell + new_frontier = state.frontier.shift + # For each of its neighbors + adjacent_neighbors(new_frontier).each do |neighbor| + # That have not been visited and are not walls + unless state.visited.has_key?(neighbor) || state.walls.has_key?(neighbor) + # Add them to the frontier and mark them as visited in the first grid + state.visited[neighbor] = true + # Unless the target has been visited + unless state.visited.has_key?(state.target) + # Mark the neighbor as visited in the second grid as well + state.early_exit_visited[neighbor] = true + end + + # Add the neighbor to the frontier and remember which cell it came from + state.frontier << neighbor + state.came_from[neighbor] = new_frontier + end + end + end + end + + + # Returns a list of adjacent cells + # Used to determine what the next cells to be added to the frontier are + def adjacent_neighbors(cell) + neighbors = [] + + # Gets all the valid neighbors into the array + # From southern neighbor, clockwise + neighbors << [cell.x, cell.y - 1] unless cell.y == 0 + neighbors << [cell.x - 1, cell.y] unless cell.x == 0 + neighbors << [cell.x, cell.y + 1] unless cell.y == grid.height - 1 + neighbors << [cell.x + 1, cell.y] unless cell.x == grid.width - 1 + + # Sorts the neighbors so the rendered path is a zigzag path + # Cells in a diagonal direction are given priority + # Comment this line to see the difference + neighbors = neighbors.sort_by { |neighbor_x, neighbor_y| proximity_to_star(neighbor_x, neighbor_y) } + + neighbors + end + + # Finds the vertical and horizontal distance of a cell from the star + # and returns the larger value + # This method is used to have a zigzag pattern in the rendered path + # A cell that is [5, 5] from the star, + # is explored before over a cell that is [0, 7] away. + # So, if possible, the search tries to go diagonal (zigzag) first + def proximity_to_star(x, y) + distance_x = (state.star.x - x).abs + distance_y = (state.star.y - y).abs + + if distance_x > distance_y + return distance_x + else + return distance_y + end + end + + # When the user grabs the star and puts their cursor to the far right + # and moves up and down, the star is supposed to move along the grid as well + # Finding the cell closest to the mouse helps with this + def cell_closest_to_mouse + # Closest cell to the mouse in the first grid + x = (inputs.mouse.point.x / grid.cell_size).to_i + y = (inputs.mouse.point.y / grid.cell_size).to_i + # Bound x and y to the grid + x = grid.width - 1 if x > grid.width - 1 + y = grid.height - 1 if y > grid.height - 1 + # Return closest cell + [x, y] + end + + # When the user grabs the star and puts their cursor to the far right + # and moves up and down, the star is supposed to move along the grid as well + # Finding the cell closest to the mouse in the second grid helps with this + def cell_closest_to_mouse2 + # Closest cell grid to the mouse in the second + x = (inputs.mouse.point.x / grid.cell_size).to_i + y = (inputs.mouse.point.y / grid.cell_size).to_i + # Translate the cell to the first grid + x -= grid.width + 1 + # Bound x and y to the first grid + x = grid.width - 1 if x > grid.width - 1 + y = grid.height - 1 if y > grid.height - 1 + # Return closest cell + [x, y] + end + + # Signal that the user is going to be moving the star from the first grid + def star_clicked? + inputs.mouse.down && inputs.mouse.point.inside_rect?(scale_up(state.star)) + end + + # Signal that the user is going to be moving the star from the second grid + def star2_clicked? + inputs.mouse.down && inputs.mouse.point.inside_rect?(early_exit_scale_up(state.star)) + end + + # Signal that the user is going to be moving the target from the first grid + def target_clicked? + inputs.mouse.down && inputs.mouse.point.inside_rect?(scale_up(state.target)) + end + + # Signal that the user is going to be moving the target from the second grid + def target2_clicked? + inputs.mouse.down && inputs.mouse.point.inside_rect?(early_exit_scale_up(state.target)) + end + + # Signal that the user is going to be removing walls from the first grid + def wall_clicked? + inputs.mouse.down && mouse_inside_wall? + end + + # Signal that the user is going to be removing walls from the second grid + def wall2_clicked? + inputs.mouse.down && mouse_inside_wall2? + end + + # Signal that the user is going to be adding walls from the first grid + def grid_clicked? + inputs.mouse.down && mouse_inside_grid? + end + + # Signal that the user is going to be adding walls from the second grid + def grid2_clicked? + inputs.mouse.down && mouse_inside_grid2? + end + + # Returns whether the mouse is inside of a wall in the first grid + # Part of the condition that checks whether the user is removing a wall + def mouse_inside_wall? + state.walls.each_key do | wall | + return true if inputs.mouse.point.inside_rect?(scale_up(wall)) + end + + false + end + + # Returns whether the mouse is inside of a wall in the second grid + # Part of the condition that checks whether the user is removing a wall + def mouse_inside_wall2? + state.walls.each_key do | wall | + return true if inputs.mouse.point.inside_rect?(early_exit_scale_up(wall)) + end + + false + end + + # Returns whether the mouse is inside of the first grid + # Part of the condition that checks whether the user is adding a wall + def mouse_inside_grid? + inputs.mouse.point.inside_rect?(scale_up(grid.rect)) + end + + # Returns whether the mouse is inside of the second grid + # Part of the condition that checks whether the user is adding a wall + def mouse_inside_grid2? + inputs.mouse.point.inside_rect?(early_exit_scale_up(grid.rect)) + end + + # These methods provide handy aliases to colors + + # Light brown + def unvisited_color + [221, 212, 213] + end + + # Camo Green + def wall_color + [134, 134, 120] + end + + # Pastel White + def path_color + [231, 230, 228] + end + + def red + [255, 0, 0] + end + + # Makes code more concise + def grid + state.grid + end +end + +# Method that is called by DragonRuby periodically +# Used for updating animations and calculations +def tick args + + # Pressing r will reset the application + if args.inputs.keyboard.key_down.r + args.gtk.reset + reset + return + end + + # Every tick, new args are passed, and the Breadth First Search tick is called + $early_exit_breadth_first_search ||= EarlyExitBreadthFirstSearch.new(args) + $early_exit_breadth_first_search.args = args + $early_exit_breadth_first_search.tick +end + + +def reset + $early_exit_breadth_first_search = nil +end + +</code></pre> +<h3 id='----path-finding-algorithms---dijkstra---main.rb'>Path Finding Algorithms - Dijkstra - main.rb</h3> +<pre><code class="language-ruby"># ./samples/13_path_finding_algorithms/05_dijkstra/app/main.rb +# Demonstrates how Dijkstra's Algorithm allows movement costs to be considered + +# Inspired by https://www.redblobgames.com/pathfinding/a-star/introduction.html + +# The first grid is a breadth first search with an early exit. +# It shows a heat map of all the cells that were visited by the search and their relative distance. + +# The second grid is an implementation of Dijkstra's algorithm. +# Light green cells have 5 times the movement cost of regular cells. +# The heat map will darken based on movement cost. + +# Dark green cells are walls, and the search cannot go through them. +class Movement_Costs + attr_gtk + + # This method is called every frame/tick + # Every tick, the current state of the search is rendered on the screen, + # User input is processed, and + # The next step in the search is calculated + def tick + defaults + render + input + calc + end + + def defaults + # Variables to edit the size and appearance of the grid + # Freely customizable to user's liking + grid.width ||= 10 + grid.height ||= 10 + grid.cell_size ||= 60 + grid.rect ||= [0, 0, grid.width, grid.height] + + # The location of the star and walls of the grid + # They can be modified to have a different initial grid + # Walls are stored in a hash for quick look up when doing the search + state.star ||= [1, 5] + state.target ||= [8, 4] + state.walls ||= {[1, 1] => true, [2, 1] => true, [3, 1] => true, [1, 2] => true, [2, 2] => true, [3, 2] => true} + state.hills ||= { + [4, 1] => true, + [5, 1] => true, + [4, 2] => true, + [5, 2] => true, + [6, 2] => true, + [4, 3] => true, + [5, 3] => true, + [6, 3] => true, + [3, 4] => true, + [4, 4] => true, + [5, 4] => true, + [6, 4] => true, + [7, 4] => true, + [3, 5] => true, + [4, 5] => true, + [5, 5] => true, + [6, 5] => true, + [7, 5] => true, + [4, 6] => true, + [5, 6] => true, + [6, 6] => true, + [7, 6] => true, + [4, 7] => true, + [5, 7] => true, + [6, 7] => true, + [4, 8] => true, + [5, 8] => true, + } + + # What the user is currently editing on the grid + # We store this value, because we want to remember the value even when + # the user's cursor is no longer over what they're interacting with, but + # they are still clicking down on the mouse. + state.user_input ||= :none + + # Values that are used for the breadth first search + # Keeping track of what cells were visited prevents counting cells multiple times + breadth_first_search.visited ||= {} + # The cells from which the breadth first search will expand + breadth_first_search.frontier ||= [] + # Keeps track of which cell all cells were searched from + # Used to recreate the path from the target to the star + breadth_first_search.came_from ||= {} + + # Keeps track of the movement cost so far to be at a cell + # Allows the costs of new cells to be quickly calculated + # Also doubles as a way to check if cells have already been visited + dijkstra_search.cost_so_far ||= {} + # The cells from which the Dijkstra search will expand + dijkstra_search.frontier ||= [] + # Keeps track of which cell all cells were searched from + # Used to recreate the path from the target to the star + dijkstra_search.came_from ||= {} + end + + # Draws everything onto the screen + def render + render_background + + render_heat_maps + + render_star + render_target + render_hills + render_walls + + render_paths + end + # The methods below subdivide the task of drawing everything to the screen + + # Draws what the grid looks like with nothing on it + def render_background + render_unvisited + render_grid_lines + render_labels + end + + # Draws two rectangles the size of the grid in the default cell color + # Used as part of the background + def render_unvisited + outputs.solids << [scale_up(grid.rect), unvisited_color] + outputs.solids << [move_and_scale_up(grid.rect), unvisited_color] + end + + # Draws grid lines to show the division of the grid into cells + def render_grid_lines + for x in 0..grid.width + outputs.lines << vertical_line(x) + outputs.lines << shifted_vertical_line(x) + end + + for y in 0..grid.height + outputs.lines << horizontal_line(y) + outputs.lines << shifted_horizontal_line(y) + end + end + + # Easy way to draw vertical lines given an index for the first grid + def vertical_line column + scale_up([column, 0, column, grid.height]) + end + + # Easy way to draw horizontal lines given an index for the second grid + def horizontal_line row + scale_up([0, row, grid.width, row]) + end + + # Easy way to draw vertical lines given an index for the first grid + def shifted_vertical_line column + scale_up([column + grid.width + 1, 0, column + grid.width + 1, grid.height]) + end + + # Easy way to draw horizontal lines given an index for the second grid + def shifted_horizontal_line row + scale_up([grid.width + 1, row, grid.width + grid.width + 1, row]) + end + + # Labels the grids + def render_labels + outputs.labels << [175, 650, "Number of steps", 3] + outputs.labels << [925, 650, "Distance", 3] + end + + def render_paths + render_breadth_first_search_path + render_dijkstra_path + end + + def render_heat_maps + render_breadth_first_search_heat_map + render_dijkstra_heat_map + end + + # Renders the breadth first search on the first grid + def render_breadth_first_search + end + + # This heat map shows the cells explored by the breadth first search and how far they are from the star. + def render_breadth_first_search_heat_map + # For each cell explored + breadth_first_search.visited.each_key do | visited_cell | + # Find its distance from the star + distance = (state.star.x - visited_cell.x).abs + (state.star.y - visited_cell.y).abs + max_distance = grid.width + grid.height + # Get it as a percent of the maximum distance and scale to 255 for use as an alpha value + alpha = 255.to_i * distance.to_i / max_distance.to_i + outputs.solids << [scale_up(visited_cell), red, alpha] + end + end + + def render_breadth_first_search_path + # If the search found the target + if breadth_first_search.visited.has_key?(state.target) + # Start from the target + endpoint = state.target + # And the cell it came from + next_endpoint = breadth_first_search.came_from[endpoint] + while endpoint and next_endpoint + # Draw a path between these two cells + path = get_path_between(endpoint, next_endpoint) + outputs.solids << [scale_up(path), path_color] + # And get the next pair of cells + endpoint = next_endpoint + next_endpoint = breadth_first_search.came_from[endpoint] + # Continue till there are no more cells + end + end + end + + # Renders the Dijkstra search on the second grid + def render_dijkstra + end + + def render_dijkstra_heat_map + dijkstra_search.cost_so_far.each do |visited_cell, cost| + max_cost = (grid.width + grid.height) #* 5 + alpha = 255.to_i * cost.to_i / max_cost.to_i + outputs.solids << [move_and_scale_up(visited_cell), red, alpha] + end + end + + def render_dijkstra_path + # If the search found the target + if dijkstra_search.came_from.has_key?(state.target) + # Get the target and the cell it came from + endpoint = state.target + next_endpoint = dijkstra_search.came_from[endpoint] + while endpoint and next_endpoint + # Draw a path between them + path = get_path_between(endpoint, next_endpoint) + outputs.solids << [move_and_scale_up(path), path_color] + + # Shift one cell down the path + endpoint = next_endpoint + next_endpoint = dijkstra_search.came_from[endpoint] + + # Repeat till the end of the path + end + end + end + + # Renders the star on both grids + def render_star + outputs.sprites << [scale_up(state.star), 'star.png'] + outputs.sprites << [move_and_scale_up(state.star), 'star.png'] + end + + # Renders the target on both grids + def render_target + outputs.sprites << [scale_up(state.target), 'target.png'] + outputs.sprites << [move_and_scale_up(state.target), 'target.png'] + end + + def render_hills + state.hills.each_key do |hill| + outputs.solids << [scale_up(hill), hill_color] + outputs.solids << [move_and_scale_up(hill), hill_color] + end + end + + # Draws the walls on both grids + def render_walls + state.walls.each_key do |wall| + outputs.solids << [scale_up(wall), wall_color] + outputs.solids << [move_and_scale_up(wall), wall_color] + end + end + + def get_path_between(cell_one, cell_two) + path = nil + if cell_one.x == cell_two.x + if cell_one.y < cell_two.y + path = [cell_one.x + 0.3, cell_one.y + 0.3, 0.4, 1.4] + else + path = [cell_two.x + 0.3, cell_two.y + 0.3, 0.4, 1.4] + end + else + if cell_one.x < cell_two.x + path = [cell_one.x + 0.3, cell_one.y + 0.3, 1.4, 0.4] + else + path = [cell_two.x + 0.3, cell_two.y + 0.3, 1.4, 0.4] + end + end + path + end + + # Representation of how far away visited cells are from the star + # Replaces the render_visited method + # Visually demonstrates the effectiveness of early exit for pathfinding + def render_breadth_first_search_heat_map + breadth_first_search.visited.each_key do | visited_cell | + distance = (state.star.x - visited_cell.x).abs + (state.star.y - visited_cell.y).abs + max_distance = grid.width + grid.height + alpha = 255.to_i * distance.to_i / max_distance.to_i + outputs.solids << [scale_up(visited_cell), red, alpha] + end + end + + # Translates the given cell grid.width + 1 to the right and then scales up + # Used to draw cells for the second grid + # This method does not work for lines, + # so separate methods exist for the grid lines + def move_and_scale_up(cell) + cell_clone = cell.clone + cell_clone.x += grid.width + 1 + scale_up(cell_clone) + end + + # In code, the cells are represented as 1x1 rectangles + # When drawn, the cells are larger than 1x1 rectangles + # This method is used to scale up cells, and lines + # Objects are scaled up according to the grid.cell_size variable + # This allows for easy customization of the visual scale of the grid + def scale_up(cell) + # Prevents the original value of cell from being edited + cell = cell.clone + + # If cell is just an x and y coordinate + if cell.size == 2 + # Add a width and height of 1 + cell << 1 + cell << 1 + end + + # Scale all the values up + cell.map! { |value| value * grid.cell_size } + + # Returns the scaled up cell + cell + end + + # Handles user input every tick so the grid can be edited + # Separate input detection and processing is needed + # For example: Adding walls is started by clicking down on a hill, + # but the mouse doesn't need to remain over hills to add walls + def input + # If the mouse was lifted this tick + if inputs.mouse.up + # Set current input to none + state.user_input = :none + end + + # If the mouse was clicked this tick + if inputs.mouse.down + # Determine what the user is editing and edit the state.user_input variable + determine_input + end + + # Process user input based on user_input variable and current mouse position + process_input + end + + # Determines what the user is editing and stores the value + # This method is called the tick the mouse is clicked + # Storing the value allows the user to continue the same edit as long as the + # mouse left click is held + def determine_input + # If the mouse is over the star in the first grid + if mouse_over_star? + # The user is editing the star from the first grid + state.user_input = :star + # If the mouse is over the star in the second grid + elsif mouse_over_star2? + # The user is editing the star from the second grid + state.user_input = :star2 + # If the mouse is over the target in the first grid + elsif mouse_over_target? + # The user is editing the target from the first grid + state.user_input = :target + # If the mouse is over the target in the second grid + elsif mouse_over_target2? + # The user is editing the target from the second grid + state.user_input = :target2 + # If the mouse is over a wall in the first grid + elsif mouse_over_wall? + # The user is removing a wall from the first grid + state.user_input = :remove_wall + # If the mouse is over a wall in the second grid + elsif mouse_over_wall2? + # The user is removing a wall from the second grid + state.user_input = :remove_wall2 + # If the mouse is over a hill in the first grid + elsif mouse_over_hill? + # The user is adding a wall from the first grid + state.user_input = :add_wall + # If the mouse is over a hill in the second grid + elsif mouse_over_hill2? + # The user is adding a wall from the second grid + state.user_input = :add_wall2 + # If the mouse is over the first grid + elsif mouse_over_grid? + # The user is adding a hill from the first grid + state.user_input = :add_hill + # If the mouse is over the second grid + elsif mouse_over_grid2? + # The user is adding a hill from the second grid + state.user_input = :add_hill2 + end + end + + # Processes click and drag based on what the user is currently dragging + def process_input + if state.user_input == :star + input_star + elsif state.user_input == :star2 + input_star2 + elsif state.user_input == :target + input_target + elsif state.user_input == :target2 + input_target2 + elsif state.user_input == :remove_wall + input_remove_wall + elsif state.user_input == :remove_wall2 + input_remove_wall2 + elsif state.user_input == :add_hill + input_add_hill + elsif state.user_input == :add_hill2 + input_add_hill2 + elsif state.user_input == :add_wall + input_add_wall + elsif state.user_input == :add_wall2 + input_add_wall2 + end + end + + # Calculates the two searches + def calc + # If the searches have not started + if breadth_first_search.visited.empty? + # Calculate the two searches + calc_breadth_first + calc_dijkstra + end + end + + + def calc_breadth_first + # Sets up the Breadth First Search + breadth_first_search.visited[state.star] = true + breadth_first_search.frontier << state.star + breadth_first_search.came_from[state.star] = nil + + until breadth_first_search.frontier.empty? + return if breadth_first_search.visited.has_key?(state.target) + # A step in the search + # Takes the next frontier cell + new_frontier = breadth_first_search.frontier.shift + # For each of its neighbors + adjacent_neighbors(new_frontier).each do | neighbor | + # That have not been visited and are not walls + unless breadth_first_search.visited.has_key?(neighbor) || state.walls.has_key?(neighbor) + # Add them to the frontier and mark them as visited in the first grid + breadth_first_search.visited[neighbor] = true + breadth_first_search.frontier << neighbor + # Remember which cell the neighbor came from + breadth_first_search.came_from[neighbor] = new_frontier + end + end + end + end + + # Calculates the Dijkstra Search from the beginning to the end + + def calc_dijkstra + # The initial values for the Dijkstra search + dijkstra_search.frontier << [state.star, 0] + dijkstra_search.came_from[state.star] = nil + dijkstra_search.cost_so_far[state.star] = 0 + + # Until their are no more cells to be explored + until dijkstra_search.frontier.empty? + # Get the next cell to be explored from + # We get the first element of the array which is the cell. The second element is the priority. + current = dijkstra_search.frontier.shift[0] + + # Stop the search if we found the target + return if current == state.target + + # For each of the neighbors + adjacent_neighbors(current).each do | neighbor | + # Unless this cell is a wall or has already been explored. + unless dijkstra_search.came_from.has_key?(neighbor) or state.walls.has_key?(neighbor) + # Calculate the movement cost of getting to this cell and memo + new_cost = dijkstra_search.cost_so_far[current] + cost(neighbor) + dijkstra_search.cost_so_far[neighbor] = new_cost + + # Add this neighbor to the cells too be explored + dijkstra_search.frontier << [neighbor, new_cost] + dijkstra_search.came_from[neighbor] = current + end + end + + # Sort the frontier so exploration occurs that have a low cost so far. + # My implementation of a priority queue + dijkstra_search.frontier = dijkstra_search.frontier.sort_by {|cell, priority| priority} + end + end + + def cost(cell) + if state.hills.has_key?(cell) + return 5 + else + return 1 + end + end + + + + + # Moves the star to the cell closest to the mouse in the first grid + # Only resets the search if the star changes position + # Called whenever the user is editing the star (puts mouse down on star) + def input_star + old_star = state.star.clone + unless cell_closest_to_mouse == state.target + state.star = cell_closest_to_mouse + end + unless old_star == state.star + reset_search + end + end + + # Moves the star to the cell closest to the mouse in the second grid + # Only resets the search if the star changes position + # Called whenever the user is editing the star (puts mouse down on star) + def input_star2 + old_star = state.star.clone + unless cell_closest_to_mouse2 == state.target + state.star = cell_closest_to_mouse2 + end + unless old_star == state.star + reset_search + end + end + + # Moves the target to the grid closest to the mouse in the first grid + # Only reset_searchs the search if the target changes position + # Called whenever the user is editing the target (puts mouse down on target) + def input_target + old_target = state.target.clone + unless cell_closest_to_mouse == state.star + state.target = cell_closest_to_mouse + end + unless old_target == state.target + reset_search + end + end + + # Moves the target to the cell closest to the mouse in the second grid + # Only reset_searchs the search if the target changes position + # Called whenever the user is editing the target (puts mouse down on target) + def input_target2 + old_target = state.target.clone + unless cell_closest_to_mouse2 == state.star + state.target = cell_closest_to_mouse2 + end + unless old_target == state.target + reset_search + end + end + + # Removes walls in the first grid that are under the cursor + def input_remove_wall + # The mouse needs to be inside the grid, because we only want to remove walls + # the cursor is directly over + # Recalculations should only occur when a wall is actually deleted + if mouse_over_grid? + if state.walls.has_key?(cell_closest_to_mouse) or state.hills.has_key?(cell_closest_to_mouse) + state.walls.delete(cell_closest_to_mouse) + state.hills.delete(cell_closest_to_mouse) + reset_search + end + end + end + + # Removes walls in the second grid that are under the cursor + def input_remove_wall2 + # The mouse needs to be inside the grid, because we only want to remove walls + # the cursor is directly over + # Recalculations should only occur when a wall is actually deleted + if mouse_over_grid2? + if state.walls.has_key?(cell_closest_to_mouse2) or state.hills.has_key?(cell_closest_to_mouse2) + state.walls.delete(cell_closest_to_mouse2) + state.hills.delete(cell_closest_to_mouse2) + reset_search + end + end + end + + # Adds a hill in the first grid in the cell the mouse is over + def input_add_hill + if mouse_over_grid? + unless state.hills.has_key?(cell_closest_to_mouse) + state.hills[cell_closest_to_mouse] = true + reset_search + end + end + end + + + # Adds a hill in the second grid in the cell the mouse is over + def input_add_hill2 + if mouse_over_grid2? + unless state.hills.has_key?(cell_closest_to_mouse2) + state.hills[cell_closest_to_mouse2] = true + reset_search + end + end + end + + # Adds a wall in the first grid in the cell the mouse is over + def input_add_wall + if mouse_over_grid? + unless state.walls.has_key?(cell_closest_to_mouse) + state.hills.delete(cell_closest_to_mouse) + state.walls[cell_closest_to_mouse] = true + reset_search + end + end + end + + # Adds a wall in the second grid in the cell the mouse is over + def input_add_wall2 + if mouse_over_grid2? + unless state.walls.has_key?(cell_closest_to_mouse2) + state.hills.delete(cell_closest_to_mouse2) + state.walls[cell_closest_to_mouse2] = true + reset_search + end + end + end + + # Whenever the user edits the grid, + # The search has to be reset_searchd upto the current step + # with the current grid as the initial state of the grid + def reset_search + breadth_first_search.visited = {} + breadth_first_search.frontier = [] + breadth_first_search.came_from = {} + + dijkstra_search.frontier = [] + dijkstra_search.came_from = {} + dijkstra_search.cost_so_far = {} + end + + + + # Returns a list of adjacent cells + # Used to determine what the next cells to be added to the frontier are + def adjacent_neighbors(cell) + neighbors = [] + + # Gets all the valid neighbors into the array + # From southern neighbor, clockwise + neighbors << [cell.x , cell.y - 1] unless cell.y == 0 + neighbors << [cell.x - 1, cell.y ] unless cell.x == 0 + neighbors << [cell.x , cell.y + 1] unless cell.y == grid.height - 1 + neighbors << [cell.x + 1, cell.y ] unless cell.x == grid.width - 1 + + # Sorts the neighbors so the rendered path is a zigzag path + # Cells in a diagonal direction are given priority + # Comment this line to see the difference + neighbors = neighbors.sort_by { |neighbor_x, neighbor_y| proximity_to_star(neighbor_x, neighbor_y) } + + neighbors + end + + # Finds the vertical and horizontal distance of a cell from the star + # and returns the larger value + # This method is used to have a zigzag pattern in the rendered path + # A cell that is [5, 5] from the star, + # is explored before over a cell that is [0, 7] away. + # So, if possible, the search tries to go diagonal (zigzag) first + def proximity_to_star(x, y) + distance_x = (state.star.x - x).abs + distance_y = (state.star.y - y).abs + + if distance_x > distance_y + return distance_x + else + return distance_y + end + end + + # When the user grabs the star and puts their cursor to the far right + # and moves up and down, the star is supposed to move along the grid as well + # Finding the cell closest to the mouse helps with this + def cell_closest_to_mouse + # Closest cell to the mouse in the first grid + x = (inputs.mouse.point.x / grid.cell_size).to_i + y = (inputs.mouse.point.y / grid.cell_size).to_i + # Bound x and y to the grid + x = grid.width - 1 if x > grid.width - 1 + y = grid.height - 1 if y > grid.height - 1 + # Return closest cell + [x, y] + end + + # When the user grabs the star and puts their cursor to the far right + # and moves up and down, the star is supposed to move along the grid as well + # Finding the cell closest to the mouse in the second grid helps with this + def cell_closest_to_mouse2 + # Closest cell grid to the mouse in the second + x = (inputs.mouse.point.x / grid.cell_size).to_i + y = (inputs.mouse.point.y / grid.cell_size).to_i + # Translate the cell to the first grid + x -= grid.width + 1 + # Bound x and y to the first grid + x = 0 if x < 0 + y = 0 if y < 0 + x = grid.width - 1 if x > grid.width - 1 + y = grid.height - 1 if y > grid.height - 1 + # Return closest cell + [x, y] + end + + # Signal that the user is going to be moving the star from the first grid + def mouse_over_star? + inputs.mouse.point.inside_rect?(scale_up(state.star)) + end + + # Signal that the user is going to be moving the star from the second grid + def mouse_over_star2? + inputs.mouse.point.inside_rect?(move_and_scale_up(state.star)) + end + + # Signal that the user is going to be moving the target from the first grid + def mouse_over_target? + inputs.mouse.point.inside_rect?(scale_up(state.target)) + end + + # Signal that the user is going to be moving the target from the second grid + def mouse_over_target2? + inputs.mouse.point.inside_rect?(move_and_scale_up(state.target)) + end + + # Signal that the user is going to be removing walls from the first grid + def mouse_over_wall? + state.walls.each_key do | wall | + return true if inputs.mouse.point.inside_rect?(scale_up(wall)) + end + + false + end + + # Signal that the user is going to be removing walls from the second grid + def mouse_over_wall2? + state.walls.each_key do | wall | + return true if inputs.mouse.point.inside_rect?(move_and_scale_up(wall)) + end + + false + end + + # Signal that the user is going to be removing hills from the first grid + def mouse_over_hill? + state.hills.each_key do | hill | + return true if inputs.mouse.point.inside_rect?(scale_up(hill)) + end + + false + end + + # Signal that the user is going to be removing hills from the second grid + def mouse_over_hill2? + state.hills.each_key do | hill | + return true if inputs.mouse.point.inside_rect?(move_and_scale_up(hill)) + end + + false + end + + # Signal that the user is going to be adding walls from the first grid + def mouse_over_grid? + inputs.mouse.point.inside_rect?(scale_up(grid.rect)) + end + + # Signal that the user is going to be adding walls from the second grid + def mouse_over_grid2? + inputs.mouse.point.inside_rect?(move_and_scale_up(grid.rect)) + end + + # These methods provide handy aliases to colors + + # Light brown + def unvisited_color + [221, 212, 213] + end + + # Camo Green + def wall_color + [134, 134, 120] + end + + # Pastel White + def path_color + [231, 230, 228] + end + + def red + [255, 0, 0] + end + + # A Green + def hill_color + [139, 173, 132] + end + + # Makes code more concise + def grid + state.grid + end + + def breadth_first_search + state.breadth_first_search + end + + def dijkstra_search + state.dijkstra_search + end +end + +# Method that is called by DragonRuby periodically +# Used for updating animations and calculations +def tick args + + # Pressing r will reset the application + if args.inputs.keyboard.key_down.r + args.gtk.reset + reset + return + end + + # Every tick, new args are passed, and the Dijkstra tick method is called + $movement_costs ||= Movement_Costs.new(args) + $movement_costs.args = args + $movement_costs.tick +end + + +def reset + $movement_costs = nil +end + +</code></pre> +<h3 id='----path-finding-algorithms---heuristic---main.rb'>Path Finding Algorithms - Heuristic - main.rb</h3> +<pre><code class="language-ruby"># ./samples/13_path_finding_algorithms/06_heuristic/app/main.rb +# This program is inspired by https://www.redblobgames.com/pathfinding/a-star/introduction.html + +# This time the heuristic search still explored less of the grid, hence finishing faster. +# However, it did not find the shortest path between the star and the target. + +# The only difference between this app and Heuristic is the change of the starting position. + +class Heuristic_With_Walls + attr_gtk + + def tick + defaults + render + input + # If animation is playing, and max steps have not been reached + # Move the search a step forward + if state.play && state.current_step < state.max_steps + # Variable that tells the program what step to recalculate up to + state.current_step += 1 + move_searches_one_step_forward + end + end + + def defaults + # Variables to edit the size and appearance of the grid + # Freely customizable to user's liking + grid.width ||= 15 + grid.height ||= 15 + grid.cell_size ||= 40 + grid.rect ||= [0, 0, grid.width, grid.height] + + grid.star ||= [0, 2] + grid.target ||= [14, 12] + grid.walls ||= {} + # There are no hills in the Heuristic Search Demo + + # What the user is currently editing on the grid + # We store this value, because we want to remember the value even when + # the user's cursor is no longer over what they're interacting with, but + # they are still clicking down on the mouse. + state.user_input ||= :none + + # These variables allow the breadth first search to take place + # Came_from is a hash with a key of a cell and a value of the cell that was expanded from to find the key. + # Used to prevent searching cells that have already been found + # and to trace a path from the target back to the starting point. + # Frontier is an array of cells to expand the search from. + # The search is over when there are no more cells to search from. + # Path stores the path from the target to the star, once the target has been found + # It prevents calculating the path every tick. + bfs.came_from ||= {} + bfs.frontier ||= [] + bfs.path ||= [] + + heuristic.came_from ||= {} + heuristic.frontier ||= [] + heuristic.path ||= [] + + # Stores which step of the animation is being rendered + # When the user moves the star or messes with the walls, + # the searches are recalculated up to this step + + # Unless the current step has a value + unless state.current_step + # Set the current step to 10 + state.current_step = 10 + # And calculate the searches up to step 10 + recalculate_searches + end + + # At some step the animation will end, + # and further steps won't change anything (the whole grid will be explored) + # This step is roughly the grid's width * height + # When anim_steps equals max_steps no more calculations will occur + # and the slider will be at the end + state.max_steps = grid.width * grid.height + + # Whether the animation should play or not + # If true, every tick moves anim_steps forward one + # Pressing the stepwise animation buttons will pause the animation + # An if statement instead of the ||= operator is used for assigning a boolean value. + # The || operator does not differentiate between nil and false. + if state.play == nil + state.play = false + end + + # Store the rects of the buttons that control the animation + # They are here for user customization + # Editing these might require recentering the text inside them + # Those values can be found in the render_button methods + buttons.left = [470, 600, 50, 50] + buttons.center = [520, 600, 200, 50] + buttons.right = [720, 600, 50, 50] + + # The variables below are related to the slider + # They allow the user to customize them + # They also give a central location for the render and input methods to get + # information from + # x & y are the coordinates of the leftmost part of the slider line + slider.x = 440 + slider.y = 675 + # This is the width of the line + slider.w = 360 + # This is the offset for the circle + # Allows the center of the circle to be on the line, + # as opposed to the upper right corner + slider.offset = 20 + # This is the spacing between each of the notches on the slider + # Notches are places where the circle can rest on the slider line + # There needs to be a notch for each step before the maximum number of steps + slider.spacing = slider.w.to_f / state.max_steps.to_f + end + + # All methods with render draw stuff on the screen + # UI has buttons, the slider, and labels + # The search specific rendering occurs in the respective methods + def render + render_ui + render_bfs + render_heuristic + end + + def render_ui + render_buttons + render_slider + render_labels + end + + def render_buttons + render_left_button + render_center_button + render_right_button + end + + def render_bfs + render_bfs_grid + render_bfs_star + render_bfs_target + render_bfs_visited + render_bfs_walls + render_bfs_frontier + render_bfs_path + end + + def render_heuristic + render_heuristic_grid + render_heuristic_star + render_heuristic_target + render_heuristic_visited + render_heuristic_walls + render_heuristic_frontier + render_heuristic_path + end + + # This method handles user input every tick + def input + # Check and handle button input + input_buttons + + # If the mouse was lifted this tick + if inputs.mouse.up + # Set current input to none + state.user_input = :none + end + + # If the mouse was clicked this tick + if inputs.mouse.down + # Determine what the user is editing and appropriately edit the state.user_input variable + determine_input + end + + # Process user input based on user_input variable and current mouse position + process_input + end + + # Determines what the user is editing + # This method is called when the mouse is clicked down + def determine_input + if mouse_over_slider? + state.user_input = :slider + # If the mouse is over the star in the first grid + elsif bfs_mouse_over_star? + # The user is editing the star from the first grid + state.user_input = :bfs_star + # If the mouse is over the star in the second grid + elsif heuristic_mouse_over_star? + # The user is editing the star from the second grid + state.user_input = :heuristic_star + # If the mouse is over the target in the first grid + elsif bfs_mouse_over_target? + # The user is editing the target from the first grid + state.user_input = :bfs_target + # If the mouse is over the target in the second grid + elsif heuristic_mouse_over_target? + # The user is editing the target from the second grid + state.user_input = :heuristic_target + # If the mouse is over a wall in the first grid + elsif bfs_mouse_over_wall? + # The user is removing a wall from the first grid + state.user_input = :bfs_remove_wall + # If the mouse is over a wall in the second grid + elsif heuristic_mouse_over_wall? + # The user is removing a wall from the second grid + state.user_input = :heuristic_remove_wall + # If the mouse is over the first grid + elsif bfs_mouse_over_grid? + # The user is adding a wall from the first grid + state.user_input = :bfs_add_wall + # If the mouse is over the second grid + elsif heuristic_mouse_over_grid? + # The user is adding a wall from the second grid + state.user_input = :heuristic_add_wall + end + end + + # Processes click and drag based on what the user is currently dragging + def process_input + if state.user_input == :slider + process_input_slider + elsif state.user_input == :bfs_star + process_input_bfs_star + elsif state.user_input == :heuristic_star + process_input_heuristic_star + elsif state.user_input == :bfs_target + process_input_bfs_target + elsif state.user_input == :heuristic_target + process_input_heuristic_target + elsif state.user_input == :bfs_remove_wall + process_input_bfs_remove_wall + elsif state.user_input == :heuristic_remove_wall + process_input_heuristic_remove_wall + elsif state.user_input == :bfs_add_wall + process_input_bfs_add_wall + elsif state.user_input == :heuristic_add_wall + process_input_heuristic_add_wall + end + end + + def render_slider + # Using primitives hides the line under the white circle of the slider + # Draws the line + outputs.primitives << [slider.x, slider.y, slider.x + slider.w, slider.y].line + # The circle needs to be offset so that the center of the circle + # overlaps the line instead of the upper right corner of the circle + # The circle's x value is also moved based on the current seach step + circle_x = (slider.x - slider.offset) + (state.current_step * slider.spacing) + circle_y = (slider.y - slider.offset) + circle_rect = [circle_x, circle_y, 37, 37] + outputs.primitives << [circle_rect, 'circle-white.png'].sprite + end + + def render_labels + outputs.labels << [205, 625, "Breadth First Search"] + outputs.labels << [820, 625, "Heuristic Best-First Search"] + end + + def render_left_button + # Draws the button_color button, and a black border + # The border separates the buttons visually + outputs.solids << [buttons.left, button_color] + outputs.borders << [buttons.left] + + # Renders an explanatory label in the center of the button + # Explains to the user what the button does + # If the button size is changed, the label might need to be edited as well + # to keep the label in the center of the button + label_x = buttons.left.x + 20 + label_y = buttons.left.y + 35 + outputs.labels << [label_x, label_y, "<"] + end + + def render_center_button + # Draws the button_color button, and a black border + # The border separates the buttons visually + outputs.solids << [buttons.center, button_color] + outputs.borders << [buttons.center] + + # Renders an explanatory label in the center of the button + # Explains to the user what the button does + # If the button size is changed, the label might need to be edited as well + # to keep the label in the center of the button + label_x = buttons.center.x + 37 + label_y = buttons.center.y + 35 + label_text = state.play ? "Pause Animation" : "Play Animation" + outputs.labels << [label_x, label_y, label_text] + end + + def render_right_button + # Draws the button_color button, and a black border + # The border separates the buttons visually + outputs.solids << [buttons.right, button_color] + outputs.borders << [buttons.right] + + # Renders an explanatory label in the center of the button + # Explains to the user what the button does + label_x = buttons.right.x + 20 + label_y = buttons.right.y + 35 + outputs.labels << [label_x, label_y, ">"] + end + + def render_bfs_grid + # A large rect the size of the grid + outputs.solids << [bfs_scale_up(grid.rect), default_color] + + # The vertical grid lines + for x in 0..grid.width + outputs.lines << bfs_vertical_line(x) + end + + # The horizontal grid lines + for y in 0..grid.height + outputs.lines << bfs_horizontal_line(y) + end + end + + def render_heuristic_grid + # A large rect the size of the grid + outputs.solids << [heuristic_scale_up(grid.rect), default_color] + + # The vertical grid lines + for x in 0..grid.width + outputs.lines << heuristic_vertical_line(x) + end + + # The horizontal grid lines + for y in 0..grid.height + outputs.lines << heuristic_horizontal_line(y) + end + end + + # Returns a vertical line for a column of the first grid + def bfs_vertical_line column + bfs_scale_up([column, 0, column, grid.height]) + end + + # Returns a horizontal line for a column of the first grid + def bfs_horizontal_line row + bfs_scale_up([0, row, grid.width, row]) + end + + # Returns a vertical line for a column of the second grid + def heuristic_vertical_line column + bfs_scale_up([column + grid.width + 1, 0, column + grid.width + 1, grid.height]) + end + + # Returns a horizontal line for a column of the second grid + def heuristic_horizontal_line row + bfs_scale_up([grid.width + 1, row, grid.width + grid.width + 1, row]) + end + + # Renders the star on the first grid + def render_bfs_star + outputs.sprites << [bfs_scale_up(grid.star), 'star.png'] + end + + # Renders the star on the second grid + def render_heuristic_star + outputs.sprites << [heuristic_scale_up(grid.star), 'star.png'] + end + + # Renders the target on the first grid + def render_bfs_target + outputs.sprites << [bfs_scale_up(grid.target), 'target.png'] + end + + # Renders the target on the second grid + def render_heuristic_target + outputs.sprites << [heuristic_scale_up(grid.target), 'target.png'] + end + + # Renders the walls on the first grid + def render_bfs_walls + grid.walls.each_key do | wall | + outputs.solids << [bfs_scale_up(wall), wall_color] + end + end + + # Renders the walls on the second grid + def render_heuristic_walls + grid.walls.each_key do | wall | + outputs.solids << [heuristic_scale_up(wall), wall_color] + end + end + + # Renders the visited cells on the first grid + def render_bfs_visited + bfs.came_from.each_key do | visited_cell | + outputs.solids << [bfs_scale_up(visited_cell), visited_color] + end + end + + # Renders the visited cells on the second grid + def render_heuristic_visited + heuristic.came_from.each_key do | visited_cell | + outputs.solids << [heuristic_scale_up(visited_cell), visited_color] + end + end + + # Renders the frontier cells on the first grid + def render_bfs_frontier + bfs.frontier.each do | frontier_cell | + outputs.solids << [bfs_scale_up(frontier_cell), frontier_color, 200] + end + end + + # Renders the frontier cells on the second grid + def render_heuristic_frontier + heuristic.frontier.each do | frontier_cell | + outputs.solids << [heuristic_scale_up(frontier_cell), frontier_color, 200] + end + end + + # Renders the path found by the breadth first search on the first grid + def render_bfs_path + bfs.path.each do | path | + outputs.solids << [bfs_scale_up(path), path_color] + end + end + + # Renders the path found by the heuristic search on the second grid + def render_heuristic_path + heuristic.path.each do | path | + outputs.solids << [heuristic_scale_up(path), path_color] + end + end + + # Returns the rect for the path between two cells based on their relative positions + def get_path_between(cell_one, cell_two) + path = nil + + # If cell one is above cell two + if cell_one.x == cell_two.x and cell_one.y > cell_two.y + # Path starts from the center of cell two and moves upward to the center of cell one + path = [cell_two.x + 0.3, cell_two.y + 0.3, 0.4, 1.4] + # If cell one is below cell two + elsif cell_one.x == cell_two.x and cell_one.y < cell_two.y + # Path starts from the center of cell one and moves upward to the center of cell two + path = [cell_one.x + 0.3, cell_one.y + 0.3, 0.4, 1.4] + # If cell one is to the left of cell two + elsif cell_one.x > cell_two.x and cell_one.y == cell_two.y + # Path starts from the center of cell two and moves rightward to the center of cell one + path = [cell_two.x + 0.3, cell_two.y + 0.3, 1.4, 0.4] + # If cell one is to the right of cell two + elsif cell_one.x < cell_two.x and cell_one.y == cell_two.y + # Path starts from the center of cell one and moves rightward to the center of cell two + path = [cell_one.x + 0.3, cell_one.y + 0.3, 1.4, 0.4] + end + + path + end + + # In code, the cells are represented as 1x1 rectangles + # When drawn, the cells are larger than 1x1 rectangles + # This method is used to scale up cells, and lines + # Objects are scaled up according to the grid.cell_size variable + # This allows for easy customization of the visual scale of the grid + # This method scales up cells for the first grid + def bfs_scale_up(cell) + # Prevents the original value of cell from being edited + cell = cell.clone + + # If cell is just an x and y coordinate + if cell.size == 2 + # Add a width and height of 1 + cell << 1 + cell << 1 + end + + # Scale all the values up + cell.map! { |value| value * grid.cell_size } + + # Returns the scaled up cell + cell + end + + # Translates the given cell grid.width + 1 to the right and then scales up + # Used to draw cells for the second grid + # This method does not work for lines, + # so separate methods exist for the grid lines + def heuristic_scale_up(cell) + # Prevents the original value of cell from being edited + cell = cell.clone + # Translates the cell to the second grid equivalent + cell.x += grid.width + 1 + # Proceeds as if scaling up for the first grid + bfs_scale_up(cell) + end + + # Checks and handles input for the buttons + # Called when the mouse is lifted + def input_buttons + input_left_button + input_center_button + input_right_button + end + + # Checks if the previous step button is clicked + # If it is, it pauses the animation and moves the search one step backward + def input_left_button + if left_button_clicked? + state.play = false + state.current_step -= 1 + recalculate_searches + end + end + + # Controls the play/pause button + # Inverses whether the animation is playing or not when clicked + def input_center_button + if center_button_clicked? || inputs.keyboard.key_down.space + state.play = !state.play + end + end + + # Checks if the next step button is clicked + # If it is, it pauses the animation and moves the search one step forward + def input_right_button + if right_button_clicked? + state.play = false + state.current_step += 1 + move_searches_one_step_forward + end + end + + # These methods detect when the buttons are clicked + def left_button_clicked? + inputs.mouse.point.inside_rect?(buttons.left) && inputs.mouse.up + end + + def center_button_clicked? + inputs.mouse.point.inside_rect?(buttons.center) && inputs.mouse.up + end + + def right_button_clicked? + inputs.mouse.point.inside_rect?(buttons.right) && inputs.mouse.up + end + + + # Signal that the user is going to be moving the slider + # Is the mouse over the circle of the slider? + def mouse_over_slider? + circle_x = (slider.x - slider.offset) + (state.current_step * slider.spacing) + circle_y = (slider.y - slider.offset) + circle_rect = [circle_x, circle_y, 37, 37] + inputs.mouse.point.inside_rect?(circle_rect) + end + + # Signal that the user is going to be moving the star from the first grid + def bfs_mouse_over_star? + inputs.mouse.point.inside_rect?(bfs_scale_up(grid.star)) + end + + # Signal that the user is going to be moving the star from the second grid + def heuristic_mouse_over_star? + inputs.mouse.point.inside_rect?(heuristic_scale_up(grid.star)) + end + + # Signal that the user is going to be moving the target from the first grid + def bfs_mouse_over_target? + inputs.mouse.point.inside_rect?(bfs_scale_up(grid.target)) + end + + # Signal that the user is going to be moving the target from the second grid + def heuristic_mouse_over_target? + inputs.mouse.point.inside_rect?(heuristic_scale_up(grid.target)) + end + + # Signal that the user is going to be removing walls from the first grid + def bfs_mouse_over_wall? + grid.walls.each_key do | wall | + return true if inputs.mouse.point.inside_rect?(bfs_scale_up(wall)) + end + + false + end + + # Signal that the user is going to be removing walls from the second grid + def heuristic_mouse_over_wall? + grid.walls.each_key do | wall | + return true if inputs.mouse.point.inside_rect?(heuristic_scale_up(wall)) + end + + false + end + + # Signal that the user is going to be adding walls from the first grid + def bfs_mouse_over_grid? + inputs.mouse.point.inside_rect?(bfs_scale_up(grid.rect)) + end + + # Signal that the user is going to be adding walls from the second grid + def heuristic_mouse_over_grid? + inputs.mouse.point.inside_rect?(heuristic_scale_up(grid.rect)) + end + + # This method is called when the user is editing the slider + # It pauses the animation and moves the white circle to the closest integer point + # on the slider + # Changes the step of the search to be animated + def process_input_slider + state.play = false + mouse_x = inputs.mouse.point.x + + # Bounds the mouse_x to the closest x value on the slider line + mouse_x = slider.x if mouse_x < slider.x + mouse_x = slider.x + slider.w if mouse_x > slider.x + slider.w + + # Sets the current search step to the one represented by the mouse x value + # The slider's circle moves due to the render_slider method using anim_steps + state.current_step = ((mouse_x - slider.x) / slider.spacing).to_i + + recalculate_searches + end + + # Moves the star to the cell closest to the mouse in the first grid + # Only resets the search if the star changes position + # Called whenever the user is editing the star (puts mouse down on star) + def process_input_bfs_star + old_star = grid.star.clone + unless bfs_cell_closest_to_mouse == grid.target + grid.star = bfs_cell_closest_to_mouse + end + unless old_star == grid.star + recalculate_searches + end + end + + # Moves the star to the cell closest to the mouse in the second grid + # Only resets the search if the star changes position + # Called whenever the user is editing the star (puts mouse down on star) + def process_input_heuristic_star + old_star = grid.star.clone + unless heuristic_cell_closest_to_mouse == grid.target + grid.star = heuristic_cell_closest_to_mouse + end + unless old_star == grid.star + recalculate_searches + end + end + + # Moves the target to the grid closest to the mouse in the first grid + # Only recalculate_searchess the search if the target changes position + # Called whenever the user is editing the target (puts mouse down on target) + def process_input_bfs_target + old_target = grid.target.clone + unless bfs_cell_closest_to_mouse == grid.star + grid.target = bfs_cell_closest_to_mouse + end + unless old_target == grid.target + recalculate_searches + end + end + + # Moves the target to the cell closest to the mouse in the second grid + # Only recalculate_searchess the search if the target changes position + # Called whenever the user is editing the target (puts mouse down on target) + def process_input_heuristic_target + old_target = grid.target.clone + unless heuristic_cell_closest_to_mouse == grid.star + grid.target = heuristic_cell_closest_to_mouse + end + unless old_target == grid.target + recalculate_searches + end + end + + # Removes walls in the first grid that are under the cursor + def process_input_bfs_remove_wall + # The mouse needs to be inside the grid, because we only want to remove walls + # the cursor is directly over + # Recalculations should only occur when a wall is actually deleted + if bfs_mouse_over_grid? + if grid.walls.has_key?(bfs_cell_closest_to_mouse) + grid.walls.delete(bfs_cell_closest_to_mouse) + recalculate_searches + end + end + end + + # Removes walls in the second grid that are under the cursor + def process_input_heuristic_remove_wall + # The mouse needs to be inside the grid, because we only want to remove walls + # the cursor is directly over + # Recalculations should only occur when a wall is actually deleted + if heuristic_mouse_over_grid? + if grid.walls.has_key?(heuristic_cell_closest_to_mouse) + grid.walls.delete(heuristic_cell_closest_to_mouse) + recalculate_searches + end + end + end + # Adds a wall in the first grid in the cell the mouse is over + def process_input_bfs_add_wall + if bfs_mouse_over_grid? + unless grid.walls.has_key?(bfs_cell_closest_to_mouse) + grid.walls[bfs_cell_closest_to_mouse] = true + recalculate_searches + end + end + end + + # Adds a wall in the second grid in the cell the mouse is over + def process_input_heuristic_add_wall + if heuristic_mouse_over_grid? + unless grid.walls.has_key?(heuristic_cell_closest_to_mouse) + grid.walls[heuristic_cell_closest_to_mouse] = true + recalculate_searches + end + end + end + + # When the user grabs the star and puts their cursor to the far right + # and moves up and down, the star is supposed to move along the grid as well + # Finding the cell closest to the mouse helps with this + def bfs_cell_closest_to_mouse + # Closest cell to the mouse in the first grid + x = (inputs.mouse.point.x / grid.cell_size).to_i + y = (inputs.mouse.point.y / grid.cell_size).to_i + # Bound x and y to the grid + x = grid.width - 1 if x > grid.width - 1 + y = grid.height - 1 if y > grid.height - 1 + # Return closest cell + [x, y] + end + + # When the user grabs the star and puts their cursor to the far right + # and moves up and down, the star is supposed to move along the grid as well + # Finding the cell closest to the mouse in the second grid helps with this + def heuristic_cell_closest_to_mouse + # Closest cell grid to the mouse in the second + x = (inputs.mouse.point.x / grid.cell_size).to_i + y = (inputs.mouse.point.y / grid.cell_size).to_i + # Translate the cell to the first grid + x -= grid.width + 1 + # Bound x and y to the first grid + x = 0 if x < 0 + y = 0 if y < 0 + x = grid.width - 1 if x > grid.width - 1 + y = grid.height - 1 if y > grid.height - 1 + # Return closest cell + [x, y] + end + + def recalculate_searches + # Reset the searches + bfs.came_from = {} + bfs.frontier = [] + bfs.path = [] + heuristic.came_from = {} + heuristic.frontier = [] + heuristic.path = [] + + # Move the searches forward to the current step + state.current_step.times { move_searches_one_step_forward } + end + + def move_searches_one_step_forward + bfs_one_step_forward + heuristic_one_step_forward + end + + def bfs_one_step_forward + return if bfs.came_from.has_key?(grid.target) + + # Only runs at the beginning of the search as setup. + if bfs.came_from.empty? + bfs.frontier << grid.star + bfs.came_from[grid.star] = nil + end + + # A step in the search + unless bfs.frontier.empty? + # Takes the next frontier cell + new_frontier = bfs.frontier.shift + # For each of its neighbors + adjacent_neighbors(new_frontier).each do |neighbor| + # That have not been visited and are not walls + unless bfs.came_from.has_key?(neighbor) || grid.walls.has_key?(neighbor) + # Add them to the frontier and mark them as visited + bfs.frontier << neighbor + bfs.came_from[neighbor] = new_frontier + end + end + end + + # Sort the frontier so that cells that are in a zigzag pattern are prioritized over those in an line + # Comment this line and let a path generate to see the difference + bfs.frontier = bfs.frontier.sort_by {| cell | proximity_to_star(cell) } + + # If the search found the target + if bfs.came_from.has_key?(grid.target) + # Calculate the path between the target and star + bfs_calc_path + end + end + + # Calculates the path between the target and star for the breadth first search + # Only called when the breadth first search finds the target + def bfs_calc_path + # Start from the target + endpoint = grid.target + # And the cell it came from + next_endpoint = bfs.came_from[endpoint] + while endpoint and next_endpoint + # Draw a path between these two cells and store it + path = get_path_between(endpoint, next_endpoint) + bfs.path << path + # And get the next pair of cells + endpoint = next_endpoint + next_endpoint = bfs.came_from[endpoint] + # Continue till there are no more cells + end + end + + # Moves the heuristic search forward one step + # Can be called from tick while the animation is playing + # Can also be called when recalculating the searches after the user edited the grid + def heuristic_one_step_forward + # Stop the search if the target has been found + return if heuristic.came_from.has_key?(grid.target) + + # If the search has not begun + if heuristic.came_from.empty? + # Setup the search to begin from the star + heuristic.frontier << grid.star + heuristic.came_from[grid.star] = nil + end + + # One step in the heuristic search + + # Unless there are no more cells to explore from + unless heuristic.frontier.empty? + # Get the next cell to explore from + new_frontier = heuristic.frontier.shift + # For each of its neighbors + adjacent_neighbors(new_frontier).each do |neighbor| + # That have not been visited and are not walls + unless heuristic.came_from.has_key?(neighbor) || grid.walls.has_key?(neighbor) + # Add them to the frontier and mark them as visited + heuristic.frontier << neighbor + heuristic.came_from[neighbor] = new_frontier + end + end + end + + # Sort the frontier so that cells that are in a zigzag pattern are prioritized over those in an line + heuristic.frontier = heuristic.frontier.sort_by {| cell | proximity_to_star(cell) } + # Sort the frontier so cells that are close to the target are then prioritized + heuristic.frontier = heuristic.frontier.sort_by {| cell | heuristic_heuristic(cell) } + + # If the search found the target + if heuristic.came_from.has_key?(grid.target) + # Calculate the path between the target and star + heuristic_calc_path + end + end + + # Returns one-dimensional absolute distance between cell and target + # Returns a number to compare distances between cells and the target + def heuristic_heuristic(cell) + (grid.target.x - cell.x).abs + (grid.target.y - cell.y).abs + end + + # Calculates the path between the target and star for the heuristic search + # Only called when the heuristic search finds the target + def heuristic_calc_path + # Start from the target + endpoint = grid.target + # And the cell it came from + next_endpoint = heuristic.came_from[endpoint] + while endpoint and next_endpoint + # Draw a path between these two cells and store it + path = get_path_between(endpoint, next_endpoint) + heuristic.path << path + # And get the next pair of cells + endpoint = next_endpoint + next_endpoint = heuristic.came_from[endpoint] + # Continue till there are no more cells + end + end + + # Returns a list of adjacent cells + # Used to determine what the next cells to be added to the frontier are + def adjacent_neighbors(cell) + neighbors = [] + + # Gets all the valid neighbors into the array + # From southern neighbor, clockwise + neighbors << [cell.x , cell.y - 1] unless cell.y == 0 + neighbors << [cell.x - 1, cell.y ] unless cell.x == 0 + neighbors << [cell.x , cell.y + 1] unless cell.y == grid.height - 1 + neighbors << [cell.x + 1, cell.y ] unless cell.x == grid.width - 1 + + neighbors + end + + # Finds the vertical and horizontal distance of a cell from the star + # and returns the larger value + # This method is used to have a zigzag pattern in the rendered path + # A cell that is [5, 5] from the star, + # is explored before over a cell that is [0, 7] away. + # So, if possible, the search tries to go diagonal (zigzag) first + def proximity_to_star(cell) + distance_x = (grid.star.x - cell.x).abs + distance_y = (grid.star.y - cell.y).abs + + if distance_x > distance_y + return distance_x + else + return distance_y + end + end + + # Methods that allow code to be more concise. Subdivides args.state, which is where all variables are stored. + def grid + state.grid + end + + def buttons + state.buttons + end + + def slider + state.slider + end + + def bfs + state.bfs + end + + def heuristic + state.heuristic + end + + # Descriptive aliases for colors + def default_color + [221, 212, 213] # Light Brown + end + + def wall_color + [134, 134, 120] # Camo Green + end + + def visited_color + [204, 191, 179] # Dark Brown + end + + def frontier_color + [103, 136, 204] # Blue + end + + def path_color + [231, 230, 228] # Pastel White + end + + def button_color + [190, 190, 190] # Gray + end +end +# Method that is called by DragonRuby periodically +# Used for updating animations and calculations +def tick args + + # Pressing r will reset the application + if args.inputs.keyboard.key_down.r + args.gtk.reset + reset + return + end + + # Every tick, new args are passed, and the Breadth First Search tick is called + $heuristic_with_walls ||= Heuristic_With_Walls.new(args) + $heuristic_with_walls.args = args + $heuristic_with_walls.tick +end + + +def reset + $heuristic_with_walls = nil +end + +</code></pre> +<h3 id='----path-finding-algorithms---heuristic-with-walls---main.rb'>Path Finding Algorithms - Heuristic With Walls - main.rb</h3> +<pre><code class="language-ruby"># ./samples/13_path_finding_algorithms/07_heuristic_with_walls/app/main.rb +# This program is inspired by https://www.redblobgames.com/pathfinding/a-star/introduction.html +# The effectiveness of the Heuristic search algorithm is shown through this demonstration. +# Notice that both searches find the shortest path +# The heuristic search, however, explores less of the grid, and is therefore faster. +# The heuristic search prioritizes searching cells that are closer to the target. +# Make sure to look at the Heuristic with walls program to see some of the downsides of the heuristic algorithm. + +class Heuristic + attr_gtk + + def tick + defaults + render + input + # If animation is playing, and max steps have not been reached + # Move the search a step forward + if state.play && state.current_step < state.max_steps + # Variable that tells the program what step to recalculate up to + state.current_step += 1 + move_searches_one_step_forward + end + end + + def defaults + # Variables to edit the size and appearance of the grid + # Freely customizable to user's liking + grid.width ||= 15 + grid.height ||= 15 + grid.cell_size ||= 40 + grid.rect ||= [0, 0, grid.width, grid.height] + + grid.star ||= [0, 2] + grid.target ||= [14, 12] + grid.walls ||= { + [2, 2] => true, + [3, 2] => true, + [4, 2] => true, + [5, 2] => true, + [6, 2] => true, + [7, 2] => true, + [8, 2] => true, + [9, 2] => true, + [10, 2] => true, + [11, 2] => true, + [12, 2] => true, + [12, 3] => true, + [12, 4] => true, + [12, 5] => true, + [12, 6] => true, + [12, 7] => true, + [12, 8] => true, + [12, 9] => true, + [12, 10] => true, + [12, 11] => true, + [12, 12] => true, + [2, 12] => true, + [3, 12] => true, + [4, 12] => true, + [5, 12] => true, + [6, 12] => true, + [7, 12] => true, + [8, 12] => true, + [9, 12] => true, + [10, 12] => true, + [11, 12] => true, + [12, 12] => true + } + # There are no hills in the Heuristic Search Demo + + # What the user is currently editing on the grid + # We store this value, because we want to remember the value even when + # the user's cursor is no longer over what they're interacting with, but + # they are still clicking down on the mouse. + state.user_input ||= :none + + # These variables allow the breadth first search to take place + # Came_from is a hash with a key of a cell and a value of the cell that was expanded from to find the key. + # Used to prevent searching cells that have already been found + # and to trace a path from the target back to the starting point. + # Frontier is an array of cells to expand the search from. + # The search is over when there are no more cells to search from. + # Path stores the path from the target to the star, once the target has been found + # It prevents calculating the path every tick. + bfs.came_from ||= {} + bfs.frontier ||= [] + bfs.path ||= [] + + heuristic.came_from ||= {} + heuristic.frontier ||= [] + heuristic.path ||= [] + + # Stores which step of the animation is being rendered + # When the user moves the star or messes with the walls, + # the searches are recalculated up to this step + + # Unless the current step has a value + unless state.current_step + # Set the current step to 10 + state.current_step = 10 + # And calculate the searches up to step 10 + recalculate_searches + end + + # At some step the animation will end, + # and further steps won't change anything (the whole grid will be explored) + # This step is roughly the grid's width * height + # When anim_steps equals max_steps no more calculations will occur + # and the slider will be at the end + state.max_steps = grid.width * grid.height + + # Whether the animation should play or not + # If true, every tick moves anim_steps forward one + # Pressing the stepwise animation buttons will pause the animation + # An if statement instead of the ||= operator is used for assigning a boolean value. + # The || operator does not differentiate between nil and false. + if state.play == nil + state.play = false + end + + # Store the rects of the buttons that control the animation + # They are here for user customization + # Editing these might require recentering the text inside them + # Those values can be found in the render_button methods + buttons.left = [470, 600, 50, 50] + buttons.center = [520, 600, 200, 50] + buttons.right = [720, 600, 50, 50] + + # The variables below are related to the slider + # They allow the user to customize them + # They also give a central location for the render and input methods to get + # information from + # x & y are the coordinates of the leftmost part of the slider line + slider.x = 440 + slider.y = 675 + # This is the width of the line + slider.w = 360 + # This is the offset for the circle + # Allows the center of the circle to be on the line, + # as opposed to the upper right corner + slider.offset = 20 + # This is the spacing between each of the notches on the slider + # Notches are places where the circle can rest on the slider line + # There needs to be a notch for each step before the maximum number of steps + slider.spacing = slider.w.to_f / state.max_steps.to_f + end + + # All methods with render draw stuff on the screen + # UI has buttons, the slider, and labels + # The search specific rendering occurs in the respective methods + def render + render_ui + render_bfs + render_heuristic + end + + def render_ui + render_buttons + render_slider + render_labels + end + + def render_buttons + render_left_button + render_center_button + render_right_button + end + + def render_bfs + render_bfs_grid + render_bfs_star + render_bfs_target + render_bfs_visited + render_bfs_walls + render_bfs_frontier + render_bfs_path + end + + def render_heuristic + render_heuristic_grid + render_heuristic_star + render_heuristic_target + render_heuristic_visited + render_heuristic_walls + render_heuristic_frontier + render_heuristic_path + end + + # This method handles user input every tick + def input + # Check and handle button input + input_buttons + + # If the mouse was lifted this tick + if inputs.mouse.up + # Set current input to none + state.user_input = :none + end + + # If the mouse was clicked this tick + if inputs.mouse.down + # Determine what the user is editing and appropriately edit the state.user_input variable + determine_input + end + + # Process user input based on user_input variable and current mouse position + process_input + end + + # Determines what the user is editing + # This method is called when the mouse is clicked down + def determine_input + if mouse_over_slider? + state.user_input = :slider + # If the mouse is over the star in the first grid + elsif bfs_mouse_over_star? + # The user is editing the star from the first grid + state.user_input = :bfs_star + # If the mouse is over the star in the second grid + elsif heuristic_mouse_over_star? + # The user is editing the star from the second grid + state.user_input = :heuristic_star + # If the mouse is over the target in the first grid + elsif bfs_mouse_over_target? + # The user is editing the target from the first grid + state.user_input = :bfs_target + # If the mouse is over the target in the second grid + elsif heuristic_mouse_over_target? + # The user is editing the target from the second grid + state.user_input = :heuristic_target + # If the mouse is over a wall in the first grid + elsif bfs_mouse_over_wall? + # The user is removing a wall from the first grid + state.user_input = :bfs_remove_wall + # If the mouse is over a wall in the second grid + elsif heuristic_mouse_over_wall? + # The user is removing a wall from the second grid + state.user_input = :heuristic_remove_wall + # If the mouse is over the first grid + elsif bfs_mouse_over_grid? + # The user is adding a wall from the first grid + state.user_input = :bfs_add_wall + # If the mouse is over the second grid + elsif heuristic_mouse_over_grid? + # The user is adding a wall from the second grid + state.user_input = :heuristic_add_wall + end + end + + # Processes click and drag based on what the user is currently dragging + def process_input + if state.user_input == :slider + process_input_slider + elsif state.user_input == :bfs_star + process_input_bfs_star + elsif state.user_input == :heuristic_star + process_input_heuristic_star + elsif state.user_input == :bfs_target + process_input_bfs_target + elsif state.user_input == :heuristic_target + process_input_heuristic_target + elsif state.user_input == :bfs_remove_wall + process_input_bfs_remove_wall + elsif state.user_input == :heuristic_remove_wall + process_input_heuristic_remove_wall + elsif state.user_input == :bfs_add_wall + process_input_bfs_add_wall + elsif state.user_input == :heuristic_add_wall + process_input_heuristic_add_wall + end + end + + def render_slider + # Using primitives hides the line under the white circle of the slider + # Draws the line + outputs.primitives << [slider.x, slider.y, slider.x + slider.w, slider.y].line + # The circle needs to be offset so that the center of the circle + # overlaps the line instead of the upper right corner of the circle + # The circle's x value is also moved based on the current seach step + circle_x = (slider.x - slider.offset) + (state.current_step * slider.spacing) + circle_y = (slider.y - slider.offset) + circle_rect = [circle_x, circle_y, 37, 37] + outputs.primitives << [circle_rect, 'circle-white.png'].sprite + end + + def render_labels + outputs.labels << [205, 625, "Breadth First Search"] + outputs.labels << [820, 625, "Heuristic Best-First Search"] + end + + def render_left_button + # Draws the button_color button, and a black border + # The border separates the buttons visually + outputs.solids << [buttons.left, button_color] + outputs.borders << [buttons.left] + + # Renders an explanatory label in the center of the button + # Explains to the user what the button does + # If the button size is changed, the label might need to be edited as well + # to keep the label in the center of the button + label_x = buttons.left.x + 20 + label_y = buttons.left.y + 35 + outputs.labels << [label_x, label_y, "<"] + end + + def render_center_button + # Draws the button_color button, and a black border + # The border separates the buttons visually + outputs.solids << [buttons.center, button_color] + outputs.borders << [buttons.center] + + # Renders an explanatory label in the center of the button + # Explains to the user what the button does + # If the button size is changed, the label might need to be edited as well + # to keep the label in the center of the button + label_x = buttons.center.x + 37 + label_y = buttons.center.y + 35 + label_text = state.play ? "Pause Animation" : "Play Animation" + outputs.labels << [label_x, label_y, label_text] + end + + def render_right_button + # Draws the button_color button, and a black border + # The border separates the buttons visually + outputs.solids << [buttons.right, button_color] + outputs.borders << [buttons.right] + + # Renders an explanatory label in the center of the button + # Explains to the user what the button does + label_x = buttons.right.x + 20 + label_y = buttons.right.y + 35 + outputs.labels << [label_x, label_y, ">"] + end + + def render_bfs_grid + # A large rect the size of the grid + outputs.solids << [bfs_scale_up(grid.rect), default_color] + + # The vertical grid lines + for x in 0..grid.width + outputs.lines << bfs_vertical_line(x) + end + + # The horizontal grid lines + for y in 0..grid.height + outputs.lines << bfs_horizontal_line(y) + end + end + + def render_heuristic_grid + # A large rect the size of the grid + outputs.solids << [heuristic_scale_up(grid.rect), default_color] + + # The vertical grid lines + for x in 0..grid.width + outputs.lines << heuristic_vertical_line(x) + end + + # The horizontal grid lines + for y in 0..grid.height + outputs.lines << heuristic_horizontal_line(y) + end + end + + # Returns a vertical line for a column of the first grid + def bfs_vertical_line column + bfs_scale_up([column, 0, column, grid.height]) + end + + # Returns a horizontal line for a column of the first grid + def bfs_horizontal_line row + bfs_scale_up([0, row, grid.width, row]) + end + + # Returns a vertical line for a column of the second grid + def heuristic_vertical_line column + bfs_scale_up([column + grid.width + 1, 0, column + grid.width + 1, grid.height]) + end + + # Returns a horizontal line for a column of the second grid + def heuristic_horizontal_line row + bfs_scale_up([grid.width + 1, row, grid.width + grid.width + 1, row]) + end + + # Renders the star on the first grid + def render_bfs_star + outputs.sprites << [bfs_scale_up(grid.star), 'star.png'] + end + + # Renders the star on the second grid + def render_heuristic_star + outputs.sprites << [heuristic_scale_up(grid.star), 'star.png'] + end + + # Renders the target on the first grid + def render_bfs_target + outputs.sprites << [bfs_scale_up(grid.target), 'target.png'] + end + + # Renders the target on the second grid + def render_heuristic_target + outputs.sprites << [heuristic_scale_up(grid.target), 'target.png'] + end + + # Renders the walls on the first grid + def render_bfs_walls + grid.walls.each_key do | wall | + outputs.solids << [bfs_scale_up(wall), wall_color] + end + end + + # Renders the walls on the second grid + def render_heuristic_walls + grid.walls.each_key do | wall | + outputs.solids << [heuristic_scale_up(wall), wall_color] + end + end + + # Renders the visited cells on the first grid + def render_bfs_visited + bfs.came_from.each_key do | visited_cell | + outputs.solids << [bfs_scale_up(visited_cell), visited_color] + end + end + + # Renders the visited cells on the second grid + def render_heuristic_visited + heuristic.came_from.each_key do | visited_cell | + outputs.solids << [heuristic_scale_up(visited_cell), visited_color] + end + end + + # Renders the frontier cells on the first grid + def render_bfs_frontier + bfs.frontier.each do | frontier_cell | + outputs.solids << [bfs_scale_up(frontier_cell), frontier_color, 200] + end + end + + # Renders the frontier cells on the second grid + def render_heuristic_frontier + heuristic.frontier.each do | frontier_cell | + outputs.solids << [heuristic_scale_up(frontier_cell), frontier_color, 200] + end + end + + # Renders the path found by the breadth first search on the first grid + def render_bfs_path + bfs.path.each do | path | + outputs.solids << [bfs_scale_up(path), path_color] + end + end + + # Renders the path found by the heuristic search on the second grid + def render_heuristic_path + heuristic.path.each do | path | + outputs.solids << [heuristic_scale_up(path), path_color] + end + end + + # Returns the rect for the path between two cells based on their relative positions + def get_path_between(cell_one, cell_two) + path = [] + + # If cell one is above cell two + if cell_one.x == cell_two.x and cell_one.y > cell_two.y + # Path starts from the center of cell two and moves upward to the center of cell one + path = [cell_two.x + 0.3, cell_two.y + 0.3, 0.4, 1.4] + # If cell one is below cell two + elsif cell_one.x == cell_two.x and cell_one.y < cell_two.y + # Path starts from the center of cell one and moves upward to the center of cell two + path = [cell_one.x + 0.3, cell_one.y + 0.3, 0.4, 1.4] + # If cell one is to the left of cell two + elsif cell_one.x > cell_two.x and cell_one.y == cell_two.y + # Path starts from the center of cell two and moves rightward to the center of cell one + path = [cell_two.x + 0.3, cell_two.y + 0.3, 1.4, 0.4] + # If cell one is to the right of cell two + elsif cell_one.x < cell_two.x and cell_one.y == cell_two.y + # Path starts from the center of cell one and moves rightward to the center of cell two + path = [cell_one.x + 0.3, cell_one.y + 0.3, 1.4, 0.4] + end + + path + end + + # In code, the cells are represented as 1x1 rectangles + # When drawn, the cells are larger than 1x1 rectangles + # This method is used to scale up cells, and lines + # Objects are scaled up according to the grid.cell_size variable + # This allows for easy customization of the visual scale of the grid + # This method scales up cells for the first grid + def bfs_scale_up(cell) + # Prevents the original value of cell from being edited + cell = cell.clone + + # If cell is just an x and y coordinate + if cell.size == 2 + # Add a width and height of 1 + cell << 1 + cell << 1 + end + + # Scale all the values up + cell.map! { |value| value * grid.cell_size } + + # Returns the scaled up cell + cell + end + + # Translates the given cell grid.width + 1 to the right and then scales up + # Used to draw cells for the second grid + # This method does not work for lines, + # so separate methods exist for the grid lines + def heuristic_scale_up(cell) + # Prevents the original value of cell from being edited + cell = cell.clone + # Translates the cell to the second grid equivalent + cell.x += grid.width + 1 + # Proceeds as if scaling up for the first grid + bfs_scale_up(cell) + end + + # Checks and handles input for the buttons + # Called when the mouse is lifted + def input_buttons + input_left_button + input_center_button + input_right_button + end + + # Checks if the previous step button is clicked + # If it is, it pauses the animation and moves the search one step backward + def input_left_button + if left_button_clicked? + state.play = false + state.current_step -= 1 + recalculate_searches + end + end + + # Controls the play/pause button + # Inverses whether the animation is playing or not when clicked + def input_center_button + if center_button_clicked? || inputs.keyboard.key_down.space + state.play = !state.play + end + end + + # Checks if the next step button is clicked + # If it is, it pauses the animation and moves the search one step forward + def input_right_button + if right_button_clicked? + state.play = false + state.current_step += 1 + move_searches_one_step_forward + end + end + + # These methods detect when the buttons are clicked + def left_button_clicked? + inputs.mouse.point.inside_rect?(buttons.left) && inputs.mouse.up + end + + def center_button_clicked? + inputs.mouse.point.inside_rect?(buttons.center) && inputs.mouse.up + end + + def right_button_clicked? + inputs.mouse.point.inside_rect?(buttons.right) && inputs.mouse.up + end + + + # Signal that the user is going to be moving the slider + # Is the mouse over the circle of the slider? + def mouse_over_slider? + circle_x = (slider.x - slider.offset) + (state.current_step * slider.spacing) + circle_y = (slider.y - slider.offset) + circle_rect = [circle_x, circle_y, 37, 37] + inputs.mouse.point.inside_rect?(circle_rect) + end + + # Signal that the user is going to be moving the star from the first grid + def bfs_mouse_over_star? + inputs.mouse.point.inside_rect?(bfs_scale_up(grid.star)) + end + + # Signal that the user is going to be moving the star from the second grid + def heuristic_mouse_over_star? + inputs.mouse.point.inside_rect?(heuristic_scale_up(grid.star)) + end + + # Signal that the user is going to be moving the target from the first grid + def bfs_mouse_over_target? + inputs.mouse.point.inside_rect?(bfs_scale_up(grid.target)) + end + + # Signal that the user is going to be moving the target from the second grid + def heuristic_mouse_over_target? + inputs.mouse.point.inside_rect?(heuristic_scale_up(grid.target)) + end + + # Signal that the user is going to be removing walls from the first grid + def bfs_mouse_over_wall? + grid.walls.each_key do | wall | + return true if inputs.mouse.point.inside_rect?(bfs_scale_up(wall)) + end + + false + end + + # Signal that the user is going to be removing walls from the second grid + def heuristic_mouse_over_wall? + grid.walls.each_key do | wall | + return true if inputs.mouse.point.inside_rect?(heuristic_scale_up(wall)) + end + + false + end + + # Signal that the user is going to be adding walls from the first grid + def bfs_mouse_over_grid? + inputs.mouse.point.inside_rect?(bfs_scale_up(grid.rect)) + end + + # Signal that the user is going to be adding walls from the second grid + def heuristic_mouse_over_grid? + inputs.mouse.point.inside_rect?(heuristic_scale_up(grid.rect)) + end + + # This method is called when the user is editing the slider + # It pauses the animation and moves the white circle to the closest integer point + # on the slider + # Changes the step of the search to be animated + def process_input_slider + state.play = false + mouse_x = inputs.mouse.point.x + + # Bounds the mouse_x to the closest x value on the slider line + mouse_x = slider.x if mouse_x < slider.x + mouse_x = slider.x + slider.w if mouse_x > slider.x + slider.w + + # Sets the current search step to the one represented by the mouse x value + # The slider's circle moves due to the render_slider method using anim_steps + state.current_step = ((mouse_x - slider.x) / slider.spacing).to_i + + recalculate_searches + end + + # Moves the star to the cell closest to the mouse in the first grid + # Only resets the search if the star changes position + # Called whenever the user is editing the star (puts mouse down on star) + def process_input_bfs_star + old_star = grid.star.clone + unless bfs_cell_closest_to_mouse == grid.target + grid.star = bfs_cell_closest_to_mouse + end + unless old_star == grid.star + recalculate_searches + end + end + + # Moves the star to the cell closest to the mouse in the second grid + # Only resets the search if the star changes position + # Called whenever the user is editing the star (puts mouse down on star) + def process_input_heuristic_star + old_star = grid.star.clone + unless heuristic_cell_closest_to_mouse == grid.target + grid.star = heuristic_cell_closest_to_mouse + end + unless old_star == grid.star + recalculate_searches + end + end + + # Moves the target to the grid closest to the mouse in the first grid + # Only recalculate_searchess the search if the target changes position + # Called whenever the user is editing the target (puts mouse down on target) + def process_input_bfs_target + old_target = grid.target.clone + unless bfs_cell_closest_to_mouse == grid.star + grid.target = bfs_cell_closest_to_mouse + end + unless old_target == grid.target + recalculate_searches + end + end + + # Moves the target to the cell closest to the mouse in the second grid + # Only recalculate_searchess the search if the target changes position + # Called whenever the user is editing the target (puts mouse down on target) + def process_input_heuristic_target + old_target = grid.target.clone + unless heuristic_cell_closest_to_mouse == grid.star + grid.target = heuristic_cell_closest_to_mouse + end + unless old_target == grid.target + recalculate_searches + end + end + + # Removes walls in the first grid that are under the cursor + def process_input_bfs_remove_wall + # The mouse needs to be inside the grid, because we only want to remove walls + # the cursor is directly over + # Recalculations should only occur when a wall is actually deleted + if bfs_mouse_over_grid? + if grid.walls.has_key?(bfs_cell_closest_to_mouse) + grid.walls.delete(bfs_cell_closest_to_mouse) + recalculate_searches + end + end + end + + # Removes walls in the second grid that are under the cursor + def process_input_heuristic_remove_wall + # The mouse needs to be inside the grid, because we only want to remove walls + # the cursor is directly over + # Recalculations should only occur when a wall is actually deleted + if heuristic_mouse_over_grid? + if grid.walls.has_key?(heuristic_cell_closest_to_mouse) + grid.walls.delete(heuristic_cell_closest_to_mouse) + recalculate_searches + end + end + end + # Adds a wall in the first grid in the cell the mouse is over + def process_input_bfs_add_wall + if bfs_mouse_over_grid? + unless grid.walls.has_key?(bfs_cell_closest_to_mouse) + grid.walls[bfs_cell_closest_to_mouse] = true + recalculate_searches + end + end + end + + # Adds a wall in the second grid in the cell the mouse is over + def process_input_heuristic_add_wall + if heuristic_mouse_over_grid? + unless grid.walls.has_key?(heuristic_cell_closest_to_mouse) + grid.walls[heuristic_cell_closest_to_mouse] = true + recalculate_searches + end + end + end + + # When the user grabs the star and puts their cursor to the far right + # and moves up and down, the star is supposed to move along the grid as well + # Finding the cell closest to the mouse helps with this + def bfs_cell_closest_to_mouse + # Closest cell to the mouse in the first grid + x = (inputs.mouse.point.x / grid.cell_size).to_i + y = (inputs.mouse.point.y / grid.cell_size).to_i + # Bound x and y to the grid + x = grid.width - 1 if x > grid.width - 1 + y = grid.height - 1 if y > grid.height - 1 + # Return closest cell + [x, y] + end + + # When the user grabs the star and puts their cursor to the far right + # and moves up and down, the star is supposed to move along the grid as well + # Finding the cell closest to the mouse in the second grid helps with this + def heuristic_cell_closest_to_mouse + # Closest cell grid to the mouse in the second + x = (inputs.mouse.point.x / grid.cell_size).to_i + y = (inputs.mouse.point.y / grid.cell_size).to_i + # Translate the cell to the first grid + x -= grid.width + 1 + # Bound x and y to the first grid + x = 0 if x < 0 + y = 0 if y < 0 + x = grid.width - 1 if x > grid.width - 1 + y = grid.height - 1 if y > grid.height - 1 + # Return closest cell + [x, y] + end + + def recalculate_searches + # Reset the searches + bfs.came_from = {} + bfs.frontier = [] + bfs.path = [] + heuristic.came_from = {} + heuristic.frontier = [] + heuristic.path = [] + + # Move the searches forward to the current step + state.current_step.times { move_searches_one_step_forward } + end + + def move_searches_one_step_forward + bfs_one_step_forward + heuristic_one_step_forward + end + + def bfs_one_step_forward + return if bfs.came_from.has_key?(grid.target) + + # Only runs at the beginning of the search as setup. + if bfs.came_from.empty? + bfs.frontier << grid.star + bfs.came_from[grid.star] = nil + end + + # A step in the search + unless bfs.frontier.empty? + # Takes the next frontier cell + new_frontier = bfs.frontier.shift + # For each of its neighbors + adjacent_neighbors(new_frontier).each do |neighbor| + # That have not been visited and are not walls + unless bfs.came_from.has_key?(neighbor) || grid.walls.has_key?(neighbor) + # Add them to the frontier and mark them as visited + bfs.frontier << neighbor + bfs.came_from[neighbor] = new_frontier + end + end + end + + # Sort the frontier so that cells that are in a zigzag pattern are prioritized over those in an line + # Comment this line and let a path generate to see the difference + bfs.frontier = bfs.frontier.sort_by {| cell | proximity_to_star(cell) } + + # If the search found the target + if bfs.came_from.has_key?(grid.target) + # Calculate the path between the target and star + bfs_calc_path + end + end + + # Calculates the path between the target and star for the breadth first search + # Only called when the breadth first search finds the target + def bfs_calc_path + # Start from the target + endpoint = grid.target + # And the cell it came from + next_endpoint = bfs.came_from[endpoint] + while endpoint and next_endpoint + # Draw a path between these two cells and store it + path = get_path_between(endpoint, next_endpoint) + bfs.path << path + # And get the next pair of cells + endpoint = next_endpoint + next_endpoint = bfs.came_from[endpoint] + # Continue till there are no more cells + end + end + + # Moves the heuristic search forward one step + # Can be called from tick while the animation is playing + # Can also be called when recalculating the searches after the user edited the grid + def heuristic_one_step_forward + # Stop the search if the target has been found + return if heuristic.came_from.has_key?(grid.target) + + # If the search has not begun + if heuristic.came_from.empty? + # Setup the search to begin from the star + heuristic.frontier << grid.star + heuristic.came_from[grid.star] = nil + end + + # One step in the heuristic search + + # Unless there are no more cells to explore from + unless heuristic.frontier.empty? + # Get the next cell to explore from + new_frontier = heuristic.frontier.shift + # For each of its neighbors + adjacent_neighbors(new_frontier).each do |neighbor| + # That have not been visited and are not walls + unless heuristic.came_from.has_key?(neighbor) || grid.walls.has_key?(neighbor) + # Add them to the frontier and mark them as visited + heuristic.frontier << neighbor + heuristic.came_from[neighbor] = new_frontier + end + end + end + + # Sort the frontier so that cells that are in a zigzag pattern are prioritized over those in an line + heuristic.frontier = heuristic.frontier.sort_by {| cell | proximity_to_star(cell) } + # Sort the frontier so cells that are close to the target are then prioritized + heuristic.frontier = heuristic.frontier.sort_by {| cell | heuristic_heuristic(cell) } + + # If the search found the target + if heuristic.came_from.has_key?(grid.target) + # Calculate the path between the target and star + heuristic_calc_path + end + end + + # Returns one-dimensional absolute distance between cell and target + # Returns a number to compare distances between cells and the target + def heuristic_heuristic(cell) + (grid.target.x - cell.x).abs + (grid.target.y - cell.y).abs + end + + # Calculates the path between the target and star for the heuristic search + # Only called when the heuristic search finds the target + def heuristic_calc_path + # Start from the target + endpoint = grid.target + # And the cell it came from + next_endpoint = heuristic.came_from[endpoint] + while endpoint and next_endpoint + # Draw a path between these two cells and store it + path = get_path_between(endpoint, next_endpoint) + heuristic.path << path + # And get the next pair of cells + endpoint = next_endpoint + next_endpoint = heuristic.came_from[endpoint] + # Continue till there are no more cells + end + end + + # Returns a list of adjacent cells + # Used to determine what the next cells to be added to the frontier are + def adjacent_neighbors(cell) + neighbors = [] + + # Gets all the valid neighbors into the array + # From southern neighbor, clockwise + neighbors << [cell.x , cell.y - 1] unless cell.y == 0 + neighbors << [cell.x - 1, cell.y ] unless cell.x == 0 + neighbors << [cell.x , cell.y + 1] unless cell.y == grid.height - 1 + neighbors << [cell.x + 1, cell.y ] unless cell.x == grid.width - 1 + + neighbors + end + + # Finds the vertical and horizontal distance of a cell from the star + # and returns the larger value + # This method is used to have a zigzag pattern in the rendered path + # A cell that is [5, 5] from the star, + # is explored before over a cell that is [0, 7] away. + # So, if possible, the search tries to go diagonal (zigzag) first + def proximity_to_star(cell) + distance_x = (grid.star.x - cell.x).abs + distance_y = (grid.star.y - cell.y).abs + + if distance_x > distance_y + return distance_x + else + return distance_y + end + end + + # Methods that allow code to be more concise. Subdivides args.state, which is where all variables are stored. + def grid + state.grid + end + + def buttons + state.buttons + end + + def slider + state.slider + end + + def bfs + state.bfs + end + + def heuristic + state.heuristic + end + + # Descriptive aliases for colors + def default_color + [221, 212, 213] # Light Brown + end + + def wall_color + [134, 134, 120] # Camo Green + end + + def visited_color + [204, 191, 179] # Dark Brown + end + + def frontier_color + [103, 136, 204] # Blue + end + + def path_color + [231, 230, 228] # Pastel White + end + + def button_color + [190, 190, 190] # Gray + end +end +# Method that is called by DragonRuby periodically +# Used for updating animations and calculations +def tick args + + # Pressing r will reset the application + if args.inputs.keyboard.key_down.r + args.gtk.reset + reset + return + end + + # Every tick, new args are passed, and the Breadth First Search tick is called + $heuristic ||= Heuristic.new(args) + $heuristic.args = args + $heuristic.tick +end + + +def reset + $heuristic = nil +end + +</code></pre> +<h3 id='----path-finding-algorithms---a-star---main.rb'>Path Finding Algorithms - A Star - main.rb</h3> +<pre><code class="language-ruby"># ./samples/13_path_finding_algorithms/08_a_star/app/main.rb +# This program is inspired by https://www.redblobgames.com/pathfinding/a-star/introduction.html + +# The A* Search works by incorporating both the distance from the starting point +# and the distance from the target in its heurisitic. + +# It tends to find the correct (shortest) path even when the Greedy Best-First Search does not, +# and it explores less of the grid, and is therefore faster, than Dijkstra's Search. + +class A_Star_Algorithm + attr_gtk + + def tick + defaults + render + input + + if dijkstra.came_from.empty? + calc_searches + end + end + + def defaults + # Variables to edit the size and appearance of the grid + # Freely customizable to user's liking + grid.width ||= 15 + grid.height ||= 15 + grid.cell_size ||= 27 + grid.rect ||= [0, 0, grid.width, grid.height] + + grid.star ||= [0, 2] + grid.target ||= [11, 13] + grid.walls ||= { + [2, 2] => true, + [3, 2] => true, + [4, 2] => true, + [5, 2] => true, + [6, 2] => true, + [7, 2] => true, + [8, 2] => true, + [9, 2] => true, + [10, 2] => true, + [11, 2] => true, + [12, 2] => true, + [12, 3] => true, + [12, 4] => true, + [12, 5] => true, + [12, 6] => true, + [12, 7] => true, + [12, 8] => true, + [12, 9] => true, + [12, 10] => true, + [12, 11] => true, + [12, 12] => true, + [5, 12] => true, + [6, 12] => true, + [7, 12] => true, + [8, 12] => true, + [9, 12] => true, + [10, 12] => true, + [11, 12] => true, + [12, 12] => true + } + + # What the user is currently editing on the grid + # We store this value, because we want to remember the value even when + # the user's cursor is no longer over what they're interacting with, but + # they are still clicking down on the mouse. + state.user_input ||= :none + + # These variables allow the breadth first search to take place + # Came_from is a hash with a key of a cell and a value of the cell that was expanded from to find the key. + # Used to prevent searching cells that have already been found + # and to trace a path from the target back to the starting point. + # Frontier is an array of cells to expand the search from. + # The search is over when there are no more cells to search from. + # Path stores the path from the target to the star, once the target has been found + # It prevents calculating the path every tick. + dijkstra.came_from ||= {} + dijkstra.cost_so_far ||= {} + dijkstra.frontier ||= [] + dijkstra.path ||= [] + + greedy.came_from ||= {} + greedy.frontier ||= [] + greedy.path ||= [] + + a_star.frontier ||= [] + a_star.came_from ||= {} + a_star.path ||= [] + end + + # All methods with render draw stuff on the screen + # UI has buttons, the slider, and labels + # The search specific rendering occurs in the respective methods + def render + render_labels + render_dijkstra + render_greedy + render_a_star + end + + def render_labels + outputs.labels << [150, 450, "Dijkstra's"] + outputs.labels << [550, 450, "Greedy Best-First"] + outputs.labels << [1025, 450, "A* Search"] + end + + def render_dijkstra + render_dijkstra_grid + render_dijkstra_star + render_dijkstra_target + render_dijkstra_visited + render_dijkstra_walls + render_dijkstra_path + end + + def render_greedy + render_greedy_grid + render_greedy_star + render_greedy_target + render_greedy_visited + render_greedy_walls + render_greedy_path + end + + def render_a_star + render_a_star_grid + render_a_star_star + render_a_star_target + render_a_star_visited + render_a_star_walls + render_a_star_path + end + + # This method handles user input every tick + def input + # If the mouse was lifted this tick + if inputs.mouse.up + # Set current input to none + state.user_input = :none + end + + # If the mouse was clicked this tick + if inputs.mouse.down + # Determine what the user is editing and appropriately edit the state.user_input variable + determine_input + end + + # Process user input based on user_input variable and current mouse position + process_input + end + + # Determines what the user is editing + # This method is called when the mouse is clicked down + def determine_input + # If the mouse is over the star in the first grid + if dijkstra_mouse_over_star? + # The user is editing the star from the first grid + state.user_input = :dijkstra_star + # If the mouse is over the star in the second grid + elsif greedy_mouse_over_star? + # The user is editing the star from the second grid + state.user_input = :greedy_star + # If the mouse is over the star in the third grid + elsif a_star_mouse_over_star? + # The user is editing the star from the third grid + state.user_input = :a_star_star + # If the mouse is over the target in the first grid + elsif dijkstra_mouse_over_target? + # The user is editing the target from the first grid + state.user_input = :dijkstra_target + # If the mouse is over the target in the second grid + elsif greedy_mouse_over_target? + # The user is editing the target from the second grid + state.user_input = :greedy_target + # If the mouse is over the target in the third grid + elsif a_star_mouse_over_target? + # The user is editing the target from the third grid + state.user_input = :a_star_target + # If the mouse is over a wall in the first grid + elsif dijkstra_mouse_over_wall? + # The user is removing a wall from the first grid + state.user_input = :dijkstra_remove_wall + # If the mouse is over a wall in the second grid + elsif greedy_mouse_over_wall? + # The user is removing a wall from the second grid + state.user_input = :greedy_remove_wall + # If the mouse is over a wall in the third grid + elsif a_star_mouse_over_wall? + # The user is removing a wall from the third grid + state.user_input = :a_star_remove_wall + # If the mouse is over the first grid + elsif dijkstra_mouse_over_grid? + # The user is adding a wall from the first grid + state.user_input = :dijkstra_add_wall + # If the mouse is over the second grid + elsif greedy_mouse_over_grid? + # The user is adding a wall from the second grid + state.user_input = :greedy_add_wall + # If the mouse is over the third grid + elsif a_star_mouse_over_grid? + # The user is adding a wall from the third grid + state.user_input = :a_star_add_wall + end + end + + # Processes click and drag based on what the user is currently dragging + def process_input + if state.user_input == :dijkstra_star + process_input_dijkstra_star + elsif state.user_input == :greedy_star + process_input_greedy_star + elsif state.user_input == :a_star_star + process_input_a_star_star + elsif state.user_input == :dijkstra_target + process_input_dijkstra_target + elsif state.user_input == :greedy_target + process_input_greedy_target + elsif state.user_input == :a_star_target + process_input_a_star_target + elsif state.user_input == :dijkstra_remove_wall + process_input_dijkstra_remove_wall + elsif state.user_input == :greedy_remove_wall + process_input_greedy_remove_wall + elsif state.user_input == :a_star_remove_wall + process_input_a_star_remove_wall + elsif state.user_input == :dijkstra_add_wall + process_input_dijkstra_add_wall + elsif state.user_input == :greedy_add_wall + process_input_greedy_add_wall + elsif state.user_input == :a_star_add_wall + process_input_a_star_add_wall + end + end + + def render_dijkstra_grid + # A large rect the size of the grid + outputs.solids << [dijkstra_scale_up(grid.rect), default_color] + + # The vertical grid lines + for x in 0..grid.width + outputs.lines << dijkstra_vertical_line(x) + end + + # The horizontal grid lines + for y in 0..grid.height + outputs.lines << dijkstra_horizontal_line(y) + end + end + + def render_greedy_grid + # A large rect the size of the grid + outputs.solids << [greedy_scale_up(grid.rect), default_color] + + # The vertical grid lines + for x in 0..grid.width + outputs.lines << greedy_vertical_line(x) + end + + # The horizontal grid lines + for y in 0..grid.height + outputs.lines << greedy_horizontal_line(y) + end + end + + def render_a_star_grid + # A large rect the size of the grid + outputs.solids << [a_star_scale_up(grid.rect), default_color] + + # The vertical grid lines + for x in 0..grid.width + outputs.lines << a_star_vertical_line(x) + end + + # The horizontal grid lines + for y in 0..grid.height + outputs.lines << a_star_horizontal_line(y) + end + end + + # Returns a vertical line for a column of the first grid + def dijkstra_vertical_line column + dijkstra_scale_up([column, 0, column, grid.height]) + end + + # Returns a horizontal line for a column of the first grid + def dijkstra_horizontal_line row + dijkstra_scale_up([0, row, grid.width, row]) + end + + # Returns a vertical line for a column of the second grid + def greedy_vertical_line column + dijkstra_scale_up([column + grid.width + 1, 0, column + grid.width + 1, grid.height]) + end + + # Returns a horizontal line for a column of the second grid + def greedy_horizontal_line row + dijkstra_scale_up([grid.width + 1, row, grid.width + grid.width + 1, row]) + end + + # Returns a vertical line for a column of the third grid + def a_star_vertical_line column + dijkstra_scale_up([column + (grid.width * 2) + 2, 0, column + (grid.width * 2) + 2, grid.height]) + end + + # Returns a horizontal line for a column of the third grid + def a_star_horizontal_line row + dijkstra_scale_up([(grid.width * 2) + 2, row, (grid.width * 2) + grid.width + 2, row]) + end + + # Renders the star on the first grid + def render_dijkstra_star + outputs.sprites << [dijkstra_scale_up(grid.star), 'star.png'] + end + + # Renders the star on the second grid + def render_greedy_star + outputs.sprites << [greedy_scale_up(grid.star), 'star.png'] + end + + # Renders the star on the third grid + def render_a_star_star + outputs.sprites << [a_star_scale_up(grid.star), 'star.png'] + end + + # Renders the target on the first grid + def render_dijkstra_target + outputs.sprites << [dijkstra_scale_up(grid.target), 'target.png'] + end + + # Renders the target on the second grid + def render_greedy_target + outputs.sprites << [greedy_scale_up(grid.target), 'target.png'] + end + + # Renders the target on the third grid + def render_a_star_target + outputs.sprites << [a_star_scale_up(grid.target), 'target.png'] + end + + # Renders the walls on the first grid + def render_dijkstra_walls + grid.walls.each_key do | wall | + outputs.solids << [dijkstra_scale_up(wall), wall_color] + end + end + + # Renders the walls on the second grid + def render_greedy_walls + grid.walls.each_key do | wall | + outputs.solids << [greedy_scale_up(wall), wall_color] + end + end + + # Renders the walls on the third grid + def render_a_star_walls + grid.walls.each_key do | wall | + outputs.solids << [a_star_scale_up(wall), wall_color] + end + end + + # Renders the visited cells on the first grid + def render_dijkstra_visited + dijkstra.came_from.each_key do | visited_cell | + outputs.solids << [dijkstra_scale_up(visited_cell), visited_color] + end + end + + # Renders the visited cells on the second grid + def render_greedy_visited + greedy.came_from.each_key do | visited_cell | + outputs.solids << [greedy_scale_up(visited_cell), visited_color] + end + end + + # Renders the visited cells on the third grid + def render_a_star_visited + a_star.came_from.each_key do | visited_cell | + outputs.solids << [a_star_scale_up(visited_cell), visited_color] + end + end + + # Renders the path found by the breadth first search on the first grid + def render_dijkstra_path + dijkstra.path.each do | path | + outputs.solids << [dijkstra_scale_up(path), path_color] + end + end + + # Renders the path found by the greedy search on the second grid + def render_greedy_path + greedy.path.each do | path | + outputs.solids << [greedy_scale_up(path), path_color] + end + end + + # Renders the path found by the a_star search on the third grid + def render_a_star_path + a_star.path.each do | path | + outputs.solids << [a_star_scale_up(path), path_color] + end + end + + # Returns the rect for the path between two cells based on their relative positions + def get_path_between(cell_one, cell_two) + path = [] + + # If cell one is above cell two + if cell_one.x == cell_two.x and cell_one.y > cell_two.y + # Path starts from the center of cell two and moves upward to the center of cell one + path = [cell_two.x + 0.3, cell_two.y + 0.3, 0.4, 1.4] + # If cell one is below cell two + elsif cell_one.x == cell_two.x and cell_one.y < cell_two.y + # Path starts from the center of cell one and moves upward to the center of cell two + path = [cell_one.x + 0.3, cell_one.y + 0.3, 0.4, 1.4] + # If cell one is to the left of cell two + elsif cell_one.x > cell_two.x and cell_one.y == cell_two.y + # Path starts from the center of cell two and moves rightward to the center of cell one + path = [cell_two.x + 0.3, cell_two.y + 0.3, 1.4, 0.4] + # If cell one is to the right of cell two + elsif cell_one.x < cell_two.x and cell_one.y == cell_two.y + # Path starts from the center of cell one and moves rightward to the center of cell two + path = [cell_one.x + 0.3, cell_one.y + 0.3, 1.4, 0.4] + end + + path + end + + # In code, the cells are represented as 1x1 rectangles + # When drawn, the cells are larger than 1x1 rectangles + # This method is used to scale up cells, and lines + # Objects are scaled up according to the grid.cell_size variable + # This allows for easy customization of the visual scale of the grid + # This method scales up cells for the first grid + def dijkstra_scale_up(cell) + # Prevents the original value of cell from being edited + cell = cell.clone + + # If cell is just an x and y coordinate + if cell.size == 2 + # Add a width and height of 1 + cell << 1 + cell << 1 + end + + # Scale all the values up + cell.map! { |value| value * grid.cell_size } + + # Returns the scaled up cell + cell + end + + # Translates the given cell grid.width + 1 to the right and then scales up + # Used to draw cells for the second grid + # This method does not work for lines, + # so separate methods exist for the grid lines + def greedy_scale_up(cell) + # Prevents the original value of cell from being edited + cell = cell.clone + # Translates the cell to the second grid equivalent + cell.x += grid.width + 1 + # Proceeds as if scaling up for the first grid + dijkstra_scale_up(cell) + end + + # Translates the given cell (grid.width + 1) * 2 to the right and then scales up + # Used to draw cells for the third grid + # This method does not work for lines, + # so separate methods exist for the grid lines + def a_star_scale_up(cell) + # Prevents the original value of cell from being edited + cell = cell.clone + # Translates the cell to the second grid equivalent + cell.x += grid.width + 1 + # Translates the cell to the third grid equivalent + cell.x += grid.width + 1 + # Proceeds as if scaling up for the first grid + dijkstra_scale_up(cell) + end + + # Signal that the user is going to be moving the star from the first grid + def dijkstra_mouse_over_star? + inputs.mouse.point.inside_rect?(dijkstra_scale_up(grid.star)) + end + + # Signal that the user is going to be moving the star from the second grid + def greedy_mouse_over_star? + inputs.mouse.point.inside_rect?(greedy_scale_up(grid.star)) + end + + # Signal that the user is going to be moving the star from the third grid + def a_star_mouse_over_star? + inputs.mouse.point.inside_rect?(a_star_scale_up(grid.star)) + end + + # Signal that the user is going to be moving the target from the first grid + def dijkstra_mouse_over_target? + inputs.mouse.point.inside_rect?(dijkstra_scale_up(grid.target)) + end + + # Signal that the user is going to be moving the target from the second grid + def greedy_mouse_over_target? + inputs.mouse.point.inside_rect?(greedy_scale_up(grid.target)) + end + + # Signal that the user is going to be moving the target from the third grid + def a_star_mouse_over_target? + inputs.mouse.point.inside_rect?(a_star_scale_up(grid.target)) + end + + # Signal that the user is going to be removing walls from the first grid + def dijkstra_mouse_over_wall? + grid.walls.each_key do | wall | + return true if inputs.mouse.point.inside_rect?(dijkstra_scale_up(wall)) + end + + false + end + + # Signal that the user is going to be removing walls from the second grid + def greedy_mouse_over_wall? + grid.walls.each_key do | wall | + return true if inputs.mouse.point.inside_rect?(greedy_scale_up(wall)) + end + + false + end + + # Signal that the user is going to be removing walls from the third grid + def a_star_mouse_over_wall? + grid.walls.each_key do | wall | + return true if inputs.mouse.point.inside_rect?(a_star_scale_up(wall)) + end + + false + end + + # Signal that the user is going to be adding walls from the first grid + def dijkstra_mouse_over_grid? + inputs.mouse.point.inside_rect?(dijkstra_scale_up(grid.rect)) + end + + # Signal that the user is going to be adding walls from the second grid + def greedy_mouse_over_grid? + inputs.mouse.point.inside_rect?(greedy_scale_up(grid.rect)) + end + + # Signal that the user is going to be adding walls from the third grid + def a_star_mouse_over_grid? + inputs.mouse.point.inside_rect?(a_star_scale_up(grid.rect)) + end + + # Moves the star to the cell closest to the mouse in the first grid + # Only resets the search if the star changes position + # Called whenever the user is editing the star (puts mouse down on star) + def process_input_dijkstra_star + old_star = grid.star.clone + unless dijkstra_cell_closest_to_mouse == grid.target + grid.star = dijkstra_cell_closest_to_mouse + end + unless old_star == grid.star + reset_searches + end + end + + # Moves the star to the cell closest to the mouse in the second grid + # Only resets the search if the star changes position + # Called whenever the user is editing the star (puts mouse down on star) + def process_input_greedy_star + old_star = grid.star.clone + unless greedy_cell_closest_to_mouse == grid.target + grid.star = greedy_cell_closest_to_mouse + end + unless old_star == grid.star + reset_searches + end + end + + # Moves the star to the cell closest to the mouse in the third grid + # Only resets the search if the star changes position + # Called whenever the user is editing the star (puts mouse down on star) + def process_input_a_star_star + old_star = grid.star.clone + unless a_star_cell_closest_to_mouse == grid.target + grid.star = a_star_cell_closest_to_mouse + end + unless old_star == grid.star + reset_searches + end + end + + # Moves the target to the grid closest to the mouse in the first grid + # Only reset_searchess the search if the target changes position + # Called whenever the user is editing the target (puts mouse down on target) + def process_input_dijkstra_target + old_target = grid.target.clone + unless dijkstra_cell_closest_to_mouse == grid.star + grid.target = dijkstra_cell_closest_to_mouse + end + unless old_target == grid.target + reset_searches + end + end + + # Moves the target to the cell closest to the mouse in the second grid + # Only reset_searchess the search if the target changes position + # Called whenever the user is editing the target (puts mouse down on target) + def process_input_greedy_target + old_target = grid.target.clone + unless greedy_cell_closest_to_mouse == grid.star + grid.target = greedy_cell_closest_to_mouse + end + unless old_target == grid.target + reset_searches + end + end + + # Moves the target to the cell closest to the mouse in the third grid + # Only reset_searchess the search if the target changes position + # Called whenever the user is editing the target (puts mouse down on target) + def process_input_a_star_target + old_target = grid.target.clone + unless a_star_cell_closest_to_mouse == grid.star + grid.target = a_star_cell_closest_to_mouse + end + unless old_target == grid.target + reset_searches + end + end + + # Removes walls in the first grid that are under the cursor + def process_input_dijkstra_remove_wall + # The mouse needs to be inside the grid, because we only want to remove walls + # the cursor is directly over + # Recalculations should only occur when a wall is actually deleted + if dijkstra_mouse_over_grid? + if grid.walls.has_key?(dijkstra_cell_closest_to_mouse) + grid.walls.delete(dijkstra_cell_closest_to_mouse) + reset_searches + end + end + end + + # Removes walls in the second grid that are under the cursor + def process_input_greedy_remove_wall + # The mouse needs to be inside the grid, because we only want to remove walls + # the cursor is directly over + # Recalculations should only occur when a wall is actually deleted + if greedy_mouse_over_grid? + if grid.walls.has_key?(greedy_cell_closest_to_mouse) + grid.walls.delete(greedy_cell_closest_to_mouse) + reset_searches + end + end + end + + # Removes walls in the third grid that are under the cursor + def process_input_a_star_remove_wall + # The mouse needs to be inside the grid, because we only want to remove walls + # the cursor is directly over + # Recalculations should only occur when a wall is actually deleted + if a_star_mouse_over_grid? + if grid.walls.has_key?(a_star_cell_closest_to_mouse) + grid.walls.delete(a_star_cell_closest_to_mouse) + reset_searches + end + end + end + + # Adds a wall in the first grid in the cell the mouse is over + def process_input_dijkstra_add_wall + if dijkstra_mouse_over_grid? + unless grid.walls.has_key?(dijkstra_cell_closest_to_mouse) + grid.walls[dijkstra_cell_closest_to_mouse] = true + reset_searches + end + end + end + + # Adds a wall in the second grid in the cell the mouse is over + def process_input_greedy_add_wall + if greedy_mouse_over_grid? + unless grid.walls.has_key?(greedy_cell_closest_to_mouse) + grid.walls[greedy_cell_closest_to_mouse] = true + reset_searches + end + end + end + + # Adds a wall in the third grid in the cell the mouse is over + def process_input_a_star_add_wall + if a_star_mouse_over_grid? + unless grid.walls.has_key?(a_star_cell_closest_to_mouse) + grid.walls[a_star_cell_closest_to_mouse] = true + reset_searches + end + end + end + + # When the user grabs the star and puts their cursor to the far right + # and moves up and down, the star is supposed to move along the grid as well + # Finding the cell closest to the mouse helps with this + def dijkstra_cell_closest_to_mouse + # Closest cell to the mouse in the first grid + x = (inputs.mouse.point.x / grid.cell_size).to_i + y = (inputs.mouse.point.y / grid.cell_size).to_i + # Bound x and y to the grid + x = grid.width - 1 if x > grid.width - 1 + y = grid.height - 1 if y > grid.height - 1 + # Return closest cell + [x, y] + end + + # When the user grabs the star and puts their cursor to the far right + # and moves up and down, the star is supposed to move along the grid as well + # Finding the cell closest to the mouse in the second grid helps with this + def greedy_cell_closest_to_mouse + # Closest cell grid to the mouse in the second + x = (inputs.mouse.point.x / grid.cell_size).to_i + y = (inputs.mouse.point.y / grid.cell_size).to_i + # Translate the cell to the first grid + x -= grid.width + 1 + # Bound x and y to the first grid + x = 0 if x < 0 + y = 0 if y < 0 + x = grid.width - 1 if x > grid.width - 1 + y = grid.height - 1 if y > grid.height - 1 + # Return closest cell + [x, y] + end + + # When the user grabs the star and puts their cursor to the far right + # and moves up and down, the star is supposed to move along the grid as well + # Finding the cell closest to the mouse in the third grid helps with this + def a_star_cell_closest_to_mouse + # Closest cell grid to the mouse in the second + x = (inputs.mouse.point.x / grid.cell_size).to_i + y = (inputs.mouse.point.y / grid.cell_size).to_i + # Translate the cell to the first grid + x -= (grid.width + 1) * 2 + # Bound x and y to the first grid + x = 0 if x < 0 + y = 0 if y < 0 + x = grid.width - 1 if x > grid.width - 1 + y = grid.height - 1 if y > grid.height - 1 + # Return closest cell + [x, y] + end + + def reset_searches + # Reset the searches + dijkstra.came_from = {} + dijkstra.cost_so_far = {} + dijkstra.frontier = [] + dijkstra.path = [] + + greedy.came_from = {} + greedy.frontier = [] + greedy.path = [] + a_star.came_from = {} + a_star.frontier = [] + a_star.path = [] + end + + def calc_searches + calc_dijkstra + calc_greedy + calc_a_star + # Move the searches forward to the current step + # state.current_step.times { move_searches_one_step_forward } + end + + def calc_dijkstra + # Sets up the search to begin from the star + dijkstra.frontier << grid.star + dijkstra.came_from[grid.star] = nil + dijkstra.cost_so_far[grid.star] = 0 + + # Until the target is found or there are no more cells to explore from + until dijkstra.came_from.has_key?(grid.target) or dijkstra.frontier.empty? + # Take the next frontier cell. The first element is the cell, the second is the priority. + new_frontier = dijkstra.frontier.shift#[0] + # For each of its neighbors + adjacent_neighbors(new_frontier).each do | neighbor | + # That have not been visited and are not walls + unless dijkstra.came_from.has_key?(neighbor) or grid.walls.has_key?(neighbor) + # Add them to the frontier and mark them as visited + dijkstra.frontier << neighbor + dijkstra.came_from[neighbor] = new_frontier + dijkstra.cost_so_far[neighbor] = dijkstra.cost_so_far[new_frontier] + 1 + end + end + + # Sort the frontier so that cells that are in a zigzag pattern are prioritized over those in an line + # Comment this line and let a path generate to see the difference + dijkstra.frontier = dijkstra.frontier.sort_by {| cell | proximity_to_star(cell) } + dijkstra.frontier = dijkstra.frontier.sort_by {| cell | dijkstra.cost_so_far[cell] } + end + + + # If the search found the target + if dijkstra.came_from.has_key?(grid.target) + # Calculate the path between the target and star + dijkstra_calc_path + end + end + + def calc_greedy + # Sets up the search to begin from the star + greedy.frontier << grid.star + greedy.came_from[grid.star] = nil + + # Until the target is found or there are no more cells to explore from + until greedy.came_from.has_key?(grid.target) or greedy.frontier.empty? + # Take the next frontier cell + new_frontier = greedy.frontier.shift + # For each of its neighbors + adjacent_neighbors(new_frontier).each do | neighbor | + # That have not been visited and are not walls + unless greedy.came_from.has_key?(neighbor) or grid.walls.has_key?(neighbor) + # Add them to the frontier and mark them as visited + greedy.frontier << neighbor + greedy.came_from[neighbor] = new_frontier + end + end + # Sort the frontier so that cells that are in a zigzag pattern are prioritized over those in an line + # Comment this line and let a path generate to see the difference + greedy.frontier = greedy.frontier.sort_by {| cell | proximity_to_star(cell) } + # Sort the frontier so cells that are close to the target are then prioritized + greedy.frontier = greedy.frontier.sort_by {| cell | greedy_heuristic(cell) } + end + + + # If the search found the target + if greedy.came_from.has_key?(grid.target) + # Calculate the path between the target and star + greedy_calc_path + end + end + + def calc_a_star + # Setup the search to start from the star + a_star.came_from[grid.star] = nil + a_star.cost_so_far[grid.star] = 0 + a_star.frontier << grid.star + + # Until there are no more cells to explore from or the search has found the target + until a_star.frontier.empty? or a_star.came_from.has_key?(grid.target) + # Get the next cell to expand from + current_frontier = a_star.frontier.shift + + # For each of that cells neighbors + adjacent_neighbors(current_frontier).each do | neighbor | + # That have not been visited and are not walls + unless a_star.came_from.has_key?(neighbor) or grid.walls.has_key?(neighbor) + # Add them to the frontier and mark them as visited + a_star.frontier << neighbor + a_star.came_from[neighbor] = current_frontier + a_star.cost_so_far[neighbor] = a_star.cost_so_far[current_frontier] + 1 + end + end + + # Sort the frontier so that cells that are in a zigzag pattern are prioritized over those in an line + # Comment this line and let a path generate to see the difference + a_star.frontier = a_star.frontier.sort_by {| cell | proximity_to_star(cell) } + a_star.frontier = a_star.frontier.sort_by {| cell | a_star.cost_so_far[cell] + greedy_heuristic(cell) } + end + + # If the search found the target + if a_star.came_from.has_key?(grid.target) + # Calculate the path between the target and star + a_star_calc_path + end + end + + # Calculates the path between the target and star for the breadth first search + # Only called when the breadth first search finds the target + def dijkstra_calc_path + # Start from the target + endpoint = grid.target + # And the cell it came from + next_endpoint = dijkstra.came_from[endpoint] + while endpoint and next_endpoint + # Draw a path between these two cells and store it + path = get_path_between(endpoint, next_endpoint) + dijkstra.path << path + # And get the next pair of cells + endpoint = next_endpoint + next_endpoint = dijkstra.came_from[endpoint] + # Continue till there are no more cells + end + end + + # Returns one-dimensional absolute distance between cell and target + # Returns a number to compare distances between cells and the target + def greedy_heuristic(cell) + (grid.target.x - cell.x).abs + (grid.target.y - cell.y).abs + end + + # Calculates the path between the target and star for the greedy search + # Only called when the greedy search finds the target + def greedy_calc_path + # Start from the target + endpoint = grid.target + # And the cell it came from + next_endpoint = greedy.came_from[endpoint] + while endpoint and next_endpoint + # Draw a path between these two cells and store it + path = get_path_between(endpoint, next_endpoint) + greedy.path << path + # And get the next pair of cells + endpoint = next_endpoint + next_endpoint = greedy.came_from[endpoint] + # Continue till there are no more cells + end + end + + # Calculates the path between the target and star for the a_star search + # Only called when the a_star search finds the target + def a_star_calc_path + # Start from the target + endpoint = grid.target + # And the cell it came from + next_endpoint = a_star.came_from[endpoint] + + while endpoint and next_endpoint + # Draw a path between these two cells and store it + path = get_path_between(endpoint, next_endpoint) + a_star.path << path + # And get the next pair of cells + endpoint = next_endpoint + next_endpoint = a_star.came_from[endpoint] + # Continue till there are no more cells + end + end + + # Returns a list of adjacent cells + # Used to determine what the next cells to be added to the frontier are + def adjacent_neighbors(cell) + neighbors = [] + + # Gets all the valid neighbors into the array + # From southern neighbor, clockwise + neighbors << [cell.x , cell.y - 1] unless cell.y == 0 + neighbors << [cell.x - 1, cell.y ] unless cell.x == 0 + neighbors << [cell.x , cell.y + 1] unless cell.y == grid.height - 1 + neighbors << [cell.x + 1, cell.y ] unless cell.x == grid.width - 1 + + neighbors + end + + # Finds the vertical and horizontal distance of a cell from the star + # and returns the larger value + # This method is used to have a zigzag pattern in the rendered path + # A cell that is [5, 5] from the star, + # is explored before over a cell that is [0, 7] away. + # So, if possible, the search tries to go diagonal (zigzag) first + def proximity_to_star(cell) + distance_x = (grid.star.x - cell.x).abs + distance_y = (grid.star.y - cell.y).abs + + if distance_x > distance_y + return distance_x + else + return distance_y + end + end + + # Methods that allow code to be more concise. Subdivides args.state, which is where all variables are stored. + def grid + state.grid + end + + def dijkstra + state.dijkstra + end + + def greedy + state.greedy + end + + def a_star + state.a_star + end + + # Descriptive aliases for colors + def default_color + [221, 212, 213] # Light Brown + end + + def wall_color + [134, 134, 120] # Camo Green + end + + def visited_color + [204, 191, 179] # Dark Brown + end + + def path_color + [231, 230, 228] # Pastel White + end + + def button_color + [190, 190, 190] # Gray + end +end + + +# Method that is called by DragonRuby periodically +# Used for updating animations and calculations +def tick args + + # Pressing r will reset the application + if args.inputs.keyboard.key_down.r + args.gtk.reset + reset + return + end + + # Every tick, new args are passed, and the Breadth First Search tick is called + $a_star_algorithm ||= A_Star_Algorithm.new(args) + $a_star_algorithm.args = args + $a_star_algorithm.tick +end + + +def reset + $a_star_algorithm = nil +end + +</code></pre> +<h3 id='----path-finding-algorithms---tower-defense---main.rb'>Path Finding Algorithms - Tower Defense - main.rb</h3> +<pre><code class="language-ruby"># ./samples/13_path_finding_algorithms/09_tower_defense/app/main.rb +# An example of some major components in a tower defence game +# The pathing of the tanks is determined by A* algorithm -- try editing the walls + +# The turrets shoot bullets at the closest tank. The bullets are heat-seeking + +def tick args + $gtk.reset if args.inputs.keyboard.key_down.r + defaults args + render args + calc args +end + +def defaults args + args.outputs.background_color = wall_color + args.state.grid_size = 5 + args.state.tile_size = 50 + args.state.grid_start ||= [0, 0] + args.state.grid_goal ||= [4, 4] + + # Try editing these walls to see the path change! + args.state.walls ||= { + [0, 4] => true, + [1, 3] => true, + [3, 1] => true, + # [4, 0] => true, + } + + args.state.a_star.frontier ||= [] + args.state.a_star.came_from ||= {} + args.state.a_star.path ||= [] + + args.state.tanks ||= [] + args.state.tank_spawn_period ||= 60 + args.state.tank_sprite_path ||= 'sprites/circle/white.png' + args.state.tank_speed ||= 1 + + args.state.turret_shoot_period = 10 + # Turrets can be entered as [x, y] but are immediately mapped to hashes + # Walls are also added where the turrets are to prevent tanks from pathing over them + args.state.turrets ||= [ + [2, 2] + ].each { |turret| args.state.walls[turret] = true}.map do |x, y| + { + x: x * args.state.tile_size, + y: y * args.state.tile_size, + w: args.state.tile_size, + h: args.state.tile_size, + path: 'sprites/circle/gray.png', + range: 100 + } + end + + args.state.bullet_size ||= 25 + args.state.bullets ||= [] + args.state.bullet_path ||= 'sprites/circle/orange.png' + # +end + +def render args + render_grid args + render_a_star args + args.outputs.sprites << args.state.tanks + args.outputs.sprites << args.state.turrets + args.outputs.sprites << args.state.bullets +end + +def render_grid args + # Draw a square the size and color of the grid + args.outputs.solids << [ + 0, + 0, + args.state.grid_size * args.state.tile_size, + args.state.grid_size * args.state.tile_size, + grid_color + ] + + # Draw lines across the grid to show tiles + (args.state.grid_size + 1).times do | value | + render_horizontal_line(args, value) + render_vertical_line(args, value) + end + + # Render special tiles + render_tile(args, args.state.grid_start, start_color) + render_tile(args, args.state.grid_goal, goal_color) + args.state.walls.keys.each { |wall| render_tile(args, wall, wall_color) } +end + +def render_vertical_line args, x + args.outputs.lines << [ + x * args.state.tile_size, + 0, + x * args.state.tile_size, + args.state.tile_size * args.state.grid_size, + ] +end + +def render_horizontal_line args, y + args.outputs.lines << [ + 0, + y * args.state.tile_size, + args.state.tile_size * args.state.grid_size, + y * args.state.tile_size, + ] +end + +def render_tile args, tile, color + args.outputs.solids << [ + tile.x * args.state.tile_size, + tile.y * args.state.tile_size, + args.state.tile_size, + args.state.tile_size, + color + ] +end + +def calc args + calc_a_star args + calc_tanks args + calc_turrets args + calc_bullets args +end + +def calc_a_star args + # Only does this one time + return unless args.state.a_star.path.empty? + + # Start the search from the grid start + args.state.a_star.frontier << args.state.grid_start + args.state.a_star.came_from[args.state.grid_start] = nil + + # Until a path to the goal has been found or there are no more tiles to explore + until (args.state.a_star.came_from.has_key?(args.state.grid_goal)|| args.state.a_star.frontier.empty?) + # For the first tile in the frontier + tile_to_expand_from = args.state.a_star.frontier.shift + # Add each of its neighbors to the frontier + neighbors(args, tile_to_expand_from).each do | tile | + args.state.a_star.frontier << tile + args.state.a_star.came_from[tile] = tile_to_expand_from + end + end + + # Stop calculating a path if the goal was never reached + return unless args.state.a_star.came_from.has_key? args.state.grid_goal + + # Fill path by tracing back from the goal + current_cell = args.state.grid_goal + while current_cell + args.state.a_star.path.unshift current_cell + current_cell = args.state.a_star.came_from[current_cell] + end + + puts "The path has been calculated" + puts args.state.a_star.path +end + +def calc_tanks args + spawn_tank args + move_tanks args +end + +def move_tanks args + # Remove tanks that have reached the end of their path + args.state.tanks.reject! { |tank| tank[:a_star].empty? } + + # Tanks have an array that has each tile it has to go to in order from a* path + args.state.tanks.each do | tank | + destination = tank[:a_star][0] + # Move the tank towards the destination + tank[:x] += copy_sign(args.state.tank_speed, ((destination.x * args.state.tile_size) - tank[:x])) + tank[:y] += copy_sign(args.state.tank_speed, ((destination.y * args.state.tile_size) - tank[:y])) + # If the tank has reached its destination + if (destination.x * args.state.tile_size) == tank[:x] and + (destination.y * args.state.tile_size) == tank[:y] + # Set the destination to the next point in the path + tank[:a_star].shift + end + end +end + +def calc_turrets args + return unless args.state.tick_count.mod_zero? args.state.turret_shoot_period + args.state.turrets.each do | turret | + # Finds the closest tank + target = nil + shortest_distance = turret[:range] + 1 + args.state.tanks.each do | tank | + distance = distance_between(turret[:x], turret[:y], tank[:x], tank[:y]) + if distance < shortest_distance + target = tank + shortest_distance = distance + end + end + # If there is a tank in range, fires a bullet + if target + args.state.bullets << { + x: turret[:x], + y: turret[:y], + w: args.state.bullet_size, + h: args.state.bullet_size, + path: args.state.bullet_path, + # Note that this makes it heat-seeking, because target is passed by reference + # Could do target.clone to make the bullet go to where the tank initially was + target: target + } + end + end +end + +def calc_bullets args + # Bullets aim for the center of their targets + args.state.bullets.each { |bullet| move bullet, center_of(bullet[:target])} + args.state.bullets.reject! { |b| b.intersect_rect? b[:target] } +end + +def center_of object + object = object.clone + object[:x] += 0.5 + object[:y] += 0.5 + object +end + +def render_a_star args + args.state.a_star.path.map do |tile| + # Map each x, y coordinate to the center of the tile and scale up + [(tile.x + 0.5) * args.state.tile_size, (tile.y + 0.5) * args.state.tile_size] + end.inject do | point_a, point_b | + # Render the line between each point + args.outputs.lines << [point_a.x, point_a.y, point_b.x, point_b.y, a_star_color] + point_b + end +end + +# Moves object to target at speed +def move object, target, speed = 1 + if target.is_a? Hash + object[:x] += copy_sign(speed, target[:x] - object[:x]) + object[:y] += copy_sign(speed, target[:y] - object[:y]) + else + object[:x] += copy_sign(speed, target.x - object[:x]) + object[:y] += copy_sign(speed, target.y - object[:y]) + end +end +# +# +def distance_between a_x, a_y, b_x, b_y + (((b_x - a_x) ** 2) + ((b_y - a_y) ** 2)) ** 0.5 +end + +def copy_sign value, sign + return 0 if sign == 0 + return value if sign > 0 + -value +end +# +def spawn_tank args + return unless args.state.tick_count.mod_zero? args.state.tank_spawn_period + args.state.tanks << { + x: args.state.grid_start.x, + y: args.state.grid_start.y, + w: args.state.tile_size, + h: args.state.tile_size, + path: args.state.tank_sprite_path, + a_star: args.state.a_star.path.clone + } +end + +def neighbors args, tile + [[tile.x, tile.y - 1], + [tile.x, tile.y + 1], + [tile.x + 1, tile.y], + [tile.x - 1, tile.y]].reject do | neighbor | + args.state.a_star.came_from.has_key?(neighbor) or + tile_out_of_bounds?(args, neighbor) or + args.state.walls.has_key? neighbor + end +end + +def tile_out_of_bounds? args, tile + tile.x < 0 or tile.y < 0 or tile.x >= args.state.grid_size or tile.y >= args.state.grid_size +end + +def grid_color + [133, 226, 144] +end + +def start_color + [226, 144, 133] +end + +def goal_color + [226, 133, 144] +end + +def wall_color + [133, 144, 226] +end + +def a_star_color + [0, 0, 255] +end + +</code></pre> +<h3 id='----3d---3d-cube---main.rb'>3d - 3d Cube - main.rb</h3> +<pre><code class="language-ruby"># ./samples/99_genre_3d/01_3d_cube/app/main.rb STARTX = 0.0 STARTY = 0.0 ENDY = 20.0 @@ -10647,9 +21795,359 @@ end $gtk.reset -</pre> -<h1 id='--arcade---dueling-starships---main.rb'>Arcade - Dueling Starships - main.rb</h1> -<pre># ./samples/99_genre_arcade/dueling_starships/app/main.rb +</code></pre> +<h3 id='----3d---wireframe---main.rb'>3d - Wireframe - main.rb</h3> +<pre><code class="language-ruby"># ./samples/99_genre_3d/02_wireframe/app/main.rb +def tick args + args.state.model ||= Object3D.new('data/shuttle.off') + args.state.mtx ||= rotate3D(0, 0, 0) + args.state.inv_mtx ||= rotate3D(0, 0, 0) + delta_mtx = rotate3D(args.inputs.up_down * 0.01, input_roll(args) * 0.01, args.inputs.left_right * 0.01) + args.outputs.lines << args.state.model.edges + args.state.model.fast_3x3_transform! args.state.inv_mtx + args.state.inv_mtx = mtx_mul(delta_mtx.transpose, args.state.inv_mtx) + args.state.mtx = mtx_mul(args.state.mtx, delta_mtx) + args.state.model.fast_3x3_transform! args.state.mtx + args.outputs.background_color = [0, 0, 0] + args.outputs.debug << args.gtk.framerate_diagnostics_primitives +end + +def input_roll args + roll = 0 + roll += 1 if args.inputs.keyboard.e + roll -= 1 if args.inputs.keyboard.q + roll +end + +def rotate3D(theta_x = 0.1, theta_y = 0.1, theta_z = 0.1) + c_x, s_x = Math.cos(theta_x), Math.sin(theta_x) + c_y, s_y = Math.cos(theta_y), Math.sin(theta_y) + c_z, s_z = Math.cos(theta_z), Math.sin(theta_z) + rot_x = [[1, 0, 0], [0, c_x, -s_x], [0, s_x, c_x]] + rot_y = [[c_y, 0, s_y], [0, 1, 0], [-s_y, 0, c_y]] + rot_z = [[c_z, -s_z, 0], [s_z, c_z, 0], [0, 0, 1]] + mtx_mul(mtx_mul(rot_x, rot_y), rot_z) +end + +def mtx_mul(a, b) + is = (0...a.length) + js = (0...b[0].length) + ks = (0...b.length) + is.map do |i| + js.map do |j| + ks.map do |k| + a[i][k] * b[k][j] + end.reduce(&:plus) + end + end +end + +class Object3D + attr_reader :vert_count, :face_count, :edge_count, :verts, :faces, :edges + + def initialize(path) + @vert_count = 0 + @face_count = 0 + @edge_count = 0 + @verts = [] + @faces = [] + @edges = [] + _init_from_file path + end + + def _init_from_file path + file_lines = $gtk.read_file(path).split("\n") + .reject { |line| line.start_with?('#') || line.split(' ').length == 0 } # Strip out simple comments and blank lines + .map { |line| line.split('#')[0] } # Strip out end of line comments + .map { |line| line.split(' ') } # Tokenize by splitting on whitespace + raise "OFF file did not start with OFF." if file_lines.shift != ["OFF"] # OFF meshes are supposed to begin with "OFF" as the first line. + raise "<NVertices NFaces NEdges> line malformed" if file_lines[0].length != 3 # The second line needs to have 3 numbers. Raise an error if it doesn't. + @vert_count, @face_count, @edge_count = file_lines.shift&.map(&:to_i) # Update the counts + # Only the vertex and face counts need to be accurate. Raise an error if they are inaccurate. + raise "Incorrect number of vertices and/or faces (Parsed VFE header: #{@vert_count} #{@face_count} #{@edge_count})" if file_lines.length != @vert_count + @face_count + # Grab all the lines describing vertices. + vert_lines = file_lines[0, @vert_count] + # Grab all the lines describing faces. + face_lines = file_lines[@vert_count, @face_count] + # Create all the vertices + @verts = vert_lines.map_with_index { |line, id| Vertex.new(line, id) } + # Create all the faces + @faces = face_lines.map { |line| Face.new(line, @verts) } + # Create all the edges + @edges = @faces.flat_map(&:edges).uniq do |edge| + sorted = edge.sorted + [sorted.point_a, sorted.point_b] + end + end + + def fast_3x3_transform! mtx + @verts.each { |vert| vert.fast_3x3_transform! mtx } + end +end + +class Face + + attr_reader :verts, :edges + + def initialize(data, verts) + vert_count = data[0].to_i + vert_ids = data[1, vert_count].map(&:to_i) + @verts = vert_ids.map { |i| verts[i] } + @edges = [] + (0...vert_count).each { |i| @edges[i] = Edge.new(verts[vert_ids[i - 1]], verts[vert_ids[i]]) } + @edges.rotate! 1 + end +end + +class Edge + attr_reader :point_a, :point_b + + def initialize(point_a, point_b) + @point_a = point_a + @point_b = point_b + end + + def sorted + @point_a.id < @point_b.id ? self : Edge.new(@point_b, @point_a) + end + + def draw_override ffi + ffi.draw_line(@point_a.render_x, @point_a.render_y, @point_b.render_x, @point_b.render_y, 255, 0, 0, 128) + ffi.draw_line(@point_a.render_x+1, @point_a.render_y, @point_b.render_x+1, @point_b.render_y, 255, 0, 0, 128) + ffi.draw_line(@point_a.render_x, @point_a.render_y+1, @point_b.render_x, @point_b.render_y+1, 255, 0, 0, 128) + ffi.draw_line(@point_a.render_x+1, @point_a.render_y+1, @point_b.render_x+1, @point_b.render_y+1, 255, 0, 0, 128) + end + + def primitive_marker + :line + end +end + +class Vertex + attr_accessor :x, :y, :z, :id + + def initialize(data, id) + @x = data[0].to_f + @y = data[1].to_f + @z = data[2].to_f + @id = id + end + + def fast_3x3_transform! mtx + _x, _y, _z = @x, @y, @z + @x = mtx[0][0] * _x + mtx[0][1] * _y + mtx[0][2] * _z + @y = mtx[1][0] * _x + mtx[1][1] * _y + mtx[1][2] * _z + @z = mtx[2][0] * _x + mtx[2][1] * _y + mtx[2][2] * _z + end + + def render_x + @x * (10 / (5 - @y)) * 170 + 640 + end + + def render_y + @z * (10 / (5 - @y)) * 170 + 360 + end +end +</code></pre> +<h3 id='----3d---wireframe---data---what-is-this.txt'>3d - Wireframe - Data - what-is-this.txt</h3> +<pre><code class="language-ruby"># ./samples/99_genre_3d/02_wireframe/data/what-is-this.txt +https://en.wikipedia.org/wiki/OFF_(file_format) +</code></pre> +<h3 id='----arcade---bullet-hell---main.rb'>Arcade - Bullet Hell - main.rb</h3> +<pre><code class="language-ruby"># ./samples/99_genre_arcade/bullet_hell/app/main.rb +def tick args + args.state.base_columns ||= 10.times.map { |n| 50 * n + 1280 / 2 - 5 * 50 + 5 } + args.state.base_rows ||= 5.times.map { |n| 50 * n + 720 - 5 * 50 } + args.state.offset_columns = 10.times.map { |n| (n - 4.5) * Math.sin(Kernel.tick_count.to_radians) * 12 } + args.state.offset_rows = 5.map { 0 } + args.state.columns = 10.times.map { |i| args.state.base_columns[i] + args.state.offset_columns[i] } + args.state.rows = 5.times.map { |i| args.state.base_rows[i] + args.state.offset_rows[i] } + args.state.explosions ||= [] + args.state.enemies ||= [] + args.state.score ||= 0 + args.state.wave ||= 0 + if args.state.enemies.empty? + args.state.wave += 1 + args.state.wave_root = Math.sqrt(args.state.wave) + args.state.enemies = make_enemies + end + args.state.player ||= {x: 620, y: 80, w: 40, h: 40, path: 'sprites/circle-gray.png', angle: 90, cooldown: 0, alive: true} + args.state.enemy_bullets ||= [] + args.state.player_bullets ||= [] + args.state.lives ||= 3 + args.state.missed_shots ||= 0 + args.state.fired_shots ||= 0 + + update_explosions args + update_enemy_positions args + + if args.inputs.left && args.state.player[:x] > (300 + 5) + args.state.player[:x] -= 5 + end + if args.inputs.right && args.state.player[:x] < (1280 - args.state.player[:w] - 300 - 5) + args.state.player[:x] += 5 + end + + args.state.enemy_bullets.each do |bullet| + bullet[:x] += bullet[:dx] + bullet[:y] += bullet[:dy] + end + args.state.player_bullets.each do |bullet| + bullet[:x] += bullet[:dx] + bullet[:y] += bullet[:dy] + end + + args.state.enemy_bullets = args.state.enemy_bullets.find_all { |bullet| bullet[:y].between?(-16, 736) } + args.state.player_bullets = args.state.player_bullets.find_all do |bullet| + if bullet[:y].between?(-16, 736) + true + else + args.state.missed_shots += 1 + false + end + end + + args.state.enemies = args.state.enemies.reject do |enemy| + if args.state.player[:alive] && 1500 > (args.state.player[:x] - enemy[:x]) ** 2 + (args.state.player[:y] - enemy[:y]) ** 2 + args.state.explosions << {x: enemy[:x] + 4, y: enemy[:y] + 4, w: 32, h: 32, path: 'sprites/explosion-0.png', age: 0} + args.state.explosions << {x: args.state.player[:x] + 4, y: args.state.player[:y] + 4, w: 32, h: 32, path: 'sprites/explosion-0.png', age: 0} + args.state.player[:alive] = false + true + else + false + end + end + args.state.enemy_bullets.each do |bullet| + if args.state.player[:alive] && 400 > (args.state.player[:x] - bullet[:x] + 12) ** 2 + (args.state.player[:y] - bullet[:y] + 12) ** 2 + args.state.explosions << {x: args.state.player[:x] + 4, y: args.state.player[:y] + 4, w: 32, h: 32, path: 'sprites/explosion-0.png', age: 0} + args.state.player[:alive] = false + bullet[:despawn] = true + end + end + args.state.enemies = args.state.enemies.reject do |enemy| + args.state.player_bullets.any? do |bullet| + if 400 > (enemy[:x] - bullet[:x] + 12) ** 2 + (enemy[:y] - bullet[:y] + 12) ** 2 + args.state.explosions << {x: enemy[:x] + 4, y: enemy[:y] + 4, w: 32, h: 32, path: 'sprites/explosion-0.png', age: 0} + bullet[:despawn] = true + args.state.score += 1000 * args.state.wave + true + else + false + end + end + end + + args.state.player_bullets = args.state.player_bullets.reject { |bullet| bullet[:despawn] } + args.state.enemy_bullets = args.state.enemy_bullets.reject { |bullet| bullet[:despawn] } + + args.state.player[:cooldown] -= 1 + if args.inputs.keyboard.key_held.space && args.state.player[:cooldown] <= 0 && args.state.player[:alive] + args.state.player_bullets << {x: args.state.player[:x] + 12, y: args.state.player[:y] + 28, w: 16, h: 16, path: 'sprites/star.png', dx: 0, dy: 8}.sprite + args.state.fired_shots += 1 + args.state.player[:cooldown] = 10 + 20 / args.state.wave + end + args.state.enemies.each do |enemy| + if Math.rand < 0.0005 + 0.0005 * args.state.wave && args.state.player[:alive] && enemy[:move_state] == :normal + args.state.enemy_bullets << {x: enemy[:x] + 12, y: enemy[:y] - 8, w: 16, h: 16, path: 'sprites/star.png', dx: 0, dy: -3 - args.state.wave_root}.sprite + end + end + + args.outputs.background_color = [0, 0, 0] + args.outputs.primitives << args.state.enemies.map do |enemy| + [enemy[:x], enemy[:y], 40, 40, enemy[:path], -90].sprite + end + args.outputs.primitives << args.state.player if args.state.player[:alive] + args.outputs.primitives << args.state.explosions + args.outputs.primitives << args.state.player_bullets + args.outputs.primitives << args.state.enemy_bullets + accuracy = args.state.fired_shots.zero? ? 1 : (args.state.fired_shots - args.state.missed_shots) / args.state.fired_shots + args.outputs.primitives << [ + [0, 0, 300, 720, 96, 0, 0].solid, + [1280 - 300, 0, 300, 720, 96, 0, 0].solid, + [1280 - 290, 60, "Wave #{args.state.wave}", 255, 255, 255].label, + [1280 - 290, 40, "Accuracy #{(accuracy * 100).floor}%", 255, 255, 255].label, + [1280 - 290, 20, "Score #{(args.state.score * accuracy).floor}", 255, 255, 255].label, + ] + args.outputs.primitives << args.state.lives.times.map do |n| + [1280 - 290 + 50 * n, 80, 40, 40, 'sprites/circle-gray.png', 90].sprite + end + #args.outputs.debug << args.gtk.framerate_diagnostics_primitives + + if (!args.state.player[:alive]) && args.state.enemy_bullets.empty? && args.state.explosions.empty? && args.state.enemies.all? { |enemy| enemy[:move_state] == :normal } + args.state.player[:alive] = true + args.state.player[:x] = 624 + args.state.player[:y] = 80 + args.state.lives -= 1 + if args.state.lives == -1 + args.state.clear! + end + end +end + +def make_enemies + enemies = [] + enemies += 10.times.map { |n| {x: Math.rand * 1280 * 2 - 640, y: Math.rand * 720 * 2 + 720, row: 0, col: n, path: 'sprites/circle-orange.png', move_state: :retreat} } + enemies += 10.times.map { |n| {x: Math.rand * 1280 * 2 - 640, y: Math.rand * 720 * 2 + 720, row: 1, col: n, path: 'sprites/circle-orange.png', move_state: :retreat} } + enemies += 8.times.map { |n| {x: Math.rand * 1280 * 2 - 640, y: Math.rand * 720 * 2 + 720, row: 2, col: n + 1, path: 'sprites/circle-blue.png', move_state: :retreat} } + enemies += 8.times.map { |n| {x: Math.rand * 1280 * 2 - 640, y: Math.rand * 720 * 2 + 720, row: 3, col: n + 1, path: 'sprites/circle-blue.png', move_state: :retreat} } + enemies += 4.times.map { |n| {x: Math.rand * 1280 * 2 - 640, y: Math.rand * 720 * 2 + 720, row: 4, col: n + 3, path: 'sprites/circle-green.png', move_state: :retreat} } + enemies +end + +def update_explosions args + args.state.explosions.each do |explosion| + explosion[:age] += 0.5 + explosion[:path] = "sprites/explosion-#{explosion[:age].floor}.png" + end + args.state.explosions = args.state.explosions.reject { |explosion| explosion[:age] >= 7 } +end + +def update_enemy_positions args + args.state.enemies.each do |enemy| + if enemy[:move_state] == :normal + enemy[:x] = args.state.columns[enemy[:col]] + enemy[:y] = args.state.rows[enemy[:row]] + enemy[:move_state] = :dive if Math.rand < 0.0002 + 0.00005 * args.state.wave && args.state.player[:alive] + elsif enemy[:move_state] == :dive + enemy[:target_x] ||= args.state.player[:x] + enemy[:target_y] ||= args.state.player[:y] + dx = enemy[:target_x] - enemy[:x] + dy = enemy[:target_y] - enemy[:y] + vel = Math.sqrt(dx * dx + dy * dy) + speed_limit = 2 + args.state.wave_root + if vel > speed_limit + dx /= vel / speed_limit + dy /= vel / speed_limit + end + if vel < 1 || !args.state.player[:alive] + enemy[:move_state] = :retreat + end + enemy[:x] += dx + enemy[:y] += dy + elsif enemy[:move_state] == :retreat + enemy[:target_x] = args.state.columns[enemy[:col]] + enemy[:target_y] = args.state.rows[enemy[:row]] + dx = enemy[:target_x] - enemy[:x] + dy = enemy[:target_y] - enemy[:y] + vel = Math.sqrt(dx * dx + dy * dy) + speed_limit = 2 + args.state.wave_root + if vel > speed_limit + dx /= vel / speed_limit + dy /= vel / speed_limit + elsif vel < 1 + enemy[:move_state] = :normal + enemy[:target_x] = nil + enemy[:target_y] = nil + end + enemy[:x] += dx + enemy[:y] += dy + end + end +end + +</code></pre> +<h3 id='----arcade---dueling-starships---main.rb'>Arcade - Dueling Starships - main.rb</h3> +<pre><code class="language-ruby"># ./samples/99_genre_arcade/dueling_starships/app/main.rb class DuelingSpaceships attr_accessor :state, :inputs, :outputs, :grid @@ -11016,16 +22514,16 @@ def tick args $dueling_spaceship.tick end -</pre> -<h1 id='--arcade/flappy-dragon/credits.txt'>arcade/flappy dragon/credits.txt</h1> -<pre># ./samples/99_genre_arcade/flappy_dragon/CREDITS.txt +</code></pre> +<h3 id='----arcade/flappy-dragon/credits.txt'>arcade/flappy dragon/credits.txt</h3> +<pre><code class="language-ruby"># ./samples/99_genre_arcade/flappy_dragon/CREDITS.txt code: Amir Rajan, https://twitter.com/amirrajan graphics and audio: Nick Culbertson, https://twitter.com/MobyPixel -</pre> -<h1 id='--arcade/flappy-dragon/main.rb'>arcade/flappy dragon/main.rb</h1> -<pre># ./samples/99_genre_arcade/flappy_dragon/app/main.rb +</code></pre> +<h3 id='----arcade/flappy-dragon/main.rb'>arcade/flappy dragon/main.rb</h3> +<pre><code class="language-ruby"># ./samples/99_genre_arcade/flappy_dragon/app/main.rb class FlappyDragon attr_accessor :grid, :inputs, :state, :outputs @@ -11387,9 +22885,9 @@ def tick args $flappy_dragon.tick end -</pre> -<h1 id='--arcade---pong---main.rb'>Arcade - Pong - main.rb</h1> -<pre># ./samples/99_genre_arcade/pong/app/main.rb +</code></pre> +<h3 id='----arcade---pong---main.rb'>Arcade - Pong - main.rb</h3> +<pre><code class="language-ruby"># ./samples/99_genre_arcade/pong/app/main.rb def tick args defaults args render args @@ -11550,9 +23048,9 @@ begin :assets end end -</pre> -<h1 id='--arcade---snakemoji---main.rb'>Arcade - Snakemoji - main.rb</h1> -<pre># ./samples/99_genre_arcade/snakemoji/app/main.rb +</code></pre> +<h3 id='----arcade---snakemoji---main.rb'>Arcade - Snakemoji - main.rb</h3> +<pre><code class="language-ruby"># ./samples/99_genre_arcade/snakemoji/app/main.rb # coding: utf-8 ################################ # So I was working on a snake game while @@ -11719,9 +23217,9 @@ def defaults 🎮 spawn_🍎(🎮) unless 🎮.🍎? end -</pre> -<h1 id='--arcade---solar-system---main.rb'>Arcade - Solar System - main.rb</h1> -<pre># ./samples/99_genre_arcade/solar_system/app/main.rb +</code></pre> +<h3 id='----arcade---solar-system---main.rb'>Arcade - Solar System - main.rb</h3> +<pre><code class="language-ruby"># ./samples/99_genre_arcade/solar_system/app/main.rb # Focused tutorial video: https://s3.amazonaws.com/s3.dragonruby.org/dragonruby-nddnug-workshop.mp4 # Workshop/Presentation which provides motivation for creating a game engine: https://www.youtube.com/watch?v=S3CFce1arC8 @@ -11831,9 +23329,357 @@ def r $gtk.reset end -</pre> -<h1 id='--crafting---craft-game-starting-point---main.rb'>Crafting - Craft Game Starting Point - main.rb</h1> -<pre># ./samples/99_genre_crafting/craft_game_starting_point/app/main.rb +</code></pre> +<h3 id='----arcade---sound-golf---main.rb'>Arcade - Sound Golf - main.rb</h3> +<pre><code class="language-ruby"># ./samples/99_genre_arcade/sound_golf/app/main.rb +=begin + + APIs Listing that haven't been encountered in previous sample apps: + + - sample: Chooses random element from array. + In this sample app, the target note is set by taking a sample from the collection + of available notes. + + Reminders: + - args.grid.(left|right|top|bottom): Pixel value for the boundaries of the virtual + 720 p screen (Dragon Ruby Game Toolkits's virtual resolution is always 1280x720). + + - args.state.new_entity: Used when we want to create a new object, like a sprite or button. + For example, if we want to create a new button, we would declare it as a new entity and + then define its properties. + + - String interpolation: Uses #{} syntax; everything between the #{ and the } is evaluated + as Ruby code, and the placeholder is replaced with its corresponding value or result. + + - args.outputs.labels: An array. The values generate a label. + The parameters are [X, Y, TEXT, SIZE, ALIGNMENT, RED, GREEN, BLUE, ALPHA, FONT STYLE] + For more information about labels, go to mygame/documentation/02-labels.md. + + - find_all: Finds all elements from a collection that meet a certain requirements (and excludes the ones that don't). + + - first: Returns the first element of an array. + + - inside_rect: Returns true or false depending on if the point is inside the rect. + + - to_sym: Returns symbol corresponding to string. Will create a symbol if it does + not already exist. + +=end + +# This sample app allows users to test their musical skills by matching the piano sound that plays in each +# level to the correct note. + +# Runs all the methods necessary for the game to function properly. +def tick args + defaults args + render args + calc args + input_mouse args + tick_instructions args, "Sample app shows how to play sounds. args.outputs.sounds << \"path_to_wav.wav\"" +end + +# Sets default values and creates empty collections +# Initialization happens in the first frame only +def defaults args + args.state.notes ||= [] + args.state.click_feedbacks ||= [] + args.state.current_level ||= 1 + args.state.times_wrong ||= 0 # when game starts, user hasn't guessed wrong yet +end + +# Uses a label to display current level, and shows the score +# Creates a button to play the sample note, and displays the available notes that could be a potential match +def render args + + # grid.w_half positions the label in the horizontal center of the screen. + args.outputs.labels << [args.grid.w_half, args.grid.top.shift_down(40), "Hole #{args.state.current_level} of 9", 0, 1, 0, 0, 0] + + render_score args # shows score on screen + + args.state.play_again_button ||= { x: 560, y: args.grid.h * 3 / 4 - 40, w: 160, h: 60, label: 'again' } # array definition, text/title + args.state.play_note_button ||= { x: 560, y: args.grid.h * 3 / 4 - 40, w: 160, h: 60, label: 'play' } + + if args.state.game_over # if game is over, a "play again" button is shown + # Calculations ensure that Play Again label is displayed in center of border + # Remove calculations from y parameters and see what happens to border and label placement + args.outputs.labels << [args.grid.w_half, args.grid.h * 3 / 4, "Play Again", 0, 1, 0, 0, 0] # outputs label + args.outputs.borders << args.state.play_again_button # outputs border + else # otherwise, if game is not over + # Calculations ensure that label appears in center of border + args.outputs.labels << [args.grid.w_half, args.grid.h * 3 / 4, "Play Note ##{args.state.current_level}", 0, 1, 0, 0, 0] # outputs label + args.outputs.borders << args.state.play_note_button # outputs border + end + + return if args.state.game_over # return if game is over + + args.outputs.labels << [args.grid.w_half, 400, "I think the note is a(n)...", 0, 1, 0, 0, 0] # outputs label + + # Shows all of the available notes that can be potential matches. + available_notes.each_with_index do |note, i| + args.state.notes[i] ||= piano_button(args, note, i + 1) # calls piano_button method on each note (creates label and border) + args.outputs.labels << args.state.notes[i].label # outputs note on screen with a label and a border + args.outputs.borders << args.state.notes[i].border + end + + # Shows whether or not the user is correct by filling the screen with either red or green + args.outputs.solids << args.state.click_feedbacks.map { |c| c.solid } +end + +# Shows the score (number of times the user guesses wrong) onto the screen using labels. +def render_score args + if args.state.times_wrong == 0 # if the user has guessed wrong zero times, the score is par + args.outputs.labels << [args.grid.w_half, args.grid.top.shift_down(80), "Score: PAR", 0, 1, 0, 0, 0] + else # otherwise, number of times the user has guessed wrong is shown + args.outputs.labels << [args.grid.w_half, args.grid.top.shift_down(80), "Score: +#{args.state.times_wrong}", 0, 1, 0, 0, 0] # shows score using string interpolation + end +end + +# Sets the target note for the level and performs calculations on click_feedbacks. +def calc args + args.state.target_note ||= available_notes.sample # chooses a note from available_notes collection as target note + args.state.click_feedbacks.each { |c| c.solid[-1] -= 5 } # remove this line and solid color will remain on screen indefinitely + # comment this line out and the solid color will keep flashing on screen instead of being removed from click_feedbacks collection + args.state.click_feedbacks.reject! { |c| c.solid[-1] <= 0 } +end + +# Uses input from the user to play the target note, as well as the other notes that could be a potential match. +def input_mouse args + return unless args.inputs.mouse.click # return unless the mouse is clicked + + # finds button that was clicked by user + button_clicked = args.outputs.borders.find_all do |b| # go through borders collection to find all borders that meet requirements + args.inputs.mouse.click.point.inside_rect? b # find button border that mouse was clicked inside of + end.find_all { |b| b.is_a? Hash }.first # reject, return first element + + return unless button_clicked # return unless button_clicked as a value (a button was clicked) + + queue_click_feedback args, # calls queue_click_feedback method on the button that was clicked + button_clicked.x, + button_clicked.y, + button_clicked.w, + button_clicked.h, + 150, 100, 200 # sets color of button to shade of purple + + if button_clicked[:label] == 'play' # if "play note" button is pressed + args.outputs.sounds << "sounds/#{args.state.target_note}.wav" # sound of target note is output + elsif button_clicked[:label] == 'again' # if "play game again" button is pressed + args.state.target_note = nil # no target note + args.state.current_level = 1 # starts at level 1 again + args.state.times_wrong = 0 # starts off with 0 wrong guesses + args.state.game_over = false # the game is not over (because it has just been restarted) + else # otherwise if neither of those buttons were pressed + args.outputs.sounds << "sounds/#{button_clicked[:label]}.wav" # sound of clicked note is played + if button_clicked[:label] == args.state.target_note # if clicked note is target note + args.state.target_note = nil # target note is emptied + + if args.state.current_level < 9 # if game hasn't reached level 9 + args.state.current_level += 1 # game goes to next level + else # otherwise, if game has reached level 9 + args.state.game_over = true # the game is over + end + + queue_click_feedback args, 0, 0, args.grid.w, args.grid.h, 100, 200, 100 # green shown if user guesses correctly + else # otherwise, if clicked note is not target note + args.state.times_wrong += 1 # increments times user guessed wrong + queue_click_feedback args, 0, 0, args.grid.w, args.grid.h, 200, 100, 100 # red shown is user guesses wrong + end + end +end + +# Creates a collection of all of the available notes as symbols +def available_notes + [:C3, :D3, :E3, :F3, :G3, :A3, :B3, :C4] +end + +# Creates buttons for each note, and sets a label (the note's name) and border for each note's button. +def piano_button args, note, position + args.state.new_entity(:button) do |b| # declares button as new entity + b.label = [460 + 40.mult(position), args.grid.h * 0.4, "#{note}", 0, 1, 0, 0, 0] # label definition + b.border = { x: 460 + 40.mult(position) - 20, y: args.grid.h * 0.4 - 32, w: 40, h: 40, label: note } # border definition, text/title; 20 subtracted so label is in center of border + end +end + +# Color of click feedback changes depending on what button was clicked, and whether the guess is right or wrong +# If a button is clicked, the inside of button is purple (see input_mouse method) +# If correct note is clicked, screen turns green +# If incorrect note is clicked, screen turns red (again, see input_mouse method) +def queue_click_feedback args, x, y, w, h, *color + args.state.click_feedbacks << args.state.new_entity(:click_feedback) do |c| # declares feedback as new entity + c.solid = [x, y, w, h, *color, 255] # sets color + end +end + +def tick_instructions args, text, y = 715 + return if args.state.key_event_occurred + if args.inputs.mouse.click || + args.inputs.keyboard.directional_vector || + args.inputs.keyboard.key_down.enter || + args.inputs.keyboard.key_down.escape + args.state.key_event_occurred = true + end + + args.outputs.debug << [0, y - 50, 1280, 60].solid + args.outputs.debug << [640, y, text, 1, 1, 255, 255, 255].label + args.outputs.debug << [640, y - 25, "(click to dismiss instructions)" , -2, 1, 255, 255, 255].label +end + +</code></pre> +<h3 id='----arcade---twinstick---main.rb'>Arcade - Twinstick - main.rb</h3> +<pre><code class="language-ruby"># ./samples/99_genre_arcade/twinstick/app/main.rb +def tick args + args.state.player ||= {x: 600, y: 320, w: 80, h: 80, path: 'sprites/circle-white.png', vx: 0, vy: 0, health: 10, cooldown: 0, score: 0} + args.state.enemies ||= [] + args.state.player_bullets ||= [] + args.state.tick_count ||= -1 + args.state.tick_count += 1 + spawn_enemies args + kill_enemies args + move_enemies args + move_bullets args + move_player args + fire_player args + args.state.player[:r] = args.state.player[:g] = args.state.player[:b] = (args.state.player[:health] * 25.5).clamp(0, 255) + label_color = args.state.player[:health] <= 5 ? 255 : 0 + args.outputs.labels << [ + { + x: args.state.player.x + 40, y: args.state.player.y + 60, alignment_enum: 1, text: "#{args.state.player[:health]} HP", + r: label_color, g: label_color, b: label_color + }, { + x: args.state.player.x + 40, y: args.state.player.y + 40, alignment_enum: 1, text: "#{args.state.player[:score]} PTS", + r: label_color, g: label_color, b: label_color, size_enum: 2 - args.state.player[:score].to_s.length, + } + ] + args.outputs.sprites << [args.state.player, args.state.enemies, args.state.player_bullets] + args.state.clear! if args.state.player[:health] < 0 # Reset the game if the player's health drops below zero +end + +def spawn_enemies args + # Spawn enemies more frequently as the player's score increases. + if rand < (100+args.state.player[:score])/(10000 + args.state.player[:score]) || args.state.tick_count.zero? + theta = rand * Math::PI * 2 + args.state.enemies << { + x: 600 + Math.cos(theta) * 800, y: 320 + Math.sin(theta) * 800, w: 80, h: 80, path: 'sprites/circle-white.png', + r: (256 * rand).floor, g: (256 * rand).floor, b: (256 * rand).floor + } + end +end + +def kill_enemies args + args.state.enemies.reject! do |enemy| + # Check if enemy and player are within 80 pixels of each other (i.e. overlapping) + if 6400 > (enemy.x - args.state.player.x) ** 2 + (enemy.y - args.state.player.y) ** 2 + # Enemy is touching player. Kill enemy, and reduce player HP by 1. + args.state.player[:health] -= 1 + else + args.state.player_bullets.any? do |bullet| + # Check if enemy and bullet are within 50 pixels of each other (i.e. overlapping) + if 2500 > (enemy.x - bullet.x + 30) ** 2 + (enemy.y - bullet.y + 30) ** 2 + # Increase player health by one for each enemy killed by a bullet after the first enemy, up to a maximum of 10 HP + args.state.player[:health] += 1 if args.state.player[:health] < 10 && bullet[:kills] > 0 + # Keep track of how many enemies have been killed by this particular bullet + bullet[:kills] += 1 + # Earn more points by killing multiple enemies with one shot. + args.state.player[:score] += bullet[:kills] + end + end + end + end +end + +def move_enemies args + args.state.enemies.each do |enemy| + # Get the angle from the enemy to the player + theta = Math.atan2(enemy.y - args.state.player.y, enemy.x - args.state.player.x) + # Convert the angle to a vector pointing at the player + dx, dy = theta.to_degrees.vector 5 + # Move the enemy towards thr player + enemy.x -= dx + enemy.y -= dy + end +end + +def move_bullets args + args.state.player_bullets.each do |bullet| + # Move the bullets according to the bullet's velocity + bullet.x += bullet[:vx] + bullet.y += bullet[:vy] + end + args.state.player_bullets.reject! do |bullet| + # Despawn bullets that are outside the screen area + bullet.x < -20 || bullet.y < -20 || bullet.x > 1300 || bullet.y > 740 + end +end + +def move_player args + # Get the currently held direction. + dx, dy = move_directional_vector args + # Take the weighted average of the old velocities and the desired velocities. + # Since move_directional_vector returns values between -1 and 1, + # and we want to limit the speed to 7.5, we multiply dx and dy by 7.5*0.1 to get 0.75 + args.state.player[:vx] = args.state.player[:vx] * 0.9 + dx * 0.75 + args.state.player[:vy] = args.state.player[:vy] * 0.9 + dy * 0.75 + # Move the player + args.state.player.x += args.state.player[:vx] + args.state.player.y += args.state.player[:vy] + # If the player is about to go out of bounds, put them back in bounds. + args.state.player.x = args.state.player.x.clamp(0, 1201) + args.state.player.y = args.state.player.y.clamp(0, 640) +end + + +def fire_player args + # Reduce the firing cooldown each tick + args.state.player[:cooldown] -= 1 + # If the player is allowed to fire + if args.state.player[:cooldown] <= 0 + dx, dy = shoot_directional_vector args # Get the bullet velocity + return if dx == 0 && dy == 0 # If the velocity is zero, the player doesn't want to fire. Therefore, we just return early. + # Add a new bullet to the list of player bullets. + args.state.player_bullets << { + x: args.state.player.x + 30 + 40 * dx, + y: args.state.player.y + 30 + 40 * dy, + w: 20, h: 20, + path: 'sprites/circle-white.png', + r: 0, g: 0, b: 0, + vx: 10 * dx + args.state.player[:vx] / 7.5, vy: 10 * dy + args.state.player[:vy] / 7.5, # Factor in a bit of the player's velocity + kills: 0 + } + args.state.player[:cooldown] = 30 # Reset the cooldown + end +end + +# Custom function for getting a directional vector just for movement using WASD +def move_directional_vector args + dx = 0 + dx += 1 if args.inputs.keyboard.d + dx -= 1 if args.inputs.keyboard.a + dy = 0 + dy += 1 if args.inputs.keyboard.w + dy -= 1 if args.inputs.keyboard.s + if dx != 0 && dy != 0 + dx *= 0.7071 + dy *= 0.7071 + end + [dx, dy] +end + +# Custom function for getting a directional vector just for shooting using the arrow keys +def shoot_directional_vector args + dx = 0 + dx += 1 if args.inputs.keyboard.key_down.right || args.inputs.keyboard.key_held.right + dx -= 1 if args.inputs.keyboard.key_down.left || args.inputs.keyboard.key_held.left + dy = 0 + dy += 1 if args.inputs.keyboard.key_down.up || args.inputs.keyboard.key_held.up + dy -= 1 if args.inputs.keyboard.key_down.down || args.inputs.keyboard.key_held.down + if dx != 0 && dy != 0 + dx *= 0.7071 + dy *= 0.7071 + end + [dx, dy] +end +</code></pre> +<h3 id='----crafting---craft-game-starting-point---main.rb'>Crafting - Craft Game Starting Point - main.rb</h3> +<pre><code class="language-ruby"># ./samples/99_genre_crafting/craft_game_starting_point/app/main.rb # ================================================== # A NOTE TO JAM CRAFT PARTICIPANTS: # The comments and code in here are just as small piece of DragonRuby's capabilities. @@ -12258,9 +24104,9 @@ end # Comment out the line below if you don't want this to happen. $gtk.reset -</pre> -<h1 id='--dev-tools---add-buttons-to-console---main.rb'>Dev Tools - Add Buttons To Console - main.rb</h1> -<pre># ./samples/99_genre_dev_tools/add_buttons_to_console/app/main.rb +</code></pre> +<h3 id='----dev-tools---add-buttons-to-console---main.rb'>Dev Tools - Add Buttons To Console - main.rb</h3> +<pre><code class="language-ruby"># ./samples/99_genre_dev_tools/add_buttons_to_console/app/main.rb # You can customize the buttons that show up in the Console. class GTK::Console::Menu # STEP 1: Override the custom_buttons function. @@ -12320,9 +24166,9 @@ def tick args 0, 1] end -</pre> -<h1 id='--dev-tools---animation-creator-starting-point---main.rb'>Dev Tools - Animation Creator Starting Point - main.rb</h1> -<pre># ./samples/99_genre_dev_tools/animation_creator_starting_point/app/main.rb +</code></pre> +<h3 id='----dev-tools---animation-creator-starting-point---main.rb'>Dev Tools - Animation Creator Starting Point - main.rb</h3> +<pre><code class="language-ruby"># ./samples/99_genre_dev_tools/animation_creator_starting_point/app/main.rb class OneBitLowrezPaint attr_gtk @@ -12397,24 +24243,22 @@ class OneBitLowrezPaint end def render_instructions - instructions = <<-S -* Instructions: -- All data is stored in the ~canvas~ directory. -- Hold ~d~ to set the edit mode to erase. -- Release ~d~ to set the edit mode drawing. -- Press ~a~ to added a frame to the end. -- Press ~b~ to select the previous frame. -- Press ~f~ to select the next frame. -- Press ~c~ to copy a frame. -- Press ~v~ to paste a copied frame into the selected frame. -- Press ~x~ to delete the currently selected frame. -- Press ~w~ to save the canvas and export all sprites. -- Press ~l~ to load the canvas. -S + instructions = [ + "* Hotkeys:", + "- d: hold to erase, release to draw.", + "- a: add frame.", + "- c: copy frame.", + "- v: paste frame.", + "- x: delete frame.", + "- b: go to previous frame.", + "- f: go to next frame.", + "- w: save to ./canvas directory.", + "- l: load from ./canvas." + ] - instructions.strip.each_line.with_index do |l, i| + instructions.each.with_index do |l, i| outputs.labels << { x: 840, y: 500 - (i * 20), text: "#{l}", - r: 180, g: 180, b: 180, size_enum: -3 } + r: 180, g: 180, b: 180, size_enum: 0 } end end @@ -12771,9 +24615,9 @@ end # $gtk.reset -</pre> -<h1 id='--dev-tools---tile-editor-starting-point---main.rb'>Dev Tools - Tile Editor Starting Point - main.rb</h1> -<pre># ./samples/99_genre_dev_tools/tile_editor_starting_point/app/main.rb +</code></pre> +<h3 id='----dev-tools---tile-editor-starting-point---main.rb'>Dev Tools - Tile Editor Starting Point - main.rb</h3> +<pre><code class="language-ruby"># ./samples/99_genre_dev_tools/tile_editor_starting_point/app/main.rb =begin APIs listing that haven't been encountered in previous sample apps: @@ -13166,9 +25010,901 @@ def tick_instructions args, text, y = 715 args.outputs.debug << [640, y - 25, "(click to dismiss instructions)" , -2, 1, 255, 255, 255].label end -</pre> -<h1 id='--lowrez---resolution-64x64---lowrez.rb'>Lowrez - Resolution 64x64 - lowrez.rb</h1> -<pre># ./samples/99_genre_lowrez/resolution_64x64/app/lowrez.rb +</code></pre> +<h3 id='----lowrez---nokia-3310---main.rb'>Lowrez - Nokia 3310 - main.rb</h3> +<pre><code class="language-ruby"># ./samples/99_genre_lowrez/nokia_3310/app/main.rb +require 'app/nokia.rb' + +def tick args + # ======================================================================= + # ==== HELLO WORLD ====================================================== + # ======================================================================= + # Steps to get started: + # 1. ~def tick args~ is the entry point for your game. + # 2. There are quite a few code samples below, remove the "##" + # before each line and save the file to see the changes. + # 3. 0, 0 is in bottom left and 83, 47 is in top right corner. + # 4. Be sure to come to the discord channel if you need + # more help: [[http://discord.dragonruby.org]]. + + # Commenting and uncommenting code: + # - Add a "#" infront of lines to comment out code + # - Remove the "#" infront of lines to comment out code + + # Invoke the hello_world subroutine/method + hello_world args # <---- add a "#" to the beginning of the line to stop running this subroutine/method. + + # ======================================================================= + # ==== HOW TO RENDER A LABEL ============================================ + # ======================================================================= + + # Uncomment the line below to invoke the how_to_render_a_label subroutine/method. + # Note: The method is defined in this file with the signature ~def how_to_render_a_label args~ + # Scroll down to the method to see the details. + + # Remove the "#" at the beginning of the line below + # how_to_render_a_label args # <---- remove the "#" at the beginning of this line to run the method + + + # ======================================================================= + # ==== HOW TO RENDER A FILLED SQUARE (SOLID) ============================ + # ======================================================================= + # Remove the "#" at the beginning of the line below + # how_to_render_solids args + + + # ======================================================================= + # ==== HOW TO RENDER AN UNFILLED SQUARE (BORDER) ======================== + # ======================================================================= + # Remove the "#" at the beginning of the line below + # how_to_render_borders args + + + # ======================================================================= + # ==== HOW TO RENDER A LINE ============================================= + # ======================================================================= + # Remove the "#" at the beginning of the line below + # how_to_render_lines args + + + # ======================================================================= + # == HOW TO RENDER A SPRITE ============================================= + # ======================================================================= + # Remove the "#" at the beginning of the line below + # how_to_render_sprites args + + + # ======================================================================= + # ==== HOW TO MOVE A SPRITE BASED OFF OF USER INPUT ===================== + # ======================================================================= + # Remove the "#" at the beginning of the line below + # how_to_move_a_sprite args + + + # ======================================================================= + # ==== HOW TO ANIMATE A SPRITE (SEPERATE PNGS) ========================== + # ======================================================================= + # Remove the "#" at the beginning of the line below + # how_to_animate_a_sprite args + + + # ======================================================================= + # ==== HOW TO ANIMATE A SPRITE (SPRITE SHEET) =========================== + # ======================================================================= + # Remove the "#" at the beginning of the line below + # how_to_animate_a_sprite_sheet args + + + # ======================================================================= + # ==== HOW TO DETERMINE COLLISION ============================================= + # ======================================================================= + # Remove the "#" at the beginning of the line below + # how_to_determine_collision args + + + # ======================================================================= + # ==== HOW TO CREATE BUTTONS ================================================== + # ======================================================================= + # Remove the "#" at the beginning of the line below + # how_to_create_buttons args + + # ==== The line below renders a debug grid, mouse information, and current tick + # render_debug args +end + +# ======================================================================= +# ==== HELLO WORLD ====================================================== +# ======================================================================= +def hello_world args + args.nokia.solids << { x: 0, y: 64, w: 10, h: 10, r: 255 } + + args.nokia.labels << { + x: 42, + y: 46, + text: "nokia 3310 jam 3", + size_enum: NOKIA_FONT_SM, + alignment_enum: 1, + r: 0, + g: 0, + b: 0, + a: 255, + font: NOKIA_FONT_PATH + } + + args.nokia.sprites << { + x: 42 - 10, + y: 26 - 10, + w: 20, + h: 20, + path: 'sprites/monochrome-ship.png', + a: 255, + angle: args.state.tick_count % 360 + } +end + +# ======================================================================= +# ==== HOW TO RENDER A LABEL ============================================ +# ======================================================================= +def how_to_render_a_label args + # NOTE: Text is aligned from the TOP LEFT corner + + # Render an EXTRA LARGE/XL label (remove the "#" in front of each line below) + args.nokia.labels << { x: 0, y: 46, text: "Hello World", + size_enum: NOKIA_FONT_XL, + r: 0, g: 0, b: 0, a: 255, + font: NOKIA_FONT_PATH } + + # Render a LARGE/LG label (remove the "#" in front of each line below) + args.nokia.labels << { x: 0, y: 29, text: "Hello World", + size_enum: NOKIA_FONT_LG, + r: 0, g: 0, b: 0, a: 255, + font: NOKIA_FONT_PATH } + + # Render a MEDIUM/MD label (remove the "#" in front of each line below) + args.nokia.labels << { x: 0, y: 16, text: "Hello World", + size_enum: NOKIA_FONT_MD, + r: 0, g: 0, b: 0, a: 255, + font: NOKIA_FONT_PATH } + + # Render a SMALL/SM label (remove the "#" in front of each line below) + args.nokia.labels << { x: 0, y: 7, text: "Hello World", + size_enum: NOKIA_FONT_SM, + r: 0, g: 0, b: 0, a: 255, + font: NOKIA_FONT_PATH } + + # You are provided args.nokia.default_label which returns a Hash that you + # can ~merge~ properties with + # Example 1 + args.nokia.labels << args.nokia + .default_label + .merge(text: "Default") + + # Example 2 + args.nokia.labels << args.nokia + .default_label + .merge(x: 31, + text: "Default") +end + +# ============================================================================= +# ==== HOW TO RENDER FILLED SQUARES (SOLIDS) ================================== +# ============================================================================= +def how_to_render_solids args + # Render a square at 0, 0 with a width and height of 1 + args.nokia.solids << { x: 0, y: 0, w: 1, h: 1 } + + # Render a square at 1, 1 with a width and height of 2 + args.nokia.solids << { x: 1, y: 1, w: 2, h: 2 } + + # Render a square at 3, 3 with a width and height of 3 + args.nokia.solids << { x: 3, y: 3, w: 3, h: 3 } + + # Render a square at 6, 6 with a width and height of 4 + args.nokia.solids << { x: 6, y: 6, w: 4, h: 4 } +end + +# ============================================================================= +# ==== HOW TO RENDER UNFILLED SQUARES (BORDERS) =============================== +# ============================================================================= +def how_to_render_borders args + # Render a square at 0, 0 with a width and height of 3 + args.nokia.borders << { x: 0, y: 0, w: 3, h: 3, a: 255 } + + # Render a square at 3, 3 with a width and height of 3 + args.nokia.borders << { x: 3, y: 3, w: 4, h: 4, a: 255 } + + # Render a square at 5, 5 with a width and height of 4 + args.nokia.borders << { x: 7, y: 7, w: 5, h: 5, a: 255 } +end + +# ============================================================================= +# ==== HOW TO RENDER A LINE =================================================== +# ============================================================================= +def how_to_render_lines args + # Render a horizontal line at the bottom + args.nokia.lines << { x: 0, y: 0, x2: 83, y2: 0 } + + # Render a vertical line at the left + args.nokia.lines << { x: 0, y: 0, x2: 0, y2: 47 } + + # Render a diagonal line starting from the bottom left and going to the top right + args.nokia.lines << { x: 0, y: 0, x2: 83, y2: 47 } +end + +# ============================================================================= +# == HOW TO RENDER A SPRITE =================================================== +# ============================================================================= +def how_to_render_sprites args + # Loop 10 times and create 10 sprites in 10 positions + # Render a sprite at the bottom left with a width and height of 5 and a path of 'sprites/monochrome-ship.png' + 10.times do |i| + args.nokia.sprites << { + x: i * 8.4, + y: i * 4.8, + w: 5, + h: 5, + path: 'sprites/monochrome-ship.png' + } + end + + # Given an array of positions create sprites + positions = [ + { x: 20, y: 32 }, + { x: 45, y: 15 }, + { x: 72, y: 23 }, + ] + + positions.each do |position| + # use Ruby's ~Hash#merge~ function to create a sprite + args.nokia.sprites << position.merge(path: 'sprites/monochrome-ship.png', + w: 5, + h: 5) + end +end + +# ============================================================================= +# ==== HOW TO ANIMATE A SPRITE (SEPERATE PNGS) ========================== +# ============================================================================= +def how_to_animate_a_sprite args + # STEP 1: Define when you want the animation to start. The animation in this case will start in 3 seconds + start_animation_on_tick = 180 + + # STEP 2: Get the frame_index given the start tick. + sprite_index = start_animation_on_tick.frame_index count: 7, # how many sprites? + hold_for: 8, # how long to hold each sprite? + repeat: true # should it repeat? + + # STEP 3: frame_index will return nil if the frame hasn't arrived yet + if sprite_index + # if the sprite_index is populated, use it to determine the sprite path and render it + sprite_path = "sprites/explosion-#{sprite_index}.png" + args.nokia.sprites << { x: 42 - 16, + y: 47 - 32, + w: 32, + h: 32, + path: sprite_path } + else + # if the sprite_index is nil, render a countdown instead + countdown_in_seconds = ((start_animation_on_tick - args.state.tick_count) / 60).round(1) + + args.nokia.labels << args.nokia + .default_label + .merge(x: 0, + y: 18, + text: "Count Down: #{countdown_in_seconds.to_sf}", + alignment_enum: 0) + end + + # render the current tick and the resolved sprite index + args.nokia.labels << args.nokia + .default_label + .merge(x: 0, + y: 11, + text: "Tick: #{args.state.tick_count}") + args.nokia.labels << args.nokia + .default_label + .merge(x: 0, + y: 5, + text: "sprite_index: #{sprite_index}") +end + +# ============================================================================= +# ==== HOW TO ANIMATE A SPRITE (SPRITE SHEET) ================================= +# ============================================================================= +def how_to_animate_a_sprite_sheet args + # STEP 1: Define when you want the animation to start. The animation in this case will start in 3 seconds + start_animation_on_tick = 180 + + # STEP 2: Get the frame_index given the start tick. + sprite_index = start_animation_on_tick.frame_index count: 7, # how many sprites? + hold_for: 8, # how long to hold each sprite? + repeat: true # should it repeat? + + # STEP 3: frame_index will return nil if the frame hasn't arrived yet + if sprite_index + # if the sprite_index is populated, use it to determine the source rectangle and render it + args.nokia.sprites << { + x: 42 - 16, + y: 47 - 32, + w: 32, + h: 32, + path: "sprites/explosion-sheet.png", + source_x: 32 * sprite_index, + source_y: 0, + source_w: 32, + source_h: 32 + } + else + # if the sprite_index is nil, render a countdown instead + countdown_in_seconds = ((start_animation_on_tick - args.state.tick_count) / 60).round(1) + + args.nokia.labels << args.nokia + .default_label + .merge(x: 0, + y: 18, + text: "Count Down: #{countdown_in_seconds.to_sf}", + alignment_enum: 0) + end + + # render the current tick and the resolved sprite index + args.nokia.labels << args.nokia + .default_label + .merge(x: 0, + y: 11, + text: "tick: #{args.state.tick_count}") + args.nokia.labels << args.nokia + .default_label + .merge(x: 0, + y: 5, + text: "sprite_index: #{sprite_index}") +end + +# ============================================================================= +# ==== HOW TO STORE STATE, ACCEPT INPUT, AND RENDER SPRITE BASED OFF OF STATE = +# ============================================================================= +def how_to_move_a_sprite args + args.nokia.labels << args.nokia + .default_label + .merge(x: 42, + y: 46, text: "Use Arrow Keys", + alignment_enum: 1) + + args.nokia.labels << args.nokia + .default_label + .merge(x: 42, + y: 41, text: "Or WASD", + alignment_enum: 1) + + args.nokia.labels << args.nokia + .default_label + .merge(x: 42, + y: 36, text: "Or Click", + alignment_enum: 1) + + # set the initial values for x and y using ||= ("or equal operator") + args.state.ship.x ||= 0 + args.state.ship.y ||= 0 + + # if a mouse click occurs, update the ship's x and y to be the location of the click + if args.nokia.mouse_click + args.state.ship.x = args.nokia.mouse_click.x + args.state.ship.y = args.nokia.mouse_click.y + end + + # if a or left arrow is pressed/held, decrement the ships x position + if args.nokia.keyboard.left + args.state.ship.x -= 1 + end + + # if d or right arrow is pressed/held, increment the ships x position + if args.nokia.keyboard.right + args.state.ship.x += 1 + end + + # if s or down arrow is pressed/held, decrement the ships y position + if args.nokia.keyboard.down + args.state.ship.y -= 1 + end + + # if w or up arrow is pressed/held, increment the ships y position + if args.nokia.keyboard.up + args.state.ship.y += 1 + end + + # render the sprite to the screen using the position stored in args.state.ship + args.nokia.sprites << { + x: args.state.ship.x, + y: args.state.ship.y, + w: 5, + h: 5, + path: 'sprites/monochrome-ship.png', + # parameters beyond this point are optional + angle: 0, # Note: rotation angle is denoted in degrees NOT radians + r: 255, + g: 255, + b: 255, + a: 255 + } +end + +# ======================================================================= +# ==== HOW TO DETERMINE COLLISION ======================================= +# ======================================================================= +def how_to_determine_collision args + # Render the instructions + args.nokia.labels << args.nokia + .default_label + .merge(x: 42, + y: 46, text: "Click Anywhere", + alignment_enum: 1) + + # if a mouse click occurs: + # - set ship_one if it isn't set + # - set ship_two if it isn't set + # - otherwise reset ship one and ship two + if args.nokia.mouse_click + # is ship_one set? + if !args.state.ship_one + args.state.ship_one = { x: args.nokia.mouse_click.x - 5, + y: args.nokia.mouse_click.y - 5, + w: 10, + h: 10 } + # is ship_one set? + elsif !args.state.ship_two + args.state.ship_two = { x: args.nokia.mouse_click.x - 5, + y: args.nokia.mouse_click.y - 5, + w: 10, + h: 10 } + # should we reset? + else + args.state.ship_one = nil + args.state.ship_two = nil + end + end + + # render ship one if it's set + if args.state.ship_one + # use Ruby's .merge method which is available on ~Hash~ to set the sprite and alpha + # render ship one + args.nokia.sprites << args.state.ship_one.merge(path: 'sprites/monochrome-ship.png') + end + + if args.state.ship_two + # use Ruby's .merge method which is available on ~Hash~ to set the sprite and alpha + # render ship two + args.nokia.sprites << args.state.ship_two.merge(path: 'sprites/monochrome-ship.png') + end + + # if both ship one and ship two are set, then determine collision + if args.state.ship_one && args.state.ship_two + # collision is determined using the intersect_rect? method + if args.state.ship_one.intersect_rect? args.state.ship_two + # if collision occurred, render the words collision! + args.nokia.labels << args.nokia + .default_label + .merge(x: 42, + y: 5, + text: "Collision!", + alignment_enum: 1) + else + # if collision occurred, render the words no collision. + args.nokia.labels << args.nokia + .default_label + .merge(x: 42, + y: 5, + text: "No Collision.", + alignment_enum: 1) + end + else + # if both ship one and ship two aren't set, then render -- + args.nokia.labels << args.nokia + .default_label + .merge(x: 42, + y: 6, + text: "--", + alignment_enum: 1) + end +end + +# ============================================================================= +# ==== HOW TO CREATE BUTTONS ================================================== +# ============================================================================= +def how_to_create_buttons args + # Define a button style + args.state.button_style = { w: 82, h: 10, } + + # Render instructions + args.state.button_message ||= "Press a Button!" + args.nokia.labels << args.nokia + .default_label + .merge(x: 42, + y: 82, + text: args.state.button_message, + alignment_enum: 1) + + + # Creates button one using a border and a label + args.state.button_one_border = args.state.button_style.merge( x: 1, y: 32) + args.nokia.borders << args.state.button_one_border + args.nokia.labels << args.nokia + .default_label + .merge(x: args.state.button_one_border.x + 2, + y: args.state.button_one_border.y + NOKIA_FONT_SM_HEIGHT + 2, + text: "Button One") + + # Creates button two using a border and a label + args.state.button_two_border = args.state.button_style.merge( x: 1, y: 20) + + args.nokia.borders << args.state.button_two_border + args.nokia.labels << args.nokia + .default_label + .merge(x: args.state.button_two_border.x + 2, + y: args.state.button_two_border.y + NOKIA_FONT_SM_HEIGHT + 2, + text: "Button Two") + + # Initialize the state variable that tracks which button was clicked to "" (empty stringI + args.state.last_button_clicked ||= "--" + + # If a click occurs, check to see if either button one, or button two was clicked + # using the inside_rect? method of the mouse + # set args.state.last_button_clicked accordingly + if args.nokia.mouse_click + if args.nokia.mouse_click.inside_rect? args.state.button_one_border + args.state.last_button_clicked = "One Clicked!" + elsif args.nokia.mouse_click.inside_rect? args.state.button_two_border + args.state.last_button_clicked = "Two Clicked!" + else + args.state.last_button_clicked = "--" + end + end + + # Render the current value of args.state.last_button_clicked + args.nokia.labels << args.nokia + .default_label + .merge(x: 42, + y: 5, + text: args.state.last_button_clicked, + alignment_enum: 1) +end + +def render_debug args + if !args.state.grid_rendered + (NOKIA_HEIGHT + 1).map_with_index do |i| + args.outputs.static_debug << { + x: NOKIA_X_OFFSET, + y: NOKIA_Y_OFFSET + (i * NOKIA_ZOOM), + x2: NOKIA_X_OFFSET + NOKIA_ZOOMED_WIDTH, + y2: NOKIA_Y_OFFSET + (i * NOKIA_ZOOM), + r: 128, + g: 128, + b: 128, + a: 80 + }.line + end + + (NOKIA_WIDTH + 1).map_with_index do |i| + args.outputs.static_debug << { + x: NOKIA_X_OFFSET + (i * NOKIA_ZOOM), + y: NOKIA_Y_OFFSET, + x2: NOKIA_X_OFFSET + (i * NOKIA_ZOOM), + y2: NOKIA_Y_OFFSET + NOKIA_ZOOMED_HEIGHT, + r: 128, + g: 128, + b: 128, + a: 80 + }.line + end + end + + args.state.grid_rendered = true + + args.state.last_click ||= 0 + args.state.last_up ||= 0 + args.state.last_click = args.state.tick_count if args.nokia.mouse_down # you can also use args.nokia.click + args.state.last_up = args.state.tick_count if args.nokia.mouse_up + args.state.label_style = { size_enum: -1.5 } + + args.state.watch_list = [ + "args.state.tick_count is: #{args.state.tick_count}", + "args.nokia.mouse_position is: #{args.nokia.mouse_position.x}, #{args.nokia.mouse_position.y}", + "args.nokia.mouse_down tick: #{args.state.last_click || "never"}", + "args.nokia.mouse_up tick: #{args.state.last_up || "false"}", + ] + + args.outputs.debug << args.state + .watch_list + .map_with_index do |text, i| + { + x: 5, + y: 720 - (i * 18), + text: text, + size_enum: -1.5, + r: 255, g: 255, b: 255 + }.label + end + + args.outputs.debug << { + x: 640, + y: 25, + text: "INFO: dev mode is currently enabled. Comment out the invocation of ~render_debug~ within the ~tick~ method to hide the debug layer.", + size_enum: -0.5, + alignment_enum: 1, + r: 255, g: 255, b: 255 + }.label +end + +def snake_demo args + +end + +$gtk.reset + +</code></pre> +<h3 id='----lowrez---nokia-3310---nokia.rb'>Lowrez - Nokia 3310 - nokia.rb</h3> +<pre><code class="language-ruby"># ./samples/99_genre_lowrez/nokia_3310/app/nokia.rb +# Emulation of a 64x64 canvas. Don't change this file unless you know what you're doing :-) +# Head over to main.rb and study the code there. + +NOKIA_WIDTH = 84 +NOKIA_HEIGHT = 48 +NOKIA_ZOOM = 12 +NOKIA_ZOOMED_WIDTH = NOKIA_WIDTH * NOKIA_ZOOM +NOKIA_ZOOMED_HEIGHT = NOKIA_HEIGHT * NOKIA_ZOOM +NOKIA_X_OFFSET = (1280 - NOKIA_ZOOMED_WIDTH).half +NOKIA_Y_OFFSET = ( 720 - NOKIA_ZOOMED_HEIGHT).half + +NOKIA_FONT_XL = -1 +NOKIA_FONT_XL_HEIGHT = 20 + +NOKIA_FONT_LG = -3.5 +NOKIA_FONT_LG_HEIGHT = 15 + +NOKIA_FONT_MD = -6 +NOKIA_FONT_MD_HEIGHT = 10 + +NOKIA_FONT_SM = -8.5 +NOKIA_FONT_SM_HEIGHT = 5 + +NOKIA_FONT_PATH = 'fonts/lowrez.ttf' + + +class NokiaOutputs + attr_accessor :width, :height + + def initialize args + @args = args + end + + def outputs_nokia + return @args.outputs if @args.state.tick_count <= 0 + return @args.outputs[:nokia] + end + + def solids + outputs_nokia.solids + end + + def borders + outputs_nokia.borders + end + + def sprites + outputs_nokia.sprites + end + + def labels + outputs_nokia.labels + end + + def default_label + { + x: 0, + y: 63, + text: "", + size_enum: NOKIA_FONT_SM, + alignment_enum: 0, + r: 0, + g: 0, + b: 0, + a: 255, + font: NOKIA_FONT_PATH + } + end + + def lines + outputs_nokia.lines + end + + def primitives + outputs_nokia.primitives + end + + def click + return nil unless @args.inputs.mouse.click + mouse + end + + def mouse_click + click + end + + def mouse_down + @args.inputs.mouse.down + end + + def mouse_up + @args.inputs.mouse.up + end + + def mouse + [ + ((@args.inputs.mouse.x - NOKIA_X_OFFSET).idiv(NOKIA_ZOOM)), + ((@args.inputs.mouse.y - NOKIA_Y_OFFSET).idiv(NOKIA_ZOOM)) + ] + end + + def mouse_position + mouse + end + + def keyboard + @args.inputs.keyboard + end +end + +class GTK::Args + def init_nokia + return if @nokia + @nokia = NokiaOutputs.new self + end + + def nokia + @nokia + end +end + +module GTK + class Runtime + alias_method :__original_tick_core__, :tick_core unless Runtime.instance_methods.include?(:__original_tick_core__) + + def tick_core + @args.init_nokia + + __original_tick_core__ + + return if @args.state.tick_count <= 0 + + @args.render_target(:nokia) + .labels + .each do |l| + l.y += 1 + if (l.a || 255) > 128 + l.r = 67 + l.g = 82 + l.b = 61 + l.a = 255 + else + l.a = 0 + end + end + + @args.render_target(:nokia) + .sprites + .each do |s| + if (s.a || 255) > 128 + s.a = 255 + else + s.a = 0 + end + end + + @args.render_target(:nokia) + .solids + .each do |s| + if (s.a || 255) > 128 + s.r = 67 + s.g = 82 + s.b = 61 + s.a = 255 + else + s.a = 0 + end + end + + @args.render_target(:nokia) + .borders + .each do |s| + if (s.a || 255) > 128 + s.r = 67 + s.g = 82 + s.b = 61 + s.a = 255 + else + s.a = 0 + end + end + + @args.render_target(:nokia) + .lines + .each do |l| + l.y += 1 + l.y2 += 1 + l.y2 += 1 if l.y1 != l.y2 + l.x2 += 1 if l.x1 != l.x2 + + if (l.a || 255) > 128 + l.r = 67 + l.g = 82 + l.b = 61 + l.a = 255 + else + l.a = 0 + end + end + + @args.outputs.borders << { + x: NOKIA_X_OFFSET - 1, + y: NOKIA_Y_OFFSET - 1, + w: NOKIA_ZOOMED_WIDTH + 2, + h: NOKIA_ZOOMED_HEIGHT + 2, + r: 128, g: 128, b: 128 + } + + + @args.outputs.background_color = [199, 240, 216] + + @args.outputs.solids << [0, 0, NOKIA_X_OFFSET, 720] + @args.outputs.solids << [0, 0, 1280, NOKIA_Y_OFFSET] + @args.outputs.solids << [NOKIA_X_OFFSET + NOKIA_ZOOMED_WIDTH, 0, NOKIA_X_OFFSET, 720] + @args.outputs.solids << [0, NOKIA_Y_OFFSET.from_top, 1280, NOKIA_Y_OFFSET] + + @args.outputs + .sprites << { x: NOKIA_X_OFFSET, + y: NOKIA_Y_OFFSET, + w: NOKIA_ZOOMED_WIDTH, + h: NOKIA_ZOOMED_HEIGHT, + source_x: 0, + source_y: 0, + source_w: NOKIA_WIDTH, + source_h: NOKIA_HEIGHT, + path: :nokia } + + if [email protected]_rendered + (NOKIA_HEIGHT + 1).map_with_index do |i| + @args.outputs.static_lines << { + x: NOKIA_X_OFFSET, + y: NOKIA_Y_OFFSET + (i * NOKIA_ZOOM), + x2: NOKIA_X_OFFSET + NOKIA_ZOOMED_WIDTH, + y2: NOKIA_Y_OFFSET + (i * NOKIA_ZOOM), + r: 199, + g: 240, + b: 216, + a: 100 + }.line + end + + (NOKIA_WIDTH + 1).map_with_index do |i| + @args.outputs.static_lines << { + x: NOKIA_X_OFFSET + (i * NOKIA_ZOOM), + y: NOKIA_Y_OFFSET, + x2: NOKIA_X_OFFSET + (i * NOKIA_ZOOM), + y2: NOKIA_Y_OFFSET + NOKIA_ZOOMED_HEIGHT, + r: 199, + g: 240, + b: 216, + a: 100 + }.line + end + + @args.state.overlay_rendered = true + end + end + end +end + +</code></pre> +<h3 id='----lowrez---resolution-64x64---lowrez.rb'>Lowrez - Resolution 64x64 - lowrez.rb</h3> +<pre><code class="language-ruby"># ./samples/99_genre_lowrez/resolution_64x64/app/lowrez.rb # Emulation of a 64x64 canvas. Don't change this file unless you know what you're doing :-) # Head over to main.rb and study the code there. @@ -13340,9 +26076,9 @@ module GTK end end -</pre> -<h1 id='--lowrez---resolution-64x64---main.rb'>Lowrez - Resolution 64x64 - main.rb</h1> -<pre># ./samples/99_genre_lowrez/resolution_64x64/app/main.rb +</code></pre> +<h3 id='----lowrez---resolution-64x64---main.rb'>Lowrez - Resolution 64x64 - main.rb</h3> +<pre><code class="language-ruby"># ./samples/99_genre_lowrez/resolution_64x64/app/main.rb require 'app/lowrez.rb' def tick args @@ -13957,9 +26693,9 @@ end $gtk.reset -</pre> -<h1 id='--platformer---clepto-frog---main.rb'>Platformer - Clepto Frog - main.rb</h1> -<pre># ./samples/99_genre_platformer/clepto_frog/app/main.rb +</code></pre> +<h3 id='----platformer---clepto-frog---main.rb'>Platformer - Clepto Frog - main.rb</h3> +<pre><code class="language-ruby"># ./samples/99_genre_platformer/clepto_frog/app/main.rb MAP_FILE_PATH = 'app/map.txt' require 'app/map.rb' @@ -14825,9 +27561,9 @@ def tick_instructions args, text, y = 715 args.outputs.debug << [640, y - 25, "(SPACE to dismiss instructions)" , -2, 1, 255, 255, 255].label end -</pre> -<h1 id='--platformer---clepto-frog---map.rb'>Platformer - Clepto Frog - map.rb</h1> -<pre># ./samples/99_genre_platformer/clepto_frog/app/map.rb +</code></pre> +<h3 id='----platformer---clepto-frog---map.rb'>Platformer - Clepto Frog - map.rb</h3> +<pre><code class="language-ruby"># ./samples/99_genre_platformer/clepto_frog/app/map.rb $collisions = [ [326, 463, 64, 64], [274, 462, 64, 64], @@ -15854,16 +28590,16 @@ $mugs = [ [5064, 1064, 39, 43, "sprites/square-orange.png"] ] -</pre> -<h1 id='--platformer---gorillas-basic---credits.txt'>Platformer - Gorillas Basic - credits.txt</h1> -<pre># ./samples/99_genre_platformer/gorillas_basic/CREDITS.txt +</code></pre> +<h3 id='----platformer---gorillas-basic---credits.txt'>Platformer - Gorillas Basic - credits.txt</h3> +<pre><code class="language-ruby"># ./samples/99_genre_platformer/gorillas_basic/CREDITS.txt code: Amir Rajan, https://twitter.com/amirrajan graphics: Nick Culbertson, https://twitter.com/MobyPixel -</pre> -<h1 id='--platformer---gorillas-basic---main.rb'>Platformer - Gorillas Basic - main.rb</h1> -<pre># ./samples/99_genre_platformer/gorillas_basic/app/main.rb +</code></pre> +<h3 id='----platformer---gorillas-basic---main.rb'>Platformer - Gorillas Basic - main.rb</h3> +<pre><code class="language-ruby"># ./samples/99_genre_platformer/gorillas_basic/app/main.rb class YouSoBasicGorillas attr_accessor :outputs, :grid, :state, :inputs @@ -16238,17 +28974,17 @@ def tick args $you_so_basic_gorillas.tick end -</pre> -<h1 id='--platformer---gorillas-basic---tests.rb'>Platformer - Gorillas Basic - tests.rb</h1> -<pre># ./samples/99_genre_platformer/gorillas_basic/app/tests.rb +</code></pre> +<h3 id='----platformer---gorillas-basic---tests.rb'>Platformer - Gorillas Basic - tests.rb</h3> +<pre><code class="language-ruby"># ./samples/99_genre_platformer/gorillas_basic/app/tests.rb $gtk.reset 100 $gtk.supress_framerate_warning = true $gtk.require 'app/tests/building_generation_tests.rb' $gtk.tests.start -</pre> -<h1 id='--platformer---gorillas-basic---tests---building_generation_tests.rb'>Platformer - Gorillas Basic - Tests - building_generation_tests.rb</h1> -<pre># ./samples/99_genre_platformer/gorillas_basic/app/tests/building_generation_tests.rb +</code></pre> +<h3 id='----platformer---gorillas-basic---tests---building_generation_tests.rb'>Platformer - Gorillas Basic - Tests - building_generation_tests.rb</h3> +<pre><code class="language-ruby"># ./samples/99_genre_platformer/gorillas_basic/app/tests/building_generation_tests.rb def test_solids args, assert game = YouSoBasicGorillas.new game.outputs = args.outputs @@ -16265,9 +29001,9 @@ def test_solids args, assert assert.true! static_solids == the_only_background.+(number_of_building_components), "not all parts of the buildings and background were rendered" end -</pre> -<h1 id='--platformer---the-little-probe---main.rb'>Platformer - The Little Probe - main.rb</h1> -<pre># ./samples/99_genre_platformer/the_little_probe/app/main.rb +</code></pre> +<h3 id='----platformer---the-little-probe---main.rb'>Platformer - The Little Probe - main.rb</h3> +<pre><code class="language-ruby"># ./samples/99_genre_platformer/the_little_probe/app/main.rb class FallingCircle attr_gtk @@ -17156,9 +29892,9 @@ def reset $game = nil end -</pre> -<h1 id='--platformer---the-little-probe---data---level.txt'>Platformer - The Little Probe - Data - level.txt</h1> -<pre># ./samples/99_genre_platformer/the_little_probe/data/level.txt +</code></pre> +<h3 id='----platformer---the-little-probe---data---level.txt'>Platformer - The Little Probe - Data - level.txt</h3> +<pre><code class="language-ruby"># ./samples/99_genre_platformer/the_little_probe/data/level.txt 640,8840,1180,8840 -60,10220,0,9960 -60,10220,0,10500 @@ -18640,9 +31376,9 @@ end 1600,11320,1910,11280 1910,11280,2200,11180 923.0029599285435,11398.99893503157,1264.002959928544,11351.99893503157 -</pre> -<h1 id='--platformer---the-little-probe---data---level_lava.txt'>Platformer - The Little Probe - Data - level_lava.txt</h1> -<pre># ./samples/99_genre_platformer/the_little_probe/data/level_lava.txt +</code></pre> +<h3 id='----platformer---the-little-probe---data---level_lava.txt'>Platformer - The Little Probe - Data - level_lava.txt</h3> +<pre><code class="language-ruby"># ./samples/99_genre_platformer/the_little_probe/data/level_lava.txt 100,10740,500,10780 500,10780,960,10760 960,10760,1340,10760 @@ -18878,9 +31614,9 @@ end 1480,11280,1840,11230 2200,11130,2360,11090 1840,11230,2200,11130 -</pre> -<h1 id='--rpg-narrative---choose-your-own-adventure---decision.rb'>Rpg Narrative - Choose Your Own Adventure - decision.rb</h1> -<pre># ./samples/99_genre_rpg_narrative/choose_your_own_adventure/app/decision.rb +</code></pre> +<h3 id='----rpg-narrative---choose-your-own-adventure---decision.rb'>Rpg Narrative - Choose Your Own Adventure - decision.rb</h3> +<pre><code class="language-ruby"># ./samples/99_genre_rpg_narrative/choose_your_own_adventure/app/decision.rb # Hey there! Welcome to Four Decisions. Here is how you # create your decision tree. Remove =being and =end from the text to # enable the game (just save the file). Change stuff and see what happens! @@ -18919,9 +31655,9 @@ def game } end -</pre> -<h1 id='--rpg-narrative---choose-your-own-adventure---main.rb'>Rpg Narrative - Choose Your Own Adventure - main.rb</h1> -<pre># ./samples/99_genre_rpg_narrative/choose_your_own_adventure/app/main.rb +</code></pre> +<h3 id='----rpg-narrative---choose-your-own-adventure---main.rb'>Rpg Narrative - Choose Your Own Adventure - main.rb</h3> +<pre><code class="language-ruby"># ./samples/99_genre_rpg_narrative/choose_your_own_adventure/app/main.rb =begin Reminders: @@ -19055,9 +31791,9 @@ end # Resets the game. $gtk.reset -</pre> -<h1 id='--rpg-narrative---return-of-serenity---lowrez_simulator.rb'>Rpg Narrative - Return Of Serenity - lowrez_simulator.rb</h1> -<pre># ./samples/99_genre_rpg_narrative/return_of_serenity/app/lowrez_simulator.rb +</code></pre> +<h3 id='----rpg-narrative---return-of-serenity---lowrez_simulator.rb'>Rpg Narrative - Return Of Serenity - lowrez_simulator.rb</h3> +<pre><code class="language-ruby"># ./samples/99_genre_rpg_narrative/return_of_serenity/app/lowrez_simulator.rb ################################################################################### # YOU CAN PLAY AROUND WITH THE CODE BELOW, BUT USE CAUTION AS THIS IS WHAT EMULATES # THE 64x64 CANVAS. @@ -19150,9 +31886,9 @@ def render_gridlines_if_needed args end end -</pre> -<h1 id='--rpg-narrative---return-of-serenity---main.rb'>Rpg Narrative - Return Of Serenity - main.rb</h1> -<pre># ./samples/99_genre_rpg_narrative/return_of_serenity/app/main.rb +</code></pre> +<h3 id='----rpg-narrative---return-of-serenity---main.rb'>Rpg Narrative - Return Of Serenity - main.rb</h3> +<pre><code class="language-ruby"># ./samples/99_genre_rpg_narrative/return_of_serenity/app/main.rb require 'app/require.rb' def defaults args @@ -19627,9 +32363,9 @@ def player_xs args, x, y [x, y, 1, 4, 'sprites/player-zoomed-out.png', 0, (global_alpha_percentage args)] end -</pre> -<h1 id='--rpg-narrative---return-of-serenity---require.rb'>Rpg Narrative - Return Of Serenity - require.rb</h1> -<pre># ./samples/99_genre_rpg_narrative/return_of_serenity/app/require.rb +</code></pre> +<h3 id='----rpg-narrative---return-of-serenity---require.rb'>Rpg Narrative - Return Of Serenity - require.rb</h3> +<pre><code class="language-ruby"># ./samples/99_genre_rpg_narrative/return_of_serenity/app/require.rb require 'app/lowrez_simulator.rb' require 'app/storyline_day_one.rb' require 'app/storyline_blinking_light.rb' @@ -19642,9 +32378,9 @@ require 'app/storyline_final_message.rb' require 'app/storyline_final_decision.rb' require 'app/storyline.rb' -</pre> -<h1 id='--rpg-narrative---return-of-serenity---storyline.rb'>Rpg Narrative - Return Of Serenity - storyline.rb</h1> -<pre># ./samples/99_genre_rpg_narrative/return_of_serenity/app/storyline.rb +</code></pre> +<h3 id='----rpg-narrative---return-of-serenity---storyline.rb'>Rpg Narrative - Return Of Serenity - storyline.rb</h3> +<pre><code class="language-ruby"># ./samples/99_genre_rpg_narrative/return_of_serenity/app/storyline.rb def hotspot_top [4, 61, 56, 3] end @@ -19792,9 +32528,9 @@ def reload_current_scene $gtk.console.close end -</pre> -<h1 id='--rpg-narrative---return-of-serenity---storyline_anka.rb'>Rpg Narrative - Return Of Serenity - storyline_anka.rb</h1> -<pre># ./samples/99_genre_rpg_narrative/return_of_serenity/app/storyline_anka.rb +</code></pre> +<h3 id='----rpg-narrative---return-of-serenity---storyline_anka.rb'>Rpg Narrative - Return Of Serenity - storyline_anka.rb</h3> +<pre><code class="language-ruby"># ./samples/99_genre_rpg_narrative/return_of_serenity/app/storyline_anka.rb def anka_inside_room args { background: 'sprites/inside-home.png', @@ -19923,9 +32659,9 @@ def replied_to_anka_back_home args end end -</pre> -<h1 id='--rpg-narrative---return-of-serenity---storyline_blinking_light.rb'>Rpg Narrative - Return Of Serenity - storyline_blinking_light.rb</h1> -<pre># ./samples/99_genre_rpg_narrative/return_of_serenity/app/storyline_blinking_light.rb +</code></pre> +<h3 id='----rpg-narrative---return-of-serenity---storyline_blinking_light.rb'>Rpg Narrative - Return Of Serenity - storyline_blinking_light.rb</h3> +<pre><code class="language-ruby"># ./samples/99_genre_rpg_narrative/return_of_serenity/app/storyline_blinking_light.rb def the_blinking_light args { fade: 60, @@ -20002,9 +32738,9 @@ def blinking_light_inside_mainframe args } end -</pre> -<h1 id='--rpg-narrative---return-of-serenity---storyline_day_one.rb'>Rpg Narrative - Return Of Serenity - storyline_day_one.rb</h1> -<pre># ./samples/99_genre_rpg_narrative/return_of_serenity/app/storyline_day_one.rb +</code></pre> +<h3 id='----rpg-narrative---return-of-serenity---storyline_day_one.rb'>Rpg Narrative - Return Of Serenity - storyline_day_one.rb</h3> +<pre><code class="language-ruby"># ./samples/99_genre_rpg_narrative/return_of_serenity/app/storyline_day_one.rb def day_one_beginning args { background: 'sprites/side-of-home.png', @@ -20212,9 +32948,9 @@ def explaining_the_special_power_inside_computer args } end -</pre> -<h1 id='--rpg-narrative---return-of-serenity---storyline_final_decision.rb'>Rpg Narrative - Return Of Serenity - storyline_final_decision.rb</h1> -<pre># ./samples/99_genre_rpg_narrative/return_of_serenity/app/storyline_final_decision.rb +</code></pre> +<h3 id='----rpg-narrative---return-of-serenity---storyline_final_decision.rb'>Rpg Narrative - Return Of Serenity - storyline_final_decision.rb</h3> +<pre><code class="language-ruby"># ./samples/99_genre_rpg_narrative/return_of_serenity/app/storyline_final_decision.rb def final_decision_side_of_home args { fade: 120, @@ -20352,9 +33088,9 @@ def final_decision_ship_status_shared args ] end -</pre> -<h1 id='--rpg-narrative---return-of-serenity---storyline_final_message.rb'>Rpg Narrative - Return Of Serenity - storyline_final_message.rb</h1> -<pre># ./samples/99_genre_rpg_narrative/return_of_serenity/app/storyline_final_message.rb +</code></pre> +<h3 id='----rpg-narrative---return-of-serenity---storyline_final_message.rb'>Rpg Narrative - Return Of Serenity - storyline_final_message.rb</h3> +<pre><code class="language-ruby"># ./samples/99_genre_rpg_narrative/return_of_serenity/app/storyline_final_message.rb def final_message_sad args { fade: 60, @@ -20572,9 +33308,9 @@ def final_message_summary args end end -</pre> -<h1 id='--rpg-narrative---return-of-serenity---storyline_serenity_alive.rb'>Rpg Narrative - Return Of Serenity - storyline_serenity_alive.rb</h1> -<pre># ./samples/99_genre_rpg_narrative/return_of_serenity/app/storyline_serenity_alive.rb +</code></pre> +<h3 id='----rpg-narrative---return-of-serenity---storyline_serenity_alive.rb'>Rpg Narrative - Return Of Serenity - storyline_serenity_alive.rb</h3> +<pre><code class="language-ruby"># ./samples/99_genre_rpg_narrative/return_of_serenity/app/storyline_serenity_alive.rb def serenity_alive_side_of_home args { fade: 60, @@ -20795,9 +33531,9 @@ def serenity_alive_current_message args end end -</pre> -<h1 id='--rpg-narrative---return-of-serenity---storyline_serenity_bio.rb'>Rpg Narrative - Return Of Serenity - storyline_serenity_bio.rb</h1> -<pre># ./samples/99_genre_rpg_narrative/return_of_serenity/app/storyline_serenity_bio.rb +</code></pre> +<h3 id='----rpg-narrative---return-of-serenity---storyline_serenity_bio.rb'>Rpg Narrative - Return Of Serenity - storyline_serenity_bio.rb</h3> +<pre><code class="language-ruby"># ./samples/99_genre_rpg_narrative/return_of_serenity/app/storyline_serenity_bio.rb def serenity_bio_infront_of_home args { fade: 60, @@ -20950,9 +33686,9 @@ def bad_dream_last_reply args end end -</pre> -<h1 id='--rpg-narrative---return-of-serenity---storyline_serenity_introduction.rb'>Rpg Narrative - Return Of Serenity - storyline_serenity_introduction.rb</h1> -<pre># ./samples/99_genre_rpg_narrative/return_of_serenity/app/storyline_serenity_introduction.rb +</code></pre> +<h3 id='----rpg-narrative---return-of-serenity---storyline_serenity_introduction.rb'>Rpg Narrative - Return Of Serenity - storyline_serenity_introduction.rb</h3> +<pre><code class="language-ruby"># ./samples/99_genre_rpg_narrative/return_of_serenity/app/storyline_serenity_introduction.rb # decision_graph "Message from Sasha", # "I should reply.", # [:replied_to_introduction_seriously, "Reply Seriously", "Who is this?"], @@ -21049,9 +33785,9 @@ def replied_to_introduction_side_of_home args } end -</pre> -<h1 id='--rpg-narrative---return-of-serenity---storyline_speed_of_light.rb'>Rpg Narrative - Return Of Serenity - storyline_speed_of_light.rb</h1> -<pre># ./samples/99_genre_rpg_narrative/return_of_serenity/app/storyline_speed_of_light.rb +</code></pre> +<h3 id='----rpg-narrative---return-of-serenity---storyline_speed_of_light.rb'>Rpg Narrative - Return Of Serenity - storyline_speed_of_light.rb</h3> +<pre><code class="language-ruby"># ./samples/99_genre_rpg_narrative/return_of_serenity/app/storyline_speed_of_light.rb def speed_of_light_front_of_home args { background: 'sprites/front-of-home.png', @@ -21157,9 +33893,9 @@ def speed_of_light_end_of_day args } end -</pre> -<h1 id='--rpg-roguelike---roguelike-line-of-sight---constants.rb'>Rpg Roguelike - Roguelike Line Of Sight - constants.rb</h1> -<pre># ./samples/99_genre_rpg_roguelike/roguelike_line_of_sight/app/constants.rb +</code></pre> +<h3 id='----rpg-roguelike---roguelike-starting-point---constants.rb'>Rpg Roguelike - Roguelike Starting Point - constants.rb</h3> +<pre><code class="language-ruby"># ./samples/99_genre_rpg_roguelike/01_roguelike_starting_point/app/constants.rb SHOW_LEGEND = true SOURCE_TILE_SIZE = 16 DESTINATION_TILE_SIZE = 16 @@ -21169,9 +33905,9 @@ TILE_G = 0 TILE_B = 0 TILE_A = 255 -</pre> -<h1 id='--rpg-roguelike---roguelike-line-of-sight---legend.rb'>Rpg Roguelike - Roguelike Line Of Sight - legend.rb</h1> -<pre># ./samples/99_genre_rpg_roguelike/roguelike_line_of_sight/app/legend.rb +</code></pre> +<h3 id='----rpg-roguelike---roguelike-starting-point---legend.rb'>Rpg Roguelike - Roguelike Starting Point - legend.rb</h3> +<pre><code class="language-ruby"># ./samples/99_genre_rpg_roguelike/01_roguelike_starting_point/app/legend.rb def tick_legend args return unless SHOW_LEGEND @@ -21238,9 +33974,9 @@ def tick_legend args end end -</pre> -<h1 id='--rpg-roguelike---roguelike-line-of-sight---main.rb'>Rpg Roguelike - Roguelike Line Of Sight - main.rb</h1> -<pre># ./samples/99_genre_rpg_roguelike/roguelike_line_of_sight/app/main.rb +</code></pre> +<h3 id='----rpg-roguelike---roguelike-starting-point---main.rb'>Rpg Roguelike - Roguelike Starting Point - main.rb</h3> +<pre><code class="language-ruby"># ./samples/99_genre_rpg_roguelike/01_roguelike_starting_point/app/main.rb require 'app/constants.rb' require 'app/sprite_lookup.rb' require 'app/legend.rb' @@ -21339,9 +34075,9 @@ def tile_in_game x, y, tile_key tile_key) end -</pre> -<h1 id='--rpg-roguelike---roguelike-line-of-sight---sprite_lookup.rb'>Rpg Roguelike - Roguelike Line Of Sight - sprite_lookup.rb</h1> -<pre># ./samples/99_genre_rpg_roguelike/roguelike_line_of_sight/app/sprite_lookup.rb +</code></pre> +<h3 id='----rpg-roguelike---roguelike-starting-point---sprite_lookup.rb'>Rpg Roguelike - Roguelike Starting Point - sprite_lookup.rb</h3> +<pre><code class="language-ruby"># ./samples/99_genre_rpg_roguelike/01_roguelike_starting_point/app/sprite_lookup.rb def sprite_lookup { 0 => [3, 0], @@ -21467,9 +34203,9 @@ end $gtk.args.state.reserved.sprite_lookup = sprite_lookup -</pre> -<h1 id='--rpg-roguelike---roguelike-starting-point---main.rb'>Rpg Roguelike - Roguelike Starting Point - main.rb</h1> -<pre># ./samples/99_genre_rpg_roguelike/roguelike_starting_point/app/main.rb +</code></pre> +<h3 id='----rpg-roguelike---roguelike-line-of-sight---main.rb'>Rpg Roguelike - Roguelike Line Of Sight - main.rb</h3> +<pre><code class="language-ruby"># ./samples/99_genre_rpg_roguelike/02_roguelike_line_of_sight/app/main.rb =begin APIs listing that haven't been encountered in previous sample apps: @@ -21910,9 +34646,9 @@ def tick args $game.tick end -</pre> -<h1 id='--rpg-tactical---hexagonal-grid---main.rb'>Rpg Tactical - Hexagonal Grid - main.rb</h1> -<pre># ./samples/99_genre_rpg_tactical/hexagonal_grid/app/main.rb +</code></pre> +<h3 id='----rpg-tactical---hexagonal-grid---main.rb'>Rpg Tactical - Hexagonal Grid - main.rb</h3> +<pre><code class="language-ruby"># ./samples/99_genre_rpg_tactical/hexagonal_grid/app/main.rb class HexagonTileGame attr_gtk @@ -21982,9 +34718,9 @@ end $gtk.reset -</pre> -<h1 id='--rpg-tactical---isometric-grid---main.rb'>Rpg Tactical - Isometric Grid - main.rb</h1> -<pre># ./samples/99_genre_rpg_tactical/isometric_grid/app/main.rb +</code></pre> +<h3 id='----rpg-tactical---isometric-grid---main.rb'>Rpg Tactical - Isometric Grid - main.rb</h3> +<pre><code class="language-ruby"># ./samples/99_genre_rpg_tactical/isometric_grid/app/main.rb class Isometric attr_accessor :grid, :inputs, :state, :outputs @@ -22248,9 +34984,9 @@ def tick args $isometric.tick end -</pre> -<h1 id='--rpg-topdown---topdown-starting-point---main.rb'>Rpg Topdown - Topdown Starting Point - main.rb</h1> -<pre># ./samples/99_genre_rpg_topdown/topdown_starting_point/app/main.rb +</code></pre> +<h3 id='----rpg-topdown---topdown-starting-point---main.rb'>Rpg Topdown - Topdown Starting Point - main.rb</h3> +<pre><code class="language-ruby"># ./samples/99_genre_rpg_topdown/topdown_starting_point/app/main.rb =begin APIs listing that haven't been encountered in previous sample apps: @@ -22360,9 +35096,13 @@ def move_player args, *vector vector.y * args.state.player_speed) # the box will move extremely slow end -</pre> -<h1 id='--args.rb'>args.rb</h1> -<pre># ./dragon/args.rb +</code></pre> +<h2 id='---oss'>OSS</h2> +<p> +Follows is a source code listing for all files that have been open sourced. This code can be found online at <a href='https://github.com/DragonRuby/dragonruby-game-toolkit-contrib/'>https://github.com/DragonRuby/dragonruby-game-toolkit-contrib/</a>. +</p> +<h3 id='----args.rb'>args.rb</h3> +<pre><code class="language-ruby"># ./dragon/args.rb # coding: utf-8 # Copyright 2019 DragonRuby LLC # MIT License @@ -22406,6 +35146,8 @@ module GTK # @return [Geometry] attr_accessor :geometry + attr_accessor :fn + # This is where you'll put state associated with your video game. # # @return [OpenEntity] @@ -22423,6 +35165,10 @@ module GTK attr_accessor :layout + attr_accessor :easing + + attr_accessor :string + def initialize runtime, recording @inputs = Inputs.new @outputs = Outputs.new args: self @@ -22437,8 +35183,11 @@ module GTK @pixel_arrays = {} @all_tests = [] @geometry = GTK::Geometry + @fn = GTK::Fn @wizards = Wizards.new @layout = GTK::Layout.new @grid.w, @grid.h + @easing = GTK::Easing + @string = String end @@ -22587,12 +35336,16 @@ module GTK def controller_two @inputs.controller_two end + + def autocomplete_methods + [:inputs, :outputs, :gtk, :state, :geometry, :audio, :grid, :layout, :fn] + end end end -</pre> -<h1 id='--assert.rb'>assert.rb</h1> -<pre># ./dragon/assert.rb +</code></pre> +<h3 id='----assert.rb'>assert.rb</h3> +<pre><code class="language-ruby"># ./dragon/assert.rb # coding: utf-8 # Copyright 2019 DragonRuby LLC # MIT License @@ -22699,6 +35452,16 @@ end nil end + def not_equal! actual, expected, message = nil + @assertion_performed = true + if actual == expected + actual_string = "#{actual}#{actual.nil? ? " (nil) " : " " }".strip + message = "actual:\n#{actual_string}\n\nequaled\n\nexpected:\n#{expected}.\n#{message}" + raise message + end + nil + end + =begin Assert if a value is explicitly nil (not false). @@ -22722,9 +35485,9 @@ end end end -</pre> -<h1 id='--attr_gtk.rb'>attr_gtk.rb</h1> -<pre># ./dragon/attr_gtk.rb +</code></pre> +<h3 id='----attr_gtk.rb'>attr_gtk.rb</h3> +<pre><code class="language-ruby"># ./dragon/attr_gtk.rb # coding: utf-8 # Copyright 2019 DragonRuby LLC # MIT License @@ -22769,37 +35532,50 @@ module AttrGTK def geometry args.geometry end + + def layout + args.layout + end end -</pre> -<h1 id='--attr_sprite.rb'>attr_sprite.rb</h1> -<pre># ./dragon/attr_sprite.rb +</code></pre> +<h3 id='----attr_sprite.rb'>attr_sprite.rb</h3> +<pre><code class="language-ruby"># ./dragon/attr_sprite.rb # Copyright 2019 DragonRuby LLC # MIT License # attr_sprite.rb has been released under MIT (*only this file*). # @private module AttrRect + include GTK::Geometry + def left - @x + (@x || self.x) end def right - @x + @w + (@x || self.x) + (@w || self.w) end def bottom - @y + (@y || self.y) end def top - @y + @h + (@y || self.y) + (@h || self.h) + end + + def x1 + (@x || self.x) + end + + def y1 + (@y || self.y) end end module AttrSprite include AttrRect - include GTK::Geometry attr_accessor :x, :y, :w, :h, :path, :angle, :a, :r, :g, :b, :tile_x, :tile_y, :tile_w, :tile_h, :flip_horizontally, @@ -22814,26 +35590,18 @@ module AttrSprite self end - def x1 - @x - end - def x1= value @x = value end - def y1 - @y - end - def y1= value @y = value end end -</pre> -<h1 id='--console.rb'>console.rb</h1> -<pre># ./dragon/console.rb +</code></pre> +<h3 id='----console.rb'>console.rb</h3> +<pre><code class="language-ruby"># ./dragon/console.rb # Copyright 2019 DragonRuby LLC # MIT License # console.rb has been released under MIT (*only this file*). @@ -22989,7 +35757,10 @@ module GTK @visible end - # @gtk + def open reason = nil + show reason + end + def show reason = nil @shown_at = Kernel.global_tick_count @show_reason = reason @@ -23272,6 +36043,10 @@ S if args.inputs.keyboard.key_down.control || args.inputs.keyboard.key_down.meta prompt << $gtk.ffi_misc.getclipboard end + elsif args.inputs.keyboard.key_down.home + prompt.move_cursor_home + elsif args.inputs.keyboard.key_down.end + prompt.move_cursor_end elsif args.inputs.keyboard.key_down.up if @command_history_index == -1 @nonhistory_input = current_input_str @@ -23290,9 +36065,17 @@ S self.current_input_str = @command_history[@command_history_index].dup end elsif args.inputs.keyboard.key_down.left - prompt.move_cursor_left + if args.inputs.keyboard.key_down.control + prompt.move_cursor_left_word + else + prompt.move_cursor_left + end elsif args.inputs.keyboard.key_down.right - prompt.move_cursor_right + if args.inputs.keyboard.key_down.control + prompt.move_cursor_right_word + else + prompt.move_cursor_right + end elsif inputs_scroll_up_full? args scroll_up_full elsif inputs_scroll_down_full? args @@ -23305,8 +36088,10 @@ S prompt.clear @command_history_index = -1 @nonhistory_input = '' - elsif args.inputs.keyboard.key_down.backspace || args.inputs.keyboard.key_down.delete + elsif args.inputs.keyboard.key_down.backspace prompt.backspace + elsif args.inputs.keyboard.key_down.delete + prompt.delete elsif args.inputs.keyboard.key_down.tab prompt.autocomplete end @@ -23391,7 +36176,7 @@ S end def error_markers - ["exception", "error", "undefined method", "failed", "syntax", "deprecated"] + ["exception:", "error:", "undefined method", "failed", "syntax", "deprecated"] end def include_subdued_markers? text @@ -23403,7 +36188,7 @@ S end def subdued_markers - ["reloaded", "exported the"] + ["reloaded", "exported the", "~require~"] end def calc args @@ -23588,9 +36373,9 @@ S end end -</pre> -<h1 id='--console_color.rb'>console_color.rb</h1> -<pre># ./dragon/console_color.rb +</code></pre> +<h3 id='----console_color.rb'>console_color.rb</h3> +<pre><code class="language-ruby"># ./dragon/console_color.rb # Copyright 2019 DragonRuby LLC # MIT License # console_color.rb has been released under MIT (*only this file*). @@ -23622,9 +36407,9 @@ module GTK end end -</pre> -<h1 id='--console_font_style.rb'>console_font_style.rb</h1> -<pre># ./dragon/console_font_style.rb +</code></pre> +<h3 id='----console_font_style.rb'>console_font_style.rb</h3> +<pre><code class="language-ruby"># ./dragon/console_font_style.rb # Copyright 2019 DragonRuby LLC # MIT License # console_font_style.rb has been released under MIT (*only this file*). @@ -23666,9 +36451,9 @@ module GTK end end -</pre> -<h1 id='--console_menu.rb'>console_menu.rb</h1> -<pre># ./dragon/console_menu.rb +</code></pre> +<h3 id='----console_menu.rb'>console_menu.rb</h3> +<pre><code class="language-ruby"># ./dragon/console_menu.rb # Copyright 2019 DragonRuby LLC # MIT License # console_menu.rb has been released under MIT (*only this file*). @@ -23829,9 +36614,9 @@ module GTK end end -</pre> -<h1 id='--console_prompt.rb'>console_prompt.rb</h1> -<pre># ./dragon/console_prompt.rb +</code></pre> +<h3 id='----console_prompt.rb'>console_prompt.rb</h3> +<pre><code class="language-ruby"># ./dragon/console_prompt.rb # Copyright 2019 DragonRuby LLC # MIT License # console_prompt.rb has been released under MIT (*only this file*). @@ -23842,6 +36627,9 @@ end module GTK class Console class Prompt + # ? Can be changed, it was just taken from my editor settings :> + WORD_LIMITER_CHARS = "`~!@#$%^&*-=+()[]{}\|;:'\",.<>/?_ \t\n\0".chars + attr_accessor :current_input_str, :font_style, :console_text_width, :last_input_str, :last_input_str_changed def initialize(font_style:, text_color:, console_text_width:) @@ -23866,6 +36654,7 @@ module GTK def current_input_str=(str) @current_input_str = str @cursor_position = str.length + update_cursor_position_px end def <<(str) @@ -23885,16 +36674,57 @@ module GTK reset_autocomplete end + def delete + return if current_input_str.length.zero? || @cursor_position == current_input_str.length + + @cursor_position += 1 + backspace + end + def move_cursor_left @cursor_position -= 1 if @cursor_position > 0 update_cursor_position_px end + def move_cursor_left_word + return if @cursor_position.zero? + + new_pos = @cursor_position - 1 + (is_word_boundary? @current_input_str[new_pos]) ? + (new_pos -= 1 until !(is_word_boundary? @current_input_str[new_pos - 1]) || new_pos.zero?): + (new_pos -= 1 until (is_word_boundary? @current_input_str[new_pos - 1]) || new_pos.zero?) + + @cursor_position = new_pos + update_cursor_position_px + end + def move_cursor_right @cursor_position += 1 if @cursor_position < current_input_str.length update_cursor_position_px end + def move_cursor_right_word + return if @cursor_position.equal? str_len + + new_pos = @cursor_position + 1 + (is_word_boundary? @current_input_str[new_pos]) ? + (new_pos += 1 until !(is_word_boundary? @current_input_str[new_pos]) || (new_pos.equal? str_len)): + (new_pos += 1 until (is_word_boundary? @current_input_str[new_pos]) || (new_pos.equal? str_len)) + + @cursor_position = new_pos + update_cursor_position_px + end + + def move_cursor_home + @cursor_position = 0 + update_cursor_position_px + end + + def move_cursor_end + @cursor_position = str_len + update_cursor_position_px + end + def clear @current_input_str = '' @cursor_position = 0 @@ -24046,13 +36876,24 @@ S @last_autocomplete_prefix = nil @next_candidate_index = 0 end + + def is_word_boundary? char + # Alternative method + # (WORD_LIMITER_CHARS - [char]).length != WORD_LIMITER_CHARS.length + # Production code + WORD_LIMITER_CHARS.include? char + end + + def str_len + @current_input_str.length + end end end end -</pre> -<h1 id='--controller.rb'>controller.rb</h1> -<pre># ./dragon/controller.rb +</code></pre> +<h3 id='----controller.rb'>controller.rb</h3> +<pre><code class="language-ruby"># ./dragon/controller.rb # coding: utf-8 # Copyright 2019 DragonRuby LLC # MIT License @@ -24176,9 +37017,9 @@ module GTK end -</pre> -<h1 id='--controller/config.rb'>controller/config.rb</h1> -<pre># ./dragon/controller/config.rb +</code></pre> +<h3 id='----controller/config.rb'>controller/config.rb</h3> +<pre><code class="language-ruby"># ./dragon/controller/config.rb # coding: utf-8 # Copyright 2019 DragonRuby LLC # MIT License @@ -24579,9 +37420,9 @@ module GTK end end -</pre> -<h1 id='--controller/keys.rb'>controller/keys.rb</h1> -<pre># ./dragon/controller/keys.rb +</code></pre> +<h3 id='----controller/keys.rb'>controller/keys.rb</h3> +<pre><code class="language-ruby"># ./dragon/controller/keys.rb # coding: utf-8 # Copyright 2019 DragonRuby LLC # MIT License @@ -24603,7 +37444,7 @@ module GTK ].freeze LABELS.each do |label| - attr_reader label + attr label end # Activate a key. @@ -24634,9 +37475,9 @@ module GTK end end -</pre> -<h1 id='--directional_input_helper_methods.rb'>directional_input_helper_methods.rb</h1> -<pre># ./dragon/directional_input_helper_methods.rb +</code></pre> +<h3 id='----directional_input_helper_methods.rb'>directional_input_helper_methods.rb</h3> +<pre><code class="language-ruby"># ./dragon/directional_input_helper_methods.rb # coding: utf-8 # Copyright 2019 DragonRuby LLC # MIT License @@ -24693,8 +37534,8 @@ S # - ~nil~ which denotes that no directional input exists. # - ~[ 0, 1]~ which denotes that only up is being held/pressed. # - ~[ 0, -1]~ which denotes that only down is being held/pressed. - # - ~[ 0.5, 0.5]~ which denotes that right and up are being pressed/held. - # - ~[-0.5, -0.5]~ which denotes that left and down are being pressed/held. + # - ~[-0.707, 0.707]~ which denotes that right and up are being pressed/held. + # - ~[-0.707, -0.707]~ which denotes that left and down are being pressed/held. # # @gtk def directional_vector @@ -24703,7 +37544,7 @@ S if lr == 0 && ud == 0 return nil elsif lr.abs == ud.abs - return [lr.half, ud.half] + return [45.vector_x * lr.sign, 45.vector_y * ud.sign, ud.sign] else return [lr, ud] end @@ -24730,9 +37571,9 @@ S end end -</pre> -<h1 id='--easing.rb'>easing.rb</h1> -<pre># ./dragon/easing.rb +</code></pre> +<h3 id='----easing.rb'>easing.rb</h3> +<pre><code class="language-ruby"># ./dragon/easing.rb # coding: utf-8 # Copyright 2019 DragonRuby LLC # MIT License @@ -24740,6 +37581,15 @@ end module GTK module Easing + def self.ease start_tick, current_tick, duration, *definitions + ease_extended start_tick, + current_tick, + start_tick + duration, + (initial_value *definitions), + (final_value *definitions), + *definitions + end + def self.ease_extended start_tick, current_tick, end_tick, default_before, default_after, *definitions definitions.flatten! definitions = [:identity] if definitions.length == 0 @@ -24754,7 +37604,12 @@ module GTK y end + def self.ease_spline start_tick, current_tick, duration, spline + ease_spline_extended start_tick, current_tick, start_tick + duration, spline + end + def self.ease_spline_extended start_tick, current_tick, end_tick, spline + return spline[-1][-1] if current_tick >= end_tick duration = end_tick - start_tick t = (current_tick - start_tick).fdiv duration time_allocation_per_curve = 1.fdiv(spline.length) @@ -24766,12 +37621,12 @@ module GTK def self.initial_value *definitions definitions.flatten! - return Easing.exec_definition (definitions.value(-1) || :identity), 0, 10, 0 + return Easing.exec_definition (definitions.at(-1) || :identity), 0, 10, 0 end def self.final_value *definitions definitions.flatten! - return Easing.exec_definition (definitions.value(-1) || :identity), 0, 10, 1.0 + return Easing.exec_definition (definitions.at(-1) || :identity), 0, 10, 1.0 end def self.exec_definition definition, start_tick, duration, x @@ -24816,9 +37671,104 @@ end Easing = GTK::Easing -</pre> -<h1 id='--geometry.rb'>geometry.rb</h1> -<pre># ./dragon/geometry.rb +</code></pre> +<h3 id='----entity.rb'>entity.rb</h3> +<pre><code class="language-ruby"># ./dragon/entity.rb +# coding: utf-8 +# Copyright 2019 DragonRuby LLC +# MIT License +# entity.rb has been released under MIT (*only this file*). + +module GTK + class Entity + def self.id! + @id ||= 0 + @id += 1 + @id + end + + def self.__reset_id__! + @id = 0 + end + + def self.strict_entities + @strict_entities ||= {} + @strict_entities + end + + def self.parse_serialization_data data + r = Entity.parse_serialization_data data + return r if r.is_a? OpenEntity + return r if r.is_a? StrictEntity + raise <<-S +* ERROR: +The save data looks to be corrupt. + +S + end + + def self.parse_serialization_data value + if value.is_a?(Hash) && value[:entity_id] && value[:entity_strict] + o = new_entity_strict value[:entity_name], value + o.load_entity_data! value + return o + elsif value.is_a?(Hash) && value[:entity_id] + o = OpenEntity.new + o.load_entity_data! value + return o + elsif value.is_a? Array + return value.map { |entry| Entity.parse_serialization_data entry } + else + return value + end + end + + def self.new_entity entity_type, init_hash = nil, block = nil + n = OpenEntity.new(entity_type) + n.entity_type = entity_type + n.created_at = Kernel.tick_count + n.global_created_at = Kernel.global_tick_count + + if init_hash + init_hash.each do |k, v| + n.as_hash[k] = v + end + end + + block.call(n) if block + + n + end + + def self.new_entity_strict entity_type, init_hash = nil, block = nil + if !Entity.strict_entities[entity_type] + init_hash ||= { } + + n = new_entity entity_type, init_hash, block + klass = Class.new(StrictEntity) + + klass.class_eval do + init_hash.each do |k, v| + attr_accessor k + end + + n.as_hash.each do |k, v| + attr_accessor k if !init_hash[k] + end + end + + Entity.strict_entities[entity_type] = klass + end + + klass = Entity.strict_entities[entity_type] + (klass.new entity_type, init_hash, block) + end + end +end + +</code></pre> +<h3 id='----geometry.rb'>geometry.rb</h3> +<pre><code class="language-ruby"># ./dragon/geometry.rb # coding: utf-8 # Copyright 2019 DragonRuby LLC # MIT License @@ -24847,8 +37797,8 @@ module GTK # Returns true if a primitive's rectangle is entirely inside another primitive's rectangle. # @gtk - def inside_rect? outer - Geometry.inside_rect? self, outer + def inside_rect? outer, tolerance = 0.0 + Geometry.inside_rect? self, outer, tolerance end # Returns true if a primitive's rectangle overlaps another primitive's rectangle. @@ -24897,10 +37847,10 @@ module GTK Geometry.point_inside_circle? self, circle_center_point, radius end - def center_inside_rect other_rect - offset_x = (other_rect.w - w).half - offset_y = (other_rect.h - h).half - new_rect = self.shift_rect(0, 0) + def self.center_inside_rect rect, other_rect + offset_x = (other_rect.w - rect.w).half + offset_y = (other_rect.h - rect.h).half + new_rect = rect.shift_rect(0, 0) new_rect.x = other_rect.x + offset_x new_rect.y = other_rect.y + offset_y new_rect @@ -24911,30 +37861,45 @@ center_inside_rect for self #{self} and other_rect #{other_rect}. Failed with ex S end - def center_inside_rect_y other_rect - offset_y = (other_rect.h - h).half - new_rect = self.shift_rect(0, 0) - new_rect.y = other_rect.y + offset_y + def center_inside_rect other_rect + Geometry.center_inside_rect self, other_rect + end + + def self.center_inside_rect_x rect, other_rect + offset_x = (other_rect.w - rect.w).half + new_rect = rect.shift_rect(0, 0) + new_rect.x = other_rect.x + offset_x + new_rect.y = other_rect.y new_rect rescue Exception => e raise e, <<-S * ERROR: -center_inside_rect_y for self #{self} and other_rect #{other_rect}. Failed with exception #{e}. +center_inside_rect_x for self #{self} and other_rect #{other_rect}. Failed with exception #{e}. S end def center_inside_rect_x other_rect - offset_x = (other_rect.w - w).half - new_rect = self.shift_rect(0, 0) - new_rect.x = other_rect.x + offset_x + Geometry.center_inside_rect_x self, other_rect + end + + def self.center_inside_rect_y rect, other_rect + offset_y = (other_rect.h - rect.h).half + new_rect = rect.shift_rect(0, 0) + new_rect.x = other_rect.x + new_rect.y = other_rect.y + offset_y new_rect rescue Exception => e raise e, <<-S * ERROR: -center_inside_rect_x for self #{self} and other_rect #{other_rect}. Failed with exception #{e}. +center_inside_rect_y for self #{self} and other_rect #{other_rect}. Failed with exception #{e}. S end + def center_inside_rect_y other_rect + Geometry.center_inside_rect_y self, other_rect + end + + # Returns a primitive that is anchored/repositioned based off its retangle. # @gtk def anchor_rect anchor_x, anchor_y @@ -24991,10 +37956,27 @@ S # @gtk def self.line_slope line, replace_infinity: nil + return replace_infinity if line.x2 == line.x (line.y2 - line.y).fdiv(line.x2 - line.x) .replace_infinity(replace_infinity) end + def self.line_rise_run line + rise = (line.y2 - line.y).to_f + run = (line.x2 - line.x).to_f + if rise.abs > run.abs && rise != 0 + rise = rise.fdiv rise.abs + run = run.fdiv rise.abs + elsif run.abs > rise.abs && run != 0 + rise = rise.fdiv run.abs + run = run.fdiv run.abs + else + rise = rise / rise.abs if rise != 0 + run = run / run.abs if run != 0 + end + return { x: run , y: rise } + end + # @gtk def self.ray_test point, line slope = (line.y2 - line.y).fdiv(line.x2 - line.x) @@ -25060,8 +38042,8 @@ S return false if rect_one.bottom + tolerance > rect_two.top - tolerance return true rescue Exception => e - context_help_rect_one = (rect_one.help_contract_implementation contract_intersect_rect?)[:not_implemented_methods] - context_help_rect_two = (rect_two.help_contract_implementation contract_intersect_rect?)[:not_implemented_methods] + context_help_rect_one = (rect_one.__help_contract_implementation contract_intersect_rect?)[:not_implemented_methods] + context_help_rect_two = (rect_two.__help_contract_implementation contract_intersect_rect?)[:not_implemented_methods] context_help = "" if context_help_rect_one && context_help_rect_one.length > 0 context_help += <<-S @@ -25129,11 +38111,11 @@ S end # @gtk - def self.inside_rect? inner_rect, outer_rect - inner_rect.x >= outer_rect.x && - inner_rect.right <= outer_rect.right && - inner_rect.y >= outer_rect.y && - inner_rect.top <= outer_rect.top + def self.inside_rect? inner_rect, outer_rect, tolerance = 0.0 + inner_rect.x + tolerance >= outer_rect.x - tolerance && + inner_rect.right - tolerance <= outer_rect.right + tolerance && + inner_rect.y + tolerance >= outer_rect.y - tolerance && + inner_rect.top - tolerance <= outer_rect.top + tolerance rescue Exception => e raise e, ":inside_rect? failed for inner_rect: #{inner_rect} outer_rect: #{outer_rect}." end @@ -25189,9 +38171,9 @@ S end # module Geometry end # module GTK -</pre> -<h1 id='--grid.rb'>grid.rb</h1> -<pre># ./dragon/grid.rb +</code></pre> +<h3 id='----grid.rb'>grid.rb</h3> +<pre><code class="language-ruby"># ./dragon/grid.rb # coding: utf-8 # Copyright 2019 DragonRuby LLC # MIT License @@ -25382,9 +38364,9 @@ module GTK end end -</pre> -<h1 id='--inputs.rb'>inputs.rb</h1> -<pre># ./dragon/inputs.rb +</code></pre> +<h3 id='----inputs.rb'>inputs.rb</h3> +<pre><code class="language-ruby"># ./dragon/inputs.rb # coding: utf-8 # Copyright 2019 DragonRuby LLC # MIT License @@ -25416,6 +38398,11 @@ module GTK :q, :r, :s, :t, :u, :v, :w, :x, :y, :z, :shift, :control, :alt, :meta, + :shift_left, :shift_right, + :control_left, :control_right, + :alt_left, :alt_right, + :meta_left, :meta_right, + :home, :end, :left, :right, :up, :down, :pageup, :pagedown, :char, :plus, :at, :forward_slash, :back_slash, :asterisk, :less_than, :greater_than, :carat, :ampersand, :superscript_two, @@ -25430,7 +38417,10 @@ module GTK raw_key == 1073741905 || raw_key == 1073741906 || raw_key == 1073741899 || - raw_key == 1073741902 + raw_key == 1073741902 || + raw_key == 1073741898 || + raw_key == 1073741901 || + (raw_key >= 1073742048 && raw_key <= 1073742055) # Modifier Keys char = KeyboardKeys.char_with_shift raw_key, modifier names = KeyboardKeys.char_to_method char, raw_key @@ -25474,32 +38464,32 @@ module GTK def self.char_to_method_hash @char_to_method ||= { - 'A' => [:a, :shift], - 'B' => [:b, :shift], - 'C' => [:c, :shift], - 'D' => [:d, :shift], - 'E' => [:e, :shift], - 'F' => [:f, :shift], - 'G' => [:g, :shift], - 'H' => [:h, :shift], - 'I' => [:i, :shift], - 'J' => [:j, :shift], - 'K' => [:k, :shift], - 'L' => [:l, :shift], - 'M' => [:m, :shift], - 'N' => [:n, :shift], - 'O' => [:o, :shift], - 'P' => [:p, :shift], - 'Q' => [:q, :shift], - 'R' => [:r, :shift], - 'S' => [:s, :shift], - 'T' => [:t, :shift], - 'U' => [:u, :shift], - 'V' => [:v, :shift], - 'W' => [:w, :shift], - 'X' => [:x, :shift], - 'Y' => [:y, :shift], - 'Z' => [:z, :shift], + 'A' => [:a], + 'B' => [:b], + 'C' => [:c], + 'D' => [:d], + 'E' => [:e], + 'F' => [:f], + 'G' => [:g], + 'H' => [:h], + 'I' => [:i], + 'J' => [:j], + 'K' => [:k], + 'L' => [:l], + 'M' => [:m], + 'N' => [:n], + 'O' => [:o], + 'P' => [:p], + 'Q' => [:q], + 'R' => [:r], + 'S' => [:s], + 'T' => [:t], + 'U' => [:u], + 'V' => [:v], + 'W' => [:w], + 'X' => [:x], + 'Y' => [:y], + 'Z' => [:z], "!" => [:exclamation_point], "0" => [:zero], "1" => [:one], @@ -25550,18 +38540,29 @@ module GTK "?" => [:question_mark], '%' => [:percent_sign], "º" => [:ordinal_indicator], + 1073741898 => [:home], + 1073741901 => [:end], 1073741903 => [:right], 1073741904 => [:left], 1073741905 => [:down], 1073741906 => [:up], 1073741899 => [:pageup], 1073741902 => [:pagedown], - 127 => [:delete] + 127 => [:delete], + 1073742049 => [:shift_left, :shift], + 1073742053 => [:shift_right, :shift], + 1073742048 => [:control_left, :control], + 1073742052 => [:control_right, :control], + 1073742050 => [:alt_left, :alt], + 1073742054 => [:alt_right, :alt], + 1073742051 => [:meta_left, :meta], + 1073742055 => [:meta_right, :meta] } end def self.char_to_method char, int = nil - char_to_method_hash[char] || char_to_method_hash[int] || [char.to_sym || int] + methods = char_to_method_hash[char] || char_to_method_hash[int] + methods ? methods.dup : [char.to_sym || int] end def clear @@ -25893,6 +38894,10 @@ module GTK point.point_inside_circle? center, radius end + def intersect_rect? other_rect + { x: point.x, y: point.y, w: 0, h: 0 }.intersect_rect? other_rect + end + alias_method :position, :point def clear @@ -25950,14 +38955,20 @@ module GTK attr_accessor :moved, :moved_at, :global_moved_at, + :down_at, + :global_down_at, :touch_order, + :first_tick_down, :x, :y def initialize @moved = false @moved_at = 0 @global_moved_at = 0 + @down_at = 0 + @global_down_at = 0 @touch_order = 0 + @first_tick_down = true @x = 0 @y = 0 end @@ -25983,6 +38994,9 @@ module GTK result[:touch_order] = @touch_order result[:moved] = @moved result[:moved_at] = @moved_at + result[:global_moved_at] = @global_moved_at + result[:down_at] = @down_at + result[:global_down_at] = @global_down_at result end @@ -26013,6 +39027,10 @@ module GTK # @gtk attr_reader :mouse + # @return [HTTPRequest[]] + # @gtk + attr_accessor :http_requests + # @return {FingerTouch} # @gtk attr_reader :touch @@ -26029,6 +39047,7 @@ module GTK @finger_one = nil @finger_two = nil @text = [] + @http_requests = [] end def up @@ -26104,6 +39123,7 @@ module GTK @keyboard.clear @controllers.each(&:clear) @touch.clear + @http_requests.clear @finger_one = nil @finger_two = nil end @@ -26121,9 +39141,9 @@ module GTK end end -</pre> -<h1 id='--ios_wizard.rb'>ios_wizard.rb</h1> -<pre># ./dragon/ios_wizard.rb +</code></pre> +<h3 id='----ios_wizard.rb'>ios_wizard.rb</h3> +<pre><code class="language-ruby"># ./dragon/ios_wizard.rb # Copyright 2019 DragonRuby LLC # MIT License # ios_wizard.rb has been released under MIT (*only this file*). @@ -26146,21 +39166,49 @@ class IOSWizard end def steps + @steps ||= [] + end + + def steps_development_build [ :check_for_xcode, :check_for_brew, :check_for_certs, :check_for_device, :check_for_dev_profile, + :determine_team_identifier, :determine_app_name, :determine_app_id, :blow_away_temp, :stage_app, - :write_info_plist, + :development_write_info_plist, :write_entitlements_plist, - :code_sign, + :compile_icons, + :create_payload_directory, + :code_sign_payload, :create_ipa, - :deploy, + :deploy + ] + end + + def steps_production_build + [ + :check_for_xcode, + :check_for_brew, + :check_for_certs, + :check_for_distribution_profile, + :determine_team_identifier, + :determine_app_name, + :determine_app_id, + :blow_away_temp, + :stage_app, + :production_write_info_plist, + :write_entitlements_plist, + :compile_icons, + :create_payload_directory, + :code_sign_payload, + :create_ipa, + :print_publish_help ] end @@ -26179,36 +39227,45 @@ class IOSWizard sprite_path end - def start + def start opts = nil + @opts = opts || {} + + if !(@opts.is_a? Hash) || !($gtk.args.fn.eq_any? @opts[:env], :dev, :prod) + raise WizardException.new( + "* $wizards.ios.start needs to be provided an environment option.", + "** For development builds type: $wizards.ios.start env: :dev", + "** For production builds type: $wizards.ios.start env: :prod" + ) + end + + @production_build = (@opts[:env] == :prod) + @steps = steps_development_build + @steps = steps_production_build if @production_build @certificate_name = nil init_wizard_status log_info "Starting iOS Wizard so we can deploy to your device." @start_at = Kernel.global_tick_count steps.each do |m| - begin - result = (send m) || :success if @wizard_status[m][:result] != :success - @wizard_status[m][:result] = result - rescue Exception => e - if e.is_a? WizardException - $console.log.clear - $console.archived_log.clear - log "=" * $console.console_text_width - e.console_primitives.each do |p| - $console.add_primitive p - end - log "=" * $console.console_text_width - else - log_error "Step #{m} failed." - log_error e.to_s - end - - $console.set_command "$wizards.ios.start" - - break + log_info "Running step ~:#{m}~." + result = (send m) || :success if @wizard_status[m][:result] != :success + @wizard_status[m][:result] = result + log_info "Running step ~:#{m}~ complete." + end + nil + rescue Exception => e + if e.is_a? WizardException + $console.log.clear + $console.archived_log.clear + log "=" * $console.console_text_width + e.console_primitives.each do |p| + $console.add_primitive p end + log "=" * $console.console_text_width + else + log_error e.to_s end - return nil + $console.set_command "$wizards.ios.start env: :#{@opts[:env]}" end def always_fail @@ -26265,8 +39322,31 @@ class IOSWizard start end + def check_for_distribution_profile + @provisioning_profile_path = "profiles/distribution.mobileprovision" + if !($gtk.read_file @provisioning_profile_path) + $gtk.system "mkdir -p #{relative_path}/profiles" + $gtk.system "open #{relative_path}/profiles" + $gtk.system "echo Download the mobile provisioning profile and place it here with the name distribution.mobileprovision > #{relative_path}/profiles/README.txt" + raise WizardException.new( + "* I didn't find a mobile provision.", + "** 1. Go to http://developer.apple.com and click \"Certificates, IDs & Profiles\".", + "** 2. Add an App Identifier.", + "** 3. Select the App IDs option from the list.", + { w: 700, h: 75, path: get_reserved_sprite("identifiers.png") }, + "** 4. Add your Device next. You can use idevice_id -l to get the UUID of your device.", + { w: 365, h: 69, path: get_reserved_sprite("device-link.png") }, + "** 5. Create a Profile. Associate your certs, id, and device.", + { w: 300, h: 122, path: get_reserved_sprite("profiles.png") }, + "** 6. Download the mobile provision and save it to 'profiles/development.mobileprovision'.", + { w: 200, h: 124, path: get_reserved_sprite("profiles-folder.png") }, + ) + end + end + def check_for_dev_profile - if !($gtk.read_file 'profiles/development.mobileprovision') + @provisioning_profile_path = "profiles/development.mobileprovision" + if !($gtk.read_file @provisioning_profile_path) $gtk.system "mkdir -p #{relative_path}/profiles" $gtk.system "open #{relative_path}/profiles" $gtk.system "echo Download the mobile provisioning profile and place it here with the name development.mobileprovision > #{relative_path}/profiles/README.txt" @@ -26286,13 +39366,23 @@ class IOSWizard end end + def provisioning_profile_path environment + return "profiles/distribution.mobileprovision" if environment == :prod + return "profiles/development.mobileprovision" + end + + def determine_team_identifier + @team_name = (team_identifier_from_provisioning_profile @opts[:env]) + log_info "Team Identifer is: #{@team_name}" + end + def determine_app_name - @app_name = dev_profile_xml[:children].first[:children].first[:children][1][:children].first[:data] + @app_name = (provisioning_profile_xml @opts[:env])[:children].first[:children].first[:children][1][:children].first[:data] log_info "App name is: #{@app_name}." end - def dev_profile_xml - xml = $gtk.read_file 'profiles/development.mobileprovision' + def provisioning_profile_xml environment + xml = $gtk.read_file (provisioning_profile_path environment) scrubbed = xml.each_line.map do |l| if l.strip.start_with? "<" if l.start_with? '</plist>' @@ -26311,9 +39401,36 @@ class IOSWizard $gtk.parse_xml scrubbed end + def app_id_from_provisioning_profile environment + application_identifier_index = (provisioning_profile_xml environment)[:children][0][:children][0][:children][13][:children][0][:children][0][:data] + (provisioning_profile_xml environment)[:children][0][:children][0][:children][13][:children].each.with_index do |node, i| + if node[:children] && node[:children][0] && node[:children][0][:data] == "application-identifier" + application_identifier_index = i + break + end + end + + app_id_with_team_identifier = (provisioning_profile_xml environment)[:children].first[:children].first[:children][13][:children][application_identifier_index + 1][:children].first[:data] + team_identifer = team_identifier_from_provisioning_profile environment + app_id_with_team_identifier.gsub "#{team_identifer}.", "" + end + + def team_identifier_from_provisioning_profile environment + team_identifer_index = (provisioning_profile_xml environment)[:children][0][:children][0][:children][13][:children][0][:children][0][:data] + + (provisioning_profile_xml environment)[:children][0][:children][0][:children][13][:children].each.with_index do |node, i| + if node[:children] && node[:children][0] && node[:children][0][:data] == "com.apple.developer.team-identifier" + team_identifer_index = i + break + end + end + + (provisioning_profile_xml environment)[:children].first[:children].first[:children][13][:children][team_identifer_index + 1][:children].first[:data] + end + def determine_app_id - # lol - @app_id = dev_profile_xml[:children].first[:children].first[:children][13][:children][1][:children].first[:data] + @app_id = app_id_from_provisioning_profile @opts[:env] + log_info "App Identifier is set to : #{@app_id}" end @@ -26327,7 +39444,7 @@ class IOSWizard log_error "I couldn't find a development profile at #{path}." ask_for_dev_profile else - @dev_profile_path = path + @provisioning_profile_path = path start end end @@ -26394,8 +39511,12 @@ class IOSWizard raise "You do not have any Apple development certs on this computer." end - @certificate_name = valid_certs.first[:name] - log_info "I will be using '#{@certificate_name}' to deploy to your device." + if @production_build + @certificate_name = valid_certs.find_all { |f| f[:name].include? "Distribution" }.first[:name] + else + @certificate_name = valid_certs.find_all { |f| f[:name].include? "Development" }.first[:name] + end + log_info "I will be using Certificate: '#{@certificate_name}'." end def idevice_id_cli_app @@ -26412,7 +39533,7 @@ class IOSWizard def valid_certs certs = sh("#{security_cli_app} -q find-identity -p codesigning -v").each_line.map do |l| - if l.include?(")") && !l.include?("Developer ID") && l.include?("Development") + if l.include?(")") && !l.include?("Developer ID") && (l.include?("Development") || l.include?("Distribution")) l.strip else nil @@ -26439,22 +39560,37 @@ class IOSWizard end def write_entitlements_plist - entitlement_plist_string = <<-XML + if @production_build + entitlement_plist_string = <<-XML <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <plist version="1.0"> - <dict> - <key>application-identifier</key> - <string>:app_id</string> - <key>get-task-allow</key> - <true/> - </dict> + <dict> + <key>application-identifier</key> + <string>:app_id</string> + <key>beta-reports-active</key> + <true/> + </dict> </plist> XML + else + entitlement_plist_string = <<-XML +<?xml version="1.0" encoding="UTF-8"?> +<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> +<plist version="1.0"> + <dict> + <key>application-identifier</key> + <string>:app_id</string> + <key>get-task-allow</key> + <true/> + </dict> +</plist> +XML + end log_info "Creating Entitlements.plist" - $gtk.write_file_root "tmp/ios/Entitlements.plist", entitlement_plist_string.gsub(":app_id", @app_id).strip + $gtk.write_file_root "tmp/ios/Entitlements.plist", entitlement_plist_string.gsub(":app_id", "#{@team_name}.#{@app_id}").strip sh "/usr/bin/plutil -convert binary1 \"#{tmp_directory}/Entitlements.plist\"" sh "/usr/bin/plutil -convert xml1 \"#{tmp_directory}/Entitlements.plist\"" @@ -26462,19 +39598,130 @@ XML @entitlement_plist_written = true end - def code_sign - sh "cp #{@dev_profile_path} \"#{app_path}/embedded.mobileprovision\"" - + def code_sign_payload log_info "Signing app with #{@certificate_name}." - sh "/usr/bin/plutil -convert binary1 \"#{app_path}/Info.plist\"" - - sh "CODESIGN_ALLOCATE=\"/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/codesign_allocate\" /usr/bin/codesign -f -s \"#{@certificate_name}\" --entitlements #{tmp_directory}/Entitlements.plist \"#{tmp_directory}/#{@app_name}.app\"" + sh "CODESIGN_ALLOCATE=\"/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/codesign_allocate\" /usr/bin/codesign -f -s \"#{@certificate_name}\" --entitlements #{tmp_directory}/Entitlements.plist \"#{tmp_directory}/ipa_root/Payload/#{@app_name}.app\"" + sh "CODESIGN_ALLOCATE=\"/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/codesign_allocate\" /usr/bin/codesign -f -s \"#{@certificate_name}\" --entitlements #{tmp_directory}/Entitlements.plist \"#{tmp_directory}/ipa_root/Payload/#{@app_name}.app/Runtime\"" @code_sign_completed = true end - def write_info_plist + def write_info_plist_distribution + log_info "Adding Info.plist." + + <<-XML +<?xml version="1.0" encoding="UTF-8"?> +<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> +<plist version="1.0"> + <dict> + <key>BuildMachineOSBuild</key> + <string>20D91</string> + <key>CFBundleDevelopmentRegion</key> + <string>en</string> + <key>CFBundleName</key> + <string>:app_name</string> + <key>CFBundleDisplayName</key> + <string>A Dark Room</string> + <key>CFBundleIdentifier</key> + <string>:app_id</string> + <key>CFBundleExecutable</key> + <string>:app_name</string> + <key>CFBundleInfoDictionaryVersion</key> + <string>6.0</string> + <key>CFBundlePackageType</key> + <string>APPL</string> + <key>CFBundleShortVersionString</key> + <string>5.6</string> + <key>CFBundleSignature</key> + <string>????</string> + <key>CFBundleVersion</key> + <string>5.6</string> + <key>CFBundleIcons</key> + <dict> + <key>CFBundlePrimaryIcon</key> + <dict> + <key>CFBundleIconName</key> + <string>AppIcon</string> + <key>CFBundleIconFiles</key> + <array> + <string>AppIcon60x60</string> + </array> + </dict> + </dict> + <key>CFBundleIcons~ipad</key> + <dict> + <key>CFBundlePrimaryIcon</key> + <dict> + <key>CFBundleIconName</key> + <string>AppIcon</string> + <key>CFBundleIconFiles</key> + <array> + <string>AppIcon60x60</string> + <string>AppIcon76x76</string> + <string>AppIcon83.5x83.5</string> + </array> + </dict> + </dict> + <key>UILaunchStoryboardName</key> + <string>SimpleSplash</string> + <key>UIRequiresFullScreen</key> + <true/> + <key>ITSAppUsesNonExemptEncryption</key> + <false/> + <key>UIRequiredDeviceCapabilities</key> + <array> + <string>arm64</string> + </array> + <key>MinimumOSVersion</key> + <string>10.3</string> + <key>CFBundleSupportedPlatforms</key> + <array> + <string>iPhoneOS</string> + </array> + <key>CFBundleIconFiles</key> + <array> + <string>AppIcon20x20</string> + <string>AppIcon29x29</string> + <string>AppIcon40x40</string> + <string>AppIcon60x60</string> + </array> + <key>UIDeviceFamily</key> + <array> + <integer>1</integer> + <integer>2</integer> + </array> + <key>UISupportedInterfaceOrientations</key> + <array> + <string>UIInterfaceOrientationPortrait</string> + </array> + <key>UIStatusBarStyle</key> + <string>UIStatusBarStyleDefault</string> + <key>UIBackgroundModes</key> + <array> + </array> + <key>DTXcode</key> + <string>0124</string> + <key>DTXcodeBuild</key> + <string>12D4e</string> + <key>DTSDKName</key> + <string>iphoneos14.4</string> + <key>DTSDKBuild</key> + <string>18D46</string> + <key>DTPlatformName</key> + <string>iphoneos</string> + <key>DTCompiler</key> + <string>com.apple.compilers.llvm.clang.1_0</string> + <key>DTPlatformVersion</key> + <string>14.4</string> + <key>DTPlatformBuild</key> + <string>18D46</string> + </dict> +</plist> +XML + end + + def development_write_info_plist log_info "Adding Info.plist." info_plist_string = <<-XML @@ -26497,165 +39744,136 @@ XML </dict> </dict> </dict> - <key>BuildMachineOSBuild</key> - <string>19C57</string> - <key>CFBundleDevelopmentRegion</key> - <string>en</string> - <key>CFBundleDisplayName</key> - <string>:app_name</string> - <key>CFBundleExecutable</key> - <string>Runtime</string> - <key>CFBundleIconFiles</key> - <array> - <string>AppIcon60x60</string> - </array> - <key>CFBundleIcons</key> - <dict> - <key>CFBundlePrimaryIcon</key> - <dict> - <key>CFBundleIconFiles</key> - <array> - <string>AppIcon60x60</string> - </array> - <key>CFBundleIconName</key> - <string>AppIcon</string> - </dict> - </dict> - <key>CFBundleIcons~ipad</key> - <dict> - <key>CFBundlePrimaryIcon</key> - <dict> - <key>CFBundleIconFiles</key> - <array> - <string>AppIcon60x60</string> - <string>AppIcon76x76</string> - </array> - <key>CFBundleIconName</key> - <string>AppIcon</string> - </dict> - </dict> - <key>CFBundleIdentifier</key> - <string>com.carlile.swisscheese</string> - <key>CFBundleInfoDictionaryVersion</key> - <string>6.0</string> - <key>CFBundleName</key> - <string>:app_name</string> - <key>CFBundlePackageType</key> - <string>APPL</string> - <key>CFBundleShortVersionString</key> - <string>1.0</string> - <key>CFBundleSignature</key> - <string>????</string> - <key>CFBundleSupportedPlatforms</key> - <array> - <string>iPhoneOS</string> - </array> - <key>CFBundleVersion</key> - <string>1.0</string> - <key>DTCompiler</key> - <string>com.apple.compilers.llvm.clang.1_0</string> - <key>DTPlatformBuild</key> - <string>17B102</string> - <key>DTPlatformName</key> - <string>iphoneos</string> - <key>DTPlatformVersion</key> - <string>13.2</string> - <key>DTSDKBuild</key> - <string>17B102</string> - <key>DTSDKName</key> - <string>iphoneos13.2</string> - <key>DTXcode</key> - <string>01131</string> - <key>DTXcodeBuild</key> - <string>11C505</string> - <key>ITSAppUsesNonExemptEncryption</key> - <false/> - <key>MinimumOSVersion</key> - <string>11.0</string> - <key>UIAppFonts</key> - <array/> - <key>UIBackgroundModes</key> - <array/> - <key>UIDeviceFamily</key> - <array> - <integer>1</integer> - <integer>2</integer> - </array> - <key>UILaunchImages</key> - <array> - <dict> - <key>UILaunchImageMinimumOSVersion</key> - <string>11.0</string> - <key>UILaunchImageName</key> - <string>LaunchImage-1100-Portrait-2436h</string> - <key>UILaunchImageOrientation</key> - <string>Portrait</string> - <key>UILaunchImageSize</key> - <string>{375, 812}</string> - </dict> - <dict> - <key>UILaunchImageMinimumOSVersion</key> - <string>8.0</string> - <key>UILaunchImageName</key> - <string>LaunchImage-800-Portrait-736h</string> - <key>UILaunchImageOrientation</key> - <string>Portrait</string> - <key>UILaunchImageSize</key> - <string>{414, 736}</string> - </dict> - <dict> - <key>UILaunchImageMinimumOSVersion</key> - <string>8.0</string> - <key>UILaunchImageName</key> - <string>LaunchImage-800-667h</string> - <key>UILaunchImageOrientation</key> - <string>Portrait</string> - <key>UILaunchImageSize</key> - <string>{375, 667}</string> - </dict> - <dict> - <key>UILaunchImageMinimumOSVersion</key> - <string>7.0</string> - <key>UILaunchImageName</key> - <string>LaunchImage-700</string> - <key>UILaunchImageOrientation</key> - <string>Portrait</string> - <key>UILaunchImageSize</key> - <string>{320, 480}</string> - </dict> - <dict> - <key>UILaunchImageMinimumOSVersion</key> - <string>7.0</string> - <key>UILaunchImageName</key> - <string>LaunchImage-700-568h</string> - <key>UILaunchImageOrientation</key> - <string>Portrait</string> - <key>UILaunchImageSize</key> - <string>{320, 568}</string> - </dict> - <dict> - <key>UILaunchImageMinimumOSVersion</key> - <string>7.0</string> - <key>UILaunchImageName</key> - <string>LaunchImage-700-Portrait</string> - <key>UILaunchImageOrientation</key> - <string>Portrait</string> - <key>UILaunchImageSize</key> - <string>{768, 1024}</string> - </dict> - </array> - <key>UIRequiredDeviceCapabilities</key> - <array> - <string>arm64</string> - </array> - <key>UIRequiresFullScreen</key> - <true/> - <key>UIStatusBarStyle</key> - <string>UIStatusBarStyleDefault</string> - <key>UISupportedInterfaceOrientations</key> - <array> - <string>#{device_orientation_xml}</string> - </array> + <key>BuildMachineOSBuild</key> + <string>20D91</string> + <key>CFBundleDevelopmentRegion</key> + <string>en</string> + <key>CFBundleDisplayName</key> + <string>:app_name</string> + <key>CFBundleExecutable</key> + <string>Runtime</string> + <key>CFBundleIconFiles</key> + <array> + <string>AppIcon60x60</string> + </array> + <key>CFBundleIcons</key> + <dict> + <key>CFBundlePrimaryIcon</key> + <dict> + <key>CFBundleIconFiles</key> + <array> + <string>AppIcon60x60</string> + </array> + <key>CFBundleIconName</key> + <string>AppIcon</string> + </dict> + </dict> + <key>CFBundleIcons~ipad</key> + <dict> + <key>CFBundlePrimaryIcon</key> + <dict> + <key>CFBundleIconFiles</key> + <array> + <string>AppIcon60x60</string> + <string>AppIcon76x76</string> + <string>AppIcon83.5x83.5</string> + </array> + <key>CFBundleIconName</key> + <string>AppIcon</string> + </dict> + </dict> + <key>CFBundleIdentifier</key> + <string>:app_id</string> + <key>CFBundleInfoDictionaryVersion</key> + <string>6.0</string> + <key>CFBundleName</key> + <string>:app_name</string> + <key>CFBundlePackageType</key> + <string>APPL</string> + <key>CFBundleShortVersionString</key> + <string>5.2</string> + <key>CFBundleSignature</key> + <string>????</string> + <key>CFBundleSupportedPlatforms</key> + <array> + <string>iPhoneOS</string> + </array> + <key>CFBundleVersion</key> + <string>5.2</string> + <key>DTCompiler</key> + <string>com.apple.compilers.llvm.clang.1_0</string> + <key>DTPlatformBuild</key> + <string>18D46</string> + <key>DTPlatformName</key> + <string>iphoneos</string> + <key>DTPlatformVersion</key> + <string>14.4</string> + <key>DTSDKBuild</key> + <string>18D46</string> + <key>DTSDKName</key> + <string>iphoneos14.4</string> + <key>DTXcode</key> + <string>0124</string> + <key>DTXcodeBuild</key> + <string>12D4e</string> + <key>MinimumOSVersion</key> + <string>14.4</string> + <key>UIAppFonts</key> + <array/> + <key>UIBackgroundModes</key> + <array/> + <key>UIDeviceFamily</key> + <array> + <integer>1</integer> + <integer>2</integer> + </array> + <key>UILaunchImages</key> + <array> + <dict> + <key>UILaunchImageMinimumOSVersion</key> + <string>7.0</string> + <key>UILaunchImageName</key> + <string>Default-568h@2x</string> + <key>UILaunchImageOrientation</key> + <string>Portrait</string> + <key>UILaunchImageSize</key> + <string>{320, 568}</string> + </dict> + <dict> + <key>UILaunchImageMinimumOSVersion</key> + <string>7.0</string> + <key>UILaunchImageName</key> + <string>Default-667h@2x</string> + <key>UILaunchImageOrientation</key> + <string>Portrait</string> + <key>UILaunchImageSize</key> + <string>{375, 667}</string> + </dict> + <dict> + <key>UILaunchImageMinimumOSVersion</key> + <string>7.0</string> + <key>UILaunchImageName</key> + <string>Default-736h@3x</string> + <key>UILaunchImageOrientation</key> + <string>Portrait</string> + <key>UILaunchImageSize</key> + <string>{414, 736}</string> + </dict> + </array> + <key>UILaunchStoryboardName</key> + <string>SimpleSplash</string> + <key>UIRequiredDeviceCapabilities</key> + <array> + <string>arm64</string> + </array> + <key>UIRequiresFullScreen</key> + <true/> + <key>UIStatusBarStyle</key> + <string>UIStatusBarStyleDefault</string> + <key>UISupportedInterfaceOrientations</key> + <array> + <string>UIInterfaceOrientationLandscapeRight</string> + </array> </dict> </plist> XML @@ -26663,7 +39881,163 @@ XML # <string>UIInterfaceOrientationPortrait</string> # <string>UIInterfaceOrientationLandscapeRight</string> - $gtk.write_file_root "tmp/ios/#{@app_name}.app/Info.plist", info_plist_string.gsub(":app_name", @app_name).strip + info_plist_string.gsub!(":app_name", @app_name) + info_plist_string.gsub!(":app_id", @app_id) + + $gtk.write_file_root "tmp/ios/#{@app_name}.app/Info.plist", info_plist_string.strip + + @info_plist_written = true + end + + def production_write_info_plist + log_info "Adding Info.plist." + + info_plist_string = <<-XML +<?xml version="1.0" encoding="UTF-8"?> +<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> +<plist version="1.0"> +<dict> + <key>BuildMachineOSBuild</key> + <string>20D91</string> + <key>CFBundleDevelopmentRegion</key> + <string>en</string> + <key>CFBundleDisplayName</key> + <string>:app_name</string> + <key>CFBundleExecutable</key> + <string>Runtime</string> + <key>CFBundleIconFiles</key> + <array> + <string>AppIcon60x60</string> + </array> + <key>CFBundleIcons</key> + <dict> + <key>CFBundlePrimaryIcon</key> + <dict> + <key>CFBundleIconFiles</key> + <array> + <string>AppIcon60x60</string> + </array> + <key>CFBundleIconName</key> + <string>AppIcon</string> + </dict> + </dict> + <key>CFBundleIcons~ipad</key> + <dict> + <key>CFBundlePrimaryIcon</key> + <dict> + <key>CFBundleIconFiles</key> + <array> + <string>AppIcon60x60</string> + <string>AppIcon76x76</string> + <string>AppIcon83.5x83.5</string> + </array> + <key>CFBundleIconName</key> + <string>AppIcon</string> + </dict> + </dict> + <key>CFBundleIdentifier</key> + <string>:app_id</string> + <key>CFBundleInfoDictionaryVersion</key> + <string>6.0</string> + <key>CFBundleName</key> + <string>:app_name</string> + <key>CFBundlePackageType</key> + <string>APPL</string> + <key>CFBundleShortVersionString</key> + <string>5.2</string> + <key>CFBundleSignature</key> + <string>????</string> + <key>CFBundleSupportedPlatforms</key> + <array> + <string>iPhoneOS</string> + </array> + <key>CFBundleVersion</key> + <string>5.2</string> + <key>DTCompiler</key> + <string>com.apple.compilers.llvm.clang.1_0</string> + <key>DTPlatformBuild</key> + <string>18D46</string> + <key>DTPlatformName</key> + <string>iphoneos</string> + <key>DTPlatformVersion</key> + <string>14.4</string> + <key>DTSDKBuild</key> + <string>18D46</string> + <key>DTSDKName</key> + <string>iphoneos14.4</string> + <key>DTXcode</key> + <string>0124</string> + <key>DTXcodeBuild</key> + <string>12D4e</string> + <key>MinimumOSVersion</key> + <string>14.4</string> + <key>UIAppFonts</key> + <array/> + <key>UIBackgroundModes</key> + <array/> + <key>UIDeviceFamily</key> + <array> + <integer>1</integer> + <integer>2</integer> + </array> + <key>UILaunchImages</key> + <array> + <dict> + <key>UILaunchImageMinimumOSVersion</key> + <string>7.0</string> + <key>UILaunchImageName</key> + <string>Default-568h@2x</string> + <key>UILaunchImageOrientation</key> + <string>Portrait</string> + <key>UILaunchImageSize</key> + <string>{320, 568}</string> + </dict> + <dict> + <key>UILaunchImageMinimumOSVersion</key> + <string>7.0</string> + <key>UILaunchImageName</key> + <string>Default-667h@2x</string> + <key>UILaunchImageOrientation</key> + <string>Portrait</string> + <key>UILaunchImageSize</key> + <string>{375, 667}</string> + </dict> + <dict> + <key>UILaunchImageMinimumOSVersion</key> + <string>7.0</string> + <key>UILaunchImageName</key> + <string>Default-736h@3x</string> + <key>UILaunchImageOrientation</key> + <string>Portrait</string> + <key>UILaunchImageSize</key> + <string>{414, 736}</string> + </dict> + </array> + <key>UILaunchStoryboardName</key> + <string>SimpleSplash</string> + <key>UIRequiredDeviceCapabilities</key> + <array> + <string>arm64</string> + </array> + <key>UIRequiresFullScreen</key> + <true/> + <key>UIStatusBarStyle</key> + <string>UIStatusBarStyleDefault</string> + <key>UISupportedInterfaceOrientations</key> + <array> + <string>UIInterfaceOrientationLandscapeRight</string> + </array> +</dict> +</plist> +XML + + # <string>UIInterfaceOrientationPortrait</string> + # <string>UIInterfaceOrientationLandscapeRight</string> + + info_plist_string.gsub!(":app_name", @app_name) + info_plist_string.gsub!(":app_id", @app_id) + + $gtk.write_file_root "tmp/ios/#{@app_name}.app/Info.plist", info_plist_string.strip @info_plist_written = true end @@ -26689,7 +40063,9 @@ XML $gtk.write_file "app/server_ip_address.txt", $gtk.ffi_misc.get_local_ip_address.strip end - def create_ipa + def create_payload_directory + sh "cp #{@provisioning_profile_path} \"#{app_path}/embedded.mobileprovision\"" + sh "/usr/bin/plutil -convert binary1 \"#{app_path}/Info.plist\"" write_ip_address sh "rm \"#{@app_name}\".ipa" sh "rm -rf \"#{app_path}/app\"" @@ -26705,18 +40081,11 @@ XML sh "mkdir -p #{tmp_directory}/ipa_root/Payload" sh "cp -r \"#{app_path}\" \"#{tmp_directory}/ipa_root/Payload\"" sh "chmod -R 755 \"#{tmp_directory}/ipa_root/Payload\"" + end + + def create_ipa do_zip sh "cp \"#{tmp_directory}/ipa_root/archive.zip\" \"#{tmp_directory}/#{@app_name}.ipa\"" - sh "XCODE_DIR=\"/Applications/Xcode.app/Contents/Developer\" \"#{relative_path}/dragonruby-deploy-ios\" -d \"#{@device_id}\" \"#{tmp_directory}/#{@app_name}.ipa\"" - cmd_result = `ps -e | grep civetweb` - is_civet_running = (`ps -e | grep civetweb`).strip.each_line.to_a.length > 2 - if !is_civet_running - $gtk.system "cp \"#{relative_path}/civetweb\" \"#{tmp_directory}/../src_backup/civetweb\"" - $gtk.system "open \"#{tmp_directory}/../src_backup/civetweb\" -g" - else - log "* INFO: civetweb is running already running. No need to start another instance." - end - log_info "Check your device!!" end def do_zip @@ -26739,12 +40108,40 @@ SCRIPT end def deploy + sh "XCODE_DIR=\"/Applications/Xcode.app/Contents/Developer\" \"#{relative_path}/dragonruby-deploy-ios\" -d \"#{@device_id}\" \"#{tmp_directory}/#{@app_name}.ipa\"" + log_info "Check your device!!" + end + + def print_publish_help + log_info "Go to https://appstoreconnect.apple.com/apps and create an App if you haven't already done so." + log_info "Go to https://appleid.apple.com and create a 'Application Specific Password'." + log_info "To upload your app, Download Transporter from the App Store https://apps.apple.com/us/app/transporter/id1450874784?mt=12." + log_info "Your app is located at ./tmp/ios/#{@app_name}.ipa" + end + + def compile_icons + cmd = <<-S +"/Applications/Xcode.app/Contents/Developer/usr/bin/actool" --output-format human-readable-text \ + --notices --warnings --platform iphoneos \ + --minimum-deployment-target 10.3 \ + --target-device iphone \ + --target-device ipad --app-icon 'AppIcon' \ + --output-partial-info-plist '#{app_path}/AssetCatalog-Info.plist' \ + --compress-pngs --compile "#{app_path}" \ + "#{app_path}/Assets.xcassets" +S + sh cmd + end + + def stage_native_libs + sh "cp -r \"#{root_folder}/native/\" \"#{app_path}/native/\"" + sh "CODESIGN_ALLOCATE=\"/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/codesign_allocate\" /usr/bin/codesign -f -s \"#{@certificate_name}\" --entitlements #{tmp_directory}/Entitlements.plist \"#{tmp_directory}/#{@app_name}.app/native/ios-device/ext.dylib\"" end end -</pre> -<h1 id='--itch_wizard.rb'>itch_wizard.rb</h1> -<pre># ./dragon/itch_wizard.rb +</code></pre> +<h3 id='----itch_wizard.rb'>itch_wizard.rb</h3> +<pre><code class="language-ruby"># ./dragon/itch_wizard.rb # Copyright 2019 DragonRuby LLC # MIT License # itch_wizard.rb has been released under MIT (*only this file*). @@ -26839,7 +40236,7 @@ S end if metadata[:icon].start_with?("#") || !@icon - log "* PROPT: Please provide icon path for your game." + log "* PROMPT: Please provide icon path for your game." $console.set_command "$wizards.itch.set_icon \"icon.png\"" return :need_icon end @@ -27014,9 +40411,9 @@ S end end -</pre> -<h1 id='--layout.rb'>layout.rb</h1> -<pre># ./dragon/layout.rb +</code></pre> +<h3 id='----layout.rb'>layout.rb</h3> +<pre><code class="language-ruby"># ./dragon/layout.rb # Copyright 2019 DragonRuby LLC # MIT License # layout.rb has been released under MIT (*only this file*). @@ -27378,6 +40775,18 @@ module GTK result[:h] += device.grid_area.gutter * 2 end + result[:x] += opts[:dx] if opts[:dx] + result[:y] += opts[:dy] if opts[:dy] + result[:w] += opts[:dw] if opts[:dw] + result[:h] += opts[:dh] if opts[:dh] + result[:row] = opts[:row] + result[:col] = opts[:col] + + if $gtk.args.grid.name == :center + result[:x] -= 640 + result[:y] -= 360 + end + result end @@ -27497,9 +40906,9 @@ module GTK end end -</pre> -<h1 id='--log.rb'>log.rb</h1> -<pre># ./dragon/log.rb +</code></pre> +<h3 id='----log.rb'>log.rb</h3> +<pre><code class="language-ruby"># ./dragon/log.rb # coding: utf-8 # Copyright 2019 DragonRuby LLC # MIT License @@ -27765,9 +41174,9 @@ class Object end end -</pre> -<h1 id='--numeric.rb'>numeric.rb</h1> -<pre># ./dragon/numeric.rb +</code></pre> +<h3 id='----numeric.rb'>numeric.rb</h3> +<pre><code class="language-ruby"># ./dragon/numeric.rb # coding: utf-8 # Copyright 2019 DragonRuby LLC # MIT License @@ -27777,8 +41186,12 @@ class Numeric include ValueType include NumericDeprecated + alias_method :gt, :> alias_method :gte, :>= + alias_method :lt, :< alias_method :lte, :<= + alias_method :__original_eq_eq__, :== unless Numeric.instance_methods.include? :__original_eq_eq__ + # Converts a numeric value representing seconds into frames. # @@ -27798,6 +41211,23 @@ class Numeric clamp(0, 255).to_i end + def clamp_wrap min, max + max, min = min, max if min > max + return self if self >= min && self <= max + return min if min == max + if self < min + overflow = min - self + return (max - overflow).clamp_wrap min, max + end + + if self > max + overflow = self - max + return (min + overflow).clamp_wrap min, max + end + + return self + end + def elapsed_time tick_count_override = nil (tick_count_override || Kernel.tick_count) - self end @@ -28051,16 +41481,18 @@ S vector_y max_value end - # @gtk def mod n self % n end - # @gtk def mod_zero? *ns ns.any? { |n| mod(n) == 0 } end + def zmod? n + (self % n) == 0 + end + def mult n self * n end @@ -28182,13 +41614,10 @@ S return gt other end - alias_method(:original_eq_eq, :==) unless Numeric.instance_methods.include?(:original_eq_eq) def == other - return true if self.original_eq_eq(other) - if other.is_a?(OpenEntity) - return self.original_eq_eq(other.entity_id) - end - return self.original_eq_eq(other) + return true if __original_eq_eq__ other + return __original_eq_eq__ other.entity_id if other.is_a? OpenEntity + return false end # @gtk @@ -28206,6 +41635,48 @@ S end end + def each + unless block_given? + raise <<-S +* ERROR: +A block is required for Numeric#each. + +S + end + + self.to_i.times do + yield + end + end + + def times_with_index + unless block_given? + raise <<-S +* ERROR: +A block is required for Numeric#times_with_index. + +S + end + + self.to_i.times.with_index do |i| + yield i + end + end + + def each_with_index + unless block_given? + raise <<-S +* ERROR: +A block is required for Numeric#each_with_index. + +S + end + + self.to_i.times.with_index do |i| + yield i + end + end + # @gtk def map_with_index unless block_given? @@ -28221,69 +41692,82 @@ S end end - def check_numeric! sender, other - return if other.is_a? Numeric - + def __raise_arithmetic_exception__ other, m, e raise <<-S * ERROR: -Attempted to invoke :+ on #{self} with the right hand argument of: +Attempted to invoke :#{m} on #{self} with the right hand argument of: #{other} The object above is not a Numeric. +#{e} S end def - other return nil unless other - check_numeric! :-, other super + rescue Exception => e + __raise_arithmetic_exception__ other, :-, e end def + other return nil unless other - check_numeric! :+, other super + rescue Exception => e + __raise_arithmetic_exception__ other, :+, e end def * other return nil unless other - check_numeric! :*, other super + rescue Exception => e + __raise_arithmetic_exception__ other, :*, e end def / other return nil unless other - check_numeric! :/, other super + rescue Exception => e + __raise_arithmetic_exception__ other, :/, e end def serialize self end + def self.from_top n + return 720 - n unless $gtk + $gtk.args.grid.top - n + end + def from_top - return 720 - self unless $gtk - $gtk.args.grid.h - self + Numeric.from_top self + end + + def self.from_right n + return 1280 - n unless $gtk + $gtk.args.grid.right - n end def from_right - return 1280 - self unless $gtk - $gtk.args.grid.w - self + Numeric.from_right self + end + + def self.clamp n, min, max + n.clamp min, max end end class Fixnum include ValueType - alias_method(:original_eq_eq, :==) unless Fixnum.instance_methods.include?(:original_eq_eq) - - def - other - return nil unless other - check_numeric! :-, other - super - end + alias_method :__original_eq_eq__, :== unless Fixnum.instance_methods.include? :__original_eq_eq__ + alias_method :__original_add__, :+ unless Fixnum.instance_methods.include? :__original_add__ + alias_method :__original_subtract__, :- unless Fixnum.instance_methods.include? :__original_subtract__ + alias_method :__original_multiply__, :* unless Fixnum.instance_methods.include? :__original_multiply__ + alias_method :__original_divide__, :- unless Fixnum.instance_methods.include? :__original_divide__ # Returns `true` if the numeric value is evenly divisible by 2. # @@ -28301,28 +41785,36 @@ class Fixnum def + other return nil unless other - check_numeric! :+, other super + rescue Exception => e + __raise_arithmetic_exception__ other, :+, e end def * other return nil unless other - check_numeric! :*, other super + rescue Exception => e + __raise_arithmetic_exception__ other, :*, e end def / other return nil unless other - check_numeric! :/, other super + rescue Exception => e + __raise_arithmetic_exception__ other, :/, e + end + + def - other + return nil unless other + super + rescue Exception => e + __raise_arithmetic_exception__ other, :-, e end def == other - return true if self.original_eq_eq(other) - if other.is_a?(GTK::OpenEntity) - return self.original_eq_eq(other.entity_id) - end - return self.original_eq_eq(other) + return true if __original_eq_eq__ other + return __original_eq_eq__ other.entity_id if other.is_a? GTK::OpenEntity + return false end # Returns `-1` if the number is less than `0`. `+1` if the number @@ -28362,33 +41854,50 @@ class Fixnum def sin Math.sin(self.to_radians) end + + def to_sf + "%.2f" % self + end + + def ifloor int + (self.idiv int.to_i) * int.to_i + end end class Float include ValueType + alias_method :__original_add__, :+ unless Float.instance_methods.include? :__original_add__ + alias_method :__original_subtract__, :- unless Float.instance_methods.include? :__original_subtract__ + alias_method :__original_multiply__, :* unless Float.instance_methods.include? :__original_multiply__ + alias_method :__original_divide__, :- unless Float.instance_methods.include? :__original_divide__ + def - other return nil unless other - check_numeric! :-, other super + rescue Exception => e + __raise_arithmetic_exception__ other, :-, e end def + other return nil unless other - check_numeric! :+, other super + rescue Exception => e + __raise_arithmetic_exception__ other, :+, e end def * other return nil unless other - check_numeric! :*, other super + rescue Exception => e + __raise_arithmetic_exception__ other, :*, e end def / other return nil unless other - check_numeric! :/, other super + rescue Exception => e + __raise_arithmetic_exception__ other, :/, e end def serialize @@ -28408,19 +41917,372 @@ class Float return -scalar if self < 0 return scalar if self > 0 end + + def to_sf + "%.2f" % self + end + + def ifloor int + (self.idiv int.to_i) * int.to_i + end end class Integer - alias_method(:original_round, :round) unless Fixnum.instance_methods.include?(:original_round) + alias_method :__original_round__, :round unless Integer.instance_methods.include? :__original_round__ + alias_method :__original_add__, :+ unless Integer.instance_methods.include? :__original_add__ + alias_method :__original_subtract__, :- unless Integer.instance_methods.include? :__original_subtract__ + alias_method :__original_multiply__, :* unless Integer.instance_methods.include? :__original_multiply__ + alias_method :__original_divide__, :- unless Integer.instance_methods.include? :__original_divide__ def round *args - original_round + __original_round__ + end + + def nan? + false + end +end + +</code></pre> +<h3 id='----remote_hotload_client.rb'>remote_hotload_client.rb</h3> +<pre><code class="language-ruby"># ./dragon/remote_hotload_client.rb +# coding: utf-8 +# Copyright 2019 DragonRuby LLC +# MIT License +# hotload_client.rb has been released under MIT (*only this file*). + +module GTK + class RemoteHotloadClient + attr :args + + def gtk + args.gtk + end + + def state + local_state + end + + def initialize local_ip_address + local_state.local_ip_address = local_ip_address + end + + def tick + return unless server_available? + return unless server_needed? + + if should_tick? && server_needed? && !local_state.notified + if server_available? + remote_log "* REMOTE CLIENT INFO: Hotload server found at #{get_server_ip_address}:9001." + end + local_state.notified = true + end + + tick_process_file_retrieval + tick_process_queue + tick_changes + tick_http_boot + end + + def should_tick? + (game_state.tick_count.mod_zero? 60) && game_state.tick_count > 5.seconds + end + + def game_state + args.state + end + + def local_state + @local_state ||= OpenEntity.new + @local_state.hotload_client ||= @local_state.new_entity(:hotload_client, + notes: "This enitity is used by DragonRuby Game Toolkit to provide you hotloading on remote machines.", + changes: { }, + changes_queue: [], + reloaded_files_times: []) + @local_state.hotload_client + end + + def remote_log message + log message + args.gtk.http_post "http://#{get_server_ip_address}:9001/dragon/log/", { message: "=======\n#{message}\n=======\n" }, ["Content-Type: application/x-www-form-urlencoded"] + end + + def get_server_ip_address + return local_state.ip_address if local_state.ip_address + local_state.ip_address ||= ((gtk.read_file 'app/server_ip_address.txt') || "").strip + local_state.ip_address + end + + def server_available? + return false if gtk.platform == 'Emscripten' + get_server_ip_address.length != 0 + end + + def server_needed? + return false if gtk.platform == 'Emscripten' + local_state.local_ip_address != get_server_ip_address + end + + def tick_changes + return unless should_tick? + + local_state.greatest_tick ||= 0 + local_state.last_greatest_tick ||= 0 + + tick_http_changes + end + + def tick_http_boot + return if local_state.booted_at + + + if !local_state.http_boot + # first retrieve changes.txt which has the following format + # file with latest change, + # latest file update_time key + # tmp/src_backup/src_backup_app_main.rb, 1597926596, app/main.rb + local_state.http_boot = args.gtk.http_get "http://#{get_server_ip_address}:9001/dragon/boot/" + elsif local_state.http_boot && local_state.http_boot[:http_response_code] == 200 + local_state.last_greatest_tick = local_state.http_boot[:response_data].strip.to_i + local_state.greatest_tick = local_state.http_boot[:response_data].strip.to_i + local_state.booted_at = local_state.greatest_tick + remote_log '* REMOTE CLIENT INFO: HTTP GET for local_state. boot.txt succeeded.' + remote_log "* REMOTE CLIENT INFO: Looking for changes after: #{local_state.greatest_tick}." + elsif local_state.http_boot && local_state.http_boot[:http_response_code] == -1 && local_state.http_boot[:complete] + remote_log '* REMOTE CLIENT INFO: HTTP GET for boot.txt failed. Retrying.' + local_state.http_boot = nil + end + end + + def tick_http_changes + return unless local_state.booted_at + + if !local_state.http_changes + local_state.http_changes = args.gtk.http_get "http://#{get_server_ip_address}:9001/dragon/changes/" + end + + if local_state.http_changes && local_state.http_changes[:http_response_code] == 200 && local_state.booted_at + local_state.last_greatest_tick = local_state.greatest_tick + # if the retrieval of changes.txt was successful + local_state.http_changes[:response_data].each_line do |l| + if l.strip.length != 0 + # within reload state for that specific changes hash + # set the last time the file was updated + file_name, updated_at, key = l.strip.split(',') + file_name.strip! + updated_at.strip! + key.strip! + updated_at = updated_at.to_i + file_name = file_name.gsub("tmp/src_backup/", "") + + # keep an internal clock that denotes that current time on the + # dev machine + if updated_at > local_state.greatest_tick + local_state.greatest_tick = updated_at + + # create a new entry in change tracking for the file + # for every file where the file was last updated, find all the ones where the time is not the same + # and queue those to be retrieved and required + # if the last time the dev machine time was retrieved is less than the file time changed + # then queue the file for reload + current_updated_at = (local_state.changes[key] || { updated_at: 0 })[:updated_at] + if updated_at > current_updated_at + remote_log "* REMOTE CLIENT INFO: Queueing file #{file_name} for update." + local_state.changes[key] = { key: key, + latest_file: file_name, + updated_at: updated_at } + local_state.changes_queue << local_state.changes[key] + end + end + end + end + + # set the greatest tick/current time of the machine + local_state.http_changes = nil + elsif local_state.http_changes && local_state.http_changes[:http_response_code] == -1 && local_state.http_change[:complete] && local_state.booted_at + local_state.http_changes = nil + end + end + + def tick_process_queue + return if local_state.http_file_changes # don't pop a file off the queue if there is an http request in flight + return if local_state.processing_file_changes # don't pop a file if there is a file currently being processed + return unless local_state.changes_queue.length > 0 # return if the queue is empty + + # if it isn't empty, pop the first file off the queue (FIFO) + local_state.processing_file_changes = local_state.changes_queue.shift + + # create an http request for the file + url = "http://#{get_server_ip_address}:9001/dragon/#{local_state.processing_file_changes[:latest_file]}" + remote_log "* REMOTE CLIENT INFO: Getting new version of #{local_state.processing_file_changes[:latest_file]} (#{url})." + local_state.http_file_changes = args.gtk.http_get url + end + + def tick_process_file_retrieval + return if !local_state.http_file_changes + + # if the http request has finished successfully + if local_state.http_file_changes[:http_response_code] == 200 + file_key = local_state.processing_file_changes[:key] + # notify that a file will be reloaded + remote_log "* REMOTE CLIENT INFO: Loading #{file_key}: #{local_state.processing_file_changes[:latest_file]}" + remote_log "#{local_state.http_file_changes[:response_data]}" + + # write the latest file with what came back from the response data + gtk.write_file "#{file_key}", local_state.http_file_changes[:response_data] + + # nil out the currently processing file so a new item can be processed from the queue + # local_state.reloaded_files_times << local_state.processing_file_changes[:key] + local_state.http_file_changes = nil + local_state.processing_file_changes = nil + end + end end end -</pre> -<h1 id='--runtime/draw.rb'>runtime/draw.rb</h1> -<pre># ./dragon/runtime/draw.rb +</code></pre> +<h3 id='----runtime/autocomplete.rb'>runtime/autocomplete.rb</h3> +<pre><code class="language-ruby"># ./dragon/runtime/autocomplete.rb +# Copyright 2019 DragonRuby LLC +# MIT License +# autocomplete.rb has been released under MIT (*only this file*). + +module GTK + class Runtime + module Autocomplete + def autocomplete_parse opts + if opts[:file] && !opts[:text] + opts[:text] = read_file opts[:file] + end + + text = opts[:text] + index = opts[:index] + sum = 0 + lines = text.each_line.to_a.map do |l| + sum += l.length + { line: l, length: l.length, sum: sum } + end + cursor_line = lines.find { |l| l[:sum] >= index } + previous_line = lines.find { |l| l[:sum] < index } + previous_line ||= { sum: 0 } + if cursor_line + sub_index = index - previous_line[:sum] + word = (cursor_line[:line][0..sub_index - 1]).strip + token = (word.split " ")[-1] + dots = (token.split ".") + dot = dots[-1] + end + + { + text: opts[:text], + file: opts[:file], + index: opts[:index], + cursor_line: cursor_line, + previous_line: previous_line, + word: word, + token: token, + dots: dots, + dot: dot + } + end + + def autocomplete_filter_methods keys, *ignores + ignores ||= [] + ignores = [ignores].flatten + keys = keys.map { |k| k.to_s } + others = ["def", "end"] + + [ :entity_keys_by_ref, + :entity_name, + :as_hash, + :clear!, + :created_at_elapsed, + :entity_id, + "entity_id=", + "tick_count=", + :global_created_at_elapsed, + :load_entity_data!, + :meta, + :meta!, + :new?, + :old?, + :__original_eq_eq__, :set!, + :update_entity_keys_by_ref, + :with_meta] + + ignores + keys.find_all { |k| k.to_s.to_i.to_s == k.to_s } + + final = (keys - (others.map { |m| m.to_s })).uniq + final + end + + def suggest_autocompletion opts + parse_result = autocomplete_parse opts + return [] unless parse_result[:cursor_line] + text = parse_result[:text] + word = parse_result[:word] + token = parse_result[:token] + dots = parse_result[:dots] + dot = parse_result[:dot] + + return [] if word.strip.start_with? "#" + + if word[-1] == "." && token + lookup = { + 'args' => lambda { $gtk.args.autocomplete_methods }, + 'inputs' => lambda { $gtk.args.inputs.autocomplete_methods }, + 'geometry' => lambda { $gtk.args.geometry.autocomplete_methods }, + 'outputs' => lambda { $gtk.args.outputs.autocomplete_methods }, + 'layout' => lambda { $gtk.args.layouts.autocomplete_methods }, + 'keyboard' => lambda { $gtk.args.keyboard.autocomplete_methods }, + 'key_down' => lambda { $gtk.args.keyboard.key_down.autocomplete_methods }, + 'key_up' => lambda { $gtk.args.keyboard.key_up.autocomplete_methods }, + 'state' => lambda { $gtk.args.state.autocomplete_methods }, + 'fn' => lambda { $gtk.args.fn.autocomplete_methods }, + '$gtk' => lambda { $gtk.autocomplete_methods }, + 'gtk' => lambda { $gtk.autocomplete_methods }, + 'mouse' => lambda { $gtk.args.inputs.mouse.autocomplete_methods }, + 'click' => lambda { [:x, :y, :point] } + } + + lookup_result = lookup[dot] + + return autocomplete_filter_methods lookup_result.call if lookup_result + + start_collecting = false + dots_after_state = dots.find_all do |s| + if s == "state" + start_collecting = true + false + else + start_collecting + end + end + + target = $gtk.args.state + dots_after_state.each do |k| + target = target.as_hash[k.to_sym] if target.respond_to? :as_hash + end + + return autocomplete_filter_methods target.as_hash.keys + end + + + text.gsub!("[", " ") + text.gsub!("]", " ") + text.gsub!("(", " ") + text.gsub!(")", " ") + text.gsub!(":", "") + text.gsub!(".", " ") + text.gsub!("=", " ") + return (autocomplete_filter_methods (text.split " "), + :gtk, :false, :true, :args, :suppress_mailbox, :end) + end + end # end Autocomplete + end # end Runtime +end # end GTK + +</code></pre> +<h3 id='----runtime/draw.rb'>runtime/draw.rb</h3> +<pre><code class="language-ruby"># ./dragon/runtime/draw.rb # Copyright 2019 DragonRuby LLC # MIT License # draw.rb has been released under MIT (*only this file*). @@ -28439,115 +42301,131 @@ module GTK # pass.solids.each { |s| draw_solid s } # while loops are faster than each with block idx = 0 + length = pass.solids.length while idx < pass.solids.length - draw_solid (pass.solids.value idx) # accessing an array using .value instead of [] is faster + draw_solid (pass.solids.at idx) # accessing an array using .value instead of [] is faster idx += 1 end # pass.static_solids.each { |s| draw_solid s } idx = 0 - while idx < pass.static_solids.length - draw_solid (pass.static_solids.value idx) + length = pass.static_solids.length + while idx < length + draw_solid (pass.static_solids.at idx) idx += 1 end # pass.sprites.each { |s| draw_sprite s } idx = 0 - while idx < pass.sprites.length - draw_sprite (pass.sprites.value idx) + length = pass.sprites.length + while idx < length + draw_sprite (pass.sprites.at idx) idx += 1 end # pass.static_sprites.each { |s| draw_sprite s } idx = 0 - while idx < pass.static_sprites.length - draw_sprite (pass.static_sprites.value idx) + length = pass.static_sprites.length + while idx < length + draw_sprite (pass.static_sprites.at idx) idx += 1 end # pass.primitives.each { |p| draw_primitive p } idx = 0 - while idx < pass.primitives.length - draw_primitive (pass.primitives.value idx) + length = pass.primitives.length + while idx < length + draw_primitive (pass.primitives.at idx) idx += 1 end # pass.static_primitives.each { |p| draw_primitive p } idx = 0 - while idx < pass.static_primitives.length - draw_primitive (pass.static_primitives.value idx) + length = pass.static_primitives.length + while idx < length + draw_primitive (pass.static_primitives.at idx) idx += 1 end # pass.labels.each { |l| draw_label l } idx = 0 - while idx < pass.labels.length - draw_label (pass.labels.value idx) + length = pass.labels.length + while idx < length + draw_label (pass.labels.at idx) idx += 1 end # pass.static_labels.each { |l| draw_label l } idx = 0 - while idx < pass.static_labels.length - draw_label (pass.static_labels.value idx) + length = pass.static_labels.length + while idx < length + draw_label (pass.static_labels.at idx) idx += 1 end # pass.lines.each { |l| draw_line l } idx = 0 - while idx < pass.lines.length - draw_line (pass.lines.value idx) + length = pass.lines.length + while idx < length + draw_line (pass.lines.at idx) idx += 1 end # pass.static_lines.each { |l| draw_line l } idx = 0 + length = pass.static_lines.length while idx < pass.static_lines.length - draw_line (pass.static_lines.value idx) + draw_line (pass.static_lines.at idx) idx += 1 end # pass.borders.each { |b| draw_border b } idx = 0 - while idx < pass.borders.length - draw_border (pass.borders.value idx) + length = pass.borders.length + while idx < length + draw_border (pass.borders.at idx) idx += 1 end # pass.static_borders.each { |b| draw_border b } idx = 0 - while idx < pass.static_borders.length - draw_border (pass.static_borders.value idx) + length = pass.static_borders.length + while idx < length + draw_border (pass.static_borders.at idx) idx += 1 end if !$gtk.production # pass.debug.each { |r| draw_primitive r } idx = 0 - while idx < pass.debug.length - draw_primitive (pass.debug.value idx) + length = pass.debug.length + while idx < length + draw_primitive (pass.debug.at idx) idx += 1 end # pass.static_debug.each { |r| draw_primitive r } idx = 0 - while idx < pass.static_debug.length - draw_primitive (pass.static_debug.value idx) + length = pass.static_debug.length + while idx < length + draw_primitive (pass.static_debug.at idx) idx += 1 end end # pass.reserved.each { |r| draw_primitive r } idx = 0 - while idx < pass.reserved.length - draw_primitive (pass.reserved.value idx) + length = pass.reserved.length + while idx < length + draw_primitive (pass.reserved.at idx) idx += 1 end # pass.static_reserved.each { |r| draw_primitive r } idx = 0 - while idx < pass.static_reserved.length - draw_primitive (pass.static_reserved.value idx) + length = pass.static_reserved.length + while idx < length + draw_primitive (pass.static_reserved.at idx) idx += 1 end rescue Exception => e @@ -28657,16 +42535,102 @@ module GTK end end -</pre> -<h1 id='--runtime/framerate_diagnostics.rb'>runtime/framerate_diagnostics.rb</h1> -<pre># ./dragon/runtime/framerate_diagnostics.rb +</code></pre> +<h3 id='----runtime/framerate.rb'>runtime/framerate.rb</h3> +<pre><code class="language-ruby"># ./dragon/runtime/framerate.rb +# Copyright 2019 DragonRuby LLC +# MIT License +# framerate.rb has been released under MIT (*only this file*). + +module GTK + class Runtime + module Framerate + def framerate_init + @tick_time = Time.new.to_i + end + + def delta_framerate + (current_framerate || 0) - (@previous_framerate || 0) + end + + def reset_framerate_calculation + @tick_speed_sum = 0 + @tick_speed_count = 0 + @previous_framerate = 0 + end + + def check_framerate + if @framerate_diagnostics_requested + log "================================" + log framerate_get_diagnostics + @framerate_diagnostics_requested = false + end + + if !@paused + if @tick_time + @tick_speed_count += 1 + @tick_speed_sum += Time.now.to_i - @tick_time + if @tick_speed_count > 60 * 2 + if framerate_below_threshold? + @last_framerate = current_framerate + if [email protected]? + if !@framerate_important_notification_happened + log_important framerate_warning_message + else + log framerate_warning_message + end + @framerate_important_notification_happened = true + end + end + + @previous_framerate = current_framerate.floor + end + end + + @tick_time = Time.new.to_i + else + reset_framerate_calculation + end + rescue + reset_framerate_calculation + end + + def framerate_diagnostics + # request framerate diagnostics to be printed at the end of tick + @framerate_diagnostics_requested = true + end + + def framerate_below_threshold? + @last_framerate ||= 60 + current_framerate < @last_framerate && + current_framerate < 50 && + @previous_framerate > current_framerate && + Kernel.tick_count > 600 + end + + def current_framerate + return 60 if !@tick_speed_sum || !@tick_speed_sum + r = 100.fdiv(@tick_speed_sum.fdiv(@tick_speed_count) * 100) + if (r.nan? || r.infinite? || r > 58) + r = 60 + end + r || 60 + rescue + 60 + end + end # module Framerate + end # end class Runtime +end # end module GTK + +</code></pre> +<h3 id='----runtime/framerate_diagnostics.rb'>runtime/framerate_diagnostics.rb</h3> +<pre><code class="language-ruby"># ./dragon/runtime/framerate_diagnostics.rb # Copyright 2019 DragonRuby LLC # MIT License # framerate_diagnostics.rb has been released under MIT (*only this file*). module GTK class Runtime - # @visibility private module FramerateDiagnostics def framerate_get_diagnostics <<-S @@ -28831,9 +42795,171 @@ If this warning is getting annoying put the following in your tick method: end end -</pre> -<h1 id='--string.rb'>string.rb</h1> -<pre># ./dragon/string.rb +</code></pre> +<h3 id='----runtime/hotload.rb'>runtime/hotload.rb</h3> +<pre><code class="language-ruby"># ./dragon/runtime/hotload.rb +# coding: utf-8 +# Copyright 2019 DragonRuby LLC +# MIT License +# hotlaod.rb has been released under MIT (*only this file*). + +module GTK + class Runtime + # @visibility private + module Hotload + def hotload_init + @hotload_if_needed = 0 + @mailbox_if_needed = 0 + @file_mtimes = { } + @suppress_mailbox = true + files_to_reload.each { |f| init_mtimes f } + init_mtimes 'app/mailbox.rb' + end + + def hotload_on_write_file file_name + return unless file_name.include? 'mailbox.rb' + @mailbox_if_needed = :force + end + + def files_to_reload + if @rcb_release_mode + core_files_to_reload + @required_files + else + [ + 'dragon/docs.rb', + 'dragon/help.rb', + 'dragon/kernel_docs.rb', + 'dragon/kernel.rb', + 'dragon/easing.rb', + 'dragon/top_level.rb', + 'dragon/log.rb', + 'dragon/geometry.rb', + 'dragon/attr_gtk.rb', + 'dragon/attr_sprite.rb', + 'dragon/object.rb', + 'dragon/class.rb', + 'dragon/string.rb', + 'dragon/entity.rb', + 'dragon/strict_entity.rb', + 'dragon/open_entity.rb', + 'dragon/serialize.rb', + 'dragon/primitive.rb', + 'dragon/nil_class_false_class.rb', + 'dragon/symbol.rb', + 'dragon/numeric_deprecated.rb', + 'dragon/numeric.rb', + 'dragon/hash.rb', + 'dragon/outputs_deprecated.rb', + 'dragon/array_docs.rb', + 'dragon/array.rb', + 'dragon/outputs.rb', + 'dragon/inputs.rb', + 'dragon/mouse_docs.rb', + 'dragon/recording.rb', + 'dragon/grid.rb', + 'dragon/layout.rb', + 'dragon/args_deprecated.rb', + 'dragon/fn.rb', + 'dragon/args.rb', + 'dragon/console_prompt.rb', + 'dragon/console_menu.rb', + 'dragon/console.rb', + 'dragon/assert.rb', + 'dragon/tests.rb', + 'dragon/controller_config.rb', + 'dragon/runtime/draw.rb', + 'dragon/runtime/deprecated.rb', + 'dragon/runtime/framerate.rb', + 'dragon/runtime/c_bridge.rb', + 'dragon/runtime/hotload.rb', + 'dragon/runtime/backup.rb', + 'dragon/runtime/async_require.rb', + 'dragon/runtime/autocomplete.rb', + 'dragon/api.rb', + 'dragon/runtime.rb', + 'dragon/trace.rb', + 'dragon/readme_docs.rb', + 'dragon/hotload_client.rb', + 'dragon/ios_wizard.rb', + 'dragon/itch_wizard.rb', + ] + core_files_to_reload + @required_files + end + end + + def core_files_to_reload + [ + 'repl.rb', + 'app/main.rb', + 'app/repl.rb', + 'app/tests.rb', + 'app/test.rb', + 'app/stdin.rb' + ] + end + + def init_mtimes file + current_key = "current_#{file}".to_sym + last_key = "last_#{file}".to_sym + @file_mtimes[current_key] ||= @ffi_file.mtime(file) + @file_mtimes[last_key] ||= @ffi_file.mtime(file) + end + + def hotload_source_files + @hotload_if_needed += 1 + return unless @hotload_if_needed == 60 + @hotload_if_needed = 0 + files_to_reload.each { |f| reload_if_needed f } + console.enable + end + + def mailbox_timeout + if @suppress_mailbox + 60 + else + 3 + end + end + + def check_mailbox + if @mailbox_if_needed == :force # lol + reload_if_needed 'app/mailbox.rb', true + @mailbox_if_needed = 1 + return + end + @mailbox_if_needed += 1 + return unless @mailbox_if_needed.mod_zero? mailbox_timeout + @mailbox_if_needed = 1 + reload_if_needed 'app/mailbox.rb' + end + + def hotload_if_needed + hotload_source_files + check_mailbox + end + + def on_load_succeeded file + @rcb_sender.files_reloaded << file + @rcb_sender.reloaded_files << file + Trace.untrace_classes! + end + + def reload_if_needed file, force = false + current_key = "current_#{file}".to_sym + last_key = "last_#{file}".to_sym + @file_mtimes[current_key] ||= nil + @file_mtimes[last_key] ||= nil + @file_mtimes[current_key] = @ffi_file.mtime(file) + return if !force && @file_mtimes[last_key] == @file_mtimes[current_key] + on_load_succeeded file if reload_ruby_file file + @file_mtimes[last_key] = @file_mtimes[current_key] + end + end + end +end + +</code></pre> +<h3 id='----string.rb'>string.rb</h3> +<pre><code class="language-ruby"># ./dragon/string.rb # coding: utf-8 # Copyright 2019 DragonRuby LLC # MIT License @@ -28842,7 +42968,7 @@ end class String include ValueType - def wrapped_lines_recur word, rest, length, aggregate + def self.wrapped_lines_recur word, rest, length, aggregate if word.nil? return aggregate elsif rest[0].nil? @@ -28874,9 +43000,8 @@ S self[0..-2] end - # @gtk - def wrapped_lines length - self.each_line.map do |l| + def self.wrapped_lines string, length + string.each_line.map do |l| l = l.rstrip if l.length < length l + "\n" @@ -28887,6 +43012,10 @@ S end.flatten end + def wrapped_lines length + String.wrapped_lines self, length + end + # @gtk def wrap length wrapped_lines(length).join.rstrip @@ -28940,9 +43069,9 @@ S end end -</pre> -<h1 id='--tests.rb'>tests.rb</h1> -<pre># ./dragon/tests.rb +</code></pre> +<h3 id='----tests.rb'>tests.rb</h3> +<pre><code class="language-ruby"># ./dragon/tests.rb # coding: utf-8 # Copyright 2019 DragonRuby LLC # MIT License @@ -29083,9 +43212,9 @@ S end end -</pre> -<h1 id='--trace.rb'>trace.rb</h1> -<pre># ./dragon/trace.rb +</code></pre> +<h3 id='----trace.rb'>trace.rb</h3> +<pre><code class="language-ruby"># ./dragon/trace.rb # coding: utf-8 # Copyright 2019 DragonRuby LLC # MIT License @@ -29271,9 +43400,9 @@ module GTK end end -</pre> -<h1 id='--wizards.rb'>wizards.rb</h1> -<pre># ./dragon/wizards.rb +</code></pre> +<h3 id='----wizards.rb'>wizards.rb</h3> +<pre><code class="language-ruby"># ./dragon/wizards.rb # Copyright 2019 DragonRuby LLC # MIT License # wizards.rb has been released under MIT (*only this file*). @@ -29289,7 +43418,7 @@ module GTK end end -</pre> +</code></pre> </div> </body> </html> |
