Compare commits

226 Commits

Author SHA1 Message Date
5d1c195917 Added support for rendering multiple lights, standardized shaders to use snake case for variables and camel case for functions, stubbed PBR material shader include. 2023-07-29 14:09:23 -05:00
9a6e1df032 Remove need to do a full gui recalc 3 or more times for the layout to work (current implementation slows things down a bit, but seems more reliable then brute forcing 3x+) 2023-06-18 19:42:38 -05:00
b1b8fc8556 Cache scroll width/height 2023-06-18 14:18:15 -05:00
a60b09a110 Further scrolling improvements (should be smoother/more consistent with varied frame time) 2023-06-18 13:50:32 -05:00
81a632942e Round scroll position to prevent rendering issues caused by floats 2023-06-18 12:38:12 -05:00
5ef8023aca Improved scrolling 2023-06-18 12:26:08 -05:00
2e690d7d33 Misc bug fixes and improvements 2023-06-16 14:40:16 -05:00
a9f9e20235 Remove named arguments from stats struct due to mruby lacking support 2023-04-28 22:29:53 -05:00
25c36d3788 Add CyberarmEngine::Stats::StatsPlotter for rendering frame timings graph along with labeled sub-timings 2023-04-20 21:54:44 -05:00
c26ddeef4d Refactored CyberarmEngine::Stats to track data for last N frames 2023-04-20 16:08:59 -05:00
5e3e06b74e Improve Vector arithmetic performance by 2x 2023-04-16 20:26:29 -05:00
72037efc73 Fixed Slider element not working properly under mruby (attempts to slide would jump between min/max values) 2023-03-27 17:13:51 -05:00
1462f89e24 Initial support for using cyberarm_engine as a mruby mrbgem: Remove/disable usages of defined?, update old gosu mouse and keyboard constants, and replace one usage of window.button_down? with proper Gosu.button_down? 2023-03-24 18:21:41 -05:00
98948c891a Request repaint when hiding menu 2023-03-17 11:05:17 -05:00
da16ab1ec9 Bump version 2023-02-01 15:48:29 -06:00
eb5d170733 Added #find_element_by_tag to Common module 2023-01-31 14:33:17 -06:00
14e9d4946f Add GameState#needs_repaint? and Container#remove 2023-01-31 10:17:51 -06:00
e3b8a9b102 Fixed overdrawing on BorderCanvas for left side, fixed BackgroundImage fill mode not filling correctly, fixed Progress in marquee mode not request repaint each frame. 2023-01-11 15:05:58 -06:00
458731a534 Added shaders from i-mic-fps, preload shaders if cyberarm_engine/opengl has been required. 2023-01-08 17:30:37 -06:00
d1d87db070 Made GuiState#update safe to call when it is not the active state 2023-01-08 03:36:33 -06:00
6e8948bd81 Repaint when Slider value changes 2023-01-06 16:14:18 -06:00
01a9187a57 Request a repaint when popping and shifting states 2023-01-05 08:36:38 -06:00
186ad220cc Added more triggers for repainting 2023-01-04 20:17:49 -06:00
f82c0953b2 Implemented support for dynamic repainting of gui states with GuiState#needs_repaint? 2023-01-03 22:16:07 -06:00
a2d44ea2dc Fix Element#scroll_height not accounting for padding_top and border_thickness_top 2023-01-02 16:45:43 -06:00
c46664778a Fixed Image element causing clipping issues to sibling elements due to not properly flooring @width/@height 2022-11-16 20:31:00 -06:00
c597a67ca6 Allow 1 pixel wide/tall Gui elements by only apply percentage based sizing on Floats, fixed max/min sizing not working if the element uses fill mode for either dimension 2022-10-30 11:57:20 -05:00
55382a7c14 Bump version 2022-10-23 18:40:46 -05:00
883de3db9f Added support for TextBlock's to have text_v_align to compliment text_h_align, EditLine and EditBox will now preserve their focus appear, fixed crash in ToggleButton due to using :Label instead of :TextBlock, misc. tweaks. 2022-10-23 18:38:51 -05:00
41c0b27937 Fixed layout bug for Flow when the element x + width is greater than the parent's width it would not correctly wrap 2022-10-20 09:36:20 -05:00
2fd5d398cf Fix crash when EditLine receives a paste from clipboard 2022-10-07 20:32:18 -05:00
2e66509f87 Removed use of the clipboard gem since Gosu now supports this natively! Added :static option for Text to render text using Gosu::Image.from_text/markup which results in nicer looking text, improvements to EditLine to display the caret in the correct place (doesn't position properly if :text_static is true) 2022-10-04 10:16:32 -05:00
521b3937dd Fixed Slider element's Handle not styling or positioning properly 2022-07-26 12:20:13 -05:00
ab9f9e8e7a Refactored dimentional_size to split out space_available_width/height, initial implementation of verticial and horizontal container alignment 2022-06-12 16:03:23 -05:00
705138f7ad Fixed elements in a Flow container not positioned correctly after the first wrap 2022-06-12 11:36:22 -05:00
94a65f447c Fixed long standing issue with ListBox menu not taking it's parents width only the parent has a specified width and fixed clicking on the menu's host element to hide the menu causes it to hide and reappear instantly. 2022-06-12 11:25:29 -05:00
97296b080c Bump version 2022-06-05 09:20:17 -05:00
ca73a2d8e8 Added all the callbacks to Window and GameState, removed all but one usage of global variables (). 2022-06-05 09:19:30 -05:00
0a62e5180a Add down arrow for ListBox 2022-05-03 20:01:34 -05:00
31e909eb30 Probably fix layout issues caused by floating point errors where an element is wrapped when it has space (e.g. 2 elements with a width 0.5 not fitting properly) 2022-05-02 19:08:47 -05:00
300e7c4e59 Implemented tiled mode for BackgroundImage and other improvements 2022-05-02 19:06:40 -05:00
be98fe47ad Fixed ListBox element menu getting cut off if it's near the bottom of the screen by stacking it ontop of the host element 2022-05-02 19:05:57 -05:00
a75afaf47a Added support for UI to have background_images, fixed TextBlock text overdrawing 2022-04-25 20:12:10 -05:00
d81df5f4e2 Moved min_width/height, max_width/height and fill control from Container into Element#dimensional_size so any element can use them 2022-04-25 16:48:17 -05:00
f2ea0d9942 Maybe final container fill mode fix? 2022-04-04 13:26:40 -05:00
08d068f156 Fixed weirdness for Container dynamic fill mode 2022-04-04 10:08:39 -05:00
a4d02038c3 Fixes for Container's calculating fill size and for Flow's max_width 2022-04-03 13:06:10 -05:00
37bdd6ef23 Added support for min/max width/height and for fill which dynamically sets elements width/height to the max available width for Flow parents and height for Stack parents 2022-04-03 11:07:31 -05:00
24bd769a32 Bump version 2022-03-05 14:43:50 -06:00
2be5733bc1 Fixed Container not considering padding when clipping render area 2022-01-29 10:08:33 -06:00
c8734ae98b Container child elements that are not visible are no longer drawn 2021-12-24 17:00:02 -06:00
c35d587419 Fixed passing nil for :enabled state as boolean causing it to be set as true instead of false 2021-12-22 14:58:17 -06:00
153871e7f3 Improved performance a touch: fixed never clearing list of elements to recalculate unless a full gui recalcuate occurred, don't recalculate button/text if a full gui recalculate is requested, fixed typo: apend -> append 2021-12-16 22:09:56 -06:00
cf91d4a083 Merge branch 'master' of https://github.com/cyberarm/cyberarm_engine 2021-12-03 15:33:38 -06:00
35ad687d4c Reducce cpu usage by not using clip_to for each and every element 2021-12-03 15:33:32 -06:00
54802e1254 Added support 'marquee' style progress bars i.e. non-linear progress bar with scrolling bar 2021-12-02 08:15:39 -06:00
6cf4cd73dd Fixed toggling Element enabled state not visually shown 2021-11-18 15:37:57 -06:00
5e5f8ba7ea Added set_color and set_font to Element- fixes image elements unable to change their color when hovered, fixes hovered text changing its color incorrectly 2021-11-18 13:11:02 -06:00
63a51d9d2f Improved scroll_height calculation to adapt to how Flow's flow 2021-11-18 11:58:11 -06:00
6af384536a Fixed locking up when performing text wrapping due to text width check not being satisfied 2021-11-17 22:30:30 -06:00
c1b25d2045 Fixed centered text with unequal margins/paddings/border thicknesses being offset, fixed text wrapping using parents #width (which includes padding and margins) instead of the correct #content_width (which is only the parents content width; which the text is part of) 2021-11-17 17:23:48 -06:00
a915a25699 Updated required gems, reimplemented text wrapping to use a proper binary search and correct inserting newlines in the wrong spot for certain text lengths 2021-11-16 23:35:49 -06:00
0aa9b59316 Containers that need to be recalculated but will not affect their current size can request to have themselves directly recalculated on the next update (fixes scrolling triggering many unneed recalculations), list_box now excludes the currently selected item from the menu, probably fixed list_box callback being called twice instead of the correct once. 2021-11-15 10:17:49 -06:00
d2bf406d29 Bump version 2021-09-23 14:36:19 -05:00
f82c101728 Fixed ListBox sometimes returning self in callback instead of self.value 2021-09-23 14:35:46 -05:00
2f727e9bf2 Bump version 2021-09-22 09:23:24 -05:00
24be9bfb29 Imported Console from I-MIC FPS 2021-06-26 13:12:44 +00:00
5452d149c3 Possible fix for being able to click a button that isn't visible due to scrolling, by ensuring that the host container is hit before checking children. 2021-06-18 02:57:49 +00:00
cdee6548e3 Added support for 9 sliced backgrounds in UI, fixed events not propagating to subscribers if element threw :handled in its own event handler 2021-06-04 03:00:31 +00:00
bd54fafc3f Added Text#text_width method, Text#width now returns value of Text#markup_width 2021-06-03 13:05:11 +00:00
a92d1ad746 Made Text account for shadow and border effects in width/height methods, made TextBlock support disabled state styling 2021-06-03 01:00:13 +00:00
1b080f9fb9 Use shorthand (&:method) for Element scroll_width/height max_scroll_width, possibly fix dimentional_size returning the wrong size when element has nonzero padding/margin/border_thickness 2021-05-31 01:07:24 +00:00
8057bca818 Added rendered vertices count to OpenGLRenderer 2021-05-30 14:10:32 +00:00
c1310f3073 Renamed Text's shadow to border and added proper text shadow effect 2021-05-29 00:10:44 +00:00
850bb610bb Fixed GUI having a total recalculation whenever a style changed, made tooltip not be centered on mouse, removed duplicate loc from Text 2021-05-28 00:43:40 +00:00
0b63986b64 Improved 9 slice background 2021-05-06 03:20:49 +00:00
165fc6f307 Fixed draw_rect helper missing drawing mode argument, set max view distance to a large number 2021-04-25 23:22:58 +00:00
e3c8e3bcc2 Added a bunch of tweens to the Animator, replaced :ease_in_out tween for Intro state engine logo to :swing_to 2021-04-25 14:58:12 +00:00
551a55f894 Add fade out to intro scene 2021-04-24 20:13:06 +00:00
01292ead10 Bump version 2021-04-19 19:26:42 +00:00
7ca6e1bc58 Update README 2021-04-19 19:22:43 +00:00
a70f260bc6 Update README 2021-04-19 19:17:10 +00:00
8002708695 Updated Animator to be usable, added logo texture/image, 'finished' Intro state, added Window#shift_state 2021-04-19 19:13:15 +00:00
676545f3c7 Added wip engine intro state 2021-04-19 14:42:49 +00:00
4b417f9ab7 More changes to how styles are applied, fixed bug where a Container would re-evaluate its build block when clicked, fixed background method in DSL having no effect, fixed edit line losing 'focus' when mouse moved out, misc. other changes and fixes. 2021-03-29 08:51:26 -05:00
a70c106387 Added Link element which is basically a button without a border or background, WIP: Element border, margin, padding, and other styles are now easily changable; work is needed to make style changes survive a recalculation 2021-03-29 08:51:26 -05:00
f662fabc56 Use GL_SRGB_ALPHA for textures 2021-03-29 08:13:59 -05:00
abb989f842 Added flags to use ffi-gosu optionally instead of always trying, made tooltip affected by theme (the last set theme in setup will be used to style the tooltip) 2021-03-12 08:33:16 -06:00
1e0d2004b5 More adjustments to scrolling and scroll_top, added debug_draw to elements that draws a box around the element that is atop all rendered things 2021-02-13 22:57:18 -06:00
92dd63dc1d Probably fixed scrolling for real this time, added scroll_top and scroll_top = n methods 2021-02-13 20:03:10 -06:00
20970e5aa9 Bump version 2021-02-11 09:32:37 -06:00
76eb1a85d5 Added focus event, elements can request focus 2021-02-11 09:31:44 -06:00
e9d75d17bf Updated gosu version, bumped version 2021-02-10 12:19:47 -06:00
e8bb2cac17 Update bundler in gemspec 2021-02-10 12:18:28 -06:00
29fbac7140 Added Container#apend method, EditLine caret will now stay visible while typing, EditLine#value= now sets the value of @text_input 2021-02-09 17:41:29 -06:00
d050f63c2b Increased scroll speed and added window_size_changed event for containers that is thrown from the root container 2021-01-31 13:33:20 -06:00
af24fc8690 Added vertical scrolling support for containers (no scrollbar yet) 2021-01-31 09:44:46 -06:00
f63b893c70 Bump version 2021-01-18 14:01:29 -06:00
5adc27feef Sync 2021-01-18 14:00:29 -06:00
62636158f7 Fixed word wrapping weirdness and made it the default text_wrap method from :none 2021-01-16 19:43:49 -06:00
2179e11ba1 Updated list box 2021-01-07 10:12:14 -06:00
1ac5e0695e Fixed Image element background and border not working, made button use Style as sole source of background and color colors 2021-01-05 23:15:22 -06:00
732dc2c957 Fixed new label dsl methods requiring options to be provided, upgraded Image to accept either a path or an image and to enable replacing image using #value= 2021-01-05 20:39:13 -06:00
886680ab31 Renamed Label to TextBlock, added Shoes text dsl methods; needs mroe refinements, made tooltip styleable. 2021-01-05 17:31:31 -06:00
0268a8a5fb Button element (and its decendents) can now be disabled 2021-01-04 09:34:11 -06:00
1c22c36d6b Include Common in Window 2021-01-01 19:20:52 -06:00
3e56d48556 Update dependencies, bump version 2020-12-31 07:48:44 -06:00
0c9874c53b Added support for tiled 9 slice backgrounds 2020-12-18 12:43:53 -06:00
164a46c1fb Added Background Nine Slice (Not yet usable for ui widgets) 2020-12-15 11:38:17 -06:00
26dd688124 Ran rubocop -a 2020-12-14 16:04:31 -06:00
2447dde1af Added rubocop config 2020-12-14 15:39:30 -06:00
d3ff7c1fc1 Added support for future borderless window option, added initial support for label word wrapping (needs more work), fixed toggle button checkmark not displaying due to changes in Label 2020-12-14 15:39:16 -06:00
2d57d62bc2 Don't trigger a recalc when hiding/showing elements if they're already in that state 2020-12-09 19:11:28 -06:00
fb36436d79 Added color support to Image 2020-12-09 09:19:32 -06:00
246e0c54b2 Fixed crashes when using EditBox with no content 2020-12-08 20:27:01 -06:00
d02c001989 Added download manager, added excon dependency 2020-09-25 20:51:25 -05:00
3ba635f157 Fixed ConfigFile not storing values deeper then one level, added markup_width to Text 2020-09-24 10:47:55 -05:00
695c77b183 Added support for Label text alignement, improved EditBox to correctly position caret and support mouse caret positioning, added debugging Container boundry (WIP) 2020-09-09 09:51:18 -05:00
b06ceaabce Fixed crash if GuiState @mouse_over has no parent 2020-08-28 08:37:24 -05:00
ce64b8a205 Implemented ListBox- Menu 2020-08-21 15:56:11 -05:00
da4188764c Added min/max_view_distance to attr_accessor for PerspectiveCamera 2020-07-19 09:44:09 -05:00
d01e91c3fd Added support for draw mode to Text 2020-07-18 21:34:21 -05:00
041cfcccaa Added a large portion of I-MIC-FPS's opengl rendering and model loading systems 2020-07-15 21:33:24 -05:00
d7dbcf8511 Added orthographic support to Transform 2020-07-10 14:51:02 -05:00
2a8e852b15 Added support for passing a filter method/proc to EditLine 2020-06-23 09:51:36 -05:00
d392d8249d Added support for deciding text drawing method, more work on edit_box 2020-06-16 10:12:21 -05:00
5d7e2028b1 Added support for image icon for ToggleButton, added initial implementation of EditBox, fixed a few ruby -w warnings by initializing select instance nilable variables, added clipboard support to EditLine, added drag selection to EditLine, added keyboard shortcuts to EditLine, GuiState now passes button_down/button_up callbacks to @focus 2020-06-16 00:19:30 -05:00
d902e5d111 Fixed EditLine not positioning caret correctly when clicked 2020-06-10 07:32:31 -05:00
39964e5bd4 Added support for using an image in a button 2020-06-09 10:49:31 -05:00
5b2a015421 Call recalulate less often to speed up gui updates, call root_container.recalculate a third time when recalculating gui. Gui calls recalculate 10x less when doing a full recalculate. 2020-06-09 09:11:57 -05:00
eb4d5d4d21 Use Gosu.clip_to to prevent elements from overdrawing 2020-06-08 09:01:10 -05:00
c83b204447 Removed debugging puts from EditLine 2020-06-08 05:48:30 -05:00
d0449c7b65 Add support back for fixed x and y positioning of elements, fixed theming not using deep_merge 2020-06-08 05:35:03 -05:00
a05ee57f6f Fixed EditLine text not visible if using dynamic width/height, fixed cursor in wrong position when at start/end 2020-06-07 13:18:03 -05:00
c3b227d6e7 Fix vector transform using 5th element instead if 15th 2020-06-07 12:36:37 -05:00
7aaf37d43e Added Vector#multiply_transform, needs more testing. 2020-05-12 09:32:50 -05:00
0850336e55 Bump version 2020-05-06 22:01:15 -05:00
8b6d7b6eb2 Fixed Slider default value was always 0.5 instead of middle of range 2020-05-06 12:54:52 -05:00
3226eb2bda Sliders work better, fixed CheckBox not passing along options hash, Text width/height now accept a string, changed EditLine text selection color, temporary back to back gui recalculations to fix positioning errors until Container#layout can safely be called after determining element width and height. 2020-05-06 12:08:20 -05:00
a98bb4ec82 Fixed hidden elements were interactable, added comments 2020-05-05 10:48:00 -05:00
da5d740c6e Sync: Renamed Engine to Window to be less confusing, elements are now supposed to throw a changed event if their value is changed 2020-05-04 11:17:11 -05:00
4055f645f3 Implemented Slider (still have some positioning issues to resolve but it works), added files for ListBox, Radio, and EditBox, implemented dragging support in GuiState. 2020-04-10 18:45:55 -05:00
185ab000d6 Updated rake 2020-04-06 09:40:41 -05:00
ed061c8408 Fixed incorrect element positioning when using margin 2020-04-06 09:38:30 -05:00
d8551c7428 Bump version 2020-03-24 10:34:14 -05:00
0017c841dd Added Shader.delete, shaders now clean up after themselves 2020-03-24 10:33:44 -05:00
8cedb40283 Increase shader error message buffer size 2020-03-22 17:28:03 -05:00
93a4e9a2b8 Fixed Vector#== erroring out with doing Vector == SomeObject 2020-01-30 20:08:17 -06:00
a0c0180411 Merge branch 'master' of https://github.com/cyberarm/cyberarm_engine 2020-01-29 18:22:33 -06:00
7f25cd49fe Added ConfigFile for persistent hash storage 2020-01-29 18:22:24 -06:00
7e6a17fe9f Updated README, added some docs for Shader 2020-01-29 11:52:51 -06:00
0ea1e5c2ff Add documentation for Vector 2020-01-29 10:55:51 -06:00
8c34293307 Updated required gosu to 0.15 series, bumped version 2020-01-28 10:53:58 -06:00
8172dafa8c Refactored Shader to duplicate code in shader compilation step, added support for @include preprocessor in shaders 2020-01-28 10:51:36 -06:00
9a3caabc7e Make UI events travel up parent hierarchy until event is either :handled or there is no parent to notify 2020-01-15 21:03:21 -06:00
4b25d724b5 Improved shoes-iness; no longer need to pass clear blocks an element to parent to, todo: replace global var with something else. 2020-01-13 20:13:03 -06:00
a267695b1c dry up DSL's flow/stack methods 2019-12-29 11:30:33 -06:00
d76da62ee1 Update animator pass in from and to objects 2019-12-19 16:54:33 -06:00
3a83e616d3 Added Animator for creating animations with various tweeners, supports Gosu::Color's and numbers 2019-12-19 13:08:29 -06:00
52940d1cfe Fixed element borders not always lining up when using dynamic width/height 2019-12-19 11:17:31 -06:00
e59771e412 Refactored Shader.set_uniform into multiple methods 2019-12-09 11:49:46 -06:00
b382bf6960 Fix passing Vector (as vec3) to Shader 2019-12-09 08:51:31 -06:00
a5ed650970 Got 3D rotations working as expected 2019-12-08 17:16:12 -06:00
935d6e9178 Added Transform.perspective and .view matrix helpers, fixed array index out of bounds for Transform * Vector 2019-12-07 09:59:47 -06:00
cc813c023f Added initial 3D support to Transform 2019-12-05 17:23:47 -06:00
5f359a8313 Fixed wrong method called in DSL.progress, removed unneed line, and made DSL.background use element_parent 2019-11-22 17:20:03 -06:00
c67d6f9d26 Trigger recalculate when Container#clear is called 2019-11-21 14:24:01 -06:00
7f97ec85dd Added Container#clear to remove child elements and optionally eval block, improvements to EditLine to show current selection. 2019-11-21 14:07:42 -06:00
89a54e90a1 Added Vector.xy 2019-10-25 13:57:23 -05:00
48d2924c9b Probably fixed Element dynamic width/height 2019-10-24 10:47:13 -05:00
7cc733dee4 Bumped version 2019-10-15 11:41:53 -05:00
28c4acdedf Added Gosu.draw_circle 2019-10-09 11:35:40 -05:00
57313a33a6 Bump version 2019-10-08 08:54:15 -05:00
5d16500edd Added support for passing flags to get_image, aliased Vector#weight/= to Vector#w/= 2019-10-08 08:53:37 -05:00
51dd3803fa Moved files around, renamed Matrix to Transform 2019-10-07 12:30:48 -05:00
c6beab3e99 Added Matrix, incomplete. 2019-10-06 21:35:10 -05:00
8ce594753e Bumped version 2019-10-01 14:58:00 -05:00
ae44083cf2 Fixed default theme border to thick, fixed BoundingBox#point? order dependent 2019-10-01 14:57:17 -05:00
b25f76e124 Bumped version 2019-10-01 11:48:43 -05:00
8f9e671340 Added Vector#lerp 2019-10-01 11:48:22 -05:00
bb482f4463 Sync 2019-09-26 12:41:34 -05:00
788d987da1 Elements now return :handled when they're supposed to, GameState initialization and setup are now seperated 2019-09-12 08:15:02 -05:00
6fafe98008 Removed 'require "gosu"' from background 2019-09-12 08:03:27 -05:00
002a01abfc Bump version 2019-08-13 09:52:24 -05:00
6f7bac3880 BoundingBox#intersect? now supports Ray 2019-08-13 09:51:53 -05:00
993e59aa94 Added Shader#get to retrieve instance of Shader if shader with name exists 2019-08-13 07:26:06 -05:00
e30ed7c6be Fixed a couple warnings 2019-08-11 09:38:09 -05:00
a172c24d97 Bump version 2019-08-10 09:42:29 -05:00
003715f685 Added Shader#available? method for checking if a shader useable 2019-08-10 09:42:00 -05:00
1458736f7d Bumped version 2019-08-08 21:22:31 -05:00
a2e0c07c6a Added Ray class 2019-08-07 23:03:40 -05:00
3ead2f5daf Added Shader handling class, made Text re-render text_shadow if anything affecting shadow is changed. 2019-08-07 12:02:22 -05:00
8f3d9ff193 added Element#inner_width and Element#inner_height methods, EditLine now resets caret blink cycle when clicked 2019-06-27 17:23:15 -05:00
bed78e7dc8 Bumped version 2019-06-27 15:47:49 -05:00
bdce85613b Added Progress bar, refactored Elements to be under CyberarmEngine::Element namespace. 2019-06-27 15:41:38 -05:00
b3bfa0d654 Updated ToggleButton and CheckBox width/height handling 2019-06-25 15:33:58 -05:00
a972cfac98 Added partial support for fixed element width/height and added support for dynamic width/height (currently only works on Containers) 2019-06-25 14:56:41 -05:00
2fe8e6042b Added deep_merge for Theme 2019-06-24 15:06:51 -05:00
f68a8383af Monkeypatch Gosu::Color to add <=> to support color ranges in Background 2019-06-23 14:54:34 -05:00
72df060059 Root Container width/height now always match window 2019-06-23 11:27:13 -05:00
e95b4c05e2 Added GuiState#request_recalculation to enable requesting a gui recalc on next update, GUI state will request a recalculation if the window size changes 2019-06-23 10:43:31 -05:00
4642056576 Bumped version 2019-06-21 18:54:54 -05:00
3d2402b7f7 Fixed Image and EditLine sizing messed out 2019-06-21 18:54:30 -05:00
f4a783b371 Bumped version 2019-06-21 16:15:53 -05:00
d1dec5791b Improved theme handling 2019-06-21 13:53:57 -05:00
8c5c5e1b7b Element width and height are now proper styles, removed width/height setters when element is not visible, Element.width and Element.height now return 0 if Element is invisible 2019-06-21 11:59:13 -05:00
f9324448ee Moved Element styles into Style 2019-06-21 11:40:15 -05:00
34d53ae1cb Bump version 2019-06-17 08:57:33 -05:00
74d1ddd16b Fixed not handling padding properly, fixed edit_line stuck appearing like mouse is hovering over it after mouse has left it 2019-06-17 08:56:17 -05:00
da69f057a0 Bump version 2019-06-16 12:03:45 -05:00
4f4770db0e Fixed CheckBox#value not returning a boolean, added support for toggling Element visiblity 2019-06-16 12:03:20 -05:00
b6730204e8 Bump version 2019-06-13 21:21:46 -05:00
d8dc621c62 Added proper theming support 2019-05-11 17:13:24 -05:00
739ae86966 Bump version 2019-05-06 19:21:56 -05:00
3be16c644f Fix Text height returning 0 if @text is blank 2019-05-06 19:21:30 -05:00
805077d232 Bump version 2019-05-03 13:52:44 -05:00
db1a0683e2 Added to_h (to hash) method to Vector, Text now tries to convert given text to string instead of assuming it is a string 2019-05-03 13:52:01 -05:00
f489ad162f Bump version 2019-05-01 13:47:57 -05:00
df2dc25e34 Made Vector#operator private, refactored BoundingBox initializer to accept either Vectors or numbers, added #opacity to Common 2019-05-01 13:47:17 -05:00
6b319935d3 Bump version 2019-04-30 06:43:10 -05:00
f9d551a419 Added #root and #value= to Element, Label now uses #value= and #root. 2019-04-29 13:21:37 -05:00
f34dd21644 Improved Vector, use named arguments for Engine initializer 2019-04-28 15:00:50 -05:00
025f54a752 Fix incorrect Text height 2019-04-28 07:51:46 -05:00
cbc27b5d5d Updated Text shadow rendering 2019-04-01 16:16:51 -05:00
4af377b93c Bump version 2019-03-31 17:35:32 -05:00
cc97077b0f Remove MultiLineText as gosu natively supports newlines, Update Text to correctly calculate its height, call super in button_down/button_up to support gosu's fullscreen toggle, added Window.text_input = nil to GameState to prevent fustrating bugs that stem from forgetting to set it back to nil. 2019-03-31 17:34:24 -05:00
92 changed files with 8239 additions and 1400 deletions

8
.rubocop.yml Normal file
View File

@@ -0,0 +1,8 @@
Style/StringLiterals:
EnforcedStyle: double_quotes
Metrics/MethodLength:
Max: 40
Style/EmptyMethod:
EnforcedStyle: expanded

View File

@@ -1,6 +1,6 @@
source "https://rubygems.org"
git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }
git_source(:github) { |repo_name| "https://github.com/#{repo_name}" }
# Specify your gem's dependencies in cyberarm_engine.gemspec
gemspec

View File

