├── .gitignore ├── LICENSE ├── README.md ├── app ├── grot_debug.rb ├── main.rb └── particles.rb └── sprites └── white.png /.gitignore: -------------------------------------------------------------------------------- 1 | *DS_Store* 2 | app/persist.rb 3 | app/amir.rb 4 | app/grot_debug_side.rb 5 | app/main-array-size-test.rb 6 | app/main-patterns-working.rb 7 | app/main-main.rb 8 | app/main-patterns.rb 9 | app/main-working.rb 10 | app/main-launch.rb 11 | app/mailbox.rb 12 | app/repl.rb 13 | app/mailbox-processed/mailbox-28515.rb 14 | app/autocomplete.txt 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Grotesk 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Particle Effects 2 | Some particle effects for DragonRuby. 3 | 4 | To use: require the `app/particles.rb` file. 5 | 6 | To create the particle effect: 7 | 8 | To avoid slowing down the game, particle effects must be created at the start (tick 0). 9 | 10 | e.g. 11 | 12 | ```$smoke = Smoke.new(512) if args.state.tick_count.zero?``` 13 | 14 | Effects accept one parameter: the number of particles to create. When publishing to html5, keep these numbers low to avoid low framerates. 15 | 16 | To activate an effect: 17 | 18 | ```$smoke.activate(*args.inputs.mouse.point)``` 19 | 20 | Activate accepts two parameters representing the x and y location for the effect to appear. 21 | 22 | ### Creating or customizing effects 23 | 24 | You can create a new effect by inheriting ParticleEffect. 25 | 26 | ```ruby 27 | # smokey rising grey effect 28 | class Smoke < ParticleEffect 29 | 30 | # this mandatory method must return the basic movement of the particle. 31 | # note that two values are returned (x movement and y movement). 32 | def step 33 | dir_x, dir_y = normalize((3 * rand) - 1.5, (3 * rand) - 1.5) 34 | step_x = (2 * rand) * dir_x 35 | step_y = (2 * rand) * dir_y 36 | return step_x, step_y 37 | end 38 | 39 | # this optional method allows you to calculate the next x, y position of the particle 40 | # based on the current position, the base x, y movement of the particle 41 | # and the 'keyframe' value (the number of frames to wait before drawing). 42 | # If this method is omitted, movement is based on the calculation in the step method 43 | def move(x, y, step_x, step_y, keyframe) 44 | next_y = y + Math.cos(Math.atan(step_x) * Math.atan(step_y)) * keyframe * 2 45 | next_x = x + Math.sin(step_x * step_y) * keyframe 46 | return next_x, next_y 47 | end 48 | 49 | # this mandatory method returns the colours used by the effect 50 | # each colour is accompanied by the number of times the colour will 51 | # repeat before moving on to the next colour. 52 | # Particles die when there are no more colours to display. 53 | def colour_seq 54 | [ 55 | [[54, 54, 54], 3], # charcoal 56 | [[127, 127, 180], 4], # cold mid grey 57 | [[203, 203, 255], 4], # cold pale grey 58 | [[127, 127, 180], 4], # cold mid grey 59 | [[54, 54, 54], 3] # charcoal 60 | ] 61 | end 62 | end 63 | ``` 64 | -------------------------------------------------------------------------------- /app/grot_debug.rb: -------------------------------------------------------------------------------- 1 | $gtk.reset 2 | 3 | module GROT 4 | def self.debug(active, color: [127, 127, 127, 255]) 5 | $grot_debug = active ? Debug.new(color) : nil 6 | end 7 | 8 | def self.tick_start 9 | return unless $grot_debug 10 | 11 | $grot_debug.tick_start 12 | end 13 | 14 | def self.tick_end 15 | return unless $grot_debug 16 | 17 | $grot_debug.tick_end 18 | end 19 | 20 | def self.watch(&block) 21 | return unless $grot_debug 22 | 23 | $grot_debug.watch(&block) 24 | end 25 | 26 | # the debug class 27 | class Debug 28 | attr_accessor :watchlist 29 | 30 | def initialize(color) 31 | @watchlist = [] 32 | @color = color 33 | 34 | @tick_time = [0] 35 | @tick_time_sum = 0 36 | @sys_time_diff = 0 37 | @system_time = [0] 38 | @full_tick = [0] 39 | @full_tick_time_sum = 0 40 | 41 | watch { "FPS: #{$gtk.current_framerate.to_i}" } 42 | watch { format('Time in your tick: %.4f', @tick_time_sum) } 43 | watch { format('Time in my tick: %.4f', @sys_time_diff.to_f) } 44 | end 45 | 46 | def watch(&block) 47 | @watchlist << block 48 | end 49 | 50 | def tick_start 51 | @starting = Time.now.to_f 52 | end 53 | 54 | def tick_end 55 | calc 56 | render_watchlist 57 | end 58 | 59 | def calc 60 | @system_time.unshift(Time.now.to_f) 61 | @tick_time.unshift(@system_time[0] - @starting) 62 | @full_tick.unshift(@system_time[0] - @system_time[1]) 63 | if @tick_time.length < 60 64 | @tick_time_sum += @tick_time[0] 65 | else 66 | @tick_time_sum += (@tick_time[0] - @tick_time[-1]) 67 | @tick_time.pop 68 | @full_tick.pop 69 | @system_time.pop 70 | end 71 | @sys_time_diff = (@system_time[0] - @system_time[-1]).to_f / 60.0 72 | end 73 | 74 | def render_watchlist 75 | $gtk.args.outputs.labels << @watchlist.map_with_index do |watched, i| 76 | { 77 | x: 5, y: 720 - (i * 20), 78 | text: watched.call, 79 | size_enum: -1.5, 80 | r: @color[0], 81 | g: @color[1], 82 | b: @color[2], 83 | a: @color[3] 84 | } 85 | end 86 | end 87 | end 88 | end 89 | -------------------------------------------------------------------------------- /app/main.rb: -------------------------------------------------------------------------------- 1 | $gtk.reset 2 | 3 | require 'app/grot_debug.rb' 4 | require 'app/particles.rb' 5 | 6 | def prepare(args) 7 | args.state.prepped = true 8 | if $gtk.platform == 'Emscripten' 9 | $effect = [ 10 | Disintegrate.new(64), 11 | Melt.new(64), 12 | Explode.new(64), 13 | Smoke.new(64), 14 | Swirl.new(64) 15 | ] 16 | else 17 | $effect = [ 18 | Disintegrate.new(128), 19 | Melt.new(128), 20 | Explode.new(512), 21 | Smoke.new(256), 22 | Swirl.new(512) 23 | ] 24 | end 25 | 26 | args.state.effect_counter = 0 27 | end 28 | 29 | def handle_inputs(args) 30 | if args.inputs.mouse.down || args.inputs.keyboard.key_down.space 31 | $effect[args.state.effect_counter].activate(*args.inputs.mouse.point) 32 | args.state.effect_counter += 1 33 | args.state.effect_counter = 0 if args.state.effect_counter == $effect.count 34 | end 35 | end 36 | 37 | def tick(args) 38 | $gtk.suppress_mailbox = false 39 | GROT.debug(true) if args.state.tick_count.zero? 40 | GROT.tick_start 41 | args.outputs.background_color = [0, 0, 0] 42 | prepare(args) unless args.state.prepped == true 43 | handle_inputs(args) 44 | GROT.tick_end 45 | end -------------------------------------------------------------------------------- /app/particles.rb: -------------------------------------------------------------------------------- 1 | $gtk.reset 2 | 3 | GRAVITY = 0.05 4 | 5 | # base effect class 6 | class ParticleEffect 7 | attr_accessor :particles, :colours 8 | 9 | def initialize(num_particles) 10 | @particles = [] 11 | num_particles.times do |i| 12 | @particles << Particle.new(i, self) 13 | end 14 | @colours = colour_seq 15 | end 16 | 17 | def activate(mouse_x, mouse_y) 18 | @particles.each { |p| p.activate(mouse_x, mouse_y) } 19 | end 20 | 21 | def move(x, y, step_x, step_y, keyframe) 22 | next_x = x + step_x * keyframe 23 | next_y = y + step_y * keyframe 24 | return next_x, next_y 25 | end 26 | 27 | def normalize(x, y) 28 | theta = Math.atan2(y, x) 29 | return Math.cos(theta), Math.sin(theta) 30 | end 31 | end 32 | 33 | # smokey rising grey effect 34 | class Smoke < ParticleEffect 35 | def step 36 | dir_x, dir_y = normalize((3 * rand) - 1.5, (3 * rand) - 1.5) 37 | return (2 * rand) * dir_x, (2 * rand) * dir_y 38 | end 39 | 40 | def move(x, y, step_x, step_y, keyframe) 41 | next_y = y + Math.cos(Math.atan(step_x) * Math.atan(step_y)) * keyframe * 2 42 | next_x = x + Math.sin(step_x * step_y) * keyframe 43 | return next_x, next_y 44 | end 45 | 46 | def colour_seq 47 | [ 48 | [[54, 54, 54], 3], # charcoal 49 | [[127, 127, 180], 4], # cold mid grey 50 | [[203, 203, 255], 4], # cold pale grey 51 | [[127, 127, 180], 4], # cold mid grey 52 | [[54, 54, 54], 3] # charcoal 53 | ] 54 | end 55 | end 56 | 57 | # swirling blue and purple glowing globe 58 | class Swirl < ParticleEffect 59 | def step 60 | dir_x, dir_y = normalize((3 * rand) - 1.5, (3 * rand) - 1.5) 61 | return (0.5 / rand) * dir_x, (2 * rand) * dir_y 62 | end 63 | 64 | def move(x, y, step_x, step_y, keyframe) 65 | next_x = x + Math.sin(step_x * step_y) * keyframe 66 | next_y = y + Math.cos(step_y * step_x) * keyframe + GRAVITY 67 | return next_x, next_y 68 | end 69 | 70 | def colour_seq 71 | [ 72 | [[32, 32, 126], 5], # dark blue 73 | [[145, 22, 250], 5], # purple 74 | [[234, 0, 234], 5], # magenta 75 | [[211, 212, 212], 5] # white 76 | ] 77 | end 78 | end 79 | 80 | # wide explosion with fiery colours 81 | class Explode < ParticleEffect 82 | def step 83 | dir_x, dir_y = normalize((3 * rand) - 1.5, (3 * rand) - 1.5) 84 | return (0.5 / rand) * dir_x, (2 * rand) * dir_y 85 | end 86 | 87 | def colour_seq 88 | [ 89 | [[194, 195, 199], 4], # light grey 90 | [[255, 236, 39], 1], # yellow 91 | [[255, 163, 0], 2], # orange 92 | [[255, 0, 77], 3], # red 93 | [[126, 37, 83], 1] # dark red 94 | ] 95 | end 96 | end 97 | 98 | # falling toxic greens and greys. twinkle yellow at the end. 99 | class Melt < ParticleEffect 100 | def step 101 | dir_x, dir_y = normalize((0.3 * rand) - 0.15, (1.5 * rand) - 1.5) 102 | return dir_x, (1.5 * rand) * dir_y 103 | end 104 | 105 | 106 | def colour_seq 107 | [ 108 | [[194, 195, 199], 2], # light grey 109 | [[0, 228, 54], 1], # light green 110 | [[0, 135, 81], 1], # dark green 111 | [[95, 87, 79], 1], # dark grey 112 | [[0, 228, 54], 1], # light green 113 | [[0, 135, 81], 1], # dark green 114 | [[95, 87, 79], 1], # dark grey 115 | [[0, 135, 81], 2], # dark green 116 | [[95, 87, 79], 1], # dark grey 117 | [[0, 135, 81], 2], # dark green 118 | [[95, 87, 79], 2], # dark grey 119 | [[255, 236, 39], 1] # yellow 120 | ] 121 | end 122 | end 123 | 124 | # just blow apart 125 | class Disintegrate < ParticleEffect 126 | def step 127 | dir_x, dir_y = normalize((5 * rand) - 3, (5 * rand) - 3) 128 | return (1.5 * rand) * dir_x, (1.5 * rand) * dir_y 129 | end 130 | 131 | def colour_seq 132 | [ 133 | [[41, 173, 255], 2], # light blue 134 | [[194, 195, 199], 4], # light grey 135 | [[95, 87, 79], 2] # dark grey 136 | ] 137 | end 138 | end 139 | 140 | # particle class (sprite) 141 | class Particle 142 | def initialize(id, caller) 143 | @x = - 10 144 | @y = - 10 145 | @w = 4 146 | @h = 4 147 | @path = 'sprites/white.png' 148 | @active = false 149 | @id = id 150 | @effect = caller 151 | @keyframe = 3 152 | 153 | @step_x, @step_y = @effect.step 154 | @step_y_ref = @step_y 155 | 156 | $args.outputs.static_sprites << self 157 | end 158 | 159 | # set the particle in motion 160 | def activate(x, y) 161 | @active = true 162 | @x = x 163 | @y = y 164 | @step_y = @step_y_ref 165 | @colour_count = 0 166 | @colour_ind = 0 167 | @colour_num = 0 168 | end 169 | 170 | # movement, colour changes and death 171 | def move 172 | # don't calculate move unless this is a keyframe 173 | return unless @active && $args.tick_count % @keyframe == @id % @keyframe 174 | 175 | # get the x, y change from effect object 176 | @x, @y = @effect.move(@x, @y, @step_x, @step_y, @keyframe) 177 | 178 | # apply gravity 179 | @step_y -= GRAVITY * @keyframe 180 | 181 | # die if out of bounds or out of colours 182 | if @colour_ind == @effect.colours.count || @x > 1280 || @x < -4 || @y < -4 183 | die 184 | return 185 | end 186 | 187 | # chance of changing colour 188 | return if rand(2).zero? 189 | 190 | # set sprite colour 191 | c = @effect.colours[@colour_ind] 192 | @r = c[0][0] 193 | @g = c[0][1] 194 | @b = c[0][2] 195 | 196 | # figure out which colour is next 197 | @colour_num += 1 198 | if @colour_num > c[1] - 1 199 | @colour_num = 0 200 | @colour_ind += 1 201 | end 202 | end 203 | 204 | # make inactive and move off screen 205 | def die 206 | @active = false 207 | @x = -999 208 | end 209 | 210 | # wizardry 211 | def draw_override(ffi_draw) 212 | move 213 | ffi_draw.draw_sprite_3 @x + 4, @y, @w, @h, # x, y, w, h, 214 | @path, # path, 215 | nil, # angle, 216 | nil, @r, @g, @b, # alpha, red_saturation, green_saturation, blue_saturation 217 | nil, nil, # flip_horizontally, flip_vertically, 218 | nil, nil, nil, nil, # tile_x, tile_y, tile_w, tile_h 219 | nil, nil, # angle_anchor_x, angle_anchor_y, 220 | nil, nil, nil, nil # source_x, source_y, source_w, source_h 221 | end 222 | 223 | # blah 224 | def serialize 225 | { step_x: step_x, step_y: step_y, r: r, g: g, b: b } 226 | end 227 | 228 | def inspect 229 | serialize.to_s 230 | end 231 | 232 | def to_s 233 | serialize.to_s 234 | end 235 | end 236 | -------------------------------------------------------------------------------- /sprites/white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oeloeloel/dragonruby-particle-explosions/da2a5939718c83ac088a5706b20e197b5117858e/sprites/white.png --------------------------------------------------------------------------------