@@ -1,8 +1,13 @@
# CyberarmEngine
![CyberarmEngine](https://raw.githubusercontent.com/cyberarm/cyberarm_engine/master/assets/textures/logo.png)
Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/cyberarm_engine`. To experiment with that code, run `bin/console` for an interactive prompt.
Yet Another Game Engine On Top Of Gosu
TODO: Delete this and the text above, and describe your gem
## Features
* [Shoes-like](http://shoesrb.com) GUI support
* OpenGL Shader support (requires [opengl-bindings](https://github.com/vaiorabbit/ruby-opengl) gem)
* Includes classes for handling Vectors, Rays, Bounding Boxes, and Transforms
* GameState system
* Monolithic GameObjects
## Installation
@@ -22,7 +27,33 @@ Or install it yourself as:
## Usage
TODO: Write usage instructions here
```ruby
require "cyberarm_engine"
class Hello < CyberarmEngine::GuiState
def setup
background Gosu::Color::GRAY
stack do
label "Hello World!"
button "close" do
window.close
end
end
end
end
class Window < CyberarmEngine::Window
def setup
self.show_cursor = true
push_state(Hello)
end
end
Window.new(width: 800, height: 600, fullscreen: false, resizable: true).show
```
## Development

View File

@@ -7,4 +7,4 @@ Rake::TestTask.new(:test) do |t|
t.test_files = FileList["test/**/*_test.rb"]
end
task :default => :test
task default: :test

View File

@@ -0,0 +1,30 @@
# version 330 core
layout(location = 0) out vec3 fragPosition;
layout (location = 1) out vec4 fragColor;
layout (location = 2) out vec3 fragNormal;
layout (location = 3) out vec3 fragUV;
in vec3 out_position, out_color, out_normal, out_uv, out_frag_pos, out_camera_pos;
out vec4 outputFragColor;
flat in int out_has_texture;
uniform sampler2D diffuse_texture;
void main() {
vec3 result;
if (out_has_texture == 0) {
result = out_color;
} else {
result = texture(diffuse_texture, out_uv.xy).xyz + 0.25;
}
fragPosition = out_position;
fragColor = vec4(result, 1.0);
fragNormal = out_normal;
fragUV = out_uv;
float gamma = 2.2;
outputFragColor.rgb = pow(fragColor.rgb, vec3(1.0 / gamma));
}

View File

@@ -0,0 +1,69 @@
#version 330 core
out vec4 frag_color;
@include "light_struct"
const int DIRECTIONAL = 0;
const int POINT = 1;
const int SPOT = 2;
flat in Light out_lights[7];
in vec2 out_tex_coords;
flat in int out_light_count;
uniform sampler2D diffuse, position, texcoord, normal, depth;
vec4 directionalLight(Light light) {
vec3 norm = normalize(texture(normal, out_tex_coords).rgb);
vec3 diffuse_color = texture(diffuse, out_tex_coords).rgb;
vec3 frag_pos = texture(position, out_tex_coords).rgb;
vec3 lightDir = normalize(light.position - frag_pos);
float diff = max(dot(norm, lightDir), 0);
vec3 _ambient = light.ambient;
vec3 _diffuse = light.diffuse * diff;
vec3 _specular = light.specular;
return vec4(_diffuse + _ambient + _specular, 1.0);
}
vec4 pointLight(Light light) {
return vec4(0.25, 0.25, 0.25, 1);
}
vec4 spotLight(Light light) {
return vec4(0.5, 0.5, 0.5, 1);
}
vec4 calculateLighting(Light light) {
vec4 result;
// switch(light.type) {
// case DIRECTIONAL: {
// result = directionalLight(light);
// }
// case SPOT: {
// result = spotLight(light);
// }
// default: {
// result = pointLight(light);
// }
// }
if (light.type == DIRECTIONAL) {
result = directionalLight(light);
} else {
result = pointLight(light);
}
return result;
}
void main() {
frag_color = vec4(0.0);
for(int i = 0; i < out_light_count; i++)
{
frag_color += texture(diffuse, out_tex_coords) * calculateLighting(out_lights[i]);
}
}

View File

@@ -0,0 +1,11 @@
struct Light {
int type;
vec3 direction;
vec3 position;
vec3 diffuse;
vec3 ambient;
vec3 specular;
float intensity;
};

View File

@@ -0,0 +1,16 @@
struct Material {
vec3 color;
vec3 roughness;
vec3 metalic;
vec3 specular;
bool use_color_texture;
bool use_roughness_texture;
bool use_metalic_texture;
bool use_specular_texture;
sampler2D color_tex;
sampler2D roughness_tex;
sampler2D metalic_tex;
sampler2D specular_tex;
};

View File

@@ -0,0 +1,28 @@
# version 330 core
layout(location = 0) in vec3 in_position;
layout(location = 1) in vec3 in_color;
layout(location = 2) in vec3 in_normal;
layout(location = 3) in vec3 in_uv;
uniform mat4 projection, view, model;
uniform int has_texture;
uniform vec3 camera_pos;
out vec3 out_position, out_color, out_normal, out_uv;
out vec3 out_frag_pos, out_view_pos, out_camera_pos;
flat out int out_has_texture;
void main() {
// projection * view * model * position
out_position = in_position;
out_color = in_color;
out_normal= normalize(transpose(inverse(mat3(model))) * in_normal);
out_uv = in_uv;
out_has_texture = has_texture;
out_camera_pos = camera_pos;
out_frag_pos = vec3(model * vec4(in_position, 1.0));
gl_Position = projection * view * model * vec4(in_position, 1.0);
}

View File

@@ -0,0 +1,24 @@
#version 330 core
@include "light_struct"
layout (location = 0) in vec3 in_position;
layout (location = 1) in vec2 in_tex_coords;
uniform sampler2D diffuse, position, texcoord, normal, depth;
uniform int light_count;
uniform Light lights[7];
out vec2 out_tex_coords;
flat out int out_light_count;
flat out Light out_lights[7];
void main() {
gl_Position = vec4(in_position.x, in_position.y, in_position.z, 1.0);
out_tex_coords = in_tex_coords;
out_light_count = light_count;
for(int i = 0; i < light_count; i++)
{
out_lights[i] = lights[i];
}
}

BIN
assets/textures/default.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 681 B

BIN
assets/textures/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

View File

@@ -1,5 +1,4 @@
lib = File.expand_path("../lib", __FILE__)
lib = File.expand_path("lib", __dir__)
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
require "cyberarm_engine/version"
@@ -9,8 +8,8 @@ Gem::Specification.new do |spec|
spec.authors = ["Cyberarm"]
spec.email = ["matthewlikesrobots@gmail.com"]
spec.summary = %q{Make games quickly and easily with gosu}
spec.description = %q{Yet another game making framework around gosu}
spec.summary = "Make games quickly and easily with gosu"
spec.description = "Yet another game making framework around gosu"
spec.homepage = "https://github.com/cyberarm/cyberarm_engine"
spec.license = "MIT"
@@ -21,16 +20,19 @@ Gem::Specification.new do |spec|
"public gem pushes."
end
spec.files = `git ls-files -z`.split("\x0").reject do |f|
spec.files = `git ls-files -z`.split("\x0").reject do |f|
f.match(%r{^(test|spec|features)/})
end
spec.bindir = "exe"
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
spec.require_paths = ["lib"]
spec.require_paths = %w[lib assets]
spec.add_dependency "gosu", "~> 0.14.0"
spec.add_dependency "excon", "~> 0.88"
spec.add_dependency "gosu", "~> 1.1"
spec.add_dependency "gosu_more_drawables", "~> 0.3"
# spec.add_dependency "ffi", :platforms => [:mswin, :mingw] # Required by Clipboard on Windows
spec.add_development_dependency "bundler", "~> 1.16"
spec.add_development_dependency "rake", "~> 10.0"
spec.add_development_dependency "bundler", "~> 2.2"
spec.add_development_dependency "minitest", "~> 5.0"
spec.add_development_dependency "rake", "~> 13.0"
end

View File

@@ -1,36 +1,71 @@
require "gosu"
CYBERARM_ENGINE_ROOT_PATH = File.expand_path("..", __dir__)
if ARGV.join.include?("--ffi-gosu")
require File.expand_path("../../ffi-gosu/lib/gosu", __dir__)
else
require "gosu"
end
require "json"
require "excon"
require "gosu_more_drawables"
require_relative "cyberarm_engine/version"
require_relative "cyberarm_engine/stats"
require_relative "cyberarm_engine/common"
require_relative "cyberarm_engine/game_object"
require_relative "cyberarm_engine/engine"
require_relative "cyberarm_engine/window"
require_relative "cyberarm_engine/lib/bounding_box"
require_relative "cyberarm_engine/lib/vector"
require_relative "cyberarm_engine/bounding_box"
require_relative "cyberarm_engine/vector"
require_relative "cyberarm_engine/transform"
require_relative "cyberarm_engine/ray"
require_relative "cyberarm_engine/background"
require_relative "cyberarm_engine/background_nine_slice"
require_relative "cyberarm_engine/background_image"
require_relative "cyberarm_engine/animator"
require_relative "cyberarm_engine/objects/text"
require_relative "cyberarm_engine/objects/timer"
require_relative "cyberarm_engine/objects/multi_line_text"
require_relative "cyberarm_engine/text"
require_relative "cyberarm_engine/timer"
require_relative "cyberarm_engine/config_file"
require_relative "cyberarm_engine/console"
require_relative "cyberarm_engine/console/command"
require_relative "cyberarm_engine/console/subcommand"
require_relative "cyberarm_engine/console/commands/help_command"
require_relative "cyberarm_engine/ui/dsl"
require_relative "cyberarm_engine/ui/theme"
require_relative "cyberarm_engine/ui/event"
require_relative "cyberarm_engine/ui/style"
require_relative "cyberarm_engine/ui/border_canvas"
require_relative "cyberarm_engine/ui/element"
require_relative "cyberarm_engine/ui/label"
require_relative "cyberarm_engine/ui/button"
require_relative "cyberarm_engine/ui/toggle_button"
require_relative "cyberarm_engine/ui/edit_line"
require_relative "cyberarm_engine/ui/image"
require_relative "cyberarm_engine/ui/container"
require_relative "cyberarm_engine/ui/flow"
require_relative "cyberarm_engine/ui/stack"
require_relative "cyberarm_engine/ui/check_box"
require_relative "cyberarm_engine/ui/dsl"
require_relative "cyberarm_engine/ui/elements/text_block"
require_relative "cyberarm_engine/ui/elements/button"
require_relative "cyberarm_engine/ui/elements/toggle_button"
require_relative "cyberarm_engine/ui/elements/list_box"
require_relative "cyberarm_engine/ui/elements/edit_line"
require_relative "cyberarm_engine/ui/elements/edit_box"
require_relative "cyberarm_engine/ui/elements/image"
require_relative "cyberarm_engine/ui/elements/container"
require_relative "cyberarm_engine/ui/elements/flow"
require_relative "cyberarm_engine/ui/elements/stack"
require_relative "cyberarm_engine/ui/elements/check_box"
require_relative "cyberarm_engine/ui/elements/radio"
require_relative "cyberarm_engine/ui/elements/progress"
require_relative "cyberarm_engine/ui/elements/slider"
require_relative "cyberarm_engine/game_state"
require_relative "cyberarm_engine/ui/gui_state"
require_relative "cyberarm_engine/model"
require_relative "cyberarm_engine/model_cache"
require_relative "cyberarm_engine/model/material"
require_relative "cyberarm_engine/model/model_object"
require_relative "cyberarm_engine/model/parser"
require_relative "cyberarm_engine/model/parsers/wavefront_parser"
require_relative "cyberarm_engine/model/parsers/collada_parser" if RUBY_ENGINE != "mruby" && defined?(Nokogiri)
require_relative "cyberarm_engine/builtin/intro_state"

View File

@@ -0,0 +1,219 @@
module CyberarmEngine
class Animator
def initialize(start_time:, duration:, from:, to:, tween: :linear, &block)
@start_time = start_time
@duration = duration
@from = from.dup
@to = to.dup
@tween = tween
@block = block
end
def update
@block.call(self, @from, @to) if @block
end
def progress
((Gosu.milliseconds - @start_time) / @duration.to_f).clamp(0.0, 1.0)
end
def complete?
progress >= 1.0
end
def transition(from = @from, to = @to, tween = @tween)
from + (to - from) * send("tween_#{tween}", progress)
end
def color_transition(from = @from, to = @to, _tween = @tween)
r = transition(from.red, to.red)
g = transition(from.green, to.green)
b = transition(from.blue, to.blue)
a = transition(from.alpha, to.alpha)
Gosu::Color.rgba(r, g, b, a)
end
def color_hsv_transition(from = @from, to = @to, tween = @tween)
hue = transition(from.hue, to.hue, tween)
saturation = transition(from.saturation, to.saturation, tween)
value = transition(from.value, to.value, tween)
alpha = transition(from.alpha, to.alpha, tween)
Gosu::Color.from_ahsv(alpha, hue, saturation, value)
end
# Tween functions based on those provided here: https://github.com/danro/easing-js/blob/master/easing.js
# Under MIT / BSD
def tween_linear(t)
t
end
def tween_ease_in_quad(t)
t ** 2
end
def tween_ease_out_quad(t)
-((t - 1) ** 2) -1
end
def tween_ease_in_out_quad(t)
return 0.5 * (t ** 2) if (t /= 0.5) < 1
return -0.5 * ((t -= 2) * t - 2)
end
def tween_ease_in_cubic(t)
t ** 3
end
def tween_ease_out_cubic(t)
((t - 1) ** 3) + 1
end
def tween_ease_in_out_cubic(t)
return 0.5 * (t ** 3) if ((t /= 0.5) < 1)
return 0.5 * ((t - 2) ** 3) + 2
end
def tween_ease_in_quart(t)
t ** 4
end
def tween_ease_out_quart(t)
-((t - 1) ** 4) - 1
end
def tween_ease_in_out_quart(t)
return 0.5 * (t ** 4) if ((t /= 0.5) < 1)
return -0.5 * ((t -= 2) * (t ** 3) - 2)
end
def tween_ease_in_quint(t)
t ** 5
end
def tween_ease_out_quint(t)
((t - 1) ** 5) + 1
end
def tween_ease_in_out_quint(t)
return 0.5 * (t ** 5) if ((t /= 0.5) < 1)
return 0.5 * ((t - 2) ** 5) + 2
end
def tween_ease_in(t) # sine
-Math.cos(t * (Math::PI / 2)) + 1
end
def tween_ease_out(t) # sine
Math.sin(t * (Math::PI / 2))
end
def tween_ease_in_out(t) # sine
(-0.5 * (Math.cos(Math::PI * t) - 1))
end
def tween_ease_in_expo(t)
(t == 0) ? 0 : 2 ** 10 * (t - 1)
end
def tween_ease_out_expo(t)
(t == 1) ? 1 : -(2 ** -10 * t) + 1
end
def tween_ease_in_out_expo(t)
return 0 if (t == 0)
return 1 if (t == 1)
return 0.5 * (2 ** 10 * (t - 1)) if ((t /= 0.5) < 1)
return 0.5 * (-(2 ** -10 * (t -= 1)) + 2)
end
def tween_ease_in_circ(t)
-(Math.sqrt(1 - (t * t)) - 1)
end
def tween_ease_out_circ(t)
Math.sqrt(1 - ((t - 1) ** 2))
end
def tween_ease_in_out_circ(t)
return -0.5 * (Math.sqrt(1 - t * t) - 1) if ((t /= 0.5) < 1)
return 0.5 * (Math.sqrt(1 - (t -= 2) * t) + 1)
end
def tween_ease_in_back(t)
s = 1.70158
t * t * ((s + 1) * t - s)
end
def tween_ease_out_back(t)
s = 1.70158
(t = t - 1) * t * ((s + 1) * t + s) + 1
end
def tween_ease_in_out_back(t)
s = 1.70158
return 0.5 * (t * t * (((s *= (1.525)) + 1) * t - s)) if ((t /= 0.5) < 1)
return 0.5 * ((t -= 2) * t * (((s *= (1.525)) + 1) * t + s) + 2)
end
def tween_elastic(t)
-1 * (4 ** (-8 * t)) * Math.sin((t * 6 - 1) * (2 * Math::PI) / 2) + 1
end
def tween_swing_from_to(t)
s = 1.70158
return 0.5 * (t * t * (((s *= (1.525)) + 1) * t - s)) if (t /= 0.5) < 1
return 0.5 * ((t -= 2) * t * (((s *= (1.525)) + 1) * t + s) + 2)
end
def tween_swing_from(t)
s = 1.70158;
t * t * ((s + 1) * t - s)
end
def tween_swing_to(t)
s = 1.70158
(t -= 1) * t * ((s + 1) * t + s) + 1
end
def tween_bounce(t)
if (t < (1 / 2.75))
(7.5625 * t * t)
elsif (t < (2 / 2.75))
(7.5625 * (t -= (1.5 / 2.75)) * t + 0.75)
elsif (t < (2.5 / 2.75))
(7.5625 * (t -= (2.25 / 2.75)) * t + 0.9375)
else
(7.5625 * (t -= (2.625 / 2.75)) * t + 0.984375)
end
end
def tween_bounce_past(t)
if (t < (1 / 2.75))
# missing "2 -"?
(7.5625 * t * t)
elsif (t < (2 / 2.75))
2 - (7.5625 * (t -= (1.5 / 2.75)) * t + 0.75)
elsif (t < (2.5 / 2.75))
2 - (7.5625 * (t -= (2.25 / 2.75)) * t + 0.9375)
else
2 - (7.5625 * (t -= (2.625 / 2.75)) * t + 0.984375)
end
end
def tween_ease_from_to(t)
return 0.5 * (t ** 4) if ((t /= 0.5) < 1)
return -0.5 * ((t -= 2) * (t ** 3) - 2)
end
def tween_ease_from(t)
t ** 4
end
def tween_ease_to(t)
t ** 0.25
end
end
end

View File

@@ -1,12 +1,14 @@
require "gosu"
module CyberarmEngine
class Background
attr_accessor :x, :y, :z, :width, :height, :angle, :debug
attr_reader :background
def initialize(x: 0, y: 0, z: 0, width: 0, height: 0, background: Gosu::Color::BLACK, angle: 0, debug: false)
@x,@y,@z = x,y,z
@width,@height = width,height
@x = x
@y = y
@z = z
@width = width
@height = height
@debug = debug
@paint = Paint.new(background)
@@ -33,8 +35,8 @@ module CyberarmEngine
end
def update
origin_x = (@x + (@width/2))
origin_y = (@y + (@height/2))
origin_x = (@x + (@width / 2))
origin_y = (@y + (@height / 2))
points = [
@top_left = Vector.new(@x, @y),
@@ -49,8 +51,8 @@ module CyberarmEngine
# 90 is up here, while gosu uses 0 for up.
radians = (@angle + 90).gosu_to_radians
vector.x = (@x + (@width/2)) + ((temp_x * Math.cos(radians)) - (temp_y * Math.sin(radians)))
vector.y = (@y + (@height/2)) + ((temp_x * Math.sin(radians)) + (temp_y * Math.cos(radians)))
vector.x = (@x + (@width / 2)) + ((temp_x * Math.cos(radians)) - (temp_y * Math.sin(radians)))
vector.y = (@y + (@height / 2)) + ((temp_x * Math.sin(radians)) + (temp_y * Math.cos(radians)))
end
# [
@@ -69,11 +71,11 @@ module CyberarmEngine
a = la.x - lb.x
b = la.y - lb.y
c = Gosu.distance(la.x, la.y, lb.x, lb.y)
p a,b,c
d = (a * point.x + b * point.y + c).abs / (Math.sqrt(a * a + b * b))
p a, b, c
d = (a * point.x + b * point.y + c).abs / Math.sqrt(a * a + b * b)
puts "Distance: #{d}"
exit!
return d
d
end
def debug_outline
@@ -119,6 +121,7 @@ module CyberarmEngine
class Paint
attr_accessor :top_left, :top_right, :bottom_left, :bottom_right
def initialize(background)
set(background)
end
@@ -126,7 +129,6 @@ module CyberarmEngine
def set(background)
@background = background
if background.is_a?(Numeric)
@top_left = background
@top_right = background
@@ -158,9 +160,20 @@ module CyberarmEngine
@top_right = background[:top_right]
@bottom_left = background[:bottom_left]
@bottom_right = background[:bottom_right]
elsif background.is_a?(Range)
set([background.begin, background.begin, background.end, background.end])
else
raise ArgumentError, "background '#{background}' of type '#{background.class}' was not able to be processed"
end
end
end
end
end
# Add <=> method to support Range based gradients
module Gosu
class Color
def <=>(_other)
self
end
end
end

View File

@@ -0,0 +1,93 @@
module CyberarmEngine
class BackgroundImage
include CyberarmEngine::Common
attr_accessor :x, :y, :z, :mode
attr_reader :image, :width, :height, :color
def initialize(image_path: nil, x: 0, y: 0, z: 0, width: 0, height: 0, mode: :fill, color: Gosu::Color::WHITE)
@image_path = image_path
@image = get_image(image_path) if image_path
@x = x
@y = y
@z = z
@width = width
@height = height
@mode = mode
@color = color
@cached_tiling = nil
end
def image=(image_path)
@cached_tiling = nil if image_path != @image_path
@image_path = image_path
@image = image_path ? get_image(image_path) : image_path
end
def width=(n)
@cached_tiling = nil if @width != n
@width = n
end
def height=(n)
@cached_tiling = nil if @height != n
@height = n
end
def color=(c)
@cached_tiling = nil if @color != c
@color = c
end
def width_scale
(@width.to_f / @image.width).abs
end
def height_scale
(@height.to_f / @image.height).abs
end
def draw
return unless @image
Gosu.clip_to(@x, @y, @width, @height) do
send(:"draw_#{mode}")
end
end
def draw_stretch
@image.draw(@x, @y, @z, width_scale, height_scale, @color)
end
def draw_tiled
@cached_tiling ||= Gosu.record(@width, @height) do
height_scale.ceil.times do |y|
width_scale.ceil.times do |x|
@image.draw(x * @image.width, y * @image.height, @z, 1, 1, @color)
end
end
end
@cached_tiling.draw(@x, @y, @z)
end
def draw_fill
if (@image.width * width_scale) >= @width && (@image.height * width_scale) >= @height
draw_fill_width
else
draw_fill_height
end
end
def draw_fill_width
@image.draw(@x, @y, @z, width_scale, width_scale, @color)
end
def draw_fill_height
@image.draw(@x, @y, @z, height_scale, height_scale, @color)
end
end
end

View File

@@ -0,0 +1,142 @@
module CyberarmEngine
class BackgroundNineSlice
include CyberarmEngine::Common
attr_accessor :x, :y, :z, :width, :height, :left, :top, :right, :bottom, :mode, :color
attr_reader :image
def initialize(image_path: nil, x: 0, y: 0, z: 0, width: 0, height: 0, mode: :tiled, left: 1, top: 1, right: 1, bottom: 1, color: Gosu::Color::WHITE)
@image = get_image(image_path) if image_path
@x = x
@y = y
@z = z
@width = width
@height = height
@mode = mode
@left = left
@top = top
@right = right
@bottom = bottom
@color = color
nine_slice if @image
end
def image=(image_path)
old_image = @image
@image = image_path ? get_image(image_path) : image_path
nine_slice if @image && old_image != @image
end
def nine_slice
# pp [@left, @top, @right, @bottom, @image.width]
@segment_top_left = @image.subimage(0, 0, @left, @top)
@segment_top_right = @image.subimage(@image.width - @right, 0, @right, @top)
@segment_left = @image.subimage(0, @top, @left, @image.height - (@top + @bottom))
@segment_right = @image.subimage(@image.width - @right, @top, @left, @image.height - (@top + @bottom))
@segment_bottom_left = @image.subimage(0, @image.height - @bottom, @left, @bottom)
@segment_bottom_right = @image.subimage(@image.width - @right, @image.height - @bottom, @right, @bottom)
@segment_top = @image.subimage(@left, 0, @image.width - (@left + @right), @top)
@segment_bottom = @image.subimage(@left, @image.height - @bottom, @image.width - (@left + @right), @bottom)
@segment_middle = @image.subimage(@left, @top, @image.width - (@left + @right), @image.height - (@top + @bottom))
end
def cx
@x + @left
end
def cy
@y + @top
end
def cwidth
@cx - @width
end
def cheight
@cy - @height
end
def width_scale
scale = (@width.to_f - (@left + @right)) / (@image.width - (@left + @right))
scale.abs
end
def height_scale
scale = (@height - (@top + @bottom)).to_f / (@image.height - (@top + @bottom))
scale.abs
end
def draw
return unless @image && @segment_top_left
@mode == :tiled ? draw_tiled : draw_stretched
end
def draw_stretched
@segment_top_left.draw(@x, @y, @z, 1, 1, @color)
@segment_top.draw(@x + @segment_top_left.width, @y, @z, width_scale, 1, @color) # SCALE X
@segment_top_right.draw((@x + @width) - @segment_top_right.width, @y, @z, 1, 1, @color)
@segment_right.draw((@x + @width) - @segment_right.width, @y + @top, @z, 1, height_scale, @color) # SCALE Y
@segment_bottom_right.draw((@x + @width) - @segment_bottom_right.width, @y + @height - @segment_bottom_right.height, @z, 1, 1, @color)
@segment_bottom.draw(@x + @segment_bottom_left.width, (@y + @height) - @segment_bottom.height, @z, width_scale, 1, @color) # SCALE X
@segment_bottom_left.draw(@x, (@y + @height) - @segment_bottom_left.height, @z, 1, 1, @color)
@segment_left.draw(@x, @y + @top, @z, 1, height_scale, @color) # SCALE Y
@segment_middle.draw(@x + @segment_top_left.width, @y + @segment_top.height, @z, width_scale, height_scale, @color) # SCALE X and SCALE Y
end
def draw_tiled
@segment_top_left.draw(@x, @y, @z, 1, 1, @color)
# p [width_scale, height_scale]
Gosu.clip_to(@x + @segment_top_left.width, @y, @segment_top.width * width_scale, @segment_top.height) do
width_scale.ceil.times do |i|
@segment_top.draw(@x + @segment_top_left.width + (@segment_top.width * i), @y, @z, 1, 1, @color) # SCALE X
end
end
@segment_top_right.draw((@x + @width) - @segment_top_right.width, @y, @z, 1, 1, @color)
Gosu.clip_to(@x + @width - @segment_top_right.width, @y + @top, @segment_right.width, @segment_right.height * height_scale) do
height_scale.ceil.times do |i|
@segment_right.draw((@x + @width) - @segment_right.width, @y + @top + (@segment_right.height * i), @z, 1, 1, @color) # SCALE Y
end
end
@segment_bottom_right.draw((@x + @width) - @segment_bottom_right.width, @y + @height - @segment_bottom_right.height, @z, 1, 1, @color)
Gosu.clip_to(@x + @segment_top_left.width, @y + @height - @segment_bottom.height, @segment_top.width * width_scale, @segment_bottom.height) do
width_scale.ceil.times do |i|
@segment_bottom.draw(@x + @segment_bottom_left.width + (@segment_bottom.width * i), (@y + @height) - @segment_bottom.height, @z, 1, 1, @color) # SCALE X
end
end
@segment_bottom_left.draw(@x, (@y + @height) - @segment_bottom_left.height, @z, 1, 1, @color)
Gosu.clip_to(@x, @y + @top, @segment_left.width, @segment_left.height * height_scale) do
height_scale.ceil.times do |i|
@segment_left.draw(@x, @y + @top + (@segment_left.height * i), @z, 1, 1, @color) # SCALE Y
end
end
Gosu.clip_to(@x + @segment_top_left.width, @y + @segment_top.height, @width - (@segment_left.width + @segment_right.width), @height - (@segment_top.height + @segment_bottom.height)) do
height_scale.ceil.times do |y|
width_scale.ceil.times do |x|
@segment_middle.draw(@x + @segment_top_left.width + (@segment_middle.width * x), @y + @segment_top.height + (@segment_middle.height * y), @z, 1, 1, @color) # SCALE X and SCALE Y
end
end
end
end
end
end

View File

@@ -0,0 +1,150 @@
module CyberarmEngine
class BoundingBox
attr_accessor :min, :max
def initialize(*args)
case args.size
when 0
@min = Vector.new(0, 0, 0)
@max = Vector.new(0, 0, 0)
when 2
@min = args.first.clone
@max = args.last.clone
when 4
@min = Vector.new(args[0], args[1], 0)
@max = Vector.new(args[2], args[3], 0)
when 6
@min = Vector.new(args[0], args[1], args[2])
@max = Vector.new(args[3], args[4], args[5])
else
raise "Invalid number of arguments! Got: #{args.size}, expected: 0, 2, 4, or 6."
end
end
def ==(other)
@min == other.min &&
@max == other.max
end
# returns a new bounding box that includes both bounding boxes
def union(other)
temp = BoundingBox.new
temp.min.x = [@min.x, other.min.x].min
temp.min.y = [@min.y, other.min.y].min
temp.min.z = [@min.z, other.min.z].min
temp.max.x = [@max.x, other.max.x].max
temp.max.y = [@max.y, other.max.y].max
temp.max.z = [@max.z, other.max.z].max
temp
end
# returns the difference between both bounding boxes
def difference(other)
temp = BoundingBox.new
temp.min = @min - other.min
temp.max = @max - other.max
temp
end
# returns whether bounding box intersects other
def intersect?(other)
if other.is_a?(Ray)
other.intersect?(self)
elsif other.is_a?(BoundingBox)
(@min.x <= other.max.x && @max.x >= other.min.x) &&
(@min.y <= other.max.y && @max.y >= other.min.y) &&
(@min.z <= other.max.z && @max.z >= other.min.z)
else
raise "Unknown collider: #{other.class}"
end
end
# does this bounding box envelop other bounding box? (inclusive of border)
def contains?(other)
other.min.x >= min.x && other.min.y >= min.y && other.min.z >= min.z &&
other.max.x <= max.x && other.max.y <= max.y && other.max.z <= max.z
end
# returns whether the 3D vector is inside of the bounding box
def inside?(vector)
(vector.x.between?(@min.x, @max.x) || vector.x.between?(@max.x, @min.x)) &&
(vector.y.between?(@min.y, @max.y) || vector.y.between?(@max.y, @min.y)) &&
(vector.z.between?(@min.z, @max.z) || vector.z.between?(@max.z, @min.z))
end
# returns whether the 2D vector is inside of the bounding box
def point?(vector)
(vector.x.between?(@min.x, @max.x) || vector.x.between?(@max.x, @min.x)) &&
(vector.y.between?(@min.y, @max.y) || vector.y.between?(@max.y, @min.y))
end
def volume
width * height * depth
end
def width
@max.x - @min.x
end
def height
@max.y - @min.y
end
def depth
@max.z - @min.z
end
def normalize(entity)
temp = BoundingBox.new
temp.min.x = @min.x.to_f * entity.scale.x
temp.min.y = @min.y.to_f * entity.scale.y
temp.min.z = @min.z.to_f * entity.scale.z
temp.max.x = @max.x.to_f * entity.scale.x
temp.max.y = @max.y.to_f * entity.scale.y
temp.max.z = @max.z.to_f * entity.scale.z
temp
end
def normalize_with_offset(entity)
temp = BoundingBox.new
temp.min.x = @min.x.to_f * entity.scale.x + entity.position.x
temp.min.y = @min.y.to_f * entity.scale.y + entity.position.y
temp.min.z = @min.z.to_f * entity.scale.z + entity.position.z
temp.max.x = @max.x.to_f * entity.scale.x + entity.position.x
temp.max.y = @max.y.to_f * entity.scale.y + entity.position.y
temp.max.z = @max.z.to_f * entity.scale.z + entity.position.z
temp
end
def +(other)
box = BoundingBox.new
box.min = min + other.min
box.min = max + other.max
box
end
def -(other)
box = BoundingBox.new
box.min = min - other.min
box.min = max - other.max
box
end
def sum
@min.sum + @max.sum
end
def clone
BoundingBox.new(@min.x, @min.y, @min.z, @max.x, @max.y, @max.z)
end
end
end

View File

@@ -0,0 +1,131 @@
module CyberarmEngine
class IntroState < CyberarmEngine::GameState
def setup
@display_width = 800
@display_height = 600
@title_size = 56
@caption_size = 24
@title = CyberarmEngine::Text.new("", size: @title_size, shadow_color: 0xaa_222222, static: true)
@caption = CyberarmEngine::Text.new("", size: @caption_size, shadow_color: 0xaa_222222, static: true)
@spacer_width = 256
@spacer_height = 6
@padding = 6
@cyberarm_engine_logo = get_image "#{CYBERARM_ENGINE_ROOT_PATH}/assets/textures/logo.png"
@gosu_logo = generate_proxy("Gosu", "Game Library", 0xff_111111)
@ruby_logo = generate_proxy("Ruby", "Programming Language", 0xff_880000)
@opengl_logo = generate_proxy("OpenGL", "Graphics API", 0xff_5586a4) if RUBY_ENGINE != "mruby" && defined?(OpenGL)
base_time = Gosu.milliseconds
@born_time = Gosu.milliseconds
@continue_after = 5_000
@animators = [
Animator.new(start_time: base_time += 1000, duration: 100, from: 0.0, to: 1.0, tween: :ease_in_out),
Animator.new(start_time: base_time += -500, duration: 1_000, from: 0.0, to: 1.0, tween: :ease_in_out),
Animator.new(start_time: base_time += 500, duration: 1_000, from: 0.0, to: 1.0, tween: :ease_in_out),
Animator.new(start_time: base_time += 500, duration: 1_000, from: 0.0, to: 1.0, tween: :ease_in_out),
Animator.new(start_time: base_time + 500, duration: 1_000, from: 0.0, to: 1.0, tween: :ease_in_out),
Animator.new(start_time: Gosu.milliseconds + @continue_after - 1_000, duration: 1_000, from: 0.0, to: 1.0, tween: :ease_in_out),
Animator.new(start_time: Gosu.milliseconds + 250, duration: 500, from: 0.0, to: 1.0, tween: :swing_to) # CyberarmEngine LOGO
]
end
def draw
Gosu.draw_rect(0, 0, window.width, window.height, 0xff_222222)
scale = (@display_width - @padding * 2).to_f / @cyberarm_engine_logo.width * @animators.last.transition
@cyberarm_engine_logo.draw_rot(
window.width / 2,
(window.height) / 2 - @cyberarm_engine_logo.height / 2 - @padding * 2,
2,
0,
0.5,
0.5,
scale,
scale
)
Gosu.draw_rect(
window.width / 2 - (@display_width / 2 + @padding),
window.height / 2 - @spacer_height / 2,
@display_width + @padding,
@spacer_height * @animators[0].transition,
Gosu::Color::WHITE
)
@title.x = window.width / 2 - @title.text_width / 2
@title.y = (window.height / 2 + (@spacer_height / 2) + @padding) * @animators[1].transition
@title.text = "Powered By"
Gosu.clip_to(0, window.height / 2 + (@spacer_height / 2), window.width, @title.height) do
@title.draw
end
y = @title.y + @title.height * 2
Gosu.clip_to(0, y, window.width, @gosu_logo.height) do
Gosu.translate(@opengl_logo.nil? ? @ruby_logo.width / 2 : 0, 0) do
@gosu_logo.draw(
window.width.to_f / 2 - @ruby_logo.width / 2 - (@ruby_logo.width - @padding),
y * @animators[2].transition,
2
)
@ruby_logo.draw(
window.width.to_f / 2 - @ruby_logo.width / 2,
y * @animators[3].transition,
2
)
@opengl_logo&.draw(
window.width.to_f / 2 - @ruby_logo.width / 2 + (@ruby_logo.width - @padding),
y * @animators[4].transition,
2
)
end
end
Gosu.draw_rect(0, 0, window.width, window.height, Gosu::Color.rgba(0, 0, 0, 255 * @animators[5].transition), 10_000)
end
def update
@animators.each(&:update)
return unless Gosu.milliseconds - @born_time >= @continue_after
pop_state
push_state(@options[:forward], @options[:forward_options] || {}) if @options[:forward]
end
def button_down(_id)
@continue_after = 0
end
def generate_proxy(title, caption, color_hint)
@title.text = title
@caption.text = caption
width = @spacer_width + 2 * @padding
height = @title_size + @caption_size + @spacer_height + 2 * @padding + @spacer_height
Gosu.record(width.ceil, height.ceil) do
@title.x = (width - @padding * 2) / 2 - @title.text_width / 2
@title.y = @padding
@title.draw
Gosu.draw_rect(0, @padding + @title_size + @padding, @spacer_width, @spacer_height, Gosu::Color::WHITE)
Gosu.draw_rect(1, @padding + @title_size + @padding + 1, @spacer_width - 2, @spacer_height - 2, color_hint)
@caption.x = (width - @padding * 2) / 2 - @caption.text_width / 2
@caption.y = @padding + @title_size + @padding + @spacer_height + @padding
@caption.draw
end
end
end
end

View File

@@ -0,0 +1,4 @@
module CyberarmEngine
module Cache
end
end

View File

@@ -0,0 +1,121 @@
module CyberarmEngine
module Cache
class DownloadManager
attr_reader :downloads
def initialize(max_parallel_downloads: 4)
@max_parallel_downloads = max_parallel_downloads
@downloads = []
end
def download(url:, save_as: nil, &callback)
uri = URI(url)
save_as ||= "filename_path" # TODO: if no save_as path is provided, then get one from the Cache controller
@downloads << Download.new(uri: uri, save_as: save_as, callback: callback)
end
def status
if active_downloads > 0
:busy
else
:idle
end
end
def progress
remaining_bytes = @downloads.map { |d| d.remaining_bytes }.sum
total_bytes = @downloads.map { |d| d.total_bytes }.sum
v = 1.0 - (remaining_bytes.to_f / total_bytes)
return 0.0 if v.nan?
v
end
def active_downloads
@downloads.select { |d| %i[pending downloading].include?(d.status) }
end
def update
@downloads.each do |download|
if download.status == :pending && active_downloads.size <= @max_parallel_downloads
download.status = :downloading
Thread.start { download.download }
end
end
end
def prune
@downloads.delete_if { |d| d.status == :finished || d.status == :failed }
end
class Download
attr_accessor :status
attr_reader :uri, :save_as, :callback, :remaining_bytes, :total_downloaded_bytes, :total_bytes,
:error_message, :started_at, :finished_at
def initialize(uri:, save_as:, callback: nil)
@uri = uri
@save_as = save_as
@callback = callback
@status = :pending
@remaining_bytes = 0.0
@total_downloaded_bytes = 0.0
@total_bytes = 0.0
@error_message = ""
end
def progress
v = 1.0 - (@remaining_bytes.to_f / total_bytes)
return 0.0 if v.nan?
v
end
def download
@status = :downloading
@started_at = Time.now # TODO: monotonic time
io = File.open(@save_as, "w")
streamer = lambda do |chunk, remaining_bytes, total_bytes|
io.write(chunk)
@remaining_bytes = remaining_bytes.to_f
@total_downloaded_bytes += chunk.size
@total_bytes = total_bytes.to_f
end
begin
response = Excon.get(
@uri.to_s,
middlewares: Excon.defaults[:middlewares] + [Excon::Middleware::RedirectFollower],
response_block: streamer
)
if response.status == 200
@status = :finished
@finished_at = Time.now # TODO: monotonic time
@callback.call(self) if @callback
else
@error_message = "Got a non 200 HTTP status of #{response.status}"
@status = :failed
@finished_at = Time.now # TODO: monotonic time
@callback.call(self) if @callback
end
rescue StandardError => e # TODO: cherrypick errors to cature
@status = :failed
@finished_at = Time.now # TODO: monotonic time
@error_message = e.message
@callback.call(self) if @callback
end
ensure
io.close if io
end
end
end
end
end

View File

@@ -1,54 +1,79 @@
module CyberarmEngine
module Common
def push_state(klass, options={})
$window.push_state(klass, options)
def push_state(klass, options = {})
window.push_state(klass, options)
end
def current_state
$window.current_state
window.current_state
end
def previous_state
$window.previous_state
def previous_state(state = nil)
raise "Only available for CyberarmEngine::GameState and subclasses" unless is_a?(CyberarmEngine::GameState) || state.is_a?(CyberarmEngine::GameState)
i = window.states.index(state || self)
window.states[i - 1] unless (i - 1).negative?
end
def pop_state
$window.pop_state
window.pop_state
end
def shift_state
window.shift_state
end
def show_cursor
$window.show_cursor
window.show_cursor
end
def show_cursor=boolean
$window.show_cursor = boolean
def show_cursor=(boolean)
window.show_cursor = boolean
end
def draw_rect(x, y, width, height, color, z = 0)
$window.draw_rect(x,y,width,height,color,z)
def find_element_by_tag(container, tag, list = [])
return unless container
container.children.each do |child|
list << child if child.style.tag == tag
find_element_by_tag(child, tag, list) if child.is_a?(CyberarmEngine::Element::Container)
end
list.first
end
def draw_rect(x, y, width, height, color, z = 0, mode = :default)
Gosu.draw_rect(x, y, width, height, color, z, mode)
end
def fill(color, z = 0)
draw_rect(0, 0, $window.width, $window.height, color, z)
draw_rect(0, 0, window.width, window.height, color, z)
end
def lighten(color, amount = 25)
if defined?(color.alpha)
return Gosu::Color.rgba(color.red+amount, color.green+amount, color.blue+amount, color.alpha)
if color.respond_to?(:alpha)
Gosu::Color.rgba(color.red + amount, color.green + amount, color.blue + amount, color.alpha)
else
return Gosu::Color.rgb(color.red+amount, color.green+amount, color.blue+amount)
Gosu::Color.rgb(color.red + amount, color.green + amount, color.blue + amount)
end
end
def darken(color, amount = 25)
if defined?(color.alpha)
return Gosu::Color.rgba(color.red-amount, color.green-amount, color.blue-amount, color.alpha)
if color.respond_to?(:alpha)
Gosu::Color.rgba(color.red - amount, color.green - amount, color.blue - amount, color.alpha)
else
return Gosu::Color.rgb(color.red-amount, color.green-amount, color.blue-amount)
Gosu::Color.rgb(color.red - amount, color.green - amount, color.blue - amount)
end
end
def get_asset(path, hash, klass)
def opacity(color, ratio = 1.0)
alpha = 255 * ratio
Gosu::Color.rgba(color.red, color.green, color.blue, alpha)
end
def get_asset(path, hash, klass, retro = false, tileable = false)
asset = nil
hash.detect do |_asset, instance|
if _asset == path
@@ -58,28 +83,46 @@ module CyberarmEngine
end
unless asset
instance = klass.new(path)
instance = nil
instance = if klass == Gosu::Image
klass.new(path, retro: retro, tileable: tileable)
else
klass.new(path)
end
hash[path] = instance
asset = instance
end
return asset
asset
end
def get_image(path)
get_asset(path, Engine::IMAGES, Gosu::Image)
def get_image(path, retro: false, tileable: false)
get_asset(path, Window::IMAGES, Gosu::Image, retro, tileable)
end
def get_sample(path)
get_asset(path, Engine::SAMPLES, Gosu::Sample)
get_asset(path, Window::SAMPLES, Gosu::Sample)
end
def get_song(path)
get_asset(path, Engine::SONGS, Gosu::Song)
get_asset(path, Window::SONGS, Gosu::Song)
end
def window
$window
CyberarmEngine::Window.instance
end
def control_down?
Gosu.button_down?(Gosu::KB_LEFT_CONTROL) || Gosu.button_down?(Gosu::KB_RIGHT_CONTROL)
end
def shift_down?
Gosu.button_down?(Gosu::KB_LEFT_SHIFT) || Gosu.button_down?(Gosu::KB_RIGHT_SHIFT)
end
def alt_down?
Gosu.button_down?(Gosu::KB_LEFT_ALT) || Gosu.button_down?(Gosu::KB_RIGHT_ALT)
end
end
end
end

View File

@@ -0,0 +1,46 @@
module CyberarmEngine
class ConfigFile
def initialize(file:)
@file = file
if File.exist?(@file)
deserialize
else
@data = {}
end
end
def []=(*keys, value)
last_key = keys.last
if keys.size == 1
hash = @data
else
keys.pop
hash = @data[keys.shift] ||= {}
keys.each do |key|
hash = hash[key] ||= {}
end
end
hash[last_key] = value
end
def get(*keys)
@data.dig(*keys)
end
def serialize
JSON.dump(@data)
end
def deserialize
@data = JSON.parse(File.read(@file), symbolize_names: true)
end
def save!
File.open(@file, "w") { |f| f.write(serialize) }
end
end
end

View File

@@ -0,0 +1,248 @@
# frozen_string_literal: true
module CyberarmEngine
class Console
Z = 100_000
PADDING = 2
include Common
attr_reader :text_input
def initialize(font: Gosu.default_font_name)
@text_input = Gosu::TextInput.new
@width = window.width / 4 * 3
@height = window.height / 4 * 3
@input = Text.new("", x: 4, y: @height - (PADDING * 2), z: Console::Z + 1, font: font)
@input.y -= @input.height
@history = Text.new("", x: 4, z: Console::Z + 1, font: font, border: true, border_color: Gosu::Color::BLACK)
update_history_y
@command_history = []
@command_history_index = 0
@memory = ""
@background_color = Gosu::Color.rgba(0, 0, 0, 200)
@foreground_color = Gosu::Color.rgba(100, 100, 100, 100)
@input_color = Gosu::Color.rgba(100, 100, 100, 200)
@showing_cursor = false
@active_text_input = nil
@show_caret = true
@caret_last_change = Gosu.milliseconds
@caret_interval = 250
@caret_color = Gosu::Color::WHITE
@selection_color = Gosu::Color.new(0x5522ff22)
end
def draw
# Background/Border
draw_rect(0, 0, @width, @height, @background_color, Console::Z)
# Foregound/History
draw_rect(PADDING, PADDING, @width - (PADDING * 2), @height - (PADDING * 2), @foreground_color, Console::Z)
# Text bar
draw_rect(2, @input.y, @width - (PADDING * 2), @input.height, @input_color, Console::Z)
@history.draw
@input.draw
# Caret
if @show_caret
draw_rect(@input.x + caret_from_left, @input.y, Console::PADDING, @input.height, @caret_color, Console::Z + 2)
end
# Caret selection
if caret_start != caret_end
if caret_start < @text_input.selection_start
draw_rect(@input.x + caret_from_left, @input.y, caret_selection_width, @input.height, @selection_color, Console::Z)
else
draw_rect((@input.x + caret_from_left) - caret_selection_width, @input.y, caret_selection_width, @input.height, @selection_color, Console::Z)
end
end
end
def caret_from_left
return 0 if @text_input.caret_pos.zero?
@input.textobject.text_width(@text_input.text[0..@text_input.caret_pos - 1])
end
def caret_selection_width
@input.textobject.text_width(@text_input.text[caret_start..(caret_end - 1)])
end
def caret_pos
@text_input.caret_pos
end
def caret_start
@text_input.selection_start < @text_input.caret_pos ? @text_input.selection_start : @text_input.caret_pos
end
def caret_end
@text_input.selection_start > @text_input.caret_pos ? @text_input.selection_start : @text_input.caret_pos
end
def update
if Gosu.milliseconds - @caret_last_change >= @caret_interval
@caret_last_change = Gosu.milliseconds
@show_caret = !@show_caret
end
if @width != window.width || @height != @height
@width = window.width / 4 * 3
@height = window.height / 4 * 3
@input.y = @height - (PADDING * 2 + @input.height)
update_history_y
end
@input.text = @text_input.text
end
def button_down(id)
case id
when Gosu::KB_ENTER, Gosu::KB_RETURN
return unless @text_input.text.length.positive?
@history.text += "\n<c=999999>> #{@text_input.text}</c>"
@command_history << @text_input.text
@command_history_index = @command_history.size
update_history_y
handle_command
@text_input.text = ""
when Gosu::KB_UP
@command_history_index -= 1
@command_history_index = 0 if @command_history_index.negative?
@text_input.text = @command_history[@command_history_index]
when Gosu::KB_DOWN
@command_history_index += 1
if @command_history_index > @command_history.size - 1
@text_input.text = "" unless @command_history_index > @command_history.size
@command_history_index = @command_history.size
else
@text_input.text = @command_history[@command_history_index]
end
when Gosu::KB_TAB
split = @text_input.text.split(" ")
if !@text_input.text.end_with?(" ") && split.size == 1
list = abbrev_search(Console::Command.list_commands.map { |cmd| cmd.command.to_s }, @text_input.text)
if list.size == 1
@text_input.text = "#{list.first} "
elsif list.size.positive?
stdin("\n#{list.map { |cmd| Console::Style.highlight(cmd) }.join(', ')}")
end
elsif split.size.positive? && cmd = Console::Command.find(split.first)
cmd.autocomplete(self)
end
when Gosu::KB_BACKTICK
# Remove backtick character from input
@text_input.text = if @text_input.text.size > 1
@text_input.text[0..@text_input.text.size - 2]
else
""
end
# Copy
when Gosu::KB_C
if control_down? && shift_down?
@memory = @text_input.text[caret_start..caret_end - 1] if caret_start != caret_end
p @memory
elsif control_down?
@text_input.text = ""
end
# Paste
when Gosu::KB_V
if control_down? && shift_down?
string = @text_input.text.chars.insert(caret_pos, @memory).join
_caret_pos = caret_pos
@text_input.text = string
@text_input.caret_pos = _caret_pos + @memory.length
@text_input.selection_start = _caret_pos + @memory.length
end
# Cut
when Gosu::KB_X
if control_down? && shift_down?
@memory = @text_input.text[caret_start..caret_end - 1] if caret_start != caret_end
string = @text_input.text.chars
Array(caret_start..caret_end - 1).each_with_index do |i, j|
string.delete_at(i - j)
end
@text_input.text = string.join
end
# Delete word to left of caret
when Gosu::KB_W
if control_down?
split = @text_input.text.split(" ")
split.delete(split.last)
@text_input.text = split.join(" ")
end
# Clear history
when Gosu::KB_L
@history.text = "" if control_down?
end
end
def button_up(id)
end
def update_history_y
@history.y = @height - (PADDING * 2) - @input.height - (@history.text.lines.count * @history.textobject.height)
end
def handle_command
string = @text_input.text
split = string.split(" ")
command = split.first
arguments = split.length.positive? ? split[1..split.length - 1] : []
CyberarmEngine::Console::Command.use(command, arguments, self)
end
def abbrev_search(array, text)
return [] unless text.length.positive?
list = []
Abbrev.abbrev(array).each do |abbrev, value|
next unless abbrev&.start_with?(text)
list << value
end
list.uniq
end
def stdin(string)
@history.text += "\n#{string}"
update_history_y
end
def focus
@active_text_input = window.text_input
window.text_input = @text_input
@showing_cursor = window.needs_cursor
window.needs_cursor = true
@show_caret = true
@caret_last_change = Gosu.milliseconds
end
def blur
window.text_input = @active_text_input
window.needs_cursor = @showing_cursor
end
end
end

View File

@@ -0,0 +1,158 @@
# frozen_string_literal: true
module CyberarmEngine
class Console
module Style
def self.error(string)
"<c=ff5555>#{string}</c>"
end
def self.warn(string)
"<c=ff7700>#{string}</c>"
end
def self.notice(string)
"<c=55ff55>#{string}</c>"
end
def self.highlight(string, color = "5555ff")
"<c=#{color}>#{string}</c>"
end
end
class Command
def self.inherited(subclass)
@list ||= []
@commands ||= []
@list << subclass
end
def self.setup
@list ||= []
@commands = []
@list.each do |subclass|
cmd = subclass.new
if @commands.detect { |c| c.command == cmd.command }
raise "Command '#{cmd.command}' from '#{cmd.class}' already exists!"
end
@commands << cmd
end
end
def self.use(command, arguments, console)
found_command = @commands.detect { |cmd| cmd.command == command.to_sym }
if found_command
found_command.handle(arguments, console)
else
console.stdin("Command #{Style.error(command)} not found.")
end
end
def self.find(command)
@commands.detect { |cmd| cmd.command == command.to_sym }
end
def self.list_commands
@commands
end
def initialize
@store = {}
@subcommands = []
setup
end
def setup
end
def subcommand(command, type)
if @subcommands.detect { |subcmd| subcmd.command == command.to_sym }
raise "Subcommand '#{command}' for '#{self.command}' already exists!"
end
@subcommands << SubCommand.new(self, command, type)
end
def get(key)
@store[key]
end
def set(key, value)
@store[key] = value
end
def group
raise NotImplementedError
end
def command
raise NotImplementedError
end
def handle(arguments, console)
raise NotImplementedError
end
def autocomplete(console)
split = console.text_input.text.split(" ")
if @subcommands.size.positive?
if !console.text_input.text.end_with?(" ") && split.size == 2
list = console.abbrev_search(@subcommands.map { |cmd| cmd.command.to_s }, split.last)
if list.size == 1
console.text_input.text = "#{split.first} #{list.first} "
else
return unless list.size.positive?
console.stdin(list.map { |cmd| Console::Style.highlight(cmd) }.join(", ").to_s)
end
# List available options on subcommand
elsif (console.text_input.text.end_with?(" ") && split.size == 2) || !console.text_input.text.end_with?(" ") && split.size == 3
subcommand = @subcommands.detect { |cmd| cmd.command.to_s == (split[1]) }
if subcommand
if split.size == 2
console.stdin("Available options: #{subcommand.values.map { |value| Console::Style.highlight(value) }.join(',')}")
else
list = console.abbrev_search(subcommand.values, split.last)
if list.size == 1
console.text_input.text = "#{split.first} #{split[1]} #{list.first} "
elsif list.size.positive?
console.stdin("Available options: #{list.map { |value| Console::Style.highlight(value) }.join(',')}")
end
end
end
# List available subcommands if command was entered and has only a space after it
elsif console.text_input.text.end_with?(" ") && split.size == 1
console.stdin("Available subcommands: #{@subcommands.map { |cmd| Console::Style.highlight(cmd.command) }.join(', ')}")
end
end
end
def handle_subcommand(arguments, console)
if arguments.size.zero?
console.stdin(usage)
return
end
subcommand = arguments.delete_at(0)
found_command = @subcommands.detect { |cmd| cmd.command == subcommand.to_sym }
if found_command
found_command.handle(arguments, console)
else
console.stdin("Unknown subcommand #{Style.error(subcommand)} for #{Style.highlight(command)}")
end
end
def usage
raise NotImplementedError
end
end
end
end

View File

@@ -0,0 +1,43 @@
# frozen_string_literal: true
module CyberarmEngine
class Console
class HelpCommand < CyberarmEngine::Console::Command
def group
:global
end
def command
:help
end
def handle(arguments, console)
console.stdin(usage(arguments.first))
end
def autocomplete(console)
split = console.text_input.text.split(" ")
if !console.text_input.text.start_with?(" ") && split.size == 2
list = console.abbrev_search(Command.list_commands.map { |cmd| cmd.command.to_s }, split.last)
if list.size == 1
console.text_input.text = "#{split.first} #{list.first} "
elsif list.size > 1
console.stdin(list.map { |cmd| Style.highlight(cmd) }.join(", "))
end
end
end
def usage(command = nil)
if command
if cmd = Command.find(command)
cmd.usage
else
"#{Style.error(command)} is not a command"
end
else
"Available commands:\n#{Command.list_commands.map { |cmd| Style.highlight(cmd.command).to_s }.join(', ')}"
end
end
end
end
end

View File

@@ -0,0 +1,100 @@
# frozen_string_literal: true
module CyberarmEngine
class Console
class Command
class SubCommand
def initialize(parent, command, type)
@parent = parent
@command = command
@type = type
end
attr_reader :command
def handle(arguments, console)
if arguments.size > 1
console.stdin("to many arguments for #{Style.highlight(command.to_s)}, got #{Style.error(arguments.size)} expected #{Style.notice(1)}.")
return
end
case @type
when :boolean
case arguments.last
when "", nil
var = @parent.get(command.to_sym) || false
console.stdin("#{command}: #{Style.highlight(var)}")
when "on"
var = @parent.set(command.to_sym, true)
console.stdin("#{command} => #{Style.highlight(var)}")
when "off"
var = @parent.set(command.to_sym, false)
console.stdin("#{command} => #{Style.highlight(var)}")
else
console.stdin("Invalid argument for #{Style.highlight(command.to_s)}, got #{Style.error(arguments.last)} expected #{Style.notice('on')}, or #{Style.notice('off')}.")
end
when :string
case arguments.last
when "", nil
var = @parent.get(command.to_sym) || "\"\""
console.stdin("#{command}: #{Style.highlight(var)}")
else
var = @parent.set(command.to_sym, arguments.last)
console.stdin("#{command} => #{Style.highlight(var)}")
end
when :integer
case arguments.last
when "", nil
var = @parent.get(command.to_sym) || "nil"
console.stdin("#{command}: #{Style.highlight(var)}")
else
begin
var = @parent.set(command.to_sym, Integer(arguments.last))
console.stdin("#{command} => #{Style.highlight(var)}")
rescue ArgumentError
console.stdin("Error: #{Style.error("Expected an integer, got '#{arguments.last}'")}")
end
end
when :decimal
case arguments.last
when "", nil
var = @parent.get(command.to_sym) || "nil"
console.stdin("#{command}: #{Style.highlight(var)}")
else
begin
var = @parent.set(command.to_sym, Float(arguments.last))
console.stdin("#{command} => #{Style.highlight(var)}")
rescue ArgumentError
console.stdin("Error: #{Style.error("Expected a decimal or integer, got '#{arguments.last}'")}")
end
end
else
raise RuntimeError
end
end
def values
case @type
when :boolean
%w[on off]
else
[]
end
end
def usage
case @type
when :boolean
"#{Style.highlight(command)} #{Style.notice('[on|off]')}"
when :string
"#{Style.highlight(command)} #{Style.notice('[string]')}"
when :integer
"#{Style.highlight(command)} #{Style.notice('[0]')}"
when :decimal
"#{Style.highlight(command)} #{Style.notice('[0.0]')}"
end
end
end
end
end
end

View File

@@ -1,95 +0,0 @@
module CyberarmEngine
class Engine < Gosu::Window
IMAGES = {}
SAMPLES= {}
SONGS = {}
attr_accessor :show_cursor
attr_reader :current_state, :last_frame_time
def self.now
Gosu.milliseconds
end
def self.dt
$window.last_frame_time/1000.0
end
def initialize(width = 800, height = 600, fullscreen = false, update_interval = 1000.0/60)
@show_cursor = false
super(width, height, fullscreen, update_interval)
$window = self
@last_frame_time = Gosu.milliseconds-1
@current_frame_time = Gosu.milliseconds
self.caption = "CyberarmEngine #{CyberarmEngine::VERSION} #{Gosu.language}"
@states = []
setup if defined?(setup)
end
def draw
current_state.draw if current_state
end
def update
current_state.update if current_state
@last_frame_time = Gosu.milliseconds-@current_frame_time
@current_frame_time = Gosu.milliseconds
end
def needs_cursor?
@show_cursor
end
def dt
@last_frame_time/1000.0
end
def button_down(id)
current_state.button_down(id) if current_state
end
def button_up(id)
current_state.button_up(id) if current_state
end
def push_state(klass, options={})
if klass.instance_of?(klass.class) && defined?(klass.options)
@states << klass
else
@states << klass.new(options) if child_of?(klass, GameState)
@states << klass.new if child_of?(klass, Container)
end
end
private def child_of?(input, klass)
input.ancestors.detect {|c| c == klass}
end
def current_state
@states.last
end
def previous_state
if @states.size > 1 && state = @states[@states.size-2]
return state
else
return nil
end
end
def pop_state
@states.pop
end
# Sourced from https://gist.github.com/ippa/662583
def draw_circle(cx,cy,r, z = 9999,color = Gosu::Color::GREEN, step = 10)
0.step(360, step) do |a1|
a2 = a1 + step
draw_line(cx + Gosu.offset_x(a1, r), cy + Gosu.offset_y(a1, r), color, cx + Gosu.offset_x(a2, r), cy + Gosu.offset_y(a2, r), color, z)
end
end
end
end

View File

@@ -3,56 +3,62 @@ module CyberarmEngine
include Common
attr_accessor :image, :angle, :position, :velocity, :center_x, :center_y, :scale_x, :scale_y,
:color, :alpha, :mode, :options, :paused, :radius, :last_position
def initialize(options={})
if options[:auto_manage] || options[:auto_manage] == nil
$window.current_state.add_game_object(self)
end
:color, :mode, :options, :paused, :radius, :last_position
attr_reader :alpha
def initialize(options = {})
window.current_state.add_game_object(self) if options[:auto_manage] || options[:auto_manage].nil?
@options = options
@image = options[:image] ? image(options[:image]) : nil
x = options[:x] ? options[:x] : 0
y = options[:y] ? options[:y] : 0
z = options[:z] ? options[:z] : 0
x = options[:x] || 0
y = options[:y] || 0
z = options[:z] || 0
@position = Vector.new(x, y, z)
@velocity = Vector.new
@last_position = Vector.new
@angle = options[:angle] ? options[:angle] : 0
@angle = options[:angle] || 0
@center_x = options[:center_x] ? options[:center_x] : 0.5
@center_y = options[:center_y] ? options[:center_y] : 0.5
@center_x = options[:center_x] || 0.5
@center_y = options[:center_y] || 0.5
@scale_x = options[:scale_x] ? options[:scale_x] : 1
@scale_y = options[:scale_y] ? options[:scale_y] : 1
@scale_x = options[:scale_x] || 1
@scale_y = options[:scale_y] || 1
@color = options[:color] ? options[:color] : Gosu::Color.argb(0xff_ffffff)
@alpha = options[:alpha] ? options[:alpha] : 255
@mode = options[:mode] ? options[:mode] : :default
@color = options[:color] || Gosu::Color.argb(0xff_ffffff)
@alpha = options[:alpha] || 255
@mode = options[:mode] || :default
@paused = false
@speed = 0
@debug_color = Gosu::Color::GREEN
@world_center_point = Vector.new(0,0)
@world_center_point = Vector.new(0, 0)
setup
@debug_text = MultiLineText.new("", color: @debug_color, y: @position.y-(self.height*self.scale), z: 9999)
@debug_text = Text.new("", color: @debug_color, y: @position.y - (height * scale), z: 9999)
@debug_text.x = @position.x
if @radius == 0 || @radius == nil
@radius = options[:radius] ? options[:radius] : defined?(@image.width) ? ((@image.width+@image.height)/4)*scale : 1
if @radius == 0 || @radius.nil?
@radius = if options[:radius]
options[:radius]
else
@image.respond_to?(:width) ? ((@image.width + @image.height) / 4) * scale : 1
end
end
end
def draw
if @image
@image.draw_rot(@position.x, @position.y, @position.z, @angle, @center_x, @center_y, @scale_x, @scale_y, @color, @mode)
@image.draw_rot(@position.x, @position.y, @position.z, @angle, @center_x, @center_y, @scale_x, @scale_y,
@color, @mode)
end
if $debug
show_debug_heading
$window.draw_circle(@position.x, @position.y, radius, 9999, @debug_color)
Gosu.draw_circle(@position.x, @position.y, radius, 9999, @debug_color)
if @debug_text.text != ""
$window.draw_rect(@debug_text.x-10, (@debug_text.y-10), @debug_text.width+20, @debug_text.height+20, Gosu::Color.rgba(0,0,0,200), 9999)
Gosu.draw_rect(@debug_text.x - 10, (@debug_text.y - 10), @debug_text.width + 20, @debug_text.height + 20,
Gosu::Color.rgba(0, 0, 0, 200), 9999)
@debug_text.draw
end
end
@@ -63,13 +69,13 @@ module CyberarmEngine
def debug_text(text)
@debug_text.text = text
@debug_text.x = @position.x-(@debug_text.width / 2)
@debug_text.y = @position.y-(@debug_text.height + self.radius + self.height)
@debug_text.x = @position.x - (@debug_text.width / 2)
@debug_text.y = @position.y - (@debug_text.height + radius + height)
end
def scale
if @scale_x == @scale_y
return @scale_x
@scale_x
else
false
# maths?
@@ -79,7 +85,7 @@ module CyberarmEngine
def scale=(int)
self.scale_x = int
self.scale_y = int
self.radius = ((@image.width+@image.height)/4)*self.scale
self.radius = ((@image.width + @image.height) / 4) * scale
end
def visible
@@ -96,16 +102,16 @@ module CyberarmEngine
end
def _x_visible
self.x.between?(($window.width/2)-(@world_center_point.x), ($window.width/2)+@world_center_point.x) ||
self.x.between?(((@world_center_point.x)-$window.width/2), ($window.width/2)+@world_center_point.x)
x.between?((window.width / 2) - @world_center_point.x, (window.width / 2) + @world_center_point.x) ||
x.between?((@world_center_point.x - window.width / 2), (window.width / 2) + @world_center_point.x)
end
def _y_visible
self.y.between?(($window.height/2)-(@world_center_point.y), ($window.height/2)+@world_center_point.y) ||
self.y.between?((@world_center_point.y)-($window.height/2), ($window.height/2)+@world_center_point.y)
y.between?((window.height / 2) - @world_center_point.y, (window.height / 2) + @world_center_point.y) ||
y.between?(@world_center_point.y - (window.height / 2), (window.height / 2) + @world_center_point.y)
end
def heading(ahead_by = 100, object = nil, angle_only = false)
def heading(ahead_by = 100, _object = nil, angle_only = false)
direction = Gosu.angle(@last_position.x, @last_position.x, @position.x, position.y).gosu_to_radians
_x = @position.x + (ahead_by * Math.cos(direction))
@@ -121,11 +127,11 @@ module CyberarmEngine
end
def width
@image ? @image.width : 0
@image ? @image.width * scale : 0
end
def height
@image ? @image.height : 0
@image ? @image.height * scale : 0
end
def pause
@@ -137,24 +143,20 @@ module CyberarmEngine
end
def rotate(int)
self.angle+=int
self.angle%=360
self.angle += int
self.angle %= 360
end
def alpha=int # 0-255
def alpha=(int) # 0-255
@alpha = int
@alpha = 255 if @alpha > 255
@color = Gosu::Color.rgba(@color.red, @color.green, @color.blue, int)
end
def draw_rect(x, y, width, height, color, z = 0)
$window.draw_rect(x,y,width,height,color,z)
end
def button_up(id)
end
def button_down?(id)
def button_down(id)
end
def find_closest(game_object_class)
@@ -162,14 +164,14 @@ module CyberarmEngine
best_distance = 100_000_000_000 # Huge default number
game_object_class.all.each do |object|
distance = Gosu::distance(self.x, self.y, object.x, object.y)
distance = Gosu.distance(x, y, object.x, object.y)
if distance <= best_distance
best_object = object
best_distance = distance
end
end
return best_object
best_object
end
def look_at(object)
@@ -177,80 +179,66 @@ module CyberarmEngine
end
def circle_collision?(object)
distance = Gosu.distance(self.x, self.y, object.x, object.y)
if distance <= self.radius+object.radius
true
else
false
end
distance = Gosu.distance(x, y, object.x, object.y)
distance <= radius + object.radius
end
# Duplication... so DRY.
def each_circle_collision(object, resolve_with = :width, &block)
def each_circle_collision(object, _resolve_with = :width, &block)
if object.class != Class && object.instance_of?(object.class)
$window.current_state.game_objects.select {|i| i.class == object.class}.each do |o|
distance = Gosu.distance(self.x, self.y, object.x, object.y)
if distance <= self.radius+object.radius
block.call(o, object) if block
end
window.current_state.game_objects.select { |i| i.instance_of?(object.class) }.each do |o|
distance = Gosu.distance(x, y, object.x, object.y)
block.call(o, object) if distance <= radius + object.radius && block
end
else
list = $window.current_state.game_objects.select {|i| i.class == object}
list = window.current_state.game_objects.select { |i| i.instance_of?(object) }
list.each do |o|
next if self == o
distance = Gosu.distance(self.x, self.y, o.x, o.y)
if distance <= self.radius+o.radius
block.call(self, o) if block
end
distance = Gosu.distance(x, y, o.x, o.y)
block.call(self, o) if distance <= radius + o.radius && block
end
end
end
def destroy
if $window.current_state
$window.current_state.game_objects.each do |o|
if o.is_a?(self.class) && o == self
$window.current_state.game_objects.delete(o)
end
if window.current_state
window.current_state.game_objects.each do |o|
window.current_state.game_objects.delete(o) if o.is_a?(self.class) && o == self
end
end
end
# NOTE: This could be implemented more reliably
def all
INSTANCES.select {|i| i.class == self}
INSTANCES.select { |i| i.instance_of?(self) }
end
def self.each_circle_collision(object, resolve_with = :width, &block)
def self.each_circle_collision(object, _resolve_with = :width, &block)
if object.class != Class && object.instance_of?(object.class)
$window.current_state.game_objects.select {|i| i.class == self}.each do |o|
window.current_state.game_objects.select { |i| i.instance_of?(self) }.each do |o|
distance = Gosu.distance(o.x, o.y, object.x, object.y)
if distance <= o.radius+object.radius
block.call(o, object) if block
end
block.call(o, object) if distance <= o.radius + object.radius && block
end
else
lista = $window.current_state.game_objects.select {|i| i.class == self}
listb = $window.current_state.game_objects.select {|i| i.class == object}
lista = window.current_state.game_objects.select { |i| i.instance_of?(self) }
listb = window.current_state.game_objects.select { |i| i.instance_of?(object) }
lista.product(listb).each do |o, o2|
next if o == o2
distance = Gosu.distance(o.x, o.y, o2.x, o2.y)
if distance <= o.radius+o2.radius
block.call(o, o2) if block
end
block.call(o, o2) if distance <= o.radius + o2.radius && block
end
end
end
def self.destroy_all
INSTANCES.clear
if $window.current_state
$window.current_state.game_objects.each do |o|
if o.is_a?(self.class)
$window.current_state.game_objects.delete(o)
end
if window.current_state
window.current_state.game_objects.each do |o|
window.current_state.game_objects.delete(o) if o.is_a?(self.class)
end
end
end
end
end
end

View File

@@ -3,21 +3,25 @@ module CyberarmEngine
include Common
attr_accessor :options, :global_pause
attr_reader :game_objects, :containers
attr_reader :game_objects
def initialize(options={})
def initialize(options = {})
@options = options
@game_objects = []
@global_pause = false
window.text_input = nil unless options[:preserve_text_input]
@down_keys = {}
setup
end
def setup
end
# Called immediately after setup returns.
# GuiState uses this to set current_theme for ToolTip
def post_setup
end
def draw
@game_objects.each(&:draw)
end
@@ -26,45 +30,27 @@ module CyberarmEngine
@game_objects.each(&:update)
end
def draw_bounding_box(box)
x,y, max_x, max_y = box.x, box.y, box.max_x, box.max_y
color = Gosu::Color.rgba(255, 127, 64, 240)
# pipe = 4
# Gosu.draw_rect(x-width, y-height, x+(width*2), y+(height*2), color, Float::INFINITY)
# puts "BB render: #{x}:#{y} w:#{x.abs+width} h:#{y.abs+height}"
# Gosu.draw_rect(x, y, x.abs+width, y.abs+height, color, Float::INFINITY)
# TOP LEFT to BOTTOM LEFT
$window.draw_line(
x, y, color,
x, max_y, color,
Float::INFINITY
)
# BOTTOM LEFT to BOTTOM RIGHT
$window.draw_line(
x, max_y, color,
max_x, max_y, color,
Float::INFINITY
)
# BOTTOM RIGHT to TOP RIGHT
$window.draw_line(
max_x, max_y, color,
max_x, y, color,
Float::INFINITY
)
# TOP RIGHT to TOP LEFT
$window.draw_line(
max_x, y, color,
x, y, color,
Float::INFINITY
)
def needs_redraw?
true
end
def destroy
@options.clear
@game_objects.clear
def needs_repaint?
true
end
def drop(filename)
end
def gamepad_connected(index)
end
def gamepad_disconnected(index)
end
def gain_focus
end
def lose_focus
end
def button_down(id)
@@ -83,8 +69,56 @@ module CyberarmEngine
end
end
def close
window.close!
end
def draw_bounding_box(box)
x = box.x
y = box.y
max_x = box.max_x
max_y = box.max_y
color = Gosu::Color.rgba(255, 127, 64, 240)
# pipe = 4
# Gosu.draw_rect(x-width, y-height, x+(width*2), y+(height*2), color, Float::INFINITY)
# puts "BB render: #{x}:#{y} w:#{x.abs+width} h:#{y.abs+height}"
# Gosu.draw_rect(x, y, x.abs+width, y.abs+height, color, Float::INFINITY)
# TOP LEFT to BOTTOM LEFT
Gosu.draw_line(
x, y, color,
x, max_y, color,
Float::INFINITY
)
# BOTTOM LEFT to BOTTOM RIGHT
Gosu.draw_line(
x, max_y, color,
max_x, max_y, color,
Float::INFINITY
)
# BOTTOM RIGHT to TOP RIGHT
Gosu.draw_line(
max_x, max_y, color,
max_x, y, color,
Float::INFINITY
)
# TOP RIGHT to TOP LEFT
Gosu.draw_line(
max_x, y, color,
x, y, color,
Float::INFINITY
)
end
def destroy
@options.clear
@game_objects.clear
end
def add_game_object(object)
@game_objects << object
end
end
end
end

View File

@@ -1,124 +0,0 @@
module CyberarmEngine
class BoundingBox
attr_accessor :min, :max
def initialize(minx = 0, miny = 0, minz = 0, maxx = 0, maxy = 0, maxz = 0)
@min = Vector.new(minx, miny, minz)
@max = Vector.new(maxx, maxy, maxz)
end
def ==(other)
@min == other.min &&
@max == other.max
end
# returns a new bounding box that includes both bounding boxes
def union(other)
temp = BoundingBox.new
temp.min.x = [@min.x, other.min.x].min
temp.min.y = [@min.y, other.min.y].min
temp.min.z = [@min.z, other.min.z].min
temp.max.x = [@max.x, other.max.x].max
temp.max.y = [@max.y, other.max.y].max
temp.max.z = [@max.z, other.max.z].max
return temp
end
# returns the difference between both bounding boxes
def difference(other)
temp = BoundingBox.new
temp.min = @min - other.min
temp.max = @max - other.max
return temp
end
# returns whether both bounding boxes intersect
def intersect?(other)
(@min.x <= other.max.x && @max.x >= other.min.x) &&
(@min.y <= other.max.y && @max.y >= other.min.y) &&
(@min.z <= other.max.z && @max.z >= other.min.z)
end
# does this bounding box envelop other bounding box? (inclusive of border)
def contains?(other)
other.min.x >= min.x && other.min.y >= min.y && other.min.z >= min.z &&
other.max.x <= max.x && other.max.y <= max.y && other.max.z <= max.z
end
# returns whether the vector is inside of the bounding box
def point?(vector)
vector.x.between?(@min.x, @max.x) &&
vector.y.between?(@min.y, @max.y) &&
vector.z.between?(@min.z, @max.z)
end
def volume
width * height * depth
end
def width
@max.x - @min.x
end
def height
@max.y - @min.y
end
def depth
@max.z - @min.z
end
def normalize(entity)
temp = BoundingBox.new
temp.min.x = @min.x.to_f * entity.scale
temp.min.y = @min.y.to_f * entity.scale
temp.min.z = @min.z.to_f * entity.scale
temp.max.x = @max.x.to_f * entity.scale
temp.max.y = @max.y.to_f * entity.scale
temp.max.z = @max.z.to_f * entity.scale
return temp
end
def normalize_with_offset(entity)
temp = BoundingBox.new
temp.min.x = @min.x.to_f * entity.scale + entity.position.x
temp.min.y = @min.y.to_f * entity.scale + entity.position.y
temp.min.z = @min.z.to_f * entity.scale + entity.position.z
temp.max.x = @max.x.to_f * entity.scale + entity.position.x
temp.max.y = @max.y.to_f * entity.scale + entity.position.y
temp.max.z = @max.z.to_f * entity.scale + entity.position.z
return temp
end
def +(other)
box = BoundingBox.new
box.min = self.min + other.min
box.min = self.max + other.max
return box
end
def -(other)
box = BoundingBox.new
box.min = self.min - other.min
box.min = self.max - other.max
return box
end
def sum
@min.sum + @max.sum
end
def clone
BoundingBox.new(@min.x, @min.y, @min.z, @max.x, @max.y, @max.z)
end
end
end

View File

@@ -1,97 +0,0 @@
module CyberarmEngine
class Vector
def initialize(x = 0, y = 0, z = 0, weight = 0)
@x, @y, @z, @weight = x, y, z, weight
end
def x; @x; end
def x=(n); @x = n; end
def y; @y; end
def y=(n); @y = n; end
def z; @z; end
def z=(n); @z = n; end
def weight; @weight; end
def weight=(n); @weight = n; end
# def xy=(nx, ny); @x = nx; @y = ny; end
# def xyz=(nx, ny, nz); @x = nx; @y = ny; @z = nz; end
# def xyzw=(nx, ny, nz, nw); @x = nx; @y = ny; @z = nz; @weight = nw; end
def ==(other)
if other.is_a?(Numeric)
@x == other &&
@y == other &&
@z == other &&
@weight == other
else
@x == other.x &&
@y == other.y &&
@z == other.z &&
@weight == other.weight
end
end
def +(other)
Vector.new(
@x + other.x,
@y + other.y,
@z + other.z,
@weight + other.weight
)
end
def -(other)
Vector.new(
@x - other.x,
@y - other.y,
@z - other.z,
@weight - other.weight
)
end
def *(other)
Vector.new(
@x * other.x,
@y * other.y,
@z * other.z,
@weight * other.weight
)
end
def /(other)
# Endeavors to prevent division by zero
Vector.new(
@x == 0 || other.x == 0 ? 0 : @x / other.x,
@y == 0 || other.y == 0 ? 0 : @y / other.y,
@z == 0 || other.z == 0 ? 0 : @z / other.z,
@weight == 0 || other.weight == 0 ? 0 : @weight / other.weight
)
end
# returns magnitude of Vector, ignoring #weight
def magnitude
Math.sqrt((@x * @x) + (@y * @y) + (@z * @z))
end
def normalized
mag = magnitude
self / Vector.new(mag, mag, mag)
end
def sum
@x + @y + @z + @weight
end
def to_a
[@x, @y, @z, @weight]
end
def to_s
"X: #{@x}, Y: #{@y}, Z: #{@z}, Weight: #{@weight}"
end
end
end

View File

@@ -0,0 +1,213 @@
module CyberarmEngine
class Model
attr_accessor :objects, :materials, :vertices, :uvs, :texures, :normals, :faces, :colors, :bones, :material_file,
:current_material, :current_object, :vertex_count, :smoothing
attr_reader :position, :bounding_box, :textured_material, :file_path, :positions_buffer_id, :colors_buffer_id,
:normals_buffer_id, :uvs_buffer_id, :textures_buffer_id, :vertex_array_id, :aabb_tree,
:vertices_count
def initialize(file_path:)
@file_path = file_path
@material_file = nil
@current_object = nil
@current_material = nil
@vertex_count = 0
@objects = []
@materials = {}
@vertices = []
@colors = []
@uvs = []
@normals = []
@faces = []
@bones = []
@smoothing = 0
@vertices_count = 0
@bounding_box = BoundingBox.new
start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC, :float_millisecond)
type = File.basename(file_path).split(".").last.to_sym
parser = Model::Parser.find(type)
raise "Unsupported model type '.#{type}', supported models are: #{Model::Parser.supported_formats}" unless parser
parse(parser)
@vertices_count = @vertices.size
@has_texture = false
@materials.each do |_key, material|
@has_texture = true if material.texture_id
end
allocate_gl_objects
populate_vertex_buffer
configure_vao
@objects.each { |o| @vertex_count += o.vertices.size }
# start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC, :float_millisecond)
# build_collision_tree
# puts " Building mesh collision tree took #{((Process.clock_gettime(Process::CLOCK_MONOTONIC, :float_millisecond) - start_time) / 1000.0).round(2)} seconds"
end
def parse(parser)
parser.new(self).parse
end
def calculate_bounding_box(vertices, bounding_box)
unless bounding_box.min.x.is_a?(Float)
vertex = vertices.last
bounding_box.min.x = vertex.x
bounding_box.min.y = vertex.y
bounding_box.min.z = vertex.z
bounding_box.max.x = vertex.x
bounding_box.max.y = vertex.y
bounding_box.max.z = vertex.z
end
vertices.each do |vertex|
bounding_box.min.x = vertex.x if vertex.x <= bounding_box.min.x
bounding_box.min.y = vertex.y if vertex.y <= bounding_box.min.y
bounding_box.min.z = vertex.z if vertex.z <= bounding_box.min.z
bounding_box.max.x = vertex.x if vertex.x >= bounding_box.max.x
bounding_box.max.y = vertex.y if vertex.y >= bounding_box.max.y
bounding_box.max.z = vertex.z if vertex.z >= bounding_box.max.z
end
end
def allocate_gl_objects
# Allocate arrays for future use
@vertex_array_id = nil
buffer = " " * 4
glGenVertexArrays(1, buffer)
@vertex_array_id = buffer.unpack1("L2")
# Allocate buffers for future use
@positions_buffer_id = nil
buffer = " " * 4
glGenBuffers(1, buffer)
@positions_buffer_id = buffer.unpack1("L2")
@colors_buffer_id = nil
buffer = " " * 4
glGenBuffers(1, buffer)
@colors_buffer_id = buffer.unpack1("L2")
@normals_buffer_id = nil
buffer = " " * 4
glGenBuffers(1, buffer)
@normals_buffer_id = buffer.unpack1("L2")
@uvs_buffer_id = nil
buffer = " " * 4
glGenBuffers(1, buffer)
@uvs_buffer_id = buffer.unpack1("L2")
end
def populate_vertex_buffer
pos = []
colors = []
norms = []
uvs = []
@faces.each do |face|
pos << face.vertices.map { |vert| [vert.x, vert.y, vert.z] }
colors << face.colors.map { |color| [color.red, color.green, color.blue] }
norms << face.normals.map { |vert| [vert.x, vert.y, vert.z, vert.weight] }
uvs << face.uvs.map { |vert| [vert.x, vert.y, vert.z] } if has_texture?
end
glBindBuffer(GL_ARRAY_BUFFER, @positions_buffer_id)
glBufferData(GL_ARRAY_BUFFER, pos.flatten.size * Fiddle::SIZEOF_FLOAT, pos.flatten.pack("f*"), GL_STATIC_DRAW)
glBindBuffer(GL_ARRAY_BUFFER, @colors_buffer_id)
glBufferData(GL_ARRAY_BUFFER, colors.flatten.size * Fiddle::SIZEOF_FLOAT, colors.flatten.pack("f*"),
GL_STATIC_DRAW)
glBindBuffer(GL_ARRAY_BUFFER, @normals_buffer_id)
glBufferData(GL_ARRAY_BUFFER, norms.flatten.size * Fiddle::SIZEOF_FLOAT, norms.flatten.pack("f*"), GL_STATIC_DRAW)
if has_texture?
glBindBuffer(GL_ARRAY_BUFFER, @uvs_buffer_id)
glBufferData(GL_ARRAY_BUFFER, uvs.flatten.size * Fiddle::SIZEOF_FLOAT, uvs.flatten.pack("f*"), GL_STATIC_DRAW)
end
glBindBuffer(GL_ARRAY_BUFFER, 0)
end
def configure_vao
glBindVertexArray(@vertex_array_id)
gl_error?
# index, size, type, normalized, stride, pointer
# vertices (positions)
glBindBuffer(GL_ARRAY_BUFFER, @positions_buffer_id)
gl_error?
# in_position
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, nil)
gl_error?
# colors
glBindBuffer(GL_ARRAY_BUFFER, @colors_buffer_id)
# in_color
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 0, nil)
gl_error?
# normals
glBindBuffer(GL_ARRAY_BUFFER, @normals_buffer_id)
# in_normal
glVertexAttribPointer(2, 4, GL_FLOAT, GL_FALSE, 0, nil)
gl_error?
if has_texture?
# uvs
glBindBuffer(GL_ARRAY_BUFFER, @uvs_buffer_id)
# in_uv
glVertexAttribPointer(3, 3, GL_FLOAT, GL_FALSE, 0, nil)
gl_error?
end
glBindBuffer(GL_ARRAY_BUFFER, 0)
glBindVertexArray(0)
end
def build_collision_tree
@aabb_tree = IMICFPS::AABBTree.new
@faces.each do |face|
box = BoundingBox.new
box.min = face.vertices.first.dup
box.max = face.vertices.first.dup
face.vertices.each do |vertex|
if vertex.sum < box.min.sum
box.min = vertex.dup
elsif vertex.sum > box.max.sum
box.max = vertex.dup
end
end
# FIXME: Handle negatives
box.min *= 1.5
box.max *= 1.5
@aabb_tree.insert(face, box)
end
end
def has_texture?
@has_texture
end
def release_gl_resources
if @vertex_array_id
end
end
end
end

View File

@@ -0,0 +1,21 @@
module CyberarmEngine
class Model
class Material
attr_accessor :name, :ambient, :diffuse, :specular
attr_reader :texture_id
def initialize(name)
@name = name
@ambient = Color.new(1, 1, 1, 1)
@diffuse = Color.new(1, 1, 1, 1)
@specular = Color.new(1, 1, 1, 1)
@texture = nil
@texture_id = nil
end
def set_texture(texture_path)
@texture_id = Texture.new(path: texture_path).id
end
end
end
end

View File

@@ -0,0 +1,131 @@
module CyberarmEngine
class Model
class ModelObject
attr_reader :id, :name, :vertices, :uvs, :normals, :materials, :bounding_box, :debug_color
attr_accessor :faces, :scale
def initialize(id, name)
@id = id
@name = name
@vertices = []
@uvs = []
@normals = []
@faces = []
@materials = []
@bounding_box = BoundingBox.new
@debug_color = Color.new(1.0, 1.0, 1.0)
@scale = 1.0
# Faces array packs everything:
# vertex = index[0]
# uv = index[1]
# normal = index[2]
# material = index[3]
end
def has_texture?
@materials.find { |mat| mat.texture_id } ? true : false
end
def reflatten
@vertices_list = nil
@uvs_list = nil
@normals_list = nil
flattened_vertices
flattened_uvs
flattened_normals
end
def flattened_vertices
unless @vertices_list
@debug_color = @faces.first.material.diffuse
list = []
@faces.each do |face|
face.vertices.each do |v|
next unless v
list << v.x * @scale
list << v.y * @scale
list << v.z * @scale
list << v.weight
end
end
@vertices_list_size = list.size
@vertices_list = list.pack("f*")
end
@vertices_list
end
def flattened_vertices_size
@vertices_list_size
end
def flattened_uvs
unless @uvs_list
list = []
@faces.each do |face|
face.uvs.each do |v|
next unless v
list << v.x
list << v.y
list << v.z
end
end
@uvs_list_size = list.size
@uvs_list = list.pack("f*")
end
@uvs_list
end
def flattened_normals
unless @normals_list
list = []
@faces.each do |face|
face.normals.each do |n|
next unless n
list << n.x
list << n.y
list << n.z
end
end
@normals_list_size = list.size
@normals_list = list.pack("f*")
end
@normals_list
end
def flattened_materials
unless @materials_list
list = []
@faces.each do |face|
material = face.material
next unless material
face.vertices.each do # Add material to each vertex
list << material.diffuse.red
list << material.diffuse.green
list << material.diffuse.blue
# list << material.alpha
end
end
@materials_list_size = list.size
@materials_list = list.pack("f*")
end
@materials_list
end
end
end
end

View File

@@ -0,0 +1,74 @@
module CyberarmEngine
TextureCoordinate = Struct.new(:u, :v, :weight)
Point = Struct.new(:x, :y)
Color = Struct.new(:red, :green, :blue, :alpha)
Face = Struct.new(:vertices, :uvs, :normals, :colors, :material, :smoothing)
class Model
class Parser
@@parsers = []
def self.handles
raise NotImplementedError,
"Model::Parser#handles must return an array of file extensions that this parser supports"
end
def self.inherited(parser)
@@parsers << parser
end
def self.find(file_type)
@@parsers.find do |parser|
parser.handles.include?(file_type)
end
end
def self.supported_formats
@@parsers.map { |parser| parser.handles }.flatten.map { |s| ".#{s}" }.join(", ")
end
def initialize(model)
@model = model
end
def parse
end
def set_object(id: nil, name: nil)
_model = nil
if id
_model = @model.objects.find { |o| o.id == id }
elsif name
_model = @model.objects.find { |o| o.name == name }
else
raise "Must provide either an id: or name:"
end
if _model
@model.current_object = _model
else
raise "Couldn't find ModelObject!"
end
end
def change_object(id, name)
@model.objects << Model::ModelObject.new(id, name)
@model.current_object = @model.objects.last
end
def set_material(name)
@model.current_material = name
@model.current_object.materials << current_material
end
def add_material(name, material)
@model.materials[name] = material
end
def current_material
@model.materials[@model.current_material]
end
end
end
end

View File

@@ -0,0 +1,138 @@
module CyberarmEngine
class ColladaParser < Model::Parser
def self.handles
[:dae]
end
def parse
@collada = Nokogiri::XML(File.read(@model.file_path))
@collada.css("library_materials material").each do |material|
parse_material(material)
end
@collada.css("library_geometries geometry").each do |geometry|
parse_geometry(geometry)
end
@model.calculate_bounding_box(@model.vertices, @model.bounding_box)
@model.objects.each do |o|
@model.calculate_bounding_box(o.vertices, o.bounding_box)
end
end
def parse_material(material)
name = material.attributes["id"].value
effect_id = material.at_css("instance_effect").attributes["url"].value
mat = Model::Material.new(name)
effect = @collada.at_css("[id=\"#{effect_id.sub('#', '')}\"]")
emission = effect.at_css("emission color")
diffuse = effect.at_css("diffuse color").children.first.to_s.split(" ").map { |c| Float(c) }
mat.diffuse = Color.new(*diffuse[0..2])
add_material(name, mat)
end
def parse_geometry(geometry)
geometry_id = geometry.attributes["id"].value
geometry_name = geometry.attributes["name"].value
change_object(geometry_id, geometry_name)
mesh = geometry.at_css("mesh")
get_positions(geometry_id, mesh)
get_normals(geometry_id, mesh)
get_texture_coordinates(geometry_id, mesh)
project_node(geometry_name)
build_faces(geometry_id, mesh)
end
def get_positions(id, mesh)
positions = mesh.at_css("[id=\"#{id}-positions\"]")
array = positions.at_css("[id=\"#{id}-positions-array\"]")
stride = Integer(positions.at_css("[source=\"##{id}-positions-array\"]").attributes["stride"].value)
list = array.children.first.to_s.split(" ").map { |f| Float(f) }.each_slice(stride).each do |slice|
position = Vector.new(*slice)
@model.current_object.vertices << position
@model.vertices << position
end
end
def get_normals(id, mesh)
normals = mesh.at_css("[id=\"#{id}-normals\"]")
array = normals.at_css("[id=\"#{id}-normals-array\"]")
stride = Integer(normals.at_css("[source=\"##{id}-normals-array\"]").attributes["stride"].value)
list = array.children.first.to_s.split(" ").map { |f| Float(f) }.each_slice(stride).each do |slice|
normal = Vector.new(*slice)
@model.current_object.normals << normal
@model.normals << normal
end
end
def get_texture_coordinates(id, mesh)
end
def project_node(name)
@collada.css("library_visual_scenes visual_scene node").each do |node|
next unless node.attributes["name"].value == name
transform = Transform.new(node.at_css("matrix").children.first.to_s.split(" ").map { |f| Float(f) })
@model.current_object.vertices.each do |vert|
v = vert.multiply_transform(transform)
vert.x = v.x
vert.y = v.y
vert.z = v.z
vert.w = v.w
end
break
end
end
def build_faces(_id, mesh)
material_name = mesh.at_css("triangles").attributes["material"].value
set_material(material_name)
positions_index = []
normals_index = []
uvs_index = []
mesh.at_css("triangles p").children.first.to_s.split(" ").map { |i| Integer(i) }.each_slice(3).each do |slice|
positions_index << slice[0]
normals_index << slice[1]
uvs_index << slice[2]
end
norm_index = 0
positions_index.each_slice(3) do |slice|
face = Face.new
face.vertices = []
face.uvs = []
face.normals = []
face.colors = []
face.material = current_material
face.smoothing = @model.smoothing
slice.each do |index|
face.vertices << @model.vertices[index]
# face.uvs << @model.uvs[index]
face.normals << @model.normals[normals_index[norm_index]]
face.colors << current_material.diffuse
norm_index += 1
end
@model.current_object.faces << face
@model.faces << face
end
end
end
end

View File

@@ -0,0 +1,154 @@
module CyberarmEngine
class WavefrontParser < Model::Parser
def self.handles
[:obj]
end
def parse
lines = 0
list = File.read(@model.file_path).split("\n")
list.each do |line|
lines += 1
line = line.strip
array = line.split(" ")
case array[0]
when "mtllib"
@model.material_file = array[1]
parse_mtllib
when "usemtl"
set_material(array[1])
when "o"
change_object(nil, array[1])
when "s"
set_smoothing(array[1])
when "v"
add_vertex(array)
when "vt"
add_texture_coordinate(array)
when "vn"
add_normal(array)
when "f"
verts = []
uvs = []
norms = []
array[1..3].each do |f|
verts << f.split("/")[0]
uvs << f.split("/")[1]
norms << f.split("/")[2]
end
face = Face.new
face.vertices = []
face.uvs = []
face.normals = []
face.colors = []
face.material = current_material
face.smoothing = @model.smoothing
mat = face.material.diffuse
color = mat
verts.each_with_index do |v, index|
if uvs.first != ""
face.vertices << @model.vertices[Integer(v) - 1]
face.uvs << @model.uvs[Integer(uvs[index]) - 1]
face.normals << @model.normals[Integer(norms[index]) - 1]
face.colors << color
else
face.vertices << @model.vertices[Integer(v) - 1]
face.uvs << nil
face.normals << @model.normals[Integer(norms[index]) - 1]
face.colors << color
end
end
@model.current_object.faces << face
@model.faces << face
end
end
@model.calculate_bounding_box(@model.vertices, @model.bounding_box)
@model.objects.each do |o|
@model.calculate_bounding_box(o.vertices, o.bounding_box)
end
end
def parse_mtllib
file = File.open(@model.file_path.sub(File.basename(@model.file_path), "") + @model.material_file, "r")
file.readlines.each do |line|
array = line.strip.split(" ")
case array.first
when "newmtl"
material = Model::Material.new(array.last)
@model.current_material = array.last
@model.materials[array.last] = material
when "Ns" # Specular Exponent
when "Ka" # Ambient color
@model.materials[@model.current_material].ambient = Color.new(Float(array[1]), Float(array[2]),
Float(array[3]))
when "Kd" # Diffuse color
@model.materials[@model.current_material].diffuse = Color.new(Float(array[1]), Float(array[2]),
Float(array[3]))
when "Ks" # Specular color
@model.materials[@model.current_material].specular = Color.new(Float(array[1]), Float(array[2]),
Float(array[3]))
when "Ke" # Emissive
when "Ni" # Unknown (Blender Specific?)
when "d" # Dissolved (Transparency)
when "illum" # Illumination model
when "map_Kd" # Diffuse texture
texture = File.basename(array[1])
texture_path = "#{File.expand_path('../../', @model.file_path)}/textures/#{texture}"
@model.materials[@model.current_material].set_texture(texture_path)
end
end
end
def set_smoothing(value)
@model.smoothing = value == "1"
end
def add_vertex(array)
@model.vertex_count += 1
vert = nil
if array.size == 5
vert = Vector.new(Float(array[1]), Float(array[2]), Float(array[3]), Float(array[4]))
elsif array.size == 4
vert = Vector.new(Float(array[1]), Float(array[2]), Float(array[3]), 1.0)
else
raise
end
@model.current_object.vertices << vert
@model.vertices << vert
end
def add_normal(array)
vert = nil
if array.size == 5
vert = Vector.new(Float(array[1]), Float(array[2]), Float(array[3]), Float(array[4]))
elsif array.size == 4
vert = Vector.new(Float(array[1]), Float(array[2]), Float(array[3]), 1.0)
else
raise
end
@model.current_object.normals << vert
@model.normals << vert
end
def add_texture_coordinate(array)
texture = nil
if array.size == 4
texture = Vector.new(Float(array[1]), 1 - Float(array[2]), Float(array[3]))
elsif array.size == 3
texture = Vector.new(Float(array[1]), 1 - Float(array[2]), 1.0)
else
raise
end
@model.uvs << texture
@model.current_object.uvs << texture
end
end
end

View File

@@ -0,0 +1,31 @@
module CyberarmEngine
module ModelCache
CACHE = {}
def self.find_or_cache(manifest:)
model_file = manifest.file_path + "/model/#{manifest.model}"
type = File.basename(model_file).split(".").last.to_sym
if model = load_model_from_cache(type, model_file)
model
else
model = CyberarmEngine::Model.new(file_path: model_file)
cache_model(type, model_file, model)
model
end
end
def self.load_model_from_cache(type, model_file)
return CACHE[type][model_file] if CACHE[type].is_a?(Hash) && (CACHE[type][model_file])
false
end
def self.cache_model(type, model_file, model)
CACHE[type] = {} unless CACHE[type].is_a?(Hash)
CACHE[type][model_file] = model
end
end
end

View File

@@ -1,67 +0,0 @@
module CyberarmEngine
class MultiLineText
attr_accessor :options, :x, :y, :width, :height
def initialize(text, options={})
@texts = []
text.split("\n").each_with_index do |line, i|
_options = options
_options[:y]+=_options[:size]
@texts << Text.new(line, _options)
end
@options = options
@x = @texts.first ? @texts.first.x : 0
@y = @texts.first ? @texts.first.y : 0
@width = 0
@height = 0
calculate_boundry
end
def draw
@texts.each(&:draw)
end
def text
string = ""
@texts.each {|t| string << t.text}
return string
end
def text=(text)
if text.lines.count < @texts.count
range = ((@texts.count - @text.lines.count)-1)..@texts.count-1
p range
@texts.slice!(range)
end
text.split("\n").each_with_index do |line, i|
if @texts[i]
@texts[i].text = line
else
@texts << Text.new(line, @options)
end
end
self.y = @y
calculate_boundry
end
def x=(int)
@x = int
@texts.each {|t| t.x = int}
end
def y=(int)
@y = int
@texts.each_with_index {|t, i| t.y=int+(i*t.size)}
end
def calculate_boundry
@width = 0
@height= 0
@texts.each {|t| @width = t.width if t.width > @width}
@texts.each {|t| @height+=t.height}
end
end
end

View File

@@ -1,96 +0,0 @@
module CyberarmEngine
class Text
CACHE = {}
attr_accessor :text, :x, :y, :z, :size, :factor_x, :factor_y, :color, :shadow, :shadow_size, :options
attr_reader :textobject
def initialize(text, options={})
@text = text || ""
@options = options
@size = options[:size] || 18
@font = options[:font] || "sans-serif"#Gosu.default_font_name
@x = options[:x] || 0
@y = options[:y] || 0
@z = options[:z] || 1025
@factor_x = options[:factor_x] || 1
@factor_y = options[:factor_y] || 1
@color = options[:color] || Gosu::Color::WHITE
@alignment= options[:alignment] || nil
@shadow = true if options[:shadow] == true
@shadow = false if options[:shadow] == false
@shadow = true if options[:shadow] == nil
@shadow_size = options[:shadow_size] ? options[:shadow_size] : 1
@shadow_alpha= options[:shadow_alpha] ? options[:shadow_alpha] : 30
@textobject = check_cache(@size, @font)
if @alignment
case @alignment
when :left
@x = 0+BUTTON_PADDING
when :center
@x = ($window.width/2)-(@textobject.text_width(@text)/2)
when :right
@x = $window.width-BUTTON_PADDING-@textobject.text_width(@text)
end
end
return self
end
def check_cache(size, font_name)
available = false
font = nil
if CACHE[size]
if CACHE[size][font_name]
font = CACHE[size][font_name]
available = true
else
available = false
end
else
available = false
end
unless available
font = Gosu::Font.new(@size, name: @font)
CACHE[@size] = {} unless CACHE[@size].is_a?(Hash)
CACHE[@size][@font] = font
end
return font
end
def width
textobject.text_width(@text)
end
def height
textobject.height
end
def draw
if @shadow && !ARGV.join.include?("--no-shadow")
_color = Gosu::Color.rgba(@color.red, @color.green, @color.blue, @shadow_alpha) if @shadow_alpha <= @color.alpha
_color = Gosu::Color.rgba(@color.red, @color.green, @color.blue, @color.alpha) unless @shadow_alpha <= @color.alpha
@textobject.draw_markup(@text, @x-@shadow_size, @y, @z, @factor_x, @factor_y, _color)
@textobject.draw_markup(@text, @x-@shadow_size, @y-@shadow_size, @z, @factor_x, @factor_y, _color)
@textobject.draw_markup(@text, @x, @y-@shadow_size, @z, @factor_x, @factor_y, _color)
@textobject.draw_markup(@text, @x+@shadow_size, @y-@shadow_size, @z, @factor_x, @factor_y, _color)
@textobject.draw_markup(@text, @x, @y+@shadow_size, @z, @factor_x, @factor_y, _color)
@textobject.draw_markup(@text, @x-@shadow_size, @y+@shadow_size, @z, @factor_x, @factor_y, _color)
@textobject.draw_markup(@text, @x+@shadow_size, @y, @z, @factor_x, @factor_y, _color)
@textobject.draw_markup(@text, @x+@shadow_size, @y+@shadow_size, @z, @factor_x, @factor_y, _color)
end
@textobject.draw_markup(@text, @x, @y, @z, @factor_x, @factor_y, @color)
end
def update; end
end
end

View File

@@ -0,0 +1,40 @@
begin
require "opengl"
rescue LoadError
puts "Required gem is not installed, please install 'opengl-bindings' and try again."
exit(1)
end
module CyberarmEngine
def gl_error?
e = glGetError
if e != GL_NO_ERROR
warn "OpenGL error detected by handler at: #{caller[0]}"
warn " #{gluErrorString(e)} (#{e})\n"
exit if Window.instance&.exit_on_opengl_error?
end
end
def preload_default_shaders
shaders = %w[g_buffer lighting]
shaders.each do |shader|
Shader.new(
name: shader,
includes_dir: "#{CYBERARM_ENGINE_ROOT_PATH}/assets/shaders/include",
vertex: "#{CYBERARM_ENGINE_ROOT_PATH}/assets/shaders/vertex/#{shader}.glsl",
fragment: "#{CYBERARM_ENGINE_ROOT_PATH}/assets/shaders/fragment/#{shader}.glsl"
)
end
end
end
require_relative "opengl/shader"
require_relative "opengl/texture"
require_relative "opengl/light"
require_relative "opengl/perspective_camera"
require_relative "opengl/orthographic_camera"
require_relative "opengl/renderer/g_buffer"
require_relative "opengl/renderer/bounding_box_renderer"
require_relative "opengl/renderer/opengl_renderer"
require_relative "opengl/renderer/renderer"

View File

@@ -0,0 +1,50 @@
module CyberarmEngine
class Light
DIRECTIONAL = 0
POINT = 1
SPOT = 2
attr_reader :light_id
attr_accessor :type, :ambient, :diffuse, :specular, :position, :direction, :intensity
def initialize(
id:,
type: Light::POINT,
ambient: Vector.new(0.5, 0.5, 0.5),
diffuse: Vector.new(1, 1, 1),
specular: Vector.new(0.2, 0.2, 0.2),
position: Vector.new(0, 0, 0),
direction: Vector.new(0, 0, 0),
intensity: 1
)
@light_id = id
@type = type
@ambient = ambient
@diffuse = diffuse
@specular = specular
@position = position
@direction = direction
@intensity = intensity
end
def draw
glLightfv(@light_id, GL_AMBIENT, convert(@ambient).pack("f*"))
glLightfv(@light_id, GL_DIFFUSE, convert(@diffuse, true).pack("f*"))
glLightfv(@light_id, GL_SPECULAR, convert(@specular, true).pack("f*"))
glLightfv(@light_id, GL_POSITION, convert(@position).pack("f*"))
glLightModeli(GL_LIGHT_MODEL_LOCAL_VIEWER, 1)
glEnable(GL_LIGHTING)
glEnable(@light_id)
end
def convert(struct, apply_intensity = false)
if apply_intensity
struct.to_a.compact.map { |i| i * @intensity }
else
struct.to_a.compact
end
end
end
end

View File

@@ -0,0 +1,46 @@
module CyberarmEngine
class OrthographicCamera
attr_accessor :position, :orientation, :zoom, :left, :right, :bottom, :top,
:min_view_distance, :max_view_distance
def initialize(
position:, right:, top:, orientation: Vector.new(0, 0, 0),
zoom: 1, left: 0, bottom: 0,
min_view_distance: 0.1, max_view_distance: 200.0
)
@position = position
@orientation = orientation
@zoom = zoom
@left = left
@right = right
@bottom = bottom
@top = top
@min_view_distance = min_view_distance
@max_view_distance = max_view_distance
end
# Immediate mode renderering fallback
def draw
glMatrixMode(GL_PROJECTION)
glLoadIdentity
glOrtho(@left, @right, @bottom, @top, @min_view_distance, @max_view_distance)
glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST)
glRotatef(@orientation.x, 1, 0, 0)
glRotatef(@orientation.y, 0, 1, 0)
glTranslatef(-@position.x, -@position.y, -@position.z)
glMatrixMode(GL_MODELVIEW)
glLoadIdentity
end
def projection_matrix
Transform.orthographic(@left, @right, @bottom, @top, @min_view_distance, @max_view_distance)
end
def view_matrix
Transform.translate_3d(@position * -1) * Transform.rotate_3d(@orientation)
end
end
end

View File

@@ -0,0 +1,38 @@
module CyberarmEngine
class PerspectiveCamera
attr_accessor :position, :orientation, :aspect_ratio, :field_of_view,
:min_view_distance, :max_view_distance
def initialize(position:, aspect_ratio:, orientation: Vector.new(0, 0, 0),
field_of_view: 70.0, min_view_distance: 0.1, max_view_distance: 1024.0)
@position = position
@orientation = orientation
@aspect_ratio = aspect_ratio
@field_of_view = field_of_view
@min_view_distance = min_view_distance
@max_view_distance = max_view_distance
end
def draw
glMatrixMode(GL_PROJECTION)
glLoadIdentity
gluPerspective(@field_of_view, @aspect_ratio, @min_view_distance, @max_view_distance)
glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST)
glRotatef(@orientation.x, 1, 0, 0)
glRotatef(@orientation.y, 0, 1, 0)
glTranslatef(-@position.x, -@position.y, -@position.z)
glMatrixMode(GL_MODELVIEW)
glLoadIdentity
end
def projection_matrix
Transform.perspective(@field_of_view, @aspect_ratio, @min_view_distance, @max_view_distance)
end
def view_matrix
Transform.translate_3d(@position * -1) * Transform.rotate_3d(@orientation)
end
end
end

View File

@@ -0,0 +1,249 @@
module CyberarmEngine
class BoundingBoxRenderer
attr_reader :bounding_boxes, :vertex_count
def initialize
@bounding_boxes = {}
@vertex_count = 0
end
def render(entities)
entities.each do |entity|
create_bounding_box(entity, color = nil)
draw_bounding_boxes
end
(@bounding_boxes.keys - entities.map { |e| e.object_id }).each do |key|
@bounding_boxes.delete(key)
end
end
def create_bounding_box(entity, color = nil)
color ||= entity.debug_color
entity_id = entity.object_id
if @bounding_boxes[entity_id]
if @bounding_boxes[entity_id][:color] != color
@bounding_boxes[entity_id][:colors] = mesh_colors(color).pack("f*")
@bounding_boxes[entity_id][:color] = color
return
else
return
end
end
@bounding_boxes[entity_id] = {
entity: entity,
color: color,
objects: []
}
box = entity.normalize_bounding_box
normals = mesh_normals
colors = mesh_colors(color)
vertices = mesh_vertices(box)
@vertex_count += vertices.size
@bounding_boxes[entity_id][:vertices_size] = vertices.size
@bounding_boxes[entity_id][:vertices] = vertices.pack("f*")
@bounding_boxes[entity_id][:normals] = normals.pack("f*")
@bounding_boxes[entity_id][:colors] = colors.pack("f*")
entity.model.objects.each do |mesh|
data = {}
box = mesh.bounding_box.normalize(entity)
normals = mesh_normals
colors = mesh_colors(mesh.debug_color)
vertices = mesh_vertices(box)
@vertex_count += vertices.size
data[:vertices_size] = vertices.size
data[:vertices] = vertices.pack("f*")
data[:normals] = normals.pack("f*")
data[:colors] = colors.pack("f*")
@bounding_boxes[entity_id][:objects] << data
end
end
def mesh_normals
[
0, 1, 0,
0, 1, 0,
0, 1, 0,
0, 1, 0,
0, 1, 0,
0, 1, 0,
0, -1, 0,
0, -1, 0,
0, -1, 0,
0, -1, 0,
0, -1, 0,
0, -1, 0,
0, 0, 1,
0, 0, 1,
0, 0, 1,
0, 0, 1,
0, 0, 1,
0, 0, 1,
1, 0, 0,
1, 0, 0,
1, 0, 0,
1, 0, 0,
1, 0, 0,
1, 0, 0,
-1, 0, 0,
-1, 0, 0,
-1, 0, 0,
-1, 0, 0,
-1, 0, 0,
-1, 0, 0,
-1, 0, 0,
-1, 0, 0,
-1, 0, 0,
-1, 0, 0,
-1, 0, 0,
-1, 0, 0
]
end
def mesh_colors(color)
[
color.red, color.green, color.blue,
color.red, color.green, color.blue,
color.red, color.green, color.blue,
color.red, color.green, color.blue,
color.red, color.green, color.blue,
color.red, color.green, color.blue,
color.red, color.green, color.blue,
color.red, color.green, color.blue,
color.red, color.green, color.blue,
color.red, color.green, color.blue,
color.red, color.green, color.blue,
color.red, color.green, color.blue,
color.red, color.green, color.blue,
color.red, color.green, color.blue,
color.red, color.green, color.blue,
color.red, color.green, color.blue,
color.red, color.green, color.blue,
color.red, color.green, color.blue,
color.red, color.green, color.blue,
color.red, color.green, color.blue,
color.red, color.green, color.blue,
color.red, color.green, color.blue,
color.red, color.green, color.blue,
color.red, color.green, color.blue,
color.red, color.green, color.blue,
color.red, color.green, color.blue,
color.red, color.green, color.blue,
color.red, color.green, color.blue,
color.red, color.green, color.blue,
color.red, color.green, color.blue,
color.red, color.green, color.blue,
color.red, color.green, color.blue,
color.red, color.green, color.blue,
color.red, color.green, color.blue,
color.red, color.green, color.blue,
color.red, color.green, color.blue
]
end
def mesh_vertices(box)
[
box.min.x, box.max.y, box.max.z,
box.min.x, box.max.y, box.min.z,
box.max.x, box.max.y, box.min.z,
box.min.x, box.max.y, box.max.z,
box.max.x, box.max.y, box.max.z,
box.max.x, box.max.y, box.min.z,
box.max.x, box.min.y, box.min.z,
box.max.x, box.min.y, box.max.z,
box.min.x, box.min.y, box.max.z,
box.max.x, box.min.y, box.min.z,
box.min.x, box.min.y, box.min.z,
box.min.x, box.min.y, box.max.z,
box.min.x, box.max.y, box.max.z,
box.min.x, box.max.y, box.min.z,
box.min.x, box.min.y, box.min.z,
box.min.x, box.min.y, box.max.z,
box.min.x, box.min.y, box.min.z,
box.min.x, box.max.y, box.max.z,
box.max.x, box.max.y, box.max.z,
box.max.x, box.max.y, box.min.z,
box.max.x, box.min.y, box.min.z,
box.max.x, box.min.y, box.max.z,
box.max.x, box.min.y, box.min.z,
box.max.x, box.max.y, box.max.z,
box.min.x, box.max.y, box.max.z,
box.max.x, box.max.y, box.max.z,
box.max.x, box.min.y, box.max.z,
box.min.x, box.max.y, box.max.z,
box.max.x, box.min.y, box.max.z,
box.min.x, box.min.y, box.max.z,
box.max.x, box.min.y, box.min.z,
box.min.x, box.min.y, box.min.z,
box.min.x, box.max.y, box.min.z,
box.max.x, box.min.y, box.min.z,
box.min.x, box.max.y, box.min.z,
box.max.x, box.max.y, box.min.z
]
end
def draw_bounding_boxes
@bounding_boxes.each do |key, bounding_box|
glPushMatrix
glTranslatef(
bounding_box[:entity].position.x,
bounding_box[:entity].position.y,
bounding_box[:entity].position.z
)
draw_bounding_box(bounding_box)
@bounding_boxes[key][:objects].each { |o| draw_bounding_box(o) }
glPopMatrix
end
end
def draw_bounding_box(bounding_box)
glEnableClientState(GL_VERTEX_ARRAY)
glEnableClientState(GL_COLOR_ARRAY)
glEnableClientState(GL_NORMAL_ARRAY)
glVertexPointer(3, GL_FLOAT, 0, bounding_box[:vertices])
glColorPointer(3, GL_FLOAT, 0, bounding_box[:colors])
glNormalPointer(GL_FLOAT, 0, bounding_box[:normals])
glPolygonMode(GL_FRONT_AND_BACK, GL_LINE)
glDisable(GL_LIGHTING)
glDrawArrays(GL_TRIANGLES, 0, bounding_box[:vertices_size] / 3)
glEnable(GL_LIGHTING)
glPolygonMode(GL_FRONT_AND_BACK, GL_FILL)
glDisableClientState(GL_VERTEX_ARRAY)
glDisableClientState(GL_COLOR_ARRAY)
glDisableClientState(GL_NORMAL_ARRAY)
end
end
end

View File

@@ -0,0 +1,165 @@
module CyberarmEngine
class GBuffer
attr_reader :screen_vbo, :vertices, :uvs
attr_reader :width, :height
def initialize(width:, height:)
@width = width
@height = height
@framebuffer = nil
@buffers = %i[position diffuse normal texcoord]
@textures = {}
@screen_vbo = nil
@ready = false
@vertices = [
-1.0, -1.0, 0,
1.0, -1.0, 0,
-1.0, 1.0, 0,
-1.0, 1.0, 0,
1.0, -1.0, 0,
1.0, 1.0, 0
].freeze
@uvs = [
0, 0,
1, 0,
0, 1,
0, 1,
1, 0,
1, 1
].freeze
create_framebuffer
create_screen_vbo
end
def create_framebuffer
buffer = " " * 4
glGenFramebuffers(1, buffer)
@framebuffer = buffer.unpack1("L2")
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, @framebuffer)
create_textures
status = glCheckFramebufferStatus(GL_FRAMEBUFFER)
if status != GL_FRAMEBUFFER_COMPLETE
message = ""
message = case status
when GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT
"GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT"
when GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT
"GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT"
when GL_FRAMEBUFFER_INCOMPLETE_DRAW_BUFFER
"GL_FRAMEBUFFER_INCOMPLETE_DRAW_BUFFER"
when GL_FRAMEBUFFER_INCOMPLETE_READ_BUFFER
"GL_FRAMEBUFFER_INCOMPLETE_READ_BUFFER"
when GL_FRAMEBUFFER_UNSUPPORTED
"GL_FRAMEBUFFER_UNSUPPORTED"
else
"Unknown error!"
end
puts "Incomplete framebuffer: #{status}\nError: #{message}"
else
@ready = true
end
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0)
end
def create_textures
@buffers.size.times do |i|
buffer = " " * 4
glGenTextures(1, buffer)
texture_id = buffer.unpack1("L2")
@textures[@buffers[i]] = texture_id
glBindTexture(GL_TEXTURE_2D, texture_id)
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA16F, @width, @height, 0, GL_RGBA, GL_FLOAT, nil)
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR)
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR)
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0 + i, GL_TEXTURE_2D, texture_id, 0)
end
buffer = " " * 4
glGenTextures(1, buffer)
texture_id = buffer.unpack1("L2")
@textures[:depth] = texture_id
glBindTexture(GL_TEXTURE_2D, texture_id)
glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT32F, @width, @height, 0, GL_DEPTH_COMPONENT, GL_FLOAT, nil)
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D, texture_id, 0)
draw_buffers = [GL_COLOR_ATTACHMENT0, GL_COLOR_ATTACHMENT1, GL_COLOR_ATTACHMENT2, GL_COLOR_ATTACHMENT3]
glDrawBuffers(draw_buffers.size, draw_buffers.pack("I*"))
end
def create_screen_vbo
buffer = " " * 4
glGenVertexArrays(1, buffer)
@screen_vbo = buffer.unpack1("L2")
buffer = " " * 4
glGenBuffers(1, buffer)
@positions_buffer_id = buffer.unpack1("L2")
buffer = " " * 4
glGenBuffers(1, buffer)
@uvs_buffer_id = buffer.unpack1("L2")
glBindVertexArray(@screen_vbo)
glBindBuffer(GL_ARRAY_BUFFER, @positions_buffer_id)
glBufferData(GL_ARRAY_BUFFER, @vertices.size * Fiddle::SIZEOF_FLOAT, @vertices.pack("f*"), GL_STATIC_DRAW)
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, nil)
glBindBuffer(GL_ARRAY_BUFFER, @uvs_buffer_id)
glBufferData(GL_ARRAY_BUFFER, @uvs.size * Fiddle::SIZEOF_FLOAT, @uvs.pack("f*"), GL_STATIC_DRAW)
glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 0, nil)
glEnableVertexAttribArray(0)
glEnableVertexAttribArray(1)
glBindVertexArray(0)
end
def bind_for_writing
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, @framebuffer)
end
def bind_for_reading
glBindFramebuffer(GL_READ_FRAMEBUFFER, @framebuffer)
end
def set_read_buffer(buffer)
glReadBuffer(GL_COLOR_ATTACHMENT0 + @textures.keys.index(buffer))
end
def set_read_buffer_depth
glReadBuffer(GL_DEPTH_ATTACHMENT)
end
def unbind_framebuffer
glBindFramebuffer(GL_FRAMEBUFFER, 0)
end
def texture(type)
@textures[type]
end
def clean_up
glDeleteFramebuffers(1, [@framebuffer].pack("L"))
glDeleteTextures(@textures.values.size, @textures.values.pack("L*"))
glDeleteBuffers(2, [@positions_buffer_id, @uvs_buffer_id].pack("L*"))
glDeleteVertexArrays(1, [@screen_vbo].pack("L"))
gl_error?
end
end
end

View File

@@ -0,0 +1,304 @@
module CyberarmEngine
class OpenGLRenderer
@@immediate_mode_warning = false
attr_accessor :show_wireframe
attr_reader :number_of_vertices
def initialize(width:, height:, show_wireframe: false)
@width = width
@height = height
@show_wireframe = show_wireframe
@number_of_vertices = 0
@g_buffer = GBuffer.new(width: @width, height: @height)
end
def canvas_size_changed
@g_buffer.unbind_framebuffer
@g_buffer.clean_up
@g_buffer = GBuffer.new(width: @width, height: @height)
end
def render(camera, lights, entities)
@number_of_vertices = 0
glViewport(0, 0, @width, @height)
glEnable(GL_DEPTH_TEST)
if Shader.available?("g_buffer") && Shader.available?("lighting")
@g_buffer.bind_for_writing
gl_error?
glClearColor(0.0, 0.0, 0.0, 0.0)
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
Shader.use("g_buffer") do |shader|
gl_error?
entities.each do |entity|
next unless entity.visible && entity.renderable
shader.uniform_transform("projection", camera.projection_matrix)
shader.uniform_transform("view", camera.view_matrix)
shader.uniform_transform("model", entity.model_matrix)
shader.uniform_vector3("camera_position", camera.position)
gl_error?
draw_model(entity.model, shader)
entity.draw
@number_of_vertices += entity.model.vertices_count
end
end
@g_buffer.unbind_framebuffer
gl_error?
@g_buffer.bind_for_reading
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0)
lighting(lights)
gl_error?
post_processing
gl_error?
# render_framebuffer
gl_error?
@g_buffer.unbind_framebuffer
gl_error?
else
unless @@immediate_mode_warning
puts "Shaders are disabled or failed to compile, using immediate mode for rendering..."
end
@@immediate_mode_warning = true
gl_error?
lights.each(&:draw)
camera.draw
glEnable(GL_NORMALIZE)
entities.each do |entity|
next unless entity.visible && entity.renderable
glPushMatrix
glTranslatef(entity.position.x, entity.position.y, entity.position.z)
glScalef(entity.scale.x, entity.scale.y, entity.scale.z)
glRotatef(entity.orientation.x, 1.0, 0, 0)
glRotatef(entity.orientation.y, 0, 1.0, 0)
glRotatef(entity.orientation.z, 0, 0, 1.0)
gl_error?
draw_mesh(entity.model)
entity.draw
glPopMatrix
@number_of_vertices += entity.model.vertices_count
end
end
gl_error?
end
def copy_g_buffer_to_screen
@g_buffer.set_read_buffer(:position)
glBlitFramebuffer(0, 0, @g_buffer.width, @g_buffer.height,
0, 0, @g_buffer.width / 2, @g_buffer.height / 2,
GL_COLOR_BUFFER_BIT, GL_LINEAR)
@g_buffer.set_read_buffer(:diffuse)
glBlitFramebuffer(0, 0, @g_buffer.width, @g_buffer.height,
0, @g_buffer.height / 2, @g_buffer.width / 2, @g_buffer.height,
GL_COLOR_BUFFER_BIT, GL_LINEAR)
@g_buffer.set_read_buffer(:normal)
glBlitFramebuffer(0, 0, @g_buffer.width, @g_buffer.height,
@g_buffer.width / 2, @g_buffer.height / 2, @g_buffer.width, @g_buffer.height,
GL_COLOR_BUFFER_BIT, GL_LINEAR)
@g_buffer.set_read_buffer(:texcoord)
glBlitFramebuffer(0, 0, @g_buffer.width, @g_buffer.height,
@g_buffer.width / 2, 0, @g_buffer.width, @g_buffer.height / 2,
GL_COLOR_BUFFER_BIT, GL_LINEAR)
end
def lighting(lights)
Shader.use("lighting") do |shader|
glBindVertexArray(@g_buffer.screen_vbo)
glDisable(GL_DEPTH_TEST)
glEnable(GL_BLEND)
glActiveTexture(GL_TEXTURE0)
glBindTexture(GL_TEXTURE_2D, @g_buffer.texture(:diffuse))
shader.uniform_integer("diffuse", 0)
glActiveTexture(GL_TEXTURE1)
glBindTexture(GL_TEXTURE_2D, @g_buffer.texture(:position))
shader.uniform_integer("position", 1)
glActiveTexture(GL_TEXTURE2)
glBindTexture(GL_TEXTURE_2D, @g_buffer.texture(:texcoord))
shader.uniform_integer("texcoord", 2)
glActiveTexture(GL_TEXTURE3)
glBindTexture(GL_TEXTURE_2D, @g_buffer.texture(:normal))
shader.uniform_integer("normal", 3)
glActiveTexture(GL_TEXTURE4)
glBindTexture(GL_TEXTURE_2D, @g_buffer.texture(:depth))
shader.uniform_integer("depth", 4)
# FIXME: Try to figure out how to up this to 32 and/or beyond
# (currently fails with more then 7 lights passed in to shader)
lights.each_slice(7).each do |light_group|
light_group.each_with_index do |light, _i|
shader.uniform_integer("light_count", light_group.size)
shader.uniform_integer("lights[#{_i}].type", light.type)
shader.uniform_vector3("lights[#{_i}].direction", light.direction)
shader.uniform_vector3("lights[#{_i}].position", light.position)
shader.uniform_vector3("lights[#{_i}].diffuse", light.diffuse)
shader.uniform_vector3("lights[#{_i}].ambient", light.ambient)
shader.uniform_vector3("lights[#{_i}].specular", light.specular)
glDrawArrays(GL_TRIANGLES, 0, @g_buffer.vertices.size)
end
end
glBindVertexArray(0)
end
end
def post_processing
end
def render_framebuffer
if Shader.available?("lighting")
Shader.use("lighting") do |shader|
glBindVertexArray(@g_buffer.screen_vbo)
glDisable(GL_DEPTH_TEST)
glEnable(GL_BLEND)
glActiveTexture(GL_TEXTURE0)
glBindTexture(GL_TEXTURE_2D, @g_buffer.texture(:diffuse))
shader.uniform_integer("diffuse_texture", 0)
glDrawArrays(GL_TRIANGLES, 0, @g_buffer.vertices.size)
glBindVertexArray(0)
end
end
end
def draw_model(model, shader)
glBindVertexArray(model.vertex_array_id)
glEnableVertexAttribArray(0)
glEnableVertexAttribArray(1)
glEnableVertexAttribArray(2)
if model.has_texture?
glEnableVertexAttribArray(3)
glEnableVertexAttribArray(4)
end
if @show_wireframe
glLineWidth(2)
glPolygonMode(GL_FRONT_AND_BACK, GL_LINE)
Shader.active_shader.uniform_boolean("disableLighting", true)
glDrawArrays(GL_TRIANGLES, 0, model.faces.count * 3)
Shader.active_shader.uniform_boolean("disableLighting", false)
glPolygonMode(GL_FRONT_AND_BACK, GL_FILL)
glLineWidth(1)
end
offset = 0
model.objects.each do |object|
shader.uniform_boolean("has_texture", object.has_texture?)
if object.has_texture?
glBindTexture(GL_TEXTURE_2D, object.materials.find { |mat| mat.texture_id }.texture_id)
else
glBindTexture(GL_TEXTURE_2D, 0)
end
glDrawArrays(GL_TRIANGLES, offset, object.faces.count * 3)
offset += object.faces.count * 3
end
if model.has_texture?
glDisableVertexAttribArray(4)
glDisableVertexAttribArray(3)
glBindTexture(GL_TEXTURE_2D, 0)
end
glDisableVertexAttribArray(2)
glDisableVertexAttribArray(1)
glDisableVertexAttribArray(0)
glBindBuffer(GL_ARRAY_BUFFER, 0)
glBindVertexArray(0)
end
def draw_mesh(model)
model.objects.each_with_index do |o, _i|
glEnable(GL_COLOR_MATERIAL)
glColorMaterial(GL_FRONT_AND_BACK, GL_AMBIENT_AND_DIFFUSE)
glShadeModel(GL_FLAT) unless o.faces.first[4]
glShadeModel(GL_SMOOTH) if o.faces.first[4]
glEnableClientState(GL_VERTEX_ARRAY)
glEnableClientState(GL_COLOR_ARRAY)
glEnableClientState(GL_NORMAL_ARRAY)
if o.has_texture?
glEnable(GL_TEXTURE_2D)
glBindTexture(GL_TEXTURE_2D, o.materials.find { |mat| mat.texture_id }.texture_id)
glEnableClientState(GL_TEXTURE_COORD_ARRAY)
glTexCoordPointer(3, GL_FLOAT, 0, o.flattened_uvs)
end
glVertexPointer(4, GL_FLOAT, 0, o.flattened_vertices)
glColorPointer(3, GL_FLOAT, 0, o.flattened_materials)
glNormalPointer(GL_FLOAT, 0, o.flattened_normals)
if @show_wireframe # This is kinda expensive
glDisable(GL_LIGHTING)
glPolygonMode(GL_FRONT_AND_BACK, GL_LINE)
glPolygonOffset(2, 0.5)
glLineWidth(3)
glDrawArrays(GL_TRIANGLES, 0, o.flattened_vertices_size / 4)
glLineWidth(1)
glPolygonOffset(0, 0)
glPolygonMode(GL_FRONT_AND_BACK, GL_FILL)
glEnable(GL_LIGHTING)
glDrawArrays(GL_TRIANGLES, 0, o.flattened_vertices_size / 4)
else
glDrawArrays(GL_TRIANGLES, 0, o.flattened_vertices_size / 4)
end
# glBindBuffer(GL_ARRAY_BUFFER, 0)
glDisableClientState(GL_VERTEX_ARRAY)
glDisableClientState(GL_COLOR_ARRAY)
glDisableClientState(GL_NORMAL_ARRAY)
if o.has_texture?
glDisableClientState(GL_TEXTURE_COORD_ARRAY)
glDisable(GL_TEXTURE_2D)
end
glDisable(GL_COLOR_MATERIAL)
end
end
end
end

View File

@@ -0,0 +1,33 @@
module CyberarmEngine
class Renderer
attr_reader :opengl_renderer, :bounding_box_renderer
def initialize
@bounding_box_renderer = BoundingBoxRenderer.new
@opengl_renderer = OpenGLRenderer.new(width: CyberarmEngine::Window.instance.width, height: CyberarmEngine::Window.instance.height)
end
def draw(camera, lights, entities)
Stats.frame.start_timing(:opengl_renderer)
Stats.frame.start_timing(:opengl_model_renderer)
@opengl_renderer.render(camera, lights, entities)
Stats.frame.end_timing(:opengl_model_renderer)
if @show_bounding_boxes
Stats.frame.start_timing(:opengl_boundingbox_renderer)
@bounding_box_renderer.render(entities)
Stats.frame.end_timing(:opengl_boundingbox_renderer)
end
Stats.frame.end_timing(:opengl_renderer)
end
def canvas_size_changed
@opengl_renderer.canvas_size_changed
end
def finalize # cleanup
end
end
end

View File

@@ -0,0 +1,406 @@
module CyberarmEngine
# Ref: https://github.com/vaiorabbit/ruby-opengl/blob/master/sample/OrangeBook/brick.rb
class Shader
include OpenGL
@@shaders = {} # Cache for {Shader} instances
PREPROCESSOR_CHARACTER = "@".freeze # magic character for preprocessor phase of {Shader} compilation
# add instance of {Shader} to cache
#
# @param name [String]
# @param instance [Shader]
def self.add(name, instance)
@@shaders[name] = instance
end
# removes {Shader} from cache and cleans up
#
# @param name [String]
def self.delete(name)
shader = @@shaders.dig(name)
if shader
@@shaders.delete(name)
glDeleteProgram(shader.program) if shader.compiled?
end
end
##
# runs _block_ using {Shader} with _name_
#
# @example
#
# CyberarmEngine::Shader.use("blur") do |shader|
# shader.uniform_float("radius", 20.0)
# # OpenGL Code that uses shader
# end
#
# @param name [String] name of {Shader} to use
# @return [void]
def self.use(name, &block)
shader = @@shaders.dig(name)
if shader
shader.use(&block)
else
raise ArgumentError, "Shader '#{name}' not found!"
end
end
# returns whether {Shader} with _name_ is in cache
#
# @param name [String]
# @return [Boolean]
def self.available?(name)
@@shaders.dig(name).is_a?(Shader)
end
# returns instance of {Shader}, if it exists
#
# @param name [String]
# @return [Shader?]
def self.get(name)
@@shaders.dig(name)
end
# returns currently active {Shader}, if one is active
#
# @return [Shader?]
class << self
attr_reader :active_shader
end
# sets currently active {Shader}
#
# @param instance [Shader] instance of {Shader} to set as active
class << self
attr_writer :active_shader
end
# stops using currently active {Shader}
def self.stop
shader = Shader.active_shader
if shader
shader.stop
else
raise ArgumentError, "No active shader to stop!"
end
end
# returns location of OpenGL Shader uniform
#
# @param variable [String]
def self.attribute_location(variable)
raise "No active shader!" unless Shader.active_shader
Shader.active_shader.attribute_location(variable)
end
# sets _variable_ to _value_
#
# @param variable [String]
# @param value
def self.set_uniform(variable, value)
raise "No active shader!" unless Shader.active_shader
Shader.active_shader.set_uniform(variable, value)
end
attr_reader :name, :program
def initialize(name:, fragment:, includes_dir: nil, vertex: "shaders/default.vert")
raise "Shader name can not be blank" if name.length == 0
@name = name
@includes_dir = includes_dir
@compiled = false
@program = nil
@error_buffer_size = 1024 * 8
@variable_missing = {}
@data = { shaders: {} }
unless shader_files_exist?(vertex: vertex, fragment: fragment)
raise ArgumentError, "Shader files not found: #{vertex} or #{fragment}"
end
create_shader(type: :vertex, source: File.read(vertex))
create_shader(type: :fragment, source: File.read(fragment))
compile_shader(type: :vertex)
compile_shader(type: :fragment)
link_shaders
@data[:shaders].each { |_key, id| glDeleteShader(id) }
# Only add shader if it successfully compiles
if @compiled
puts "compiled!"
puts "Compiled shader: #{@name}"
Shader.add(@name, self)
else
glDeleteProgram(@program)
warn "FAILED to compile shader: #{@name}", ""
end
end
# whether vertex and fragment files exist on disk
#
# @return [Boolean]
def shader_files_exist?(vertex:, fragment:)
File.exist?(vertex) && File.exist?(fragment)
end
# creates an OpenGL Shader of _type_ using _source_
#
# @param type [Symbol] valid values are: :vertex, :fragment
# @param source [String] source code for shader
def create_shader(type:, source:)
_shader = nil
case type
when :vertex
_shader = glCreateShader(GL_VERTEX_SHADER)
when :fragment
_shader = glCreateShader(GL_FRAGMENT_SHADER)
else
raise ArgumentError, "Unsupported shader type: #{type.inspect}"
end
processed_source = preprocess_source(source: source)
_source = [processed_source].pack("p")
_size = [processed_source.length].pack("I")
glShaderSource(_shader, 1, _source, _size)
@data[:shaders][type] = _shader
end
# evaluates shader preprocessors
#
# currently supported preprocessors:
#
# @include "file/path" "another/file/path" # => Replace line with contents of file; Shader includes_dir must be specified in constructor
#
# @example
# # Example Vertex Shader #
# # #version 330 core
# # @include "material_struct"
# # void main() {
# # gl_Position = vec4(1, 1, 1, 1);
# # }
#
# Shader.new(name: "model_renderer", includes_dir: "path/to/includes", vertex: "path/to/vertex_shader.glsl")
#
# @param source shader source code
def preprocess_source(source:)
lines = source.lines
lines.each_with_index do |line, i|
next unless line.start_with?(PREPROCESSOR_CHARACTER)
preprocessor = line.strip.split(" ")
lines.delete(line)
case preprocessor.first
when "@include"
unless @includes_dir
raise ArgumentError,
"Shader preprocessor include directory was not given for shader #{@name}"
end
preprocessor[1..preprocessor.length - 1].join.scan(/"([^"]*)"/).flatten.each do |file|
source = File.read("#{@includes_dir}/#{file}.glsl")
lines.insert(i, source)
end
else
warn "Unsupported preprocessor #{preprocessor.first} for #{@name}"
end
end
lines.join
end
# compile OpenGL Shader of _type_
#
# @return [Boolean] whether compilation succeeded
def compile_shader(type:)
_compiled = false
_shader = @data[:shaders][type]
raise ArgumentError, "No shader for #{type.inspect}" unless _shader
glCompileShader(_shader)
buffer = " "
glGetShaderiv(_shader, GL_COMPILE_STATUS, buffer)
compiled = buffer.unpack1("L")
if compiled == 0
log = " " * @error_buffer_size
glGetShaderInfoLog(_shader, @error_buffer_size, nil, log)
puts "Shader Error: Program \"#{@name}\""
puts " #{type.to_s.capitalize} Shader InfoLog:", " #{log.strip.split("\n").join("\n ")}\n\n"
puts " Shader Compiled status: #{compiled}"
puts " NOTE: assignment of uniforms in shaders is illegal!"
puts
else
_compiled = true
end
_compiled
end
# link compiled OpenGL Shaders in to a OpenGL Program
#
# @note linking must succeed or shader cannot be used
#
# @return [Boolean] whether linking succeeded
def link_shaders
@program = glCreateProgram
@data[:shaders].values.each do |_shader|
glAttachShader(@program, _shader)
end
glLinkProgram(@program)
buffer = " "
glGetProgramiv(@program, GL_LINK_STATUS, buffer)
linked = buffer.unpack1("L")
if linked == 0
log = " " * @error_buffer_size
glGetProgramInfoLog(@program, @error_buffer_size, nil, log)
puts "Shader Error: Program \"#{@name}\""
puts " Program InfoLog:", " #{log.strip.split("\n").join("\n ")}\n\n"
end
@compiled = !(linked == 0)
end
# Returns the location of a uniform _variable_
#
# @param variable [String]
# @return [Integer] location of uniform
def variable(variable)
loc = glGetUniformLocation(@program, variable)
if loc == -1
unless @variable_missing[variable]
puts "Shader Error: Program \"#{@name}\" has no such uniform named \"#{variable}\"",
" Is it used in the shader? GLSL may have optimized it out.", " Is it miss spelled?"
end
@variable_missing[variable] = true
end
loc
end
# @see Shader.use Shader.use
def use(&block)
return unless compiled?
raise "Another shader is already in use! #{Shader.active_shader.name.inspect}" if Shader.active_shader
Shader.active_shader = self
glUseProgram(@program)
if block
block.call(self)
stop
end
end
# stop using shader, if shader is active
def stop
Shader.active_shader = nil if Shader.active_shader == self
glUseProgram(0)
end
# @return [Boolean] whether {Shader} successfully compiled
def compiled?
@compiled
end
# returns location of a uniform _variable_
#
# @note Use {#variable} for friendly error handling
# @see #variable Shader#variable
#
# @param variable [String]
# @return [Integer]
def attribute_location(variable)
glGetUniformLocation(@program, variable)
end
# send {Transform} to {Shader}
#
# @param variable [String]
# @param value [Transform]
# @param location [Integer]
# @return [void]
def uniform_transform(variable, value, location = nil)
attr_loc = location || attribute_location(variable)
glUniformMatrix4fv(attr_loc, 1, GL_FALSE, value.to_gl.pack("F16"))
end
# send Boolean to {Shader}
#
# @param variable [String]
# @param value [Boolean]
# @param location [Integer]
# @return [void]
def uniform_boolean(variable, value, location = nil)
attr_loc = location || attribute_location(variable)
glUniform1i(attr_loc, value ? 1 : 0)
end
# send Integer to {Shader}
# @param variable [String]
# @param value [Integer]
# @param location [Integer]
# @return [void]
def uniform_integer(variable, value, location = nil)
attr_loc = location || attribute_location(variable)
glUniform1i(attr_loc, value)
end
# send Float to {Shader}
#
# @param variable [String]
# @param value [Float]
# @param location [Integer]
# @return [void]
def uniform_float(variable, value, location = nil)
attr_loc = location || attribute_location(variable)
glUniform1f(attr_loc, value)
end
# send {Vector} (x, y, z) to {Shader}
#
# @param variable [String]
# @param value [Vector]
# @param location [Integer]
# @return [void]
def uniform_vector3(variable, value, location = nil)
attr_loc = location || attribute_location(variable)
glUniform3f(attr_loc, *value.to_a[0..2])
end
# send {Vector} to {Shader}
#
# @param variable [String]
# @param value [Vector]
# @param location [Integer]
# @return [void]
def uniform_vector4(variable, value, location = nil)
attr_loc = location || attribute_location(variable)
glUniform4f(attr_loc, *value.to_a)
end
end
end

View File

@@ -0,0 +1,69 @@
module CyberarmEngine
class Texture
DEFAULT_TEXTURE = "#{CYBERARM_ENGINE_ROOT_PATH}/assets/textures/default.png".freeze
CACHE = {}
def self.release_textures
CACHE.values.each do |id|
glDeleteTextures(id)
end
end
def self.from_cache(path, retro)
CACHE.dig("#{path}?retro=#{retro}")
end
attr_reader :id
def initialize(path: nil, image: nil, retro: false)
raise "keyword :path or :image must be provided!" if path.nil? && image.nil?
@retro = retro
@path = path
if @path
unless File.exist?(@path)
warn "Missing texture at: #{@path}"
@retro = true # override retro setting
@path = DEFAULT_TEXTURE
end
if texture = Texture.from_cache(@path, @retro)
@id = texture.id
return
end
image = load_image(@path)
@id = create_from_image(image)
else
@id = create_from_image(image)
end
end
def load_image(path)
CACHE["#{path}?retro=#{@retro}"] = self
Gosu::Image.new(path, retro: @retro)
end
def create_from_image(image)
array_of_pixels = image.to_blob
tex_names_buf = " " * 4
glGenTextures(1, tex_names_buf)
texture_id = tex_names_buf.unpack1("L2")
glBindTexture(GL_TEXTURE_2D, texture_id)
glTexImage2D(GL_TEXTURE_2D, 0, GL_SRGB_ALPHA, image.width, image.height, 0, GL_RGBA, GL_UNSIGNED_BYTE, array_of_pixels)
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT)
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT)
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST) if @retro
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR) unless @retro
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR)
glGenerateMipmap(GL_TEXTURE_2D)
gl_error?
texture_id
end
end
end

View File

@@ -0,0 +1,56 @@
module CyberarmEngine
class Ray
def initialize(origin, direction, range = Float::INFINITY)
raise "Origin must be a Vector!" unless origin.is_a?(Vector)
raise "Direction must be a Vector!" unless direction.is_a?(Vector)
@origin = origin
@direction = direction
@range = range
@inverse_direction = @direction.inverse
end
def intersect?(intersectable)
if intersectable.is_a?(BoundingBox)
intersect_bounding_box?(intersectable)
else
raise NotImplementedError, "Ray intersection test for #{intersectable.class} not implemented."
end
end
# Based on: https://tavianator.com/fast-branchless-raybounding-box-intersections/
def intersect_bounding_box?(box)
tmin = -@range
tmax = @range
tx1 = (box.min.x - @origin.x) * @inverse_direction.x
tx2 = (box.max.x - @origin.x) * @inverse_direction.x
tmin = max(tmin, min(tx1, tx2))
tmax = min(tmax, max(tx1, tx2))
ty1 = (box.min.y - @origin.y) * @inverse_direction.y
ty2 = (box.max.y - @origin.y) * @inverse_direction.y
tmin = max(tmin, min(ty1, ty2))
tmax = min(tmax, max(ty1, ty2))
tz1 = (box.min.z - @origin.z) * @inverse_direction.z
tz2 = (box.max.z - @origin.z) * @inverse_direction.z
tmin = max(tmin, min(tz1, tz2))
tmax = min(tmax, max(tz1, tz2))
tmax >= max(tmin, 0.0)
end
def min(x, y)
((x) < (y) ? x : y)
end
def max(x, y)
((x) > (y) ? x : y)
end
end
end

View File

@@ -0,0 +1,192 @@
module CyberarmEngine
class Stats
@frames = []
@frame_index = -1
@max_frame_history = 1024
def self.new_frame
if @frames.size < @max_frame_history
@frames << Frame.new
else
@frames[@frame_index] = Frame.new
end
end
def self.frame
@frames[@frame_index]
end
def self.end_frame
frame&.complete
@frame_index += 1
@frame_index %= @max_frame_history
end
def self.frames
if @frames.size < @max_frame_history
@frames
else
@frames.rotate(@frame_index - (@max_frame_history - (@frames.size - 1)))
end
end
def self.frame_index
@frame_index
end
def self.max_frame_history
@max_frame_history
end
class Frame
Timing = Struct.new(:start_time, :end_time, :duration)
attr_reader :frame_timing, :counters, :timings, :multitimings
def initialize
@frame_timing = Timing.new(Gosu.milliseconds, -1, -1)
@attempted_multitiming = false
@counters = {
gui_recalculations: 0
}
@timings = {}
@multitimings = {}
end
def increment(key, number = 1)
@counters[key] ||= 0
@counters[key] += number
end
def start_timing(key)
raise "key must be a symbol!" unless key.is_a?(Symbol)
if @timings[key]
# FIXME: Make it not spammy...
# warn "Only one timing per key per frame. (Timing for #{key.inspect} already exists!)"
@attempted_multitiming = true
@multitimings[key] = true
return
end
@timings[key] = Timing.new(Gosu.milliseconds, -1, -1)
end
def end_timing(key)
timing = @timings[key]
# FIXME: Make it not spammy...
# warn "Timing #{key.inspect} already ended!" if timing.end_time != -1
timing.end_time = Gosu.milliseconds
timing.duration = timing.end_time - timing.start_time
end
def complete
@frame_timing.end_time = Gosu.milliseconds
@frame_timing.duration = @frame_timing.end_time - @frame_timing.start_time
# Lock data structures
@frame_timing.freeze
@counters.freeze
@timings.freeze
@multitimings.freeze
end
def complete?
@frame_timing.duration != -1
end
def attempted_multitiming?
@attempted_multitiming
end
end
class StatsPlotter
attr_reader :position
def initialize(x, y, z = Float::INFINITY, width = 128, height = 128)
@position = Vector.new(x, y, z)
@width = width
@height = height
@padding = 2
@text_size = 16
@max_timing_label = CyberarmEngine::Text.new("", x: x + @padding + 1, y: y + @padding, z: z, size: @text_size, border: true)
@avg_timing_label = CyberarmEngine::Text.new("", x: x + @padding + 1, y: y + @padding + @height / 2 - @text_size / 2, z: z, size: @text_size, border: true)
@min_timing_label = CyberarmEngine::Text.new("", x: x + @padding + 1, y: y + @height - (@text_size + @padding / 2), z: z, size: @text_size, border: true)
@timings_label = CyberarmEngine::Text.new("", x: x + @padding + @width + @padding, y: y + @padding, z: z, size: @text_size, border: true)
@frame_stats = []
@graphs = {
frame_timings: []
}
end
def calculate_graphs
calculate_frame_timings_graph
end
def calculate_frame_timings_graph
@graphs[:frame_timings].clear
samples = @width - @padding
nodes = Array.new(samples.ceil) { [] }
slice = 0
@frame_stats.each_slice((CyberarmEngine::Stats.max_frame_history / samples.to_f).ceil) do |bucket|
bucket.each do |frame|
nodes[slice] << frame.frame_timing.duration
end
slice += 1
end
nodes.each_with_index do |cluster, i|
break if cluster.empty?
@graphs[:frame_timings] << CyberarmEngine::Vector.new(@position.x + @padding + 1 * i, (@position.y + @height - @padding) - cluster.max)
end
end
def draw
@frame_stats = CyberarmEngine::Stats.frames.select(&:complete?)
return if @frame_stats.empty?
calculate_graphs
@max_timing_label.text = "Max: #{@frame_stats.map { |f| f.frame_timing.duration }.max.to_s.rjust(3, " ")}ms"
@avg_timing_label.text = "Avg: #{(@frame_stats.map { |f| f.frame_timing.duration }.sum / @frame_stats.size).to_s.rjust(3, " ")}ms"
@min_timing_label.text = "Min: #{@frame_stats.map { |f| f.frame_timing.duration }.min.to_s.rjust(3, " ")}ms"
Gosu.draw_rect(@position.x, @position.y, @width, @height, 0xaa_222222, @position.z)
Gosu.draw_rect(@position.x + @padding, @position.y + @padding, @width - @padding * 2, @height - @padding * 2, 0xaa_222222, @position.z)
draw_graphs
@max_timing_label.draw
@avg_timing_label.draw
@min_timing_label.draw
# TODO: Make this optional
draw_timings
end
def draw_graphs
Gosu.draw_path(@graphs[:frame_timings], Gosu::Color::WHITE, Float::INFINITY)
end
def draw_timings
frame = @frame_stats.last
@timings_label.text = "#{frame.attempted_multitiming? ? "<c=d00>Attempted Multitiming!\nTimings may be inaccurate for:\n#{frame.multitimings.map { |m, _| m}.join("\n") }</c>\n\n" : ''}#{frame.timings.map { |t, v| "#{t}: #{v.duration}ms" }.join("\n")}"
Gosu.draw_rect(@timings_label.x - @padding, @timings_label.y - @padding, @timings_label.width + @padding * 2, @timings_label.height + @padding * 2, 0xdd_222222, @position.z)
@timings_label.draw
end
end
end
end

260
lib/cyberarm_engine/text.rb Normal file
View File

@@ -0,0 +1,260 @@
module CyberarmEngine
class Text
CACHE = {}
attr_accessor :x, :y, :z, :size, :options
attr_reader :text, :textobject, :factor_x, :factor_y, :color,
:border, :border_size, :border_alpha, :border_color,
:shadow, :shadow_size, :shadow_alpha, :shadow_color
def initialize(text, options = {})
@text = text.to_s || ""
@options = options
@size = options[:size] || 18
@font = options[:font] || Gosu.default_font_name
@x = options[:x] || 0
@y = options[:y] || 0
@z = options[:z] || 1025
@factor_x = options[:factor_x] || 1
@factor_y = options[:factor_y] || 1
if options[:color]
@color = options[:color].is_a?(Gosu::Color) ? options[:color] : Gosu::Color.new(options[:color])
else
@color = Gosu::Color::WHITE
end
@mode = options[:mode] || :default
@alignment = options[:alignment] || nil
@border = options[:border]
@border = true if options[:border].nil?
@border_size = options[:border_size] || 1
@border_alpha = options[:border_alpha] || 30
@border_color = options[:border_color] || Gosu::Color::BLACK
@shadow = options[:shadow]
@shadow_size = options[:shadow_size] || 2
@shadow_alpha = options[:shadow_alpha] || 30
@shadow_color = options[:shadow_color] || Gosu::Color::BLACK
@static = options[:static] || (options[:static].nil? || options[:static] == false ? false : true)
@textobject = check_cache(@size, @font)
if @alignment
case @alignment
when :left
@x = 0 + BUTTON_PADDING
when :center
@x = (CyberarmEngine::Window.instance.width / 2) - (@textobject.text_width(@text) / 2)
when :right
@x = CyberarmEngine::Window.instance.width - BUTTON_PADDING - @textobject.text_width(@text)
end
end
end
def check_cache(size, font_name)
available = false
font = nil
if CACHE[size]
if CACHE[size][font_name]
font = CACHE[size][font_name]
available = true
else
available = false
end
else
available = false
end
unless available
font = Gosu::Font.new(@size, name: @font)
CACHE[@size] = {} unless CACHE[@size].is_a?(Hash)
CACHE[@size][@font] = font
end
font
end
def swap_font(size, font_name = @font)
if @size != size || @font != font_name
@size = size
@font = font_name
invalidate_cache!
@textobject = check_cache(size, font_name)
end
end
def text=(string)
invalidate_cache! if @text != string
@text = string
end
def factor_x=(n)
invalidate_cache! if @factor_x != n
@factor_x = n
end
def factor_y=(n)
invalidate_cache! if @factor_y != n
@factor_y = n
end
def color=(color)
old_color = @color
if color
@color = color.is_a?(Gosu::Color) ? color : Gosu::Color.new(color)
else
raise "color cannot be nil"
end
invalidate_cache! if old_color != color
end
def border=(boolean)
invalidate_cache! if @border != boolean
@border = boolean
end
def border_size=(n)
invalidate_cache! if @border_size != n
@border_size = n
end
def border_alpha=(n)
invalidate_cache! if @border_alpha != n
@border_alpha = n
end
def border_color=(n)
invalidate_cache! if @border_color != n
@border_color = n
end
def width(text = @text)
markup_width(text)
end
def text_width(text = @text)
spacing = 0
spacing += @border_size if @border
spacing += @shadow_size if @shadow
if text == @text && @static && @gosu_cached_text_image
@gosu_cached_text_image&.width + spacing
else
textobject.text_width(text) + spacing
end
end
def markup_width(text = @text)
text = text.to_s
spacing = 0
spacing += @border_size if @border
spacing += @shadow_size if @shadow
if text == @text && @static && @gosu_cached_text_image
@gosu_cached_text_image&.width + spacing
else
textobject.markup_width(text) + spacing
end
end
def height(text = @text)
if text.lines.count > 0
text.lines.count * textobject.height + @border_size + @shadow_size
else
@textobject.height + @border_size + @shadow_size
end
end
def draw(method = :draw_markup)
if @static
if @border && !@cached_text_border_image
_x = @border_size
_y = @border_size
_width = method == :draw_markup ? text_width : markup_width
img = Gosu::Image.send(:"from_#{method.to_s.split("_").last}", @text, @size, font: @font)
@cached_text_border_image = Gosu.render((_width + (@border_size * 2)).ceil, (height + (@border_size * 2)).ceil) do
img.draw(-_x, 0, @z, @factor_x, @factor_y, @border_color, @mode)
img.draw(-_x, -_y, @z, @factor_x, @factor_y, @border_color, @mode)
img.draw(0, -_y, @z, @factor_x, @factor_y, @border_color, @mode)
img.draw(_x, -_y, @z, @factor_x, @factor_y, @border_color, @mode)
img.draw(_x, 0, @z, @factor_x, @factor_y, @border_color, @mode)
img.draw(_x, _y, @z, @factor_x, @factor_y, @border_color, @mode)
img.draw(0, _y, @z, @factor_x, @factor_y, @border_color, @mode)
img.draw(-_x, _y, @z, @factor_x, @factor_y, @border_color, @mode)
end
end
@cached_text_shadow_image ||= Gosu::Image.send(:"from_#{method.to_s.split("_").last}", @text, @size, font: @font) if @shadow
@gosu_cached_text_image ||= Gosu::Image.send(:"from_#{method.to_s.split("_").last}", @text, @size, font: @font)
@cached_text_border_image.draw(@x, @y, @z, @factor_x, @factor_y, @border_color, @mode) if @border
@cached_text_shadow_image.draw(@x + @shadow_size, @y + @shadow_size, @z, @factor_x, @factor_y, @shadow_color, @mode) if @shadow
@gosu_cached_text_image.draw(@x, @y, @z, @factor_x, @factor_y, @color, @mode)
else
if @border && !ARGV.join.include?("--no-border")
border_alpha = @color.alpha <= 30 ? @color.alpha : @border_alpha
border_color = @border_color || Gosu::Color.rgba(@color.red, @color.green, @color.blue,
border_alpha)
white = Gosu::Color::WHITE
_x = @border_size
_y = @border_size
_width = method == :draw_markup ? text_width : markup_width
@cached_text_border_image ||= Gosu.render((_width + (border_size * 2)).ceil, (height + (@border_size * 2)).ceil) do
@textobject.send(method, @text, _x - @border_size, _y, @z, @factor_x, @factor_y, white, @mode)
@textobject.send(method, @text, _x - @border_size, _y - @border_size, @z, @factor_x, @factor_y, white, @mode)
@textobject.send(method, @text, _x, _y - @border_size, @z, @factor_x, @factor_y, white, @mode)
@textobject.send(method, @text, _x + @border_size, _y - @border_size, @z, @factor_x, @factor_y, white, @mode)
@textobject.send(method, @text, _x, _y + @border_size, @z, @factor_x, @factor_y, white, @mode)
@textobject.send(method, @text, _x - @border_size, _y + @border_size, @z, @factor_x, @factor_y, white, @mode)
@textobject.send(method, @text, _x + @border_size, _y, @z, @factor_x, @factor_y, white, @mode)
@textobject.send(method, @text, _x + @border_size, _y + @border_size, @z, @factor_x, @factor_y, white, @mode)
end
@cached_text_border_image.draw(@x - @border_size, @y - @border_size, @z, @factor_x, @factor_y, border_color)
end
if @shadow
shadow_color = @shadow_color || Gosu::Color.rgba(@color.red, @color.green, @color.blue, @shadow_alpha)
@textobject.send(method, @text, @x + @shadow_size, @y + @shadow_size, @z, @factor_x, @factor_y, shadow_color, @mode)
end
@textobject.send(method, @text, @x, @y, @z, @factor_x, @factor_y, @color, @mode)
end
end
def alpha=(n)
@color = Gosu::Color.rgba(@color.red, @color.green, @color.blue, n)
end
def alpha
@color.alpha
end
def update
end
def invalidate_cache!
@cached_text_border_image = nil
@cached_text_shadow_image = nil
@gosu_cached_text_image = nil
end
end
end

View File

@@ -20,4 +20,4 @@ module CyberarmEngine
end
end
end
end
end

View File

@@ -0,0 +1,296 @@
module CyberarmEngine
# Basic 4x4 matrix operations
class Transform
attr_reader :elements
def initialize(matrix)
@elements = matrix
raise "Transform is wrong size! Got #{@elements.size}, expected 16" if 16 != @elements.size
raise "Invalid value for matrix, must all be numeric!" if @elements.any? { |e| e.nil? || !e.is_a?(Numeric) }
end
def self.identity
Transform.new(
[
1, 0, 0, 0,
0, 1, 0, 0,
0, 0, 1, 0,
0, 0, 0, 1
]
)
end
### 2D Operations meant for interacting with Gosu ###
# 2d rotate operation, replicates Gosu's Gosu.rotate function
def self.rotate(angle, rotate_around = nil)
double c = Math.cos(angle).degrees_to_radians
double s = Math.sin(angle).degrees_to_radians
matrix = [
+c, +s, 0, 0,
-s, +c, 0, 0,
0, 0, 1, 0,
0, 0, 0, 1
]
rotate_matrix = Transform.new(matrix, rows: 4, columns: 4)
if rotate_around && (rotate_around.x != 0.0 || rotate_around.y != 0.0)
negative_rotate_around = Vector.new(-rotate_around.x, -rotate_around.y, -rotate_around.z)
rotate_matrix = concat(
concat(translate(negative_rotate_around), rotate_matrix),
translate(rotate_around)
)
end
rotate_matrix
end
# 2d translate operation, replicates Gosu's Gosu.translate function
def self.translate(vector)
x, y, z = vector.to_a[0..2]
matrix = [
1, 0, 0, 0,
0, 1, 0, 0,
0, 0, 1, 0,
x, y, z, 1
]
Transform.new(matrix)
end
# 2d scale operation, replicates Gosu's Gosu.rotate function
def self.scale(vector, center_around = nil)
scale_x, scale_y, scale_z = vector.to_a[0..2]
matrix = [
scale_x, 0, 0, 0,
0, scale_y, 0, 0,
0, 0, scale_z, 0,
0, 0, 0, 1
]
scale_matrix = Transform.new(matrix)
if center_around && (center_around.x != 0.0 || center_around.y != 0.0)
negative_center_around = Vector.new(-center_around.x, -center_around.y, -center_around.z)
scale_matrix = concat(
concat(translate(negative_center_around), scale_matrix),
translate(center_around)
)
end
scale_matrix
end
def self.concat(left, right)
matrix = Array.new(left.elements.size)
rows = 4
matrix.size.times do |i|
matrix[i] = 0
rows.times do |j|
matrix[i] += left.elements[i / rows * rows + j] * right.elements[i % rows + j * rows]
end
end
Transform.new(matrix)
end
#### 3D Operations meant for OpenGL ###
def self.translate_3d(vector)
x, y, z = vector.to_a[0..2]
matrix = [
1, 0, 0, x,
0, 1, 0, y,
0, 0, 1, z,
0, 0, 0, 1
]
Transform.new(matrix)
end
def self.rotate_3d(vector, _order = "zyx")
x, y, z = vector.to_a[0..2].map { |axis| axis * Math::PI / 180.0 }
rotation_x = Transform.new(
[
1, 0, 0, 0,
0, Math.cos(x), -Math.sin(x), 0,
0, Math.sin(x), Math.cos(x), 0,
0, 0, 0, 1
]
)
rotation_y = Transform.new(
[
Math.cos(y), 0, Math.sin(y), 0,
0, 1, 0, 0,
-Math.sin(y), 0, Math.cos(y), 0,
0, 0, 0, 1
]
)
rotation_z = Transform.new(
[
Math.cos(z), -Math.sin(z), 0, 0,
Math.sin(z), Math.cos(z), 0, 0,
0, 0, 1, 0,
0, 0, 0, 1
]
)
rotation_z * rotation_y * rotation_x
end
# Implements glRotatef
# https://www.khronos.org/registry/OpenGL-Refpages/gl2.1/xhtml/glRotate.xml
def self.rotate_gl(angle, axis)
radians = angle * Math::PI / 180.0
s = Math.sin(radians)
c = Math.cos(radians)
axis = axis.normalized
x, y, z = axis.to_a[0..2]
n = (1.0 - c)
Transform.new(
[
x * x * n + c, x * y * n - z * s, x * z * n + y * s, 0,
y * x * n + z * s, y * y * n + c, y * z * n - x * s, 0,
x * z * n - y * s, y * z * n + x * s, z * z * n + c, 0,
0, 0, 0, 1.0
]
)
end
def self.scale_3d(vector)
x, y, z = vector.to_a[0..2]
Transform.new(
[
x, 0, 0, 0,
0, y, 0, 0,
0, 0, z, 0,
0, 0, 0, 1
]
)
end
def self.perspective(fov_y, aspect_ratio, near, far)
f = 1.0 / Math.tan(fov_y.degrees_to_radians / 2.0) # cotangent
zn = (far + near.to_f) / (near - far.to_f)
zf = (2.0 * far * near.to_f) / (near - far.to_f)
Transform.new(
[
f / aspect_ratio, 0.0, 0.0, 0.0,
0.0, f, 0.0, 0.0,
0.0, 0.0, zn, zf,
0.0, 0.0, -1.0, 0.0
]
)
end
def self.orthographic(left, right, bottom, top, near, far)
s = Vector.new(
2 / (right - left.to_f),
2 / (top - bottom.to_f),
-2 / (far - near.to_f)
)
t = Vector.new(
(right + left.to_f) / (right - left.to_f),
(top + bottom.to_f) / (top - bottom.to_f),
(far + near.to_f) / (far - near.to_f)
)
Transform.new(
[
s.x, 0.0, 0.0, t.x,
0.0, s.y, 0.0, t.y,
0.0, 0.0, s.z, t.z,
0.0, 0.0, 0.0, 1.0
]
)
end
def self.view(eye, orientation)
# https://www.3dgep.com/understanding-the-view-matrix/#The_View_Matrix
cosPitch = Math.cos(orientation.z * Math::PI / 180.0)
sinPitch = Math.sin(orientation.z * Math::PI / 180.0)
cosYaw = Math.cos(orientation.y * Math::PI / 180.0)
sinYaw = Math.sin(orientation.y * Math::PI / 180.0)
x_axis = Vector.new(cosYaw, 0, -sinYaw)
y_axis = Vector.new(sinYaw * sinPitch, cosPitch, cosYaw * sinPitch)
z_axis = Vector.new(sinYaw * cosPitch, -sinPitch, cosPitch * cosYaw)
Transform.new(
[
x_axis.x, y_axis.y, z_axis.z, 0,
x_axis.x, y_axis.y, z_axis.z, 0,
x_axis.x, y_axis.y, z_axis.z, 0,
-x_axis.dot(eye), -y_axis.dot(eye), -z_axis.dot(eye), 1
]
)
end
def *(other)
case other
when CyberarmEngine::Vector
matrix = @elements.clone
list = other.to_a
@elements.each_with_index do |e, i|
matrix[i] = e + list[i % 4]
end
Transform.new(matrix)
when CyberarmEngine::Transform
multiply_matrices(other)
else
p other.class
raise TypeError, "Expected CyberarmEngine::Vector or CyberarmEngine::Transform got #{other.class}"
end
end
def get(x, y)
width = 4
# puts "Transform|#{self.object_id} -> #{@elements[width * y + x].inspect} (index: #{width * y + x})"
@elements[width * y + x]
end
def multiply_matrices(other)
matrix = Array.new(16, 0)
4.times do |x|
4.times do |y|
4.times do |k|
matrix[4 * y + x] += get(x, k) * other.get(k, y)
end
end
end
Transform.new(matrix)
end
# arranges Matrix in column major form
def to_gl
e = @elements
[
e[0], e[4], e[8], e[12],
e[1], e[5], e[9], e[13],
e[2], e[6], e[10], e[14],
e[3], e[7], e[11], e[15]
]
end
end
end

View File

@@ -1,6 +1,7 @@
module CyberarmEngine
class BorderCanvas
attr_reader :element, :top, :right, :bottom, :left
def initialize(element:)
@element = element
@@ -25,7 +26,7 @@ module CyberarmEngine
elsif color.is_a?(Array)
if color.size == 1
color=color.first
color = color.first
elsif color.size == 2
@top.background = color.first
@@ -61,36 +62,36 @@ module CyberarmEngine
def update
# TOP
@top.x = @element.x# + @element.border_thickness_left
@top.x = @element.x + @element.style.border_thickness_left
@top.y = @element.y
@top.z = @element.z
@top.width = @element.width
@top.height = @element.border_thickness_top
@top.width = @element.width - @element.style.border_thickness_left
@top.height = @element.style.border_thickness_top
# RIGHT
@right.x = @element.x + @element.width
@right.y = @element.y + @element.border_thickness_top
@right.y = @element.y + @element.style.border_thickness_top
@right.z = @element.z
@right.width = -@element.border_thickness_right
@right.height = @element.height - @element.border_thickness_top
@right.width = -@element.style.border_thickness_right
@right.height = @element.height - @element.style.border_thickness_top
# BOTTOM
@bottom.x = @element.x
@bottom.y = @element.y + @element.height
@bottom.z = @element.z
@bottom.width = @element.width - @element.border_thickness_right
@bottom.height = -@element.border_thickness_bottom
@bottom.width = @element.width - @element.style.border_thickness_right
@bottom.height = -@element.style.border_thickness_bottom
# LEFT
@left.x = @element.x
@left.y = @element.y
@left.z = @element.z
@left.width = @element.border_thickness_left
@left.height = @element.height - @element.border_thickness_bottom
@left.width = @element.style.border_thickness_left
@left.height = @element.height - @element.style.border_thickness_bottom
@top.update
@right.update
@@ -98,4 +99,4 @@ module CyberarmEngine
@left.update
end
end
end
end

View File

@@ -1,53 +0,0 @@
module CyberarmEngine
class Button < Label
def initialize(text, options = {}, block = nil)
super(text, options, block)
@background_canvas.background = default(:background)
end
def render
draw_text
end
def draw_text
@text.draw
end
def enter(sender)
@focus = false unless window.button_down?(Gosu::MsLeft)
if @focus
@background_canvas.background = default(:active, :background)
@text.color = default(:active, :color)
else
@background_canvas.background = default(:hover, :background)
@text.color = default(:hover, :color)
end
end
def left_mouse_button(sender, x, y)
@focus = true
@background_canvas.background = default(:active, :background)
window.current_state.focus = self
@text.color = default(:active, :color)
end
def released_left_mouse_button(sender,x, y)
enter(sender)
end
def clicked_left_mouse_button(sender, x, y)
@block.call(self) if @block
end
def leave(sender)
@background_canvas.background = default(:background)
@text.color = default(:color)
end
def blur(sender)
@focus = false
end
end
end

View File

@@ -1,52 +0,0 @@
module CyberarmEngine
class CheckBox < Flow
def initialize(text, options, block = nil)
super({}, block = nil)
options[:toggled] = options[:checked]
@toggle_button = ToggleButton.new(options)
@label = Label.new(text, options)
define_label_singletons
add(@toggle_button)
add(@label)
@width = @toggle_button.width + @label.width
@height = @toggle_button.height + @label.height
end
def text=(text)
@label.text = text
recalculate
end
def define_label_singletons
@label.define_singleton_method(:_toggle_button) do |button|
@_toggle_button = button
end
@label._toggle_button(@toggle_button)
@label.define_singleton_method(:holding_left_mouse_button) do |sender, x, y|
@_toggle_button.left_mouse_button(sender, x, y)
end
@label.define_singleton_method(:released_left_mouse_button) do |sender, x, y|
@_toggle_button.released_left_mouse_button(sender, x, y)
end
@label.define_singleton_method(:clicked_left_mouse_button) do |sender, x, y|
@_toggle_button.clicked_left_mouse_button(sender, x, y)
end
@label.define_singleton_method(:enter) do |sender|
@_toggle_button.enter(sender)
end
@label.define_singleton_method(:leave) do |sender|
@_toggle_button.leave(sender)
end
end
end
end

View File

@@ -1,154 +0,0 @@
module CyberarmEngine
class Container < Element
include Common
attr_accessor :stroke_color, :fill_color
attr_reader :children
attr_reader :scroll_x, :scroll_y
def initialize(options = {}, block = nil)
super
@scroll_x, @scroll_y = 0, 0
@scroll_speed = 10
@text_color = options[:color]
@children = []
@theme = {}
end
def build
@theme.merge(@parent.theme) if @parent
@block.call(self) if @block
recalculate
end
def add(element)
@children << element
recalculate
end
def render
@children.each(&:draw)
end
def update
@children.each(&:update)
end
def theme
@theme
end
def color(color)
@theme[:color] = color
end
def hit_element?(x, y)
@children.reverse_each do |child|
case child
when Container
if element = child.hit_element?(x, y)
return element
end
else
return child if child.hit?(x, y)
end
end
self if hit?(x, y)
end
def recalculate
@current_position = Vector.new(@margin_left, @margin_top)
layout
@width = @max_width ? @max_width : (@children.map {|c| c.x + c.width + c.margin_right }.max || 0).round
@height = @max_height ? @max_height : (@children.map {|c| c.y + c.height + c.margin_bottom}.max || 0).round
# Move child to parent after positioning
@children.each do |child|
child.x += @x
child.y += @y
# Fix child being displaced
child.recalculate
end
update_background
end
def layout
raise "Not overridden"
end
def max_width
@max_width ? @max_width : window.width - (@parent ? @parent.margin_right + @margin_right : @margin_right)
end
def fits_on_line?(element) # Flow
@current_position.x + element.outer_width <= max_width &&
@current_position.x + element.outer_width <= window.width
end
def position_on_current_line(element) # Flow
element.x = element.margin_left + @current_position.x
element.y = element.margin_top + @current_position.y
element.recalculate
@current_position.x += element.outer_width
@current_position.x = @margin_left if @current_position.x >= max_width
end
def tallest_neighbor(querier, y_position) # Flow
response = querier
@children.each do |child|
response = child if child.outer_height > response.outer_height
break if child == querier
end
return response
end
def position_on_next_line(child) # Flow
@current_position.x = @margin_left
@current_position.y += tallest_neighbor(child, @current_position.y).outer_height
child.x = child.margin_left + @current_position.x
child.y = child.margin_top + @current_position.y
child.recalculate
@current_position.x += child.outer_width
end
def move_to_next_line(element) # Stack
element.x = element.margin_left + @current_position.x
element.y = element.margin_top + @current_position.y
element.recalculate
@current_position.y += element.outer_height
end
# def mouse_wheel_up(sender, x, y)
# @children.each {|c| c.y -= @scroll_speed}
# @children.each {|c| c.recalculate}
# end
# def mouse_wheel_down(sender, x, y)
# @children.each {|c| c.y += @scroll_speed}
# @children.each {|c| c.recalculate}
# end
def value
@children.map {|c| c.class}.join(", ")
end
end
end

View File

@@ -1,82 +1,139 @@
module CyberarmEngine
module DSL
def flow(options = {}, &block)
options[:parent] = @containers.last
_container = Flow.new(options, block)
@containers << _container
_container.build
options[:parent].add(_container)
@containers.pop
return _container
container(CyberarmEngine::Element::Flow, options, &block)
end
def stack(options = {}, &block)
options[:parent] = @containers.last
_container = Stack.new(options, block)
@containers << _container
_container.build
options[:parent].add(_container)
@containers.pop
return _container
container(CyberarmEngine::Element::Stack, options, &block)
end
# TODO: Remove in version 0.16.0+
def label(text, options = {}, &block)
options[:parent] = @containers.last
_element = Label.new(text, options, block)
@containers.last.add(_element)
options[:parent] = element_parent
options[:theme] = current_theme
return _element
add_element(Element::TextBlock.new(text, options, block))
end
[
"Banner",
"Title",
"Subtitle",
"Tagline",
"Caption",
"Para",
"Inscription",
"Link"
].each do |const|
define_method(:"#{const.downcase}") do |text, options = {}, &block|
options[:parent] = element_parent
options[:theme] = current_theme
add_element(Element.const_get(const).new(text, options, block))
end
end
def button(text, options = {}, &block)
options[:parent] = @containers.last
_element = Button.new(text, options, block) { if block.is_a?(Proc); block.call; end }
@containers.last.add(_element)
options[:parent] = element_parent
options[:theme] = current_theme
return _element
add_element(Element::Button.new(text, options, block) { block.call if block.is_a?(Proc) })
end
def list_box(options = {}, &block)
options[:parent] = element_parent
options[:theme] = current_theme
add_element(Element::ListBox.new(options, block) { block.call if block.is_a?(Proc) })
end
def edit_line(text, options = {}, &block)
options[:parent] = @containers.last
_element = EditLine.new(text, options, block)
@containers.last.add(_element)
options[:parent] = element_parent
options[:theme] = current_theme
return _element
add_element(Element::EditLine.new(text, options, block))
end
def edit_box(text, options = {}, &block)
options[:parent] = element_parent
options[:theme] = current_theme
add_element(Element::EditBox.new(text, options, block))
end
def toggle_button(options = {}, &block)
options[:parent] = @containers.last
_element = ToggleButton.new(options, block)
@containers.last.add(_element)
options[:parent] = element_parent
options[:theme] = current_theme
return _element
add_element(Element::ToggleButton.new(options, block))
end
def check_box(text, options = {}, &block)
options[:parent] = @containers.last
_element = CheckBox.new(text, options, block)
@containers.last.add(_element)
options[:parent] = element_parent
options[:theme] = current_theme
return _element
add_element(Element::CheckBox.new(text, options, block))
end
def image(path, options = {}, &block)
options[:parent] = @containers.last
_element = Image.new(path, options, block)
@containers.last.add(_element)
options[:parent] = element_parent
options[:theme] = current_theme
return _element
add_element(Element::Image.new(path, options, block))
end
def progress(options = {}, &block)
options[:parent] = element_parent
options[:theme] = current_theme
add_element(Element::Progress.new(options, block))
end
def slider(options = {}, &block)
options[:parent] = element_parent
options[:theme] = current_theme
add_element(Element::Slider.new(options, block))
end
def background(color = Gosu::Color::NONE)
@containers.last.background = color
element_parent.style.default[:background] = color
end
# Foreground color, e.g. Text
def color(color)
@containers.last.color(color)
def theme(theme)
element_parent.options[:theme] = theme
end
def current_theme
element_parent.options[:theme]
end
private def add_element(element)
element_parent.add(element)
element
end
private def element_parent
CyberarmEngine::Element::Container.current_container
end
private def container(klass, options = {}, &block)
options[:parent] = element_parent
options[:theme] = current_theme
_container = klass.new(options, block)
old_parent = element_parent
CyberarmEngine::Element::Container.current_container = _container
_container.build
_container.parent.add(_container)
CyberarmEngine::Element::Container.current_container = old_parent
_container
end
end
end
end

View File

@@ -1,88 +0,0 @@
module CyberarmEngine
class EditLine < Button
def initialize(text, options = {}, block = nil)
super(text, options, block)
@type = default(:type)
@caret_width = default(:caret_width)
@caret_height= @text.height
@caret_color = default(:caret_color)
@caret_interval = default(:caret_interval)
@caret_last_interval = Gosu.milliseconds
@show_caret = true
@text_input = Gosu::TextInput.new
@text_input.text = text
return self
end
def render
Gosu.clip_to(@text.x, @text.y, @width, @text.height) do
draw_text
Gosu.draw_rect(caret_position, @text.y, @caret_width, @caret_height, @caret_color, @z + 40) if @focus && @show_caret
end
end
def update
if @type == :password
@text.text = default(:password_character) * @text_input.text.length
else
@text.text = @text_input.text
end
if Gosu.milliseconds >= @caret_last_interval + @caret_interval
@caret_last_interval = Gosu.milliseconds
@show_caret = !@show_caret
end
end
def left_mouse_button(sender, x, y)
super
window.text_input = @text_input
end
def enter(sender)
if @focus
@background_canvas.background = default(:active, :background)
@text.color = default(:active, :color)
else
@background_canvas.background = default(:hover, :background)
@text.color = default(:hover, :color)
end
end
def leave(sender)
end
def blur(sender)
@focus = false
@background_canvas.background = default(:background)
@text.color = default(:color)
window.text_input = nil
end
# TODO: Fix caret rendering in wrong position unless caret_pos is at end of text
def caret_position
if @type == :password
@text.x + @text.textobject.text_width(default(:password_character) * @text_input.text[0..@text_input.caret_pos-1].length)
else
@text.x + @text.textobject.text_width(@text_input.text[0..@text_input.caret_pos-1])
end
end
def recalculate
super
@width = default(:width)
update_background
end
def value
@text_input.text
end
end
end

View File

@@ -4,104 +4,178 @@ module CyberarmEngine
include Event
include Common
attr_accessor :x, :y, :z, :enabled
attr_reader :width, :height, :parent, :options, :event_handler, :background_canvas, :border_canvas
attr_reader :border_thickness, :border_thickness_left, :border_thickness_right, :border_thickness_top, :border_thickness_bottom
attr_reader :border_color, :border_color_left, :border_color_right, :border_color_top, :border_color_bottom
attr_reader :padding, :padding_left, :padding_right, :padding_top, :padding_bottom
attr_reader :margin, :margin_left, :margin_right, :margin_top, :margin_bottom
attr_accessor :x, :y, :z, :tip, :element_visible
attr_reader :parent, :options, :style, :event_handler, :background_canvas, :border_canvas
def initialize(options = {}, block = nil)
@parent = options[:parent] # parent Container (i.e. flow/stack)
options = theme_defaults.merge(options)
@parent = options.delete(:parent) # parent Container (i.e. flow/stack)
options = theme_defaults(options)
@options = options
@block = block
@style = Style.new(options)
@focus = false
@background_canvas = Background.new
@border_canvas = BorderCanvas.new(element: self)
@focus = !@options.key?(:focus) ? false : @options[:focus]
@enabled = !@options.key?(:enabled) ? true : @options[:enabled]
@visible = !@options.key?(:visible) ? true : @options[:visible]
@tip = @options[:tip] || ""
@x = default(:x)
@y = default(:y)
@z = default(:z)
@debug_color = @options[:debug_color].nil? ? Gosu::Color::RED : @options[:debug_color]
@fixed_x = @x if @x != 0
@fixed_y = @y if @y != 0
@style = Style.new(options)
@width = default(:width) || $window.width
@height = default(:height) || $window.height
@root ||= nil
@gui_state ||= nil
@element_visible = true
set_border_thickness(default(:border_thickness))
@x = @style.x
@y = @style.y
@z = @style.z
set_padding(default(:padding))
@width = 0
@height = 0
set_margin(default(:margin))
@style.width = default(:width) || nil
@style.height = default(:height) || nil
set_background(default(:background))
set_border_color(default(:border_color))
@style.background_canvas = Background.new
@style.background_nine_slice_canvas = BackgroundNineSlice.new
@style.background_image_canvas = BackgroundImage.new
@style.border_canvas = BorderCanvas.new(element: self)
raise "#{self.class} 'x' must be a number" unless @x.is_a?(Numeric)
raise "#{self.class} 'y' must be a number" unless @y.is_a?(Numeric)
raise "#{self.class} 'z' must be a number" unless @z.is_a?(Numeric)
raise "#{self.class} 'width' must be a number" unless @width.is_a?(Numeric) || @width.nil?
raise "#{self.class} 'height' must be a number" unless @height.is_a?(Numeric) || @height.nil?
raise "#{self.class} 'options' must be a Hash" unless @options.is_a?(Hash)
@style_event = :default
# raise "#{self.class} 'padding' must be a number" unless @padding.is_a?(Numeric)
@enabled = true
stylize
default_events
root.gui_state.request_focus(self) if @options[:autofocus]
end
def set_background(background)
@background = background
@background_canvas.background = background
def stylize
set_static_position
set_color
set_font
set_padding
set_margin
set_background
set_background_nine_slice
set_background_image
set_border_thickness
set_border_color
root.gui_state.request_repaint
end
def set_border_thickness(border_thickness)
@border_thickness = border_thickness
@border_thickness_left = default(:border_thickness_left) || @border_thickness
@border_thickness_right = default(:border_thickness_right) || @border_thickness
@border_thickness_top = default(:border_thickness_top) || @border_thickness
@border_thickness_bottom = default(:border_thickness_bottom) || @border_thickness
def safe_style_fetch(*args)
@style.hash.dig(@style_event, *args) || @style.hash.dig(:default, *args) || default(*args)
end
def set_border_color(color)
@border_color = color
@border_color_left = default(:border_color_left) || @border_color
@border_color_right = default(:border_color_right) || @border_color
@border_color_top = default(:border_color_top) || @border_color
@border_color_bottom = default(:border_color_bottom) || @border_color
@border_canvas.color = color
def set_static_position
@x = @style.x if @style.x != 0
@y = @style.y if @style.y != 0
end
def set_padding(padding)
@padding = padding
@padding_left = default(:padding_left) || @padding
@padding_right = default(:padding_right) || @padding
@padding_top = default(:padding_top) || @padding
@padding_bottom = default(:padding_bottom) || @padding
def set_color
@style.color = safe_style_fetch(:color)
@text&.color = @style.color
end
def set_margin(margin)
@margin = margin
def set_font
@text&.swap_font(safe_style_fetch(:text_size), safe_style_fetch(:font))
end
@margin_left = default(:margin_left) || @margin
@margin_right = default(:margin_right) || @margin
@margin_top = default(:margin_top) || @margin
@margin_bottom = default(:margin_bottom) || @margin
def set_background
@style.background = safe_style_fetch(:background)
@style.background_canvas.background = @style.background
end
def set_background_nine_slice
@style.background_nine_slice = safe_style_fetch(:background_nine_slice)
@style.background_nine_slice_mode = safe_style_fetch(:background_nine_slice_mode) || :stretch
@style.background_nine_slice_color = safe_style_fetch(:background_nine_slice_color) || Gosu::Color::WHITE
@style.background_nine_slice_canvas.color = @style.background_nine_slice_color
@style.background_nine_slice_from_edge = safe_style_fetch(:background_nine_slice_from_edge)
@style.background_nine_slice_left = safe_style_fetch(:background_nine_slice_left) || @style.background_nine_slice_from_edge
@style.background_nine_slice_top = safe_style_fetch(:background_nine_slice_top) || @style.background_nine_slice_from_edge
@style.background_nine_slice_right = safe_style_fetch(:background_nine_slice_right) || @style.background_nine_slice_from_edge
@style.background_nine_slice_bottom = safe_style_fetch(:background_nine_slice_bottom) || @style.background_nine_slice_from_edge
end
def set_background_image
@style.background_image = safe_style_fetch(:background_image)
@style.background_image_mode = safe_style_fetch(:background_image_mode) || :stretch
@style.background_image_color = safe_style_fetch(:background_image_color) || Gosu::Color::WHITE
@style.background_image_canvas.mode = @style.background_image_mode
@style.background_image_canvas.color = @style.background_image_color
end
def set_border_thickness
@style.border_thickness = safe_style_fetch(:border_thickness)
@style.border_thickness_left = safe_style_fetch(:border_thickness_left) || @style.border_thickness
@style.border_thickness_right = safe_style_fetch(:border_thickness_right) || @style.border_thickness
@style.border_thickness_top = safe_style_fetch(:border_thickness_top) || @style.border_thickness
@style.border_thickness_bottom = safe_style_fetch(:border_thickness_bottom) || @style.border_thickness
end
def set_border_color
@style.border_color = safe_style_fetch(:border_color)
@style.border_color_left = safe_style_fetch(:border_color_left) || @style.border_color
@style.border_color_right = safe_style_fetch(:border_color_right) || @style.border_color
@style.border_color_top = safe_style_fetch(:border_color_top) || @style.border_color
@style.border_color_bottom = safe_style_fetch(:border_color_bottom) || @style.border_color
@style.border_canvas.color = [
@style.border_color_top,
@style.border_color_right,
@style.border_color_bottom,
@style.border_color_left
]
end
def set_padding
@style.padding = safe_style_fetch(:padding)
@style.padding_left = safe_style_fetch(:padding_left) || @style.padding
@style.padding_right = safe_style_fetch(:padding_right) || @style.padding
@style.padding_top = safe_style_fetch(:padding_top) || @style.padding
@style.padding_bottom = safe_style_fetch(:padding_bottom) || @style.padding
end
def set_margin
@style.margin = safe_style_fetch(:margin)
@style.margin_left = safe_style_fetch(:margin_left) || @style.margin
@style.margin_right = safe_style_fetch(:margin_right) || @style.margin
@style.margin_top = safe_style_fetch(:margin_top) || @style.margin
@style.margin_bottom = safe_style_fetch(:margin_bottom) || @style.margin
end
def update_styles(event = :default)
old_width = width
old_height = height
@style_event = event
return if self.is_a?(ToolTip)
if old_width != width || old_height != height
root.gui_state.request_recalculate
end
stylize
end
def default_events
[:left, :middle, :right].each do |button|
%i[left middle right].each do |button|
event(:"#{button}_mouse_button")
event(:"released_#{button}_mouse_button")
event(:"clicked_#{button}_mouse_button")
@@ -115,19 +189,158 @@ module CyberarmEngine
event(:hover)
event(:leave)
event(:focus)
event(:blur)
event(:changed)
end
def enter(_sender)
@focus = false unless Gosu.button_down?(Gosu::MS_LEFT)
if !@enabled
update_styles(:disabled)
elsif @focus
update_styles(:active)
else
update_styles(:hover)
end
:handled
end
def left_mouse_button(_sender, _x, _y)
@focus = true
unless @enabled
update_styles(:disabled)
else
update_styles(:active)
end
window.current_state.focus = self
:handled
end
def released_left_mouse_button(sender, _x, _y)
enter(sender)
:handled
end
def clicked_left_mouse_button(_sender, _x, _y)
@block&.call(self) if @enabled && !self.is_a?(Container)
:handled
end
def leave(_sender)
if @enabled
update_styles
else
update_styles(:disabled)
end
:handled
end
def blur(_sender)
@focus = false
if @enabled
update_styles
else
update_styles(:disabled)
end
:handled
end
def enabled=(boolean)
root.gui_state.request_repaint if @enabled != boolean
@enabled = boolean
recalculate
@enabled
end
def enabled?
@enabled
end
def focused?
@focus
end
def visible?
@visible
end
def element_visible?
@element_visible
end
def toggle
@visible = !@visible
root.gui_state.request_recalculate
root.gui_state.request_repaint
end
def show
bool = visible?
@visible = true
root.gui_state.request_recalculate unless bool
root.gui_state.request_repaint unless bool
end
def hide
bool = visible?
@visible = false
root.gui_state.request_recalculate if bool
root.gui_state.request_repaint if bool
end
def draw
@background_canvas.draw
@border_canvas.draw
return unless visible?
return unless element_visible?
@style.background_canvas.draw
@style.background_nine_slice_canvas.draw
@style.background_image_canvas.draw
@style.border_canvas.draw
render
end
def debug_draw
# FIXME
return# if const_defined?(GUI_DEBUG_ONLY_ELEMENT) && self.class == GUI_DEBUG_ONLY_ELEMENT
Gosu.draw_line(
x, y, @debug_color,
x + outer_width, y, @debug_color,
Float::INFINITY
)
Gosu.draw_line(
x + outer_width, y, @debug_color,
x + outer_width, y + outer_height, @debug_color,
Float::INFINITY
)
Gosu.draw_line(
x + outer_width, y + outer_height, @debug_color,
x, y + outer_height, @debug_color,
Float::INFINITY
)
Gosu.draw_line(
x, outer_height, @debug_color,
x, y, @debug_color,
Float::INFINITY
)
end
def update
end
@@ -137,61 +350,275 @@ module CyberarmEngine
def button_up(id)
end
def draggable?(_button)
false
end
def render
end
def hit?(x, y)
x.between?(@x, @x + width) &&
y.between?(@y, @y + height)
y.between?(@y, @y + height)
end
def width
(@border_thickness_left + @padding_left) + @width + (@padding_right + @border_thickness_right)
end
def outer_width
@margin_left + width + @margin_right
end
def height
(@border_thickness_top + @padding_top) + @height + (@padding_bottom + @border_thickness_bottom)
end
def outer_height
@margin_top + height + @margin_bottom
end
def style(hash)
if hash
@style.set(hash)
if visible?
inner_width + @width
else
@style.hash
0
end
end
def content_width
@width
end
def noncontent_width
(inner_width + outer_width) - width
end
def outer_width
@style.margin_left + width + @style.margin_right
end
def inner_width
(@style.border_thickness_left + @style.padding_left) + (@style.padding_right + @style.border_thickness_right)
end
def height
if visible?
inner_height + @height
else
0
end
end
def content_height
@height
end
def noncontent_height
(inner_height + outer_height) - height
end
def outer_height
@style.margin_top + height + @style.margin_bottom
end
def inner_height
(@style.border_thickness_top + @style.padding_top) + (@style.padding_bottom + @style.border_thickness_bottom)
end
def scroll_width
return @cached_scroll_width if @cached_scroll_width && is_a?(Container)
@cached_scroll_width = @children.sum(&:outer_width)
end
def scroll_height
return @cached_scroll_height if @cached_scroll_height && is_a?(Container)
if is_a?(CyberarmEngine::Element::Flow)
return 0 if @children.size.zero?
pairs_ = []
sorted_children_ = @children.sort_by(&:y)
a_ = []
y_position_ = sorted_children_.first.y
sorted_children_.each do |child|
unless child.y == y_position_
y_position_ = child.y
pairs_ << a_
a_ = []
end
a_ << child
end
pairs_ << a_ unless pairs_.last == a_
@cached_scroll_height = pairs_.sum { |pair| + @style.padding_top + @style.border_thickness_top + pair.map(&:outer_height).max } + @style.padding_bottom + @style.border_thickness_bottom
else
@cached_scroll_height = @style.padding_top + @style.border_thickness_top + @children.sum(&:outer_height) + @style.padding_bottom + @style.border_thickness_bottom
end
end
def max_scroll_width
(scroll_width - outer_width).positive? ? scroll_width - outer_width : scroll_width
end
def max_scroll_height
(scroll_height - outer_height).positive? ? scroll_height - outer_height : scroll_height
end
def dimensional_size(size, dimension)
raise "dimension must be either :width or :height" unless %i[width height].include?(dimension)
new_size = if size.is_a?(Float) && size.between?(0.0, 1.0)
(@parent.send(:"content_#{dimension}") * size).floor - send(:"noncontent_#{dimension}").floor
else
size
end
# Handle fill behavior
if @parent && @style.fill &&
(dimension == :width && @parent.is_a?(Flow) ||
dimension == :height && @parent.is_a?(Stack))
new_size = space_available_width - noncontent_width if dimension == :width && @parent.is_a?(Flow)
new_size = space_available_height - noncontent_height if dimension == :height && @parent.is_a?(Stack)
end
return @style.send(:"min_#{dimension}") if @style.send(:"min_#{dimension}") && new_size.to_f < @style.send(:"min_#{dimension}")
return @style.send(:"max_#{dimension}") if @style.send(:"max_#{dimension}") && new_size.to_f > @style.send(:"max_#{dimension}")
new_size
end
def space_available_width
# TODO: This may get expensive if there are a lot of children, probably should cache it somehow
fill_siblings = @parent.children.select { |c| c.style.fill }.count.to_f # include self since we're dividing
available_space = ((@parent.content_width - (@parent.children.reject { |c| c.style.fill }).map(&:outer_width).sum) / fill_siblings)
(available_space.nan? || available_space.infinite?) ? 0 : available_space.floor # The parent element might not have its dimensions, yet.
end
def space_available_height
# TODO: This may get expensive if there are a lot of children, probably should cache it somehow
fill_siblings = @parent.children.select { |c| c.style.fill }.count.to_f # include self since we're dividing
available_space = ((@parent.content_height - (@parent.children.reject { |c| c.style.fill }).map(&:outer_height).sum) / fill_siblings)
(available_space.nan? || available_space.infinite?) ? 0 : available_space.floor # The parent element might not have its dimensions, yet.
end
def background=(_background)
@background_canvas.background=(_background)
root.gui_state.request_repaint
@style.background_canvas.background = _background
update_background
end
def update_background
@background_canvas.x = @x
@background_canvas.y = @y
@background_canvas.z = @z
@background_canvas.width = width
@background_canvas.height = height
@style.background_canvas.x = @x
@style.background_canvas.y = @y
@style.background_canvas.z = @z
@style.background_canvas.width = width
@style.background_canvas.height = height
@background_canvas.update
@style.background_canvas.update
update_background_nine_slice
update_background_image
@style.border_canvas.update
end
@border_canvas.update
def background_nine_slice=(_image_path)
root.gui_state.request_repaint
@style.background_nine_slice_canvas.image = _image_path
update_background_nine_slice
end
def update_background_nine_slice
@style.background_nine_slice_canvas.x = @x
@style.background_nine_slice_canvas.y = @y
@style.background_nine_slice_canvas.z = @z
@style.background_nine_slice_canvas.width = width
@style.background_nine_slice_canvas.height = height
@style.background_nine_slice_canvas.mode = @style.background_nine_slice_mode
@style.background_nine_slice_canvas.color = @style.background_nine_slice_color
@style.background_nine_slice_canvas.left = @style.background_nine_slice_left
@style.background_nine_slice_canvas.top = @style.background_nine_slice_top
@style.background_nine_slice_canvas.right = @style.background_nine_slice_right
@style.background_nine_slice_canvas.bottom = @style.background_nine_slice_bottom
@style.background_nine_slice_canvas.image = @style.background_nine_slice
end
def background_image=(image_path)
root.gui_state.request_repaint
@style.background_image = image_path.is_a?(Gosu::Image) ? image_path : get_image(image_path)
update_background_image
end
def update_background_image
@style.background_image_canvas.x = @x
@style.background_image_canvas.y = @y
@style.background_image_canvas.z = @z
@style.background_image_canvas.width = width
@style.background_image_canvas.height = height
@style.background_image_canvas.mode = @style.background_image_mode
@style.background_image_canvas.color = @style.background_image_color
@style.background_image_canvas.image = @style.background_image
end
def root
return self if is_root?
unless @root && @root.parent.nil?
@root = parent
loop do
break unless @root&.parent
@root = @root.parent
end
end
@root
end
def is_root?
@gui_state != nil
end
def child_of?(element)
return element == self if is_root?
return false unless element.is_a?(Container)
return true if element.children.find { |child| child == self }
element.children.find { |child| child.child_of?(element) if child.is_a?(Container) }
end
# def parent_of?(element)
# return true if element.children.find { |child| child == self }
# element.children.find { |child| child.parent}
# end
def focus(_)
warn "#{self.class}#focus was not overridden!"
:handled
end
def recalculate
raise "#{self.class}#recalculate was not overridden!"
end
def reposition
end
def value
raise "#{self.class}#value was not overridden!"
end
def value=(_value)
raise "#{self.class}#value= was not overridden!"
end
def to_s
"#{self.class} x=#{x} y=#{y} width=#{width} height=#{height} value=#{value.is_a?(String) ? "\"#{value}\"" : value}"
end
def inspect
to_s
end
end
end
end

View File

@@ -0,0 +1,100 @@
module CyberarmEngine
class Element
class Button < TextBlock
def initialize(text_or_image, options = {}, block = nil)
@image = nil
@scale_x = 1
@scale_y = 1
@image = text_or_image if text_or_image.is_a?(Gosu::Image)
super(text_or_image, options, block)
@style.background_canvas.background = @style.background
end
def render
if @image
draw_image
else
draw_text
end
end
def draw_image
@image.draw(
@style.border_thickness_left + @style.padding_left + @x,
@style.border_thickness_top + @style.padding_top + @y,
@z + 2,
@scale_x, @scale_y, @text.color
)
end
def draw_text
@text.draw
end
def recalculate
unless @enabled
@style.background_canvas.background = @style.disabled[:background]
@text.color = @style.disabled[:color]
else
@style.background_canvas.background = @style.background
@text.color = @style.color
end
if @image
@width = 0
@height = 0
_width = dimensional_size(@style.image_width, :width)
_height = dimensional_size(@style.image_height, :height)
if _width && _height
@scale_x = _width.to_f / @image.width
@scale_y = _height.to_f / @image.height
elsif _width
@scale_x = _width.to_f / @image.width
@scale_y = @scale_x
elsif _height
@scale_y = _height.to_f / @image.height
@scale_x = @scale_y
else
@scale_x = 1
@scale_y = 1
end
@width = _width || @image.width.floor * @scale_x
@height = _height || @image.height.floor * @scale_y
update_background
else
super
end
end
def value
@image || super
end
def value=(value)
if value.is_a?(Gosu::Image)
@image = value
else
super
end
old_width = width
old_height = height
if old_width != width || old_height != height
root.gui_state.request_recalculate
else
recalculate
end
publish(:changed, self.value)
end
end
end
end

View File

@@ -0,0 +1,54 @@
module CyberarmEngine
class Element
class CheckBox < Flow
def initialize(text, options, block = nil)
super(options, block)
options[:toggled] = options[:checked]
options[:parent] = self
@toggle_button = ToggleButton.new(options)
options[:parent] = self
@label = TextBlock.new(text, options)
@label.subscribe(:holding_left_mouse_button) do |sender, x, y|
@toggle_button.left_mouse_button(sender, x, y)
end
@label.subscribe(:released_left_mouse_button) do |sender, x, y|
@toggle_button.released_left_mouse_button(sender, x, y)
end
@label.subscribe(:clicked_left_mouse_button) do |sender, x, y|
@toggle_button.clicked_left_mouse_button(sender, x, y)
publish(:changed, @toggle_button.value)
end
@label.subscribe(:enter) do |sender|
@toggle_button.enter(sender)
end
@label.subscribe(:leave) do |sender|
@toggle_button.leave(sender)
end
add(@toggle_button)
add(@label)
end
def text=(text)
@label.text = text
recalculate
end
def value
@toggle_button.value
end
def value=(bool)
@toggle_button.value = bool
publish(:changed, @toggle_button.value)
end
end
end
end

View File

@@ -0,0 +1,373 @@
module CyberarmEngine
class Element
class Container < Element
include Common
attr_accessor :stroke_color, :fill_color
attr_reader :children, :gui_state, :scroll_position
def self.current_container
@@current_container
end
def self.current_container=(container)
raise ArgumentError, "Expected container to an an instance of CyberarmEngine::Element::Container, got #{container.class}" unless container.is_a?(CyberarmEngine::Element::Container)
@@current_container = container
end
def initialize(options = {}, block = nil)
@gui_state = options.delete(:gui_state)
super
@last_scroll_position = Vector.new(0, 0)
@scroll_position = Vector.new(0, 0)
@scroll_target_position = Vector.new(0, 0)
@scroll_chunk = 120
@scroll_speed = 40
@text_color = options[:color]
@children = []
event(:window_size_changed)
end
def build
@block.call(self) if @block
root.gui_state.request_recalculate_for(self)
end
def add(element)
@children << element
root.gui_state.request_recalculate_for(self)
end
def remove(element)
root.gui_state.request_recalculate_for(self) if @children.delete(element)
end
def clear(&block)
@children.clear
old_container = CyberarmEngine::Element::Container.current_container
CyberarmEngine::Element::Container.current_container = self
block.call(self) if block
CyberarmEngine::Element::Container.current_container = old_container
root.gui_state.request_recalculate_for(self)
end
def append(&block)
old_container = CyberarmEngine::Element::Container.current_container
CyberarmEngine::Element::Container.current_container = self
block.call(self) if block
CyberarmEngine::Element::Container.current_container = old_container
root.gui_state.request_recalculate_for(self)
end
def render
Gosu.clip_to(
@x + @style.border_thickness_left + @style.padding_left,
@y + @style.border_thickness_top + @style.padding_top,
content_width + 1,
content_height + 1
) do
Gosu.translate(@scroll_position.x, @scroll_position.y) do
@children.each(&:draw)
end
end
end
def debug_draw
super
@children.each do |child|
child.debug_draw
end
end
def update
update_scroll
@children.each(&:update)
end
def hit_element?(x, y)
return unless hit?(x, y)
# Offset child hit point by scroll position/offset
child_x = x - @scroll_position.x
child_y = y - @scroll_position.y
@children.reverse_each do |child|
next unless child.visible?
case child
when Container
if element = child.hit_element?(child_x, child_y)
return element
end
else
return child if child.hit?(child_x, child_y)
end
end
self if hit?(x, y)
end
def update_child_element_visibity(child)
child.element_visible = child.x >= (@x - @scroll_position.x) - child.width && child.x <= (@x - @scroll_position.x) + width &&
child.y >= (@y - @scroll_position.y) - child.height && child.y <= (@y - @scroll_position.y) + height
end
def update_scroll
dt = window.dt > 1 ? 1.0 : window.dt
scroll_x_diff = (@scroll_target_position.x - @scroll_position.x)
scroll_y_diff = (@scroll_target_position.y - @scroll_position.y)
@scroll_position.x += (scroll_x_diff * @scroll_speed * 0.25 * dt).round
@scroll_position.y += (scroll_y_diff * @scroll_speed * 0.25 * dt).round
@scroll_position.x = @scroll_target_position.x if scroll_x_diff.abs < 1.0
@scroll_position.y = @scroll_target_position.y if scroll_y_diff.abs < 1.0
# Scrolled PAST top
if @scroll_position.y > 0
@scroll_target_position.y = 0
# Scrolled PAST bottom
elsif @scroll_position.y.abs > max_scroll_height
@scroll_target_position.y = -max_scroll_height
end
if @last_scroll_position != @scroll_position
@children.each { |child| update_child_element_visibity(child) }
root.gui_state.request_repaint
end
@last_scroll_position.x = @scroll_position.x
@last_scroll_position.y = @scroll_position.y
end
def recalculate
@current_position = Vector.new(@style.margin_left + @style.padding_left, @style.margin_top + @style.padding_top)
return unless visible?
Stats.frame.increment(:gui_recalculations)
stylize
# s = Gosu.milliseconds
layout
old_width = @width
old_height = @height
@cached_scroll_width = nil
@cached_scroll_height = nil
if is_root?
@width = @style.width = window.width
@height = @style.height = window.height
else
@width = 0
@height = 0
_width = dimensional_size(@style.width, :width)
_height = dimensional_size(@style.height, :height)
@width = _width || (@children.map { |c| c.x + c.outer_width }.max || 0).floor
@height = _height || (@children.map { |c| c.y + c.outer_height }.max || 0).floor
end
# FIXME: Correctly handle alignment when element has siblings
# FIXME: Enable alignment for any element, not just containers
if @style.v_align
space = space_available_height
case @style.v_align
when :center
@y = parent.height / 2 - height / 2
when :bottom
@y = parent.height - height
end
end
if @style.h_align
space = space_available_width
case @style.h_align
when :center
@x = parent.width / 2 - width / 2
when :right
@x = parent.width - width
end
end
# Move children to parent after positioning
@children.each do |child|
child.x += (@x + @style.border_thickness_left) - style.margin_left
child.y += (@y + @style.border_thickness_top) - style.margin_top
child.stylize
child.recalculate
child.reposition # TODO: Implement top,bottom,left,center, and right positioning
Stats.frame.increment(:gui_recalculations)
update_child_element_visibity(child)
end
# puts "TOOK: #{Gosu.milliseconds - s}ms to recalculate #{self.class}:0x#{self.object_id.to_s(16)}"
update_background
# Fixes resized container scrolled past bottom
self.scroll_top = -@scroll_position.y
@scroll_target_position.y = @scroll_position.y
# NOTE: Experiment for removing need to explicitly call gui_state#recalculate at least 3 times for layout to layout...
if old_width != @width || old_height != @height
if @parent
root.gui_state.request_recalculate_for(@parent)
else
root.gui_state.request_recalculate
end
end
root.gui_state.request_repaint if @width != old_width || @height != old_height
end
def layout
raise "Not overridden"
end
def max_width
# _width = dimensional_size(@style.width, :width)
# if _width
# outer_width
# else
# window.width - (@parent ? @parent.style.margin_right + @style.margin_right : @style.margin_right)
# end
outer_width
end
def fits_on_line?(element) # Flow
@current_position.x + element.outer_width <= max_width &&
@current_position.x + element.outer_width <= window.width
end
def position_on_current_line(element) # Flow
element.x = element.style.margin_left + @current_position.x
element.y = element.style.margin_top + @current_position.y
@current_position.x += element.outer_width
end
def tallest_neighbor(querier, _y_position) # Flow
response = querier
@children.each do |child|
response = child if child.outer_height > response.outer_height
break if child == querier
end
response
end
def position_on_next_line(element) # Flow
@current_position.x = @style.margin_left + @style.padding_left
@current_position.y += tallest_neighbor(element, @current_position.y).outer_height
element.x = element.style.margin_left + @current_position.x
element.y = element.style.margin_top + @current_position.y
@current_position.x += element.outer_width
end
def move_to_next_line(element) # Stack
element.x = element.style.margin_left + @current_position.x
element.y = element.style.margin_top + @current_position.y
@current_position.y += element.outer_height
end
def mouse_wheel_up(sender, x, y)
return unless @style.scroll
# Allow overscrolling UP, only if one can scroll DOWN
if height < scroll_height
if @scroll_target_position.y > 0
@scroll_target_position.y = @scroll_chunk
else
@scroll_target_position.y += @scroll_chunk
end
return :handled
end
end
def mouse_wheel_down(sender, x, y)
return unless @style.scroll
return unless height < scroll_height
if @scroll_target_position.y > 0
@scroll_target_position.y = -@scroll_chunk
else
@scroll_target_position.y -= @scroll_chunk
end
return :handled
end
def scroll_top
@scroll_position.y
end
def scroll_top=(n)
n = 0 if n <= 0
@scroll_position.y = -n
if max_scroll_height.positive?
@scroll_position.y = -max_scroll_height if @scroll_position.y.abs > max_scroll_height
else
@scroll_position.y = 0
end
end
def value
@children.map(&:class).join(", ")
end
def to_s
"#{self.class} x=#{x} y=#{y} width=#{width} height=#{height} children=#{@children.size}"
end
def write_tree(indent = "", _index = 0)
puts self
indent += " "
@children.each_with_index do |child, i|
print "#{indent}#{i}: "
if child.is_a?(Container)
child.write_tree(indent)
else
puts child
end
end
end
end
end
end

View File

@@ -0,0 +1,179 @@
module CyberarmEngine
class Element
class EditBox < EditLine
def initialize(*args)
super(*args)
@active_line = 0
@repeatable_keys = [
{
key: Gosu::KB_UP,
down: false,
repeat_delay: 50,
last_repeat: 0,
action: proc { move(:up) }
},
{
key: Gosu::KB_DOWN,
down: false,
repeat_delay: 50,
last_repeat: 0,
action: proc { move(:down) }
}
]
end
def update
super
caret_stay_left_of_last_newline
calculate_active_line
@repeatable_keys.each do |key|
if key[:down] && (Gosu.milliseconds > key[:last_repeat] + key[:repeat_delay])
key[:action].call
key[:last_repeat] = Gosu.milliseconds
end
end
end
def draw_caret
Gosu.draw_rect(caret_position, @text.y + @active_line * @text.textobject.height, @caret_width, @caret_height,
@caret_color, @z)
end
def draw_selection
selection_width = caret_position - selection_start_position
Gosu.draw_rect(selection_start_position, @text.y, selection_width, @text.textobject.height,
default(:selection_color), @z)
end
def text_input_position_for(_method)
line = @text_input.text[0...@text_input.caret_pos].lines.last
_x = @text.x + @offset_x
if @type == :password
_x + @text.width(default(:password_character) * line.length)
else
_x + @text.width(line)
end
end
def set_position(int)
int = 0 if int < 0
@text_input.selection_start = @text_input.caret_pos = int
end
def calculate_active_line
sub_text = @text_input.text[0...@text_input.caret_pos]
@active_line = sub_text.lines.size - 1
end
def caret_stay_left_of_last_newline
@text_input.text += "\n" unless @text_input.text.end_with?("\n")
eof = @text_input.text.chomp.length
set_position(eof) if @text_input.caret_pos > eof
end
def caret_position_under_mouse(mouse_x, mouse_y)
active_line = row_at(mouse_y)
right_offset = column_at(mouse_x, mouse_y)
buffer = @text_input.text.lines[0..active_line].join if active_line != 0
buffer = @text_input.text.lines.first if active_line == 0
line = buffer.lines.last
if buffer.chars.last == "\n"
(buffer.length - line.length) + right_offset - 1
else
(buffer.length - line.length) + right_offset
end
end
def move_caret_to_mouse(mouse_x, mouse_y)
set_position(caret_position_under_mouse(mouse_x, mouse_y))
end
def row_at(y)
((y - @text.y) / @text.textobject.height).floor
end
def column_at(x, y)
row = row_at(y)
buffer = @text_input.text.lines[0..row].join if row != 0
buffer = @text_input.text.lines.first if row == 0
line = @text_input.text.lines[row]
line ||= ""
column = 0
line.length.times do |_i|
break if @text.textobject.text_width(line[0...column]) >= (x - @text.x).clamp(0.0, Float::INFINITY)
column += 1
end
column
end
def button_down(id)
super
@repeatable_keys.detect do |key|
next unless key[:key] == id
key[:down] = true
key[:last_repeat] = Gosu.milliseconds + key[:repeat_delay]
return true
end
case id
when Gosu::KB_ENTER, Gosu::KB_RETURN
caret_pos = @text_input.caret_pos
@text_input.text = @text_input.text.insert(@text_input.caret_pos, "\n")
@text_input.caret_pos = @text_input.selection_start = caret_pos + 1
end
end
def button_up(id)
super
@repeatable_keys.detect do |key|
if key[:key] == id
key[:down] = false
return true
end
end
end
def move(direction)
pos = @text_input.caret_pos
line = nil
case direction
when :up
return if @active_line == 0
when :down
return if @active_line == @text_input.text.chomp.lines
text = @text_input.text.chomp.lines[0..@active_line].join("\n")
pos = text.length
end
set_position(pos)
end
def drag_update(_sender, x, y, _button)
int = caret_position_under_mouse(x, y)
int = 0 if int < 0
@text_input.caret_pos = int
:handled
end
end
end
end

View File

@@ -0,0 +1,297 @@
module CyberarmEngine
class Element
class EditLine < Button
class TextInput < Gosu::TextInput
def filter=(filter)
@filter = filter
end
def filter(text_in)
if @filter
@filter.call(text_in)
else
text_in
end
end
end
def initialize(text, options = {}, block = nil)
@filter = options.delete(:filter)
super(text, options, block)
@type = default(:type)
@caret_width = default(:caret_width)
@caret_height = @text.textobject.height
@caret_color = default(:caret_color)
@caret_interval = default(:caret_interval)
@caret_last_interval = Gosu.milliseconds
@show_caret = true
@text_input = TextInput.new
@text_input.filter = @filter
@text_input.text = text
@last_text_value = text
@last_caret_position = @text_input.caret_pos
@offset_x = 0
@offset_y = 0
event(:begin_drag)
event(:drag_update)
event(:end_drag)
end
def render
Gosu.clip_to(@text.x, @text.y, @width, @height) do
Gosu.translate(-@offset_x, -@offset_y) do
draw_selection
draw_caret if @focus && @show_caret
draw_text
end
end
end
def draw_text
@text.draw(:draw_text)
end
def draw_caret
Gosu.draw_rect(caret_position, @text.y, @caret_width, @caret_height, @caret_color, @z)
end
def draw_selection
selection_width = caret_position - selection_start_position
Gosu.draw_rect(selection_start_position, @text.y, selection_width, @text.height, default(:selection_color), @z)
end
def update
@style_event = :active if @focus
@text.text = if @type == :password
default(:password_character) * @text_input.text.length
else
@text_input.text
end
if @last_text_value != value
@last_text_value = value
@show_caret = true
@caret_last_interval = Gosu.milliseconds
root.gui_state.request_repaint
publish(:changed, value)
end
if @last_caret_position != @text_input.caret_pos
@last_caret_position = @text_input.caret_pos
root.gui_state.request_repaint
@show_caret = true
@caret_last_interval = Gosu.milliseconds
end
if Gosu.milliseconds >= @caret_last_interval + @caret_interval
root.gui_state.request_repaint
@caret_last_interval = Gosu.milliseconds
@show_caret = !@show_caret
end
keep_caret_visible
end
def button_down(id)
handle_keyboard_shortcuts(id)
end
def handle_keyboard_shortcuts(id)
return unless @focus && @enabled
if Gosu.button_down?(Gosu::KB_LEFT_CONTROL) || Gosu.button_down?(Gosu::KB_RIGHT_CONTROL)
case id
when Gosu::KB_A
@text_input.selection_start = 0
@text_input.caret_pos = @text_input.text.length
when Gosu::KB_C
Gosu.clipboard = if @text_input.selection_start < @text_input.caret_pos
@text_input.text[@text_input.selection_start...@text_input.caret_pos]
else
@text_input.text[@text_input.caret_pos...@text_input.selection_start]
end
when Gosu::KB_X
chars = @text_input.text.chars
if @text_input.selection_start < @text_input.caret_pos
Gosu.clipboard = @text_input.text[@text_input.selection_start...@text_input.caret_pos]
chars.slice!(@text_input.selection_start, @text_input.caret_pos)
else
Gosu.clipboard = @text_input.text[@text_input.caret_pos...@text_input.selection_start]
chars.slice!(@text_input.caret_pos, @text_input.selection_start)
end
@text_input.text = chars.join
when Gosu::KB_V
if instance_of?(EditLine) # EditLine assumes a single line of text
@text_input.insert_text(Gosu.clipboard.gsub("\n", ""))
else
@text_input.insert_text(Gosu.clipboard)
end
end
end
end
def caret_position_under_mouse(mouse_x)
1.upto(@text.text.length) do |i|
return i - 1 if mouse_x < @text.x - @offset_x + @text.width(@text.text[0...i])
end
@text_input.text.length
end
def move_caret_to_mouse(mouse_x, _mouse_y)
@text_input.caret_pos = @text_input.selection_start = caret_position_under_mouse(mouse_x)
end
def keep_caret_visible
caret_pos = (caret_position - @text.x) + @caret_width
@last_text ||= "/\\"
@last_pos ||= -1
@last_text = @text.text
@last_pos = caret_pos
if caret_pos.between?(@offset_x, @width + @offset_x)
# Do nothing
elsif caret_pos < @offset_x
@offset_x = if caret_pos > @width
caret_pos + @width
else
0
end
elsif caret_pos > @width
@offset_x = caret_pos - @width
else
# Reset to Zero
@offset_x = 0
end
end
def caret_position
text_input_position_for(:caret_pos)
end
def selection_start_position
text_input_position_for(:selection_start)
end
def text_input_position_for(method)
if @type == :password
@text.x + @text.width(default(:password_character) * @text_input.text[0...@text_input.send(method)].length)
else
@text.x + @text.width(@text_input.text[0...@text_input.send(method)]) - @style.border_thickness_left
end
end
def left_mouse_button(sender, x, y)
super
window.text_input = @text_input
@caret_last_interval = Gosu.milliseconds
@show_caret = true
move_caret_to_mouse(x, y)
:handled
end
def focus(sender)
@focus = true
window.text_input = @text_input
@text_input.caret_pos = @text_input.selection_start = @text_input.text.length
update_styles(:active)
:handled
end
def enter(sender)
if @enabled && @focus
update_styles(:active)
elsif @enabled && !@focus
update_styles(:hover)
else
update_styles(:disabled)
end
:handled
end
def leave(sender)
if @enabled && @focus
update_styles(:active)
elsif @enabled && !@focus
update_styles
else
update_styles(:disabled)
end
:handled
end
def blur(_sender)
super
window.text_input = nil
:handled
end
def draggable?(button)
button == :left
end
def begin_drag(_sender, x, _y, _button)
@drag_start = x
@offset_drag_start = @offset_x
@drag_caret_position = @text_input.caret_pos
:handled
end
def drag_update(_sender, x, _y, _button)
@text_input.caret_pos = caret_position_under_mouse(x)
:handled
end
def end_drag(_sender, _x, _y, _button)
:handled
end
def recalculate
super
@width = dimensional_size(@style.width, :width) || default(:width)
update_background
end
def value
@text_input.text
end
def value=(string)
@text_input.text = string
end
end
end
end

View File

@@ -0,0 +1,15 @@
module CyberarmEngine
class Element
class Flow < Container
def layout
@children.each do |child|
if fits_on_line?(child)
position_on_current_line(child)
else
position_on_next_line(child)
end
end
end
end
end
end

View File

@@ -0,0 +1,72 @@
module CyberarmEngine
class Element
class Image < Element
def initialize(path_or_image, options = {}, block = nil)
super(options, block)
@path = path_or_image if path_or_image.is_a?(String)
@image = Gosu::Image.new(path_or_image, retro: @options[:retro], tileable: @options[:tileable]) if @path
@image = path_or_image unless @path
@scale_x = 1
@scale_y = 1
end
def render
@image.draw(
@style.border_thickness_left + @style.padding_left + @x,
@style.border_thickness_top + @style.padding_top + @y,
@z + 2,
@scale_x, @scale_y, @style.color
)
end
def clicked_left_mouse_button(_sender, _x, _y)
@block.call(self) if @block
:handled
end
def recalculate
_width = dimensional_size(@style.width, :width)
_height = dimensional_size(@style.height, :height)
if _width && _height
@scale_x = _width.to_f / @image.width
@scale_y = _height.to_f / @image.height
elsif _width
@scale_x = _width.to_f / @image.width
@scale_y = @scale_x
elsif _height
@scale_y = _height.to_f / @image.height
@scale_x = @scale_y
else
@scale_x = 1
@scale_y = 1
end
@width = _width || (@image.width * @scale_x).floor
@height = _height || (@image.height * @scale_y).floor
update_background
end
def value
@image
end
def value=(path_or_image, retro: false, tileable: false)
@path = path_or_image if path_or_image.is_a?(String)
@image = Gosu::Image.new(path_or_image, retro: retro, tileable: tileable) if @path
@image = path_or_image unless @path
recalculate
end
def path
@path
end
end
end
end

View File

@@ -0,0 +1,101 @@
module CyberarmEngine
class Element
class ListBox < Button
attr_accessor :items
attr_reader :choose
def initialize(options = {}, block = nil)
@items = options[:items] || []
@choose = options[:choose] || @items.first
super(@choose, options, block)
@style.background_canvas.background = default(:background)
# TODO: "Clean Up" into own class?
@menu = Stack.new(parent: self, theme: @options[:theme])
@menu.define_singleton_method(:recalculate_menu) do
@x = @__list_box.x
@y = @__list_box.y + @__list_box.height
@y = @__list_box.y - height if @y + height > window.height
end
@menu.instance_variable_set(:"@__list_box", self)
def @menu.recalculate
super
recalculate_menu
end
self.choose = @choose
end
def render
super
w = @text.textobject.text_width("")
@text.textobject.draw_text("", @x + content_width - w, @y + @style.padding_top, @z, 1, 1, @text.color)
end
def choose=(item)
valid = @items.detect { |i| i == item }
raise "Invalid value '#{item}' for choose, valid options were: #{@items.map { |i| "#{i.inspect}" }.join(", ")}" unless valid
@choose = item
self.value = item.to_s
recalculate
end
def released_left_mouse_button(_sender, _x, _y)
show_menu
:handled
end
def clicked_left_mouse_button(_sender, _x, _y)
# @block&.call(self.value) if @enabled
:handled
end
def show_menu
@menu.clear
@menu.style.width = width
@items.each do |item|
next if item == self.value
btn = Button.new(
item,
{
parent: @menu,
width: 1.0,
theme: @options[:theme],
margin: 0,
border_color: 0x00ffffff
},
proc do
self.choose = item
@block&.call(self.value)
end
)
@menu.add(btn)
end
recalculate
root.gui_state.show_menu(@menu)
end
def recalculate
super
@menu.recalculate
end
end
end
end

View File

@@ -0,0 +1,93 @@
module CyberarmEngine
class Element
class Progress < Element
attr_reader :type
def initialize(options = {}, block = nil)
super(options, block)
@animation_speed = options[:animation_speed] || 3_000
@marquee_width = options[:marquee_width] || 0.25
@marquee_offset = 0
@marquee_animation_time = Gosu.milliseconds
@type = options[:type] || :linear
@fraction_background = Background.new(background: @style.fraction_background)
self.value = options[:fraction] || 0.0
end
def render
@fraction_background.draw
end
def recalculate
_width = dimensional_size(@style.width, :width)
_height = dimensional_size(@style.height, :height)
@width = _width
@height = _height
update_background
end
def update_background
super
@fraction_background.x = (@style.border_thickness_left + @style.padding_left + @x) + @marquee_offset
@fraction_background.y = @style.border_thickness_top + @style.padding_top + @y
@fraction_background.z = @z
@fraction_background.width = @width * (@type == :marquee ? @marquee_width : @fraction)
@fraction_background.height = @height
@fraction_background.background = @style.fraction_background
end
def update
super
return unless @type == :marquee
marquee_width = @width * @marquee_width
range = @width + marquee_width
@marquee_offset = (@width * (Gosu.milliseconds - @marquee_animation_time) / @animation_speed) - marquee_width
@marquee_animation_time = Gosu.milliseconds if @marquee_offset > range
update_background
root.gui_state.request_repaint
end
def type=(type)
@type = type
case type
when :linear
@marquee_offset = 0
when :marquee
@marquee_offset = 0
@marquee_animation_time = Gosu.milliseconds
else
raise ArgumentError, "Only types :linear and :marquee are supported"
end
update_background
end
def value
@fraction
end
def value=(decimal)
raise "value must be number" unless decimal.is_a?(Numeric)
old_value = @fraction
@fraction = decimal.clamp(0.0, 1.0)
update_background
root.gui_state.request_repaint if @fraction != old_value
publish(:changed, @fraction)
@fraction
end
end
end
end

View File

@@ -0,0 +1,6 @@
module CyberarmEngine
class Element
class Radio < Element
end
end
end

View File

@@ -0,0 +1,107 @@
module CyberarmEngine
class Element
class Slider < Container
class Handle < Button
def initialize(*args)
super(*args)
event(:begin_drag)
event(:drag_update)
event(:end_drag)
subscribe :begin_drag do |_sender, x, y, _button|
@drag_start_pos = Vector.new(x, y)
:handled
end
subscribe :drag_update do |_sender, x, y, _button|
@parent.handle_dragged_to(x, y)
:handled
end
subscribe :end_drag do
@drag_start_pos = nil
:handled
end
end
def draggable?(button)
button == :left
end
end
attr_reader :step_size, :value
attr_accessor :range, :step_size
def initialize(options = {}, block = nil)
super(options, block)
@range = @options[:range] || (0.0..1.0)
@step_size = @options[:step] || 0.1
@value = @options[:value] || (@range.first + @range.last) / 2
@handle = Handle.new("", parent: self, theme: options[:theme], width: 8, height: 1.0) { close }
add(@handle)
end
def recalculate
_width = dimensional_size(@style.width, :width)
_height = dimensional_size(@style.height, :height)
@width = _width
@height = _height
position_handle
@handle.recalculate
@handle.update_background
update_background
end
def position_handle
@handle.x = @x + @handle.style.margin_left + @style.padding_left + @style.border_thickness_left +
((content_width - @handle.outer_width) * (@value - @range.min) / (@range.max - @range.min).to_f)
@handle.y = @y + @handle.style.margin_top + @style.border_thickness_top + @style.padding_top
end
def draw
super
@handle.draw
end
def update
super
@tip = format("%.2f", value.to_f)
@handle.tip = @tip
end
def holding_left_mouse_button(_sender, x, y)
handle_dragged_to(x, y)
:handled
end
def handle_dragged_to(x, _y)
@ratio = ((x - @handle.width / 2.0) - @x) / (content_width - @handle.outer_width.to_f)
self.value = @ratio.clamp(0.0, 1.0) * (@range.max - @range.min) + @range.min
end
def value=(n)
@value = n
position_handle
@handle.recalculate
root.gui_state.request_repaint
publish(:changed, @value)
end
end
end
end

View File

@@ -0,0 +1,11 @@
module CyberarmEngine
class Element
class Stack < Container
def layout
@children.each do |child|
move_to_next_line(child)
end
end
end
end
end

View File

@@ -0,0 +1,221 @@
module CyberarmEngine
class Element
class TextBlock < Element
def initialize(text, options = {}, block = nil)
super(options, block)
@text = Text.new(
text, font: @options[:font], z: @z, color: @options[:color],
size: @options[:text_size], shadow: @options[:text_shadow],
static: @options[:text_static],
shadow_size: @options[:text_shadow_size],
shadow_color: @options[:text_shadow_color],
border: @options[:text_border],
border_size: @options[:text_border_size],
border_color: @options[:text_border_color]
)
@raw_text = text
end
def update
super
if @text.textobject.name != safe_style_fetch(:font)
set_font
root.gui_state.request_recalculate
end
end
def render
# Gosu.clip_to is too expensive to always use so check if we actually need it.
if @text.width > width || @text.height > height
Gosu.clip_to(@x, @y, width, height) do
@text.draw
end
else
@text.draw
end
end
def recalculate
unless @enabled
@text.color = @style.disabled[:color]
else
@text.color = @style.color
end
old_width = @width
old_height = @height
@width = 0
@height = 0
_width = dimensional_size(@style.width, :width)
_height = dimensional_size(@style.height, :height)
handle_text_wrapping(_width)
@width = _width || @text.width.floor
@height = _height || @text.height.floor
@text.y = @style.border_thickness_top + @style.padding_top + @y
@text.z = @z + 3
if (text_alignment = @options[:text_align] || @options[:text_h_align])
case text_alignment
when :left
@text.x = @style.border_thickness_left + @style.padding_left + @x
when :center
@text.x = if @text.width <= width
@x + width / 2 - @text.width / 2
else # Act as left aligned
@style.border_thickness_left + @style.padding_left + @x
end
when :right
@text.x = @x + outer_width - (@text.width + @style.border_thickness_right + @style.padding_right)
end
end
if (vertical_alignment = @options[:text_v_align])
case vertical_alignment
when :center
@text.y = if @text.height <= height
@y + height / 2 - @text.height / 2
else
@style.border_thickness_top + @style.padding_top + @y
end
when :bottom
@text.y = @y + outer_height - (@text.height + @style.border_thickness_bottom + @style.padding_bottom)
end
end
update_background
root.gui_state.request_repaint if @width != old_width || @height != old_height
end
def handle_text_wrapping(max_width)
max_width ||= @parent&.content_width
max_width ||= @x - (window.width + noncontent_width)
wrap_behavior = style.text_wrap
copy = @raw_text.to_s.dup
# Only perform text wrapping: if it is enabled, is possible to wrap, and text is too long to fit on one line
if wrap_behavior != :none && line_width(copy[0]) <= max_width && line_width(copy) > max_width
breaks = [] # list of indexes to insert a line break
line_start = 0
line_end = copy.length
stalled = false
stalled_interations = 0
max_stalled_iterations = 10
checked_copy_length = line_width(copy[line_start..line_end])
# find length of lines
while line_width(copy[line_start..line_end]) > max_width && stalled_interations < max_stalled_iterations
search_start = line_start
search_end = line_end
# Perform a binary search to find length of line
while search_start < search_end
midpoint = ((search_start.to_f + search_end) / 2.0).floor
if line_width(copy[line_start..midpoint]) > max_width
search_end = midpoint
else
search_start = midpoint + 1
end
end
if wrap_behavior == :word_wrap
word_search_end = search_end
failed = false
until(copy[word_search_end].to_s.match(/[[:punct:]]| /))
word_search_end -= 1
if word_search_end <= 1 || word_search_end < line_start
failed = true
break
end
end
line_start = failed ? search_end : word_search_end + 1 # walk in front of punctuation
else
line_start = search_end
end
breaks << line_start
# Prevent locking up due to outer while loop text width < max_width check not being satisfied.
stalled = checked_copy_length == line_width(copy[line_start..line_end])
checked_copy_length = line_width(copy[line_start..line_end])
stalled_interations += 1 if stalled
stalled_interations = 0 unless stalled
end
breaks.each_with_index do |pos, index|
copy.insert(pos + index, "\n") if pos + index >= 0 && pos + index < copy.length
end
end
@text.text = copy
end
def line_width(text)
(@text.textobject.markup_width(text.to_s) + noncontent_width)
end
def value
@raw_text
end
def value=(value)
old_value = @raw_text
@raw_text = value.to_s.chomp
old_width = width
old_height = height
if old_width != width || old_height != height
root.gui_state.request_recalculate
else
recalculate
end
root.gui_state.request_repaint if old_value != @raw_text
publish(:changed, self.value)
end
end
class Banner < TextBlock
end
class Title < TextBlock
end
class Subtitle < TextBlock
end
class Tagline < TextBlock
end
class Caption < TextBlock
end
class Para < TextBlock
end
class Inscription < TextBlock
end
class ToolTip < TextBlock
end
class Link < TextBlock
end
end
end

View File

@@ -0,0 +1,65 @@
module CyberarmEngine
class Element
class ToggleButton < Button
attr_reader :toggled, :value
def initialize(options, block = nil)
if options.dig(:theme, :ToggleButton, :checkmark_image)
options[:theme][:ToggleButton][:image_width] ||= options[:theme][:TextBlock][:text_size]
super(get_image(options.dig(:theme, :ToggleButton, :checkmark_image)), options, block)
@_image = @image
else
super(options[:checkmark], options, block)
end
@value = options[:checked] || false
if @value
@image = @_image if @_image
@raw_text = @options[:checkmark]
else
@image = nil
@raw_text = ""
end
end
def clicked_left_mouse_button(_sender, _x, _y)
self.value = !@value
@block.call(self) if @block
:handled
end
def recalculate
super
return if @image
_width = dimensional_size(@style.width, :width)
_height = dimensional_size(@style.height, :height)
@width = _width || @text.textobject.text_width(@options[:checkmark])
@height = _height || @text.height
update_background
end
def value=(boolean)
@value = boolean
if boolean
@image = @_image if @_image
@raw_text = @options[:checkmark]
else
@image = nil
@raw_text = ""
end
recalculate
publish(:changed, @value)
end
end
end
end

View File

@@ -15,19 +15,25 @@ module CyberarmEngine
return unless enabled?
if respond_to?(event)
return :handled if send(event, self, *args) == :handled
end
was_handled = false
was_handled = true if respond_to?(event) && (send(event, self, *args) == :handled)
@event_handler[event].reverse_each do |handler|
return :handled if handler.call(self, *args) == :handled
if handler.call(self, *args) == :handled
was_handled = true
break
end
end
return nil
return :handled if was_handled
parent.publish(event, *args) if parent
nil
end
def event(event)
@event_handler ||= Hash.new
@event_handler ||= {}
@event_handler[event] ||= []
end
end
@@ -36,11 +42,13 @@ module CyberarmEngine
attr_reader :publisher, :event, :handler
def initialize(publisher, event, handler)
@publisher, @event, @handler = publisher, event, handler
@publisher = publisher
@event = event
@handler = handler
end
def unsubscribe
@publisher.unsubscribe(self)
end
end
end
end

View File

@@ -1,15 +0,0 @@
module CyberarmEngine
class Flow < Container
include Common
def layout
@children.each do |child|
if fits_on_line?(child)
position_on_current_line(child)
else
position_on_next_line(child)
end
end
end
end
end

View File

@@ -10,23 +10,38 @@ module CyberarmEngine
@down_keys = {}
@root_container = Stack.new
@root_container = Element::Stack.new(gui_state: self)
@game_objects << @root_container
@containers = [@root_container]
CyberarmEngine::Element::Container.current_container = @root_container
@active_width = window.width
@active_height = window.height
@menu = nil
@focus = nil
@mouse_over = nil
@mouse_down_on = {}
@mouse_down_position = {}
@last_mouse_pos = nil
@dragging_element = nil
@pending_recalculate_request = false
@pending_element_recalculate_requests = []
@needs_repaint = false
setup
@menu = nil
@min_drag_distance = 0
@mouse_pos = Vector.new
end
def post_setup
@tip = Element::ToolTip.new("", parent: @root_container, z: Float::INFINITY, theme: current_theme)
end
# throws :blur event to focused element and sets GuiState focused element
# Does NOT throw :focus event at element or set element as focused
def focus=(element)
@focus.publish(:blur) if @focus and element && @focus != element
@focus.publish(:blur) if @focus && element && @focus != element
@focus = element
end
@@ -34,10 +49,77 @@ module CyberarmEngine
@focus
end
def update
def draw
super
new_mouse_over = @root_container.hit_element?(window.mouse_x, window.mouse_y)
if @menu
Gosu.flush
@menu.draw
end
if @tip && @tip.value.length.positive?
Gosu.flush
@tip.draw
end
# FIXME
if false# defined?(GUI_DEBUG)
Gosu.flush
@root_container.debug_draw
end
@needs_repaint = false
end
def needs_repaint?
@needs_repaint
end
def update
if @pending_recalculate_request
Stats.frame.start_timing(:gui_recalculate)
@root_container.recalculate
@pending_recalculate_request = false
Stats.frame.end_timing(:gui_recalculate)
end
Stats.frame.start_timing(:gui_element_recalculate_requests)
# puts "PENDING REQUESTS: #{@pending_element_recalculate_requests.size}" if @pending_element_recalculate_requests.size.positive?
@pending_element_recalculate_requests.each(&:recalculate)
@pending_element_recalculate_requests.clear
Stats.frame.end_timing(:gui_element_recalculate_requests)
if @pending_focus_request
@pending_focus_request = false
self.focus = @pending_focus_element
@pending_focus_element.publish(:focus)
end
@menu&.update
super
if @active_width != window.width || @active_height != window.height
request_recalculate
@root_container.publish(:window_size_changed)
end
@active_width = window.width
@active_height = window.height
return unless window.has_focus?
return unless window.current_state == self
new_mouse_over = @menu.hit_element?(window.mouse_x, window.mouse_y) if @menu
new_mouse_over ||= @root_container.hit_element?(window.mouse_x, window.mouse_y)
if new_mouse_over
new_mouse_over.publish(:enter) if new_mouse_over != @mouse_over
new_mouse_over.publish(:hover)
@@ -46,48 +128,84 @@ module CyberarmEngine
@mouse_over.publish(:leave) if @mouse_over && new_mouse_over != @mouse_over
@mouse_over = new_mouse_over
redirect_holding_mouse_button(:left) if @mouse_over && Gosu.button_down?(Gosu::MsLeft)
redirect_holding_mouse_button(:middle) if @mouse_over && Gosu.button_down?(Gosu::MsMiddle)
redirect_holding_mouse_button(:right) if @mouse_over && Gosu.button_down?(Gosu::MsRight)
redirect_holding_mouse_button(:left) if @mouse_over && Gosu.button_down?(Gosu::MS_LEFT)
redirect_holding_mouse_button(:middle) if @mouse_over && Gosu.button_down?(Gosu::MS_MIDDLE)
redirect_holding_mouse_button(:right) if @mouse_over && Gosu.button_down?(Gosu::MS_RIGHT)
if Vector.new(window.mouse_x, window.mouse_y) == @last_mouse_pos
if @mouse_over && (Gosu.milliseconds - @mouse_moved_at) > tool_tip_delay
@tip.value = @mouse_over.tip if @mouse_over
@tip.x = window.mouse_x
@tip.x = 0 if @tip.x < 0
@tip.x = window.width - @tip.width if @tip.x + @tip.width > window.width
@tip.y = window.mouse_y - (@tip.height + 5)
@tip.y = 0 if @tip.y < 0
@tip.y = window.height - @tip.height if @tip.y + @tip.height > window.height
@tip.update
@tip.recalculate
else
@tip.value = ""
end
else
@mouse_moved_at = Gosu.milliseconds
end
@last_mouse_pos = Vector.new(window.mouse_x, window.mouse_y)
@mouse_pos = @last_mouse_pos.clone
end
def button_down(id)
super
case id
when Gosu::MsLeft
when Gosu::MS_LEFT
redirect_mouse_button(:left)
when Gosu::MsMiddle
when Gosu::MS_MIDDLE
redirect_mouse_button(:middle)
when Gosu::MsRight
when Gosu::MS_RIGHT
redirect_mouse_button(:right)
when Gosu::KB_F5
request_recalculate
end
@focus.button_down(id) if @focus.respond_to?(:button_down)
end
def button_up(id)
super
case id
when Gosu::MsLeft
when Gosu::MS_LEFT
redirect_released_mouse_button(:left)
when Gosu::MsMiddle
when Gosu::MS_MIDDLE
redirect_released_mouse_button(:middle)
when Gosu::MsRight
when Gosu::MS_RIGHT
redirect_released_mouse_button(:right)
when Gosu::MsWheelUp
when Gosu::MS_WHEEL_UP
redirect_mouse_wheel(:up)
when Gosu::MsWheelDown
when Gosu::MS_WHEEL_DOWN
redirect_mouse_wheel(:down)
end
@focus.button_up(id) if @focus.respond_to?(:button_up)
# Prevents menu from popping back up if the listbox is clicked to hide it.
@hid_menu_for = nil
end
def tool_tip_delay
@tip.style.delay || 250 # ms
end
def redirect_mouse_button(button)
hide_menu unless @menu && (@menu == @mouse_over) || (@mouse_over&.parent == @menu)
if @focus && @mouse_over != @focus
@focus.publish(:blur)
@focus = nil
end
if @mouse_over
if @mouse_over && @hid_menu_for != @mouse_over
@mouse_down_position[button] = Vector.new(window.mouse_x, window.mouse_y)
@mouse_down_on[button] = @mouse_over
@@ -99,9 +217,19 @@ module CyberarmEngine
end
def redirect_released_mouse_button(button)
if @mouse_over
hide_menu if @menu && (@menu == @mouse_over) || (@mouse_over&.parent == @menu)
if @mouse_over && @hid_menu_for != @mouse_over
@mouse_over.publish(:"released_#{button}_mouse_button", window.mouse_x, window.mouse_y)
@mouse_over.publish(:"clicked_#{button}_mouse_button", window.mouse_x, window.mouse_y) if @mouse_over == @mouse_down_on[button]
if @mouse_over == @mouse_down_on[button]
@mouse_over.publish(:"clicked_#{button}_mouse_button", window.mouse_x,
window.mouse_y)
end
end
if @dragging_element
@dragging_element.publish(:end_drag, window.mouse_x, window.mouse_y, button)
@dragging_element = nil
end
@mouse_down_position[button] = nil
@@ -109,11 +237,65 @@ module CyberarmEngine
end
def redirect_holding_mouse_button(button)
@mouse_over.publish(:"holding_#{button}_mouse_button", window.mouse_x, window.mouse_y) if @mouse_over
if !@dragging_element && @mouse_down_on[button] && @mouse_down_on[button].draggable?(button) && @mouse_pos.distance(@mouse_down_position[button]) > @min_drag_distance
@dragging_element = @mouse_down_on[button]
@dragging_element.publish(:begin_drag, window.mouse_x, window.mouse_y, button)
end
if @dragging_element
@dragging_element.publish(:drag_update, window.mouse_x, window.mouse_y, button) if @dragging_element
elsif @mouse_over
@mouse_over.publish(:"holding_#{button}_mouse_button", window.mouse_x, window.mouse_y)
end
end
def redirect_mouse_wheel(button)
@mouse_over.publish(:"mouse_wheel_#{button}", window.mouse_x, window.mouse_y) if @mouse_over
end
# Schedule a full GUI recalculation on next update
def request_recalculate
@pending_recalculate_request = true
end
def request_recalculate_for(element)
# element is already queued
return if @pending_element_recalculate_requests.detect { |e| e == element }
@pending_element_recalculate_requests << element
end
def request_focus(element)
@pending_focus_request = true
@pending_focus_element = element
end
def request_repaint
# puts caller[0..4]
# puts
@needs_repaint = true
end
def show_menu(list_box)
@menu = list_box
end
def hide_menu
return unless @menu
request_repaint
@hid_menu_for = @menu.parent
@menu = nil
end
def to_s
# "#{self.class} children=#{@children.map { |c| c.to_s }}"
@root_container.to_s
end
def inspect
to_s
end
end
end
end

View File

@@ -1,42 +0,0 @@
module CyberarmEngine
class Image < Element
def initialize(path, options = {}, block = nil)
super(options, block)
@path = path
@image = Gosu::Image.new(path, retro: @options[:image_retro])
if @options[:width] && @options[:height]
@scale_x = @options[:width].to_f / @image.width
@scale_y = @options[:height].to_f / @image.height
elsif @options[:width]
@scale_x = @options[:width].to_f / @image.width
@scale_y = @scale_x
elsif @options[:height]
@scale_y = @options[:height].to_f / @image.height
@scale_x = @scale_y
else
@scale_x, @scale_y = 1, 1
end
raise "Scale X" unless @scale_x.is_a?(Numeric)
raise "Scale Y" unless @scale_y.is_a?(Numeric)
end
def render
@image.draw(@border_thickness_left + @padding_left + @x, @border_thickness_top + @padding_top + @y, @z + 2, @scale_x, @scale_y) # TODO: Add color support?
end
def clicked_left_mouse_button(sender, x, y)
@block.call(self) if @block
end
def recalculate
@width = @image.width * @scale_x
@height = @image.height * @scale_y
end
def value
@path
end
end
end

View File

@@ -1,34 +0,0 @@
module CyberarmEngine
class Label < Element
def initialize(text, options = {}, block = nil)
super(options, block)
@text = Text.new(text, font: @options[:font], z: @z, color: @options[:color], size: @options[:text_size], shadow: @options[:text_shadow])
return self
end
def render
@text.draw
end
def clicked_left_mouse_button(sender, x, y)
@block.call(self) if @block
end
def recalculate
@width = @text.width.round
@height= @text.height.round
@text.x = @border_thickness_left + @padding_left + @x
@text.y = @border_thickness_top + @padding_top + @y
@text.z = @z + 3
update_background
end
def value
@text.text
end
end
end

View File

@@ -1,11 +0,0 @@
module CyberarmEngine
class Stack < Container
include Common
def layout
@children.each do |child|
move_to_next_line(child)
end
end
end
end

View File

@@ -1,15 +1,50 @@
module CyberarmEngine
class Style
def initialize(hash)
@hash = hash
module Gosu
class Color
def _dump(_level)
[
"%02X" % alpha,
"%02X" % red,
"%02X" % green,
"%02X" % blue
].join
end
def hash
@hash
end
def set(hash)
@hash.merge!(hash)
def self._load(hex)
argb(hex.to_i(16))
end
end
end
end
module CyberarmEngine
class Style
attr_reader :hash
def initialize(hash = {})
h = hash
# h = Marshal.load(Marshal.dump(hash))
h[:default] = {}
h.each do |key, value|
next if value.is_a?(Hash)
h[:default][key] = value
end
@hash = h
end
def method_missing(method, *args)
if method.to_s.end_with?("=")
raise "Did not expect more than 1 argument" if args.size > 1
@hash[method.to_s.sub("=", "").to_sym] = args.first
elsif args.empty?
@hash[method]
else
raise ArgumentError, "Did not expect arguments"
end
end
end
end

View File

@@ -9,21 +9,42 @@ module CyberarmEngine
value
end
def theme_defaults
def theme_defaults(options)
raise "Error" unless self.class.ancestors.include?(CyberarmEngine::Element)
_theme = THEME
_theme = deep_merge(_theme, options[:theme]) if options[:theme]
_theme.delete(:theme) if options[:theme]
hash = {}
class_names = self.class.ancestors
class_names = class_names[0..class_names.index(CyberarmEngine::Element)].map! {|c| c.to_s.split("::").last.to_sym}.reverse!
class_names = class_names[0..class_names.index(CyberarmEngine::Element)].map! do |c|
c.to_s.split("::").last.to_sym
end.reverse!
class_names.each do |klass|
next unless data = THEME.dig(klass)
data.each do |key, value|
next unless data = _theme.dig(klass)
data.each do |_key, _value|
hash.merge!(data)
end
end
hash
deep_merge(hash, options)
end
# Derived from Rails Hash#deep_merge!
# Enables passing partial themes through Element options without issue
def deep_merge(original, intergrate, &block)
original.merge(intergrate) do |key, this_val, other_val|
if this_val.is_a?(Hash) && other_val.is_a?(Hash)
deep_merge(this_val, other_val, &block)
elsif block_given?
block.call(key, this_val, other_val)
else
other_val
end
end
end
THEME = {
@@ -32,33 +53,45 @@ module CyberarmEngine
y: 0,
z: 30,
width: nil,
width: nil,
height: nil,
color: Gosu::Color::WHITE,
color: Gosu::Color::WHITE,
background: Gosu::Color::NONE,
margin: 0,
padding: 0,
margin: 0,
padding: 0,
border_thickness: 0,
border_color: Gosu::Color::NONE,
border_radius: 0,
border_radius: 0
},
Container: { # < Element (Base class for Stack and Flow)
debug_color: Gosu::Color::YELLOW
},
Button: { # < Label
margin: 1,
padding: 4,
border_thickness: 4,
margin: 1,
padding: 4,
border_thickness: 1,
border_color: ["ffd59674".hex, "ffff8746".hex],
border_radius: 0,
background: ["ffc75e61".to_i(16), "ffe26623".to_i(16)],
text_align: :center,
text_v_align: :center,
text_wrap: :none,
hover: {
color: Gosu::Color.rgb(200,200,200),
background: ["ffB23E41".to_i(16), "ffFF7C00".to_i(16)],
color: Gosu::Color.rgb(200, 200, 200),
background: ["ffB23E41".to_i(16), "ffFF7C00".to_i(16)]
},
active: {
color: Gosu::Color::BLACK,
background: ["ffB23E41".to_i(16)]
},
disabled: {
color: Gosu::Color::GRAY,
background: 0xff303030
}
},
@@ -69,23 +102,113 @@ module CyberarmEngine
caret_width: 2,
caret_color: Gosu::Color::WHITE,
caret_interval: 500,
selection_color: Gosu::Color.rgba(255, 128, 50, 200),
text_align: :left,
text_static: false # static text causes issues correctly displaying caret position
},
Image: {
EditBox: { # < EditLine
text_v_align: :top
},
Image: { # < Element
color: Gosu::Color::WHITE,
tileable: false,
retro: false
},
Label: { # < Element
text_size: 28,
text_shadow: false,
font: "Arial",
margin: 0,
padding: 2
TextBlock: { # < Element
text_size: 28,
text_wrap: :word_wrap, # :word_wrap, :break_word, :none
text_shadow: false,
text_border: false,
text_align: :left,
font: "Arial",
margin: 0,
padding: 2,
disabled: {
color: Gosu::Color.rgb(175, 175, 175),
}
},
Banner: { # < TextBlock
text_size: 48
},
Title: { # < TextBlock
text_size: 34
},
Subtitle: { # < TextBlock
text_size: 26
},
Tagline: { # < TextBlock
text_size: 24
},
Caption: { # < TextBlock
text_size: 22
},
Para: { # < TextBlock
text_size: 18
},
Inscription: { # < TextBlock
text_size: 16
},
ToolTip: { # < TextBlock
delay: 100, # ms
color: Gosu::Color::WHITE,
padding_top: 4,
padding_bottom: 4,
padding_left: 8,
padding_right: 8,
border_thickness: 1,
border_color: 0xffaaaaaa,
background: 0xff404040
},
Link: { # < TextBlock
color: Gosu::Color::BLUE,
border_thickness: 1,
border_bottom_color: Gosu::Color::BLUE,
hover: {
color: 0xff_ff00ff,
border_bottom_color: 0xff_ff00ff
},
active: {
color: 0xff_ff0000,
border_bottom_color: 0xff_ff0000
}
},
ToggleButton: { # < Button
checkmark: ""
},
CheckBox: { # < Flow
text_wrap: :none
},
Progress: { # < Element
width: 250,
height: 36,
background: 0xff111111,
fraction_background: [0xffc75e61, 0xffe26623],
border_thickness: 1,
border_color: [0xffd59674, 0xffff8746]
},
Slider: { # < Element
width: 250,
height: 36,
background: 0xff111111,
fraction_background: [0xffc75e61, 0xffe26623],
border_thickness: 1,
border_color: [0xffd59674, 0xffff8746]
}
}
}.freeze
end
end
end

View File

@@ -1,49 +0,0 @@
module CyberarmEngine
class ToggleButton < Button
attr_reader :toggled
def initialize(options, block = nil)
super(options[:checkmark], options, block)
@toggled = options[:toggled] || false
if @toggled
@text.text = @options[:checkmark]
else
@text.text = ""
end
return self
end
def toggled=(boolean)
@toggled = !boolean
toggle
end
def clicked_left_mouse_button(sender, x, y)
toggle
@block.call(self) if @block
end
def toggle
if @toggled
@toggled = false
@text.text = ""
else
@toggled = true
@text.text = @options[:checkmark]
end
end
def recalculate
super
@width = @text.textobject.text_width(@options[:checkmark])
update_background
end
def value
@toggled
end
end
end

View File

@@ -0,0 +1,312 @@
module CyberarmEngine
class Vector
##
# Creates a up vector
#
# Vector.new(0, 1, 0)
#
# @return [CyberarmEngine::Vector]
def self.up
Vector.new(0, 1, 0)
end
##
# Creates a down vector
#
# Vector.new(0, -1, 0)
#
# @return [CyberarmEngine::Vector]
def self.down
Vector.new(0, -1, 0)
end
##
# Creates a left vector
#
# Vector.new(-1, 0, 0)
#
# @return [CyberarmEngine::Vector]
def self.left
Vector.new(-1, 0, 0)
end
##
# Creates a right vector
#
# Vector.new(1, 0, 0)
#
# @return [CyberarmEngine::Vector]
def self.right
Vector.new(1, 0, 0)
end
##
# Creates a forward vector
#
# Vector.new(0, 0, 1)
#
# @return [CyberarmEngine::Vector]
def self.forward
Vector.new(0, 0, 1)
end
##
# Creates a backward vector
#
# Vector.new(0, 0, -1)
#
# @return [CyberarmEngine::Vector]
def self.backward
Vector.new(0, 0, -1)
end
attr_accessor :x, :y, :z, :weight
def initialize(x = 0, y = 0, z = 0, weight = 0)
@x = x
@y = y
@z = z
@weight = weight
end
alias w weight
alias w= weight=
# @return [Boolean]
def ==(other)
if other.is_a?(Numeric)
@x == other &&
@y == other &&
@z == other &&
@weight == other
elsif other.is_a?(Vector)
@x == other.x &&
@y == other.y &&
@z == other.z &&
@weight == other.weight
else
other == self
end
end
# Create a new vector using {x} and {y} values
# @return [CyberarmEngine::Vector]
def xy
Vector.new(@x, @y)
end
# Adds Vector and Numeric or Vector and Vector, excluding {weight}
# @return [CyberarmEngine::Vector]
def +(other)
if other.is_a?(Numeric)
Vector.new(
@x + other,
@y + other,
@z + other
)
else
Vector.new(
@x + other.x,
@y + other.y,
@z + other.z
)
end
end
# Subtracts Vector and Numeric or Vector and Vector, excluding {weight}
# @return [CyberarmEngine::Vector]
def -(other)
if other.is_a?(Numeric)
Vector.new(
@x - other,
@y - other,
@z - other
)
else
Vector.new(
@x - other.x,
@y - other.y,
@z - other.z
)
end
end
# Multiplies Vector and Numeric or Vector and Vector, excluding {weight}
# @return [CyberarmEngine::Vector]
def *(other)
if other.is_a?(Numeric)
Vector.new(
@x * other,
@y * other,
@z * other
)
else
Vector.new(
@x * other.x,
@y * other.y,
@z * other.z
)
end
end
def multiply_transform(transform)
e = transform.elements
x = @x * e[0] + @y * e[1] + @z * e[2] + 1 * e[3]
y = @x * e[4] + @y * e[5] + @z * e[6] + 1 * e[7]
z = @x * e[8] + @y * e[9] + @z * e[10] + 1 * e[11]
w = @x * e[12] + @y * e[13] + @z * e[14] + 1 * e[15]
Vector.new(x / 1, y / 1, z / 1, w / 1)
end
# Divides Vector and Numeric or Vector and Vector, excluding {weight}
# @return [CyberarmEngine::Vector]
def /(other)
# Duplicated to protect from DivideByZero
if other.is_a?(Numeric)
Vector.new(
(@x == 0 ? 0 : @x / other),
(@y == 0 ? 0 : @y / other),
(@z == 0 ? 0 : @z / other)
)
else
Vector.new(
(@x == 0 ? 0 : @x / other.x),
(@y == 0 ? 0 : @y / other.y),
(@z == 0 ? 0 : @z / other.z)
)
end
end
# dot product of {Vector}
# @return [Integer|Float]
def dot(other)
product = 0
a = to_a
b = other.to_a
3.times do |i|
product += (a[i] * b[i])
end
product
end
# cross product of {Vector}
# @return [CyberarmEngine::Vector]
def cross(other)
a = to_a
b = other.to_a
Vector.new(
b[2] * a[1] - b[1] * a[2],
b[0] * a[2] - b[2] * a[0],
b[1] * a[0] - b[0] * a[1]
)
end
# returns degrees
# @return [Float]
def angle(other)
Math.acos(normalized.dot(other.normalized)) * 180 / Math::PI
end
# returns magnitude of Vector, ignoring #weight
# @return [Float]
def magnitude
Math.sqrt((@x * @x) + (@y * @y) + (@z * @z))
end
##
# returns normalized {Vector}
#
# @example
# CyberarmEngine::Vector.new(50, 21.2, 45).normalized
# # => <CyberarmEngine::Vector:0x001 @x=0.7089... @y=0.3005... @z=0.6380... @weight=0>
#
# @return [CyberarmEngine::Vector]
def normalized
mag = magnitude
self / Vector.new(mag, mag, mag)
end
# returns a direction {Vector}
#
# z is pitch
#
# y is yaw
#
# x is roll
# @return [CyberarmEngine::Vector]
def direction
_x = -Math.sin(@y.degrees_to_radians) * Math.cos(@z.degrees_to_radians)
_y = Math.sin(@z.degrees_to_radians)
_z = Math.cos(@y.degrees_to_radians) * Math.cos(@z.degrees_to_radians)
Vector.new(_x, _y, _z)
end
# returns an inverse {Vector}
# @return [CyberarmEngine::Vector]
def inverse
Vector.new(1.0 / @x, 1.0 / @y, 1.0 / @z)
end
# Adds up values of {x}, {y}, and {z}
# @return [Integer|Float]
def sum
@x + @y + @z
end
##
# Linear interpolation: smoothly transition between two {Vector}
#
# CyberarmEngine::Vector.new(100, 100, 100).lerp( CyberarmEngine::Vector.new(0, 0, 0), 0.75 )
# # => <CyberarmEngine::Vector:0x0001 @x=75.0, @y=75.0, @z=75.0, @weight=0>
#
# @param other [CyberarmEngine::Vector | Integer | Float] value to subtract from
# @param factor [Float] how complete transition to _other_ is, in range [0.0..1.0]
# @return [CyberarmEngine::Vector]
def lerp(other, factor)
(self - other) * factor.clamp(0.0, 1.0)
end
# 2D distance using X and Y
# @return [Float]
def distance(other)
Math.sqrt((@x - other.x)**2 + (@y - other.y)**2)
end
# 2D distance using X and Z
# @return [Float]
def gl_distance2d(other)
Math.sqrt((@x - other.x)**2 + (@z - other.z)**2)
end
# 3D distance using X, Y, and Z
# @return [Float]
def distance3d(other)
Math.sqrt((@x - other.x)**2 + (@y - other.y)**2 + (@z - other.z)**2)
end
# Converts {Vector} to Array
# @return [Array]
def to_a
[@x, @y, @z, @weight]
end
# Converts {Vector} to String
# @return [String]
def to_s
"X: #{@x}, Y: #{@y}, Z: #{@z}, Weight: #{@weight}"
end
# Converts {Vector} to Hash
# @return [Hash]
def to_h
{ x: @x, y: @y, z: @z, weight: @weight }
end
end
end

View File

@@ -1,4 +1,4 @@
module CyberarmEngine
NAME = "InDev"
VERSION = "0.1.0"
NAME = "InDev".freeze
VERSION = "0.23.0".freeze
end

View File

@@ -0,0 +1,195 @@
module CyberarmEngine
class Window < Gosu::Window
include Common
IMAGES = {}
SAMPLES = {}
SONGS = {}
attr_accessor :show_cursor, :show_stats_plotter
attr_writer :exit_on_opengl_error
attr_reader :last_frame_time, :delta_time, :states
def self.now
Gosu.milliseconds
end
def self.dt
instance.dt
end
def self.instance=(window)
raise ArgumentError, "Expected window to be a subclass of CyberarmEngine::Window, got: #{window.class}" unless window.is_a?(CyberarmEngine::Window)
@@instance = window
end
def self.instance
@@instance
end
def initialize(width: 800, height: 600, fullscreen: false, update_interval: 1000.0 / 60, resizable: false, borderless: false)
@show_cursor = false
@has_focus = false
@show_stats_plotter = false
super(width, height, fullscreen: fullscreen, update_interval: update_interval, resizable: resizable, borderless: borderless)
Window.instance = self
@last_frame_time = Gosu.milliseconds - 1
@current_frame_time = Gosu.milliseconds
@delta_time = @last_frame_time
self.caption = "CyberarmEngine #{CyberarmEngine::VERSION} #{Gosu.user_languages.join(', ')}"
@states = []
@exit_on_opengl_error = false
preload_default_shaders if respond_to?(:preload_default_shaders)
@stats_plotter = Stats::StatsPlotter.new(2, 28) # FIXME: Make positioning easy
setup
end
def setup
end
def draw
Stats.frame.start_timing(:draw)
current_state&.draw
Stats.frame.start_timing(:engine_stats_renderer)
@stats_plotter&.draw if @show_stats_plotter
Stats.frame.end_timing(:engine_stats_renderer)
Stats.frame.end_timing(:draw)
Stats.frame.start_timing(:interframe_sleep)
end
def update
# Gosu calls update() then (optionally) draw(),
# so always end last frame and start next frame when update() is called.
Stats.frame&.end_timing(:interframe_sleep)
Stats.end_frame
Stats.new_frame
@delta_time = (Gosu.milliseconds - @current_frame_time) * 0.001
@last_frame_time = Gosu.milliseconds - @current_frame_time
@current_frame_time = Gosu.milliseconds
Stats.frame.start_timing(:update)
current_state&.update
Stats.frame.end_timing(:update)
Stats.frame.start_timing(:interframe_sleep) unless needs_redraw?
end
def needs_cursor?
@show_cursor
end
def needs_redraw?
current_state ? current_state.needs_redraw? : true
end
def drop(filename)
current_state&.drop(filename)
end
def gamepad_connected(index)
current_state&.gamepad_connected(index)
end
def gamepad_disconnected(index)
current_state&.gamepad_disconnected(index)
end
def gain_focus
@has_focus = true
current_state&.gain_focus
end
def lose_focus
@has_focus = false
current_state&.lose_focus
end
def button_down(id)
super
current_state&.button_down(id)
end
def button_up(id)
super
current_state&.button_up(id)
end
def close
current_state ? current_state.close : super
end
def dt
@last_frame_time / 1000.0
end
def aspect_ratio
width / height.to_f
end
def exit_on_opengl_error?
@exit_on_opengl_error
end
def push_state(klass, options = {})
options = { setup: true }.merge(options)
if klass.instance_of?(klass.class) && klass.respond_to?(:options)
@states << klass
klass.setup if options[:setup]
klass.post_setup if options[:setup]
else
@states << klass.new(options) if child_of?(klass, GameState)
@states << klass.new if child_of?(klass, Element::Container)
current_state.setup if current_state.instance_of?(klass) && options[:setup]
current_state.post_setup if current_state.instance_of?(klass) && options[:setup]
end
end
private def child_of?(input, klass)
return false unless input
input.ancestors.detect { |c| c == klass }
end
def current_state
@states.last
end
def pop_state
@states.pop
current_state.request_repaint if current_state&.is_a?(GuiState)
end
def shift_state
@states.shift
current_state.request_repaint if current_state&.is_a?(GuiState)
end
def has_focus?
@has_focus
end
# Sourced from https://gist.github.com/ippa/662583
def draw_circle(cx, cy, r, z = 9999, color = Gosu::Color::GREEN, step = 10)
0.step(360, step) do |a1|
a2 = a1 + step
draw_line(cx + Gosu.offset_x(a1, r), cy + Gosu.offset_y(a1, r), color, cx + Gosu.offset_x(a2, r),
cy + Gosu.offset_y(a2, r), color, z)
end
end
end
end

29
mrbgem.rake Normal file
View File

@@ -0,0 +1,29 @@
MRuby::Gem::Specification.new("mruby-cyberarm_engine") do |spec|
spec.license = "MIT"
spec.authors = "cyberarm"
spec.summary = " Yet another framework for building games with Gosu"
lib_rbfiles = []
# Dir.glob("#{File.expand_path("..", __FILE__)}/lib/**/*.rb").reject do |f|
# File.basename(f.downcase, ".rb") == "cyberarm_engine" ||
# File.basename(f.downcase, ".rb") == "opengl" ||
# f.downcase.include?("/opengl/")
# end.reverse!
local_path = File.expand_path("..", __FILE__)
File.read("#{local_path}/lib/cyberarm_engine.rb").each_line do |line|
line = line.strip
next unless line.start_with?("require_relative")
file = line.split("require_relative").last.strip.gsub("\"", "")
next if file.include?(" if ")
lib_rbfiles << "#{local_path}/lib/#{file}.rb"
end
pp lib_rbfiles
spec.rbfiles = lib_rbfiles
end

View File

@@ -1,4 +1,4 @@
$LOAD_PATH.unshift File.expand_path("../../lib", __FILE__)
$LOAD_PATH.unshift File.expand_path("../lib", __dir__)
require "cyberarm_engine"
require "minitest/autorun"