lively 0.14.0 → 0.14.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 11a967d682e39c36470e4c6e1a00a22479f8c5fa6ec77589c04222a0fab904b6
4
- data.tar.gz: aee6f3ee6b51474cbc1815aad28a9b735a5d4fcfca130e85b82773d52620ea35
3
+ metadata.gz: 20a2334e950eae8e16b8e6e637e0b078dc78fa026d733161c619566f9491892d
4
+ data.tar.gz: b775297b7d46989e7ef63ac4b4a297ee1cd3e617da8e8e07d2be1c8a2cc56644
5
5
  SHA512:
6
- metadata.gz: 347a3273c3c2cab1f9cbd94a6cb77ea4f5772c5e153a79c24ca9739088353d57f424d1047b7409848c24b2a243a92cb4fbde7160464a219bf7ef85c1b4825df6
7
- data.tar.gz: 39544025f4b79166a81b975566e6e88529a21a11454cabaaf6542f34210a010f64e99c9836e78d00d93ee7023f9a51c33d17e416ab862dbef8b55e07b44243e5
6
+ metadata.gz: 6a38ccd950afea813d5e512bc15d8da87e78aa29d296c7aa61a294fcc25ac6dc052a9047e1212bf038b855fba7ab48de327ae6b54bdc9264fed9f9166303e31d
7
+ data.tar.gz: 2636d7b62105ca052a114585ecf29ddbb443bc20281ef4c6128698625e56e7b46dac7502120bcf43c649fb7954af32ddbd64e8d37f1d60f63375f8d717f0c61b
checksums.yaml.gz.sig CHANGED
Binary file
@@ -0,0 +1,989 @@
1
+ # Building a Flappy Bird Game with Live Views
2
+
3
+ This tutorial will guide you through creating a complete Flappy Bird-style game using Live Views, a Ruby framework for building real-time interactive applications.
4
+
5
+ We'll build the game step by step, starting with simple concepts and gradually adding complexity until we have a fully featured game with physics, collision detection, and visual effects.
6
+
7
+ > **Complete Working Example**: For the complete working implementation with all assets, code, and resources, see the `examples/flappy-bird` directory in this repository. This tutorial walks through the concepts step by step, while the example directory contains the final working game.
8
+
9
+ ## What You'll Build
10
+
11
+ By the end of this tutorial, you'll have created:
12
+
13
+ - A physics-based game with gravity and momentum
14
+ - Interactive bird control with keyboard and touch input
15
+ - Procedurally generated obstacles (pipes)
16
+ - Collision detection system
17
+ - Particle effects and visual feedback
18
+ - Sound effects and background music
19
+ - High score tracking and persistence
20
+ - Customizable bird skins
21
+ - Real-time updates using WebSockets
22
+
23
+ ## Prerequisites
24
+
25
+ - Basic knowledge of Ruby programming
26
+ - Understanding of classes and objects
27
+ - Familiarity with HTML/CSS basics
28
+ - Live framework installed (follow the getting started guide if needed)
29
+ - Basic understanding of coordinate systems and physics
30
+
31
+ ## Tutorial Approach
32
+
33
+ We'll build this game in stages:
34
+
35
+ 1. **Static Game World**: Create the game canvas and basic rendering
36
+ 2. **Physics Simulation**: Add gravity, movement, and time-based updates
37
+ 3. **Interactive Bird**: Create a controllable bird with jumping mechanics
38
+ 4. **Obstacle System**: Add pipes with collision detection
39
+ 5. **Game Logic**: Implement scoring, game over, and restart functionality
40
+ 6. **Visual Effects**: Add particles, animations, and polish
41
+ 7. **Audio System**: Integrate sound effects and music
42
+ 8. **Customization**: Add skin selection and high scores
43
+
44
+ ## Step 1: Setting Up the Game Canvas
45
+
46
+ First, let's create the basic structure for our game. We'll start with a simple view that renders a game area.
47
+
48
+ Create a new file called `flappy_basic.rb`:
49
+
50
+ ```ruby
51
+ require 'live'
52
+
53
+ class FlappyBasicView < Live::View
54
+ WIDTH = 420
55
+ HEIGHT = 640
56
+
57
+ def initialize(...)
58
+ super(...)
59
+
60
+ @game_started = false
61
+ @prompt = "Press space or tap to start :)"
62
+ end
63
+
64
+ def render(builder)
65
+ builder.tag(:div, class: "flappy", tabIndex: 0) do
66
+ builder.inline_tag(:div, class: "prompt") do
67
+ builder.text(@prompt)
68
+ end
69
+ end
70
+ end
71
+ end
72
+ ```
73
+
74
+ Add basic CSS styling to see our game canvas:
75
+
76
+ ```css
77
+ .flappy {
78
+ background-image: url('/assets/flappy-background.png');
79
+ background-size: auto 100%;
80
+ image-rendering: pixelated;
81
+
82
+ width: 420px;
83
+ height: 640px;
84
+ margin: auto;
85
+
86
+ position: relative;
87
+ overflow: hidden;
88
+
89
+ transform: translate3d(0,0,0);
90
+ }
91
+
92
+ .flappy .prompt {
93
+ z-index: 20;
94
+ padding: 1rem;
95
+ color: white;
96
+ text-shadow:
97
+ -1px -1px 0 #000,
98
+ 1px -1px 0 #000,
99
+ -1px 1px 0 #000,
100
+ 1px 1px 0 #000;
101
+
102
+ position: absolute;
103
+ left: 0;
104
+ right: 0;
105
+ top: 0;
106
+ bottom: 0;
107
+
108
+ text-align: center;
109
+ }
110
+ ```
111
+
112
+ **Test it now**: You should see a game canvas with a background and prompt text.
113
+
114
+ ## Step 2: Creating the Bounding Box System
115
+
116
+ Before we can create game objects, we need a way to handle positioning and collision detection. Let's create a `BoundingBox` class:
117
+
118
+ ```ruby
119
+ class BoundingBox
120
+ def initialize(x, y, width, height)
121
+ @x = x
122
+ @y = y
123
+ @width = width
124
+ @height = height
125
+ end
126
+
127
+ attr :x
128
+ attr :y
129
+ attr :width
130
+ attr :height
131
+
132
+ def right
133
+ @x + @width
134
+ end
135
+
136
+ def top
137
+ @y + @height
138
+ end
139
+
140
+ def center
141
+ [@x + @width/2, @y + @height/2]
142
+ end
143
+
144
+ def intersect?(other)
145
+ !(
146
+ self.right < other.x ||
147
+ self.x > other.right ||
148
+ self.top < other.y ||
149
+ self.y > other.top
150
+ )
151
+ end
152
+
153
+ def to_s
154
+ "#<#{self.class} (#{@x}, #{@y}, #{@width}, #{@height}>"
155
+ end
156
+ end
157
+ ```
158
+
159
+ **Key concepts:**
160
+ - `BoundingBox` represents rectangular areas in our game
161
+ - `intersect?` method detects when two rectangles overlap (collision detection)
162
+ - `center` gives us the middle point for positioning effects
163
+ - `right` and `top` calculate the boundaries of the rectangle
164
+
165
+ ## Step 3: Building the Bird with Physics
166
+
167
+ Now let's create our main character - the bird - with realistic physics simulation:
168
+
169
+ ```ruby
170
+ class Bird < BoundingBox
171
+ GRAVITY = -9.8 * 50.0 # Scaled gravity for game feel
172
+
173
+ def initialize(x = 30, y = HEIGHT / 2, width: 34, height: 24, skin: nil)
174
+ super(x, y, width, height)
175
+ @skin = skin || 'bird'
176
+ @velocity = 0.0
177
+ @jumping = false
178
+ @dying = false
179
+ end
180
+
181
+ attr :skin
182
+
183
+ def dying?
184
+ @dying != false
185
+ end
186
+
187
+ def alive?
188
+ @dying == false
189
+ end
190
+
191
+ def step(dt)
192
+ # Apply gravity to velocity
193
+ @velocity += GRAVITY * dt
194
+ # Apply velocity to position
195
+ @y += @velocity * dt
196
+
197
+ # Ground collision
198
+ if @y > HEIGHT
199
+ @y = HEIGHT
200
+ @velocity = 0.0
201
+ end
202
+
203
+ # Handle jump effect duration
204
+ if @jumping
205
+ @jumping -= dt
206
+ if @jumping < 0
207
+ @jumping = false
208
+ end
209
+ end
210
+
211
+ # Handle death animation
212
+ if @dying
213
+ @dying += dt
214
+ end
215
+ end
216
+
217
+ def jump(extreme = false)
218
+ return if @dying
219
+
220
+ @velocity = 300.0 # Upward velocity
221
+
222
+ if extreme
223
+ @jumping = 0.5 # Visual effect duration
224
+ end
225
+ end
226
+
227
+ def die
228
+ @dying = 0.0
229
+ end
230
+
231
+ def render(builder)
232
+ # Calculate rotation based on velocity
233
+ if @dying
234
+ rotation = 360.0 * (@dying * 2.0) # Spin when dying
235
+ else
236
+ rotation = (@velocity / 20.0).clamp(-40.0, 40.0) # Tilt based on velocity
237
+ end
238
+
239
+ rotate = "rotate(#{-rotation}deg)"
240
+
241
+ builder.inline_tag(:div,
242
+ class: "bird #{@skin}",
243
+ style: "left: #{@x}px; bottom: #{@y}px; width: #{@width}px; height: #{@height}px; transform: #{rotate};"
244
+ )
245
+
246
+ # Render jump particles
247
+ if @jumping
248
+ center = self.center
249
+
250
+ 10.times do |i|
251
+ angle = (360 / 10) * i
252
+ id = "bird-#{self.__id__}-particle-#{i}"
253
+
254
+ builder.inline_tag(:div,
255
+ id: id,
256
+ class: 'particle jump',
257
+ style: "left: #{center[0]}px; bottom: #{center[1]}px; --rotation-angle: #{angle}deg;"
258
+ )
259
+ end
260
+ end
261
+ end
262
+ end
263
+ ```
264
+
265
+ Add CSS for the bird:
266
+
267
+ ```css
268
+ .flappy .bird {
269
+ z-index: 1;
270
+ background-image: url('/assets/flappy-bird.webp');
271
+ position: absolute;
272
+ background-size: contain;
273
+
274
+ transform: translate3d(0,0,0);
275
+ transition: all 0.033s linear 0s;
276
+ }
277
+
278
+ @keyframes particle-jump {
279
+ 0% {
280
+ transform: rotate(var(--rotation-angle)) translate(0, 0);
281
+ opacity: 1;
282
+ }
283
+ 100% {
284
+ transform: rotate(var(--rotation-angle)) translate(100px, 100px);
285
+ opacity: 0;
286
+ }
287
+ }
288
+
289
+ .particle.jump {
290
+ --rotation-angle: 0deg;
291
+ position: absolute;
292
+ width: 5px;
293
+ height: 5px;
294
+ background: #ffee00;
295
+ border-radius: 50%;
296
+ opacity: 0;
297
+
298
+ transform: rotate(var(--rotation-angle));
299
+ animation: particle-jump 0.5s;
300
+ }
301
+ ```
302
+
303
+ **Key physics concepts:**
304
+ - **Gravity**: Constant downward acceleration
305
+ - **Velocity**: How fast the bird is moving (changes due to gravity)
306
+ - **Position**: Where the bird is (changes due to velocity)
307
+ - **Delta time (dt)**: Time between frames for smooth animation
308
+
309
+ ## Step 4: Adding Pipes (Obstacles)
310
+
311
+ Let's create the pipes that the bird must navigate through:
312
+
313
+ ```ruby
314
+ class Pipe
315
+ def initialize(x, y, offset = 100, random: 0, width: 44, height: 700)
316
+ @x = x
317
+ @y = y
318
+ @offset = offset # Gap size between upper and lower pipes
319
+ @width = width
320
+ @height = height
321
+ @difficulty = 0.0
322
+ @scored = false
323
+ @random = random
324
+ end
325
+
326
+ attr :x
327
+ attr :y
328
+ attr :offset
329
+ attr_accessor :scored
330
+
331
+ def scaled_random
332
+ @random.rand(-0.8..0.8) * [@difficulty, 1.0].min
333
+ end
334
+
335
+ def reset!
336
+ @x = WIDTH + (@random.rand * 10)
337
+ @y = HEIGHT/2 + (HEIGHT/2 * scaled_random)
338
+
339
+ # Gradually increase difficulty by making gap smaller
340
+ if @offset > 50
341
+ @offset -= 1
342
+ end
343
+
344
+ @difficulty += 0.1
345
+ @scored = false
346
+ end
347
+
348
+ def step(dt)
349
+ @x -= 100 * dt # Move left at constant speed
350
+
351
+ if self.right < 0
352
+ reset!
353
+ yield if block_given? # Notify when pipe resets
354
+ end
355
+ end
356
+
357
+ def right
358
+ @x + @width
359
+ end
360
+
361
+ def top
362
+ @y + @offset
363
+ end
364
+
365
+ def bottom
366
+ (@y - @offset) - @height
367
+ end
368
+
369
+ def center
370
+ [@x + @width/2, @y]
371
+ end
372
+
373
+ def lower_bounding_box
374
+ BoundingBox.new(@x, self.bottom, @width, @height)
375
+ end
376
+
377
+ def upper_bounding_box
378
+ BoundingBox.new(@x, self.top, @width, @height)
379
+ end
380
+
381
+ def intersect?(other)
382
+ lower_bounding_box.intersect?(other) || upper_bounding_box.intersect?(other)
383
+ end
384
+
385
+ def render(builder)
386
+ display = "display: none;" if @x > WIDTH
387
+
388
+ # Render lower pipe
389
+ builder.inline_tag(:div,
390
+ class: 'pipe',
391
+ style: "left: #{@x}px; bottom: #{self.bottom}px; width: #{@width}px; height: #{@height}px; #{display}"
392
+ )
393
+ # Render upper pipe
394
+ builder.inline_tag(:div,
395
+ class: 'pipe',
396
+ style: "left: #{@x}px; bottom: #{self.top}px; width: #{@width}px; height: #{@height}px; #{display}"
397
+ )
398
+ end
399
+ end
400
+ ```
401
+
402
+ Add CSS for pipes:
403
+
404
+ ```css
405
+ .flappy .pipe {
406
+ z-index: 5;
407
+ background-image: url('/assets/flappy-pipe.png');
408
+ position: absolute;
409
+ background-size: contain;
410
+
411
+ transform: translate3d(0,0,0);
412
+ transition: all 0.033s linear 0s;
413
+ }
414
+ ```
415
+
416
+ **Key pipe concepts:**
417
+ - **Procedural generation**: Pipes are positioned randomly within bounds
418
+ - **Scrolling**: Pipes move left at constant speed
419
+ - **Recycling**: When pipes move off-screen, they reset to the right
420
+ - **Difficulty scaling**: Gap gets smaller and positions more varied over time
421
+
422
+ ## Step 5: Creating the Bonus System
423
+
424
+ Let's add collectible gemstones that double your score:
425
+
426
+ ```ruby
427
+ class Gemstone < BoundingBox
428
+ COLLECTED_AGE = 1.0
429
+
430
+ def initialize(x, y, width: 148/2, height: 116/2)
431
+ super(x - width/2, y - height/2, width, height)
432
+ @collected = false
433
+ end
434
+
435
+ def collected?
436
+ @collected != false
437
+ end
438
+
439
+ def step(dt)
440
+ @x -= 100 * dt # Move with same speed as pipes
441
+
442
+ if @collected
443
+ @collected -= dt
444
+
445
+ if @collected < 0
446
+ @collected = false
447
+ yield if block_given? # Notify when collection animation ends
448
+ end
449
+ end
450
+ end
451
+
452
+ def collect!
453
+ @collected = COLLECTED_AGE
454
+ end
455
+
456
+ def render(builder)
457
+ if @collected
458
+ opacity = @collected / COLLECTED_AGE
459
+ else
460
+ opacity = 1.0
461
+ end
462
+
463
+ builder.inline_tag(:div,
464
+ class: 'gemstone',
465
+ style: "left: #{@x}px; bottom: #{@y}px; width: #{@width}px; height: #{@height}px; opacity: #{opacity};"
466
+ )
467
+
468
+ # Add particle effects when collected
469
+ if @collected
470
+ center = self.center
471
+
472
+ 10.times do |i|
473
+ angle = (360 / 10) * i
474
+ id = "gemstone-#{self.__id__}-particle-#{i}"
475
+
476
+ builder.inline_tag(:div,
477
+ id: id,
478
+ class: 'particle bonus',
479
+ style: "left: #{center[0]}px; bottom: #{center[1]}px; --rotation-angle: #{angle}deg;"
480
+ )
481
+ end
482
+ end
483
+ end
484
+ end
485
+ ```
486
+
487
+ Add CSS for gemstones:
488
+
489
+ ```css
490
+ .flappy .gemstone {
491
+ z-index: 0;
492
+ background-image: url('/assets/gemstone.gif');
493
+ position: absolute;
494
+ background-size: contain;
495
+
496
+ transform: translate3d(0,0,0);
497
+ transition: all 0.033s linear 0s;
498
+ }
499
+
500
+ @keyframes particle-bonus {
501
+ 0% {
502
+ transform: rotate(var(--rotation-angle)) translate(0, 0);
503
+ opacity: 1;
504
+ }
505
+ 25% {
506
+ transform: rotate(var(--rotation-angle)) translate(25px, -25px);
507
+ opacity: 0.75;
508
+ }
509
+ 50% {
510
+ transform: rotate(var(--rotation-angle)) translate(50px, 50px);
511
+ opacity: 0.5;
512
+ }
513
+ 75% {
514
+ transform: rotate(var(--rotation-angle)) translate(75px, -75px);
515
+ opacity: 0.25;
516
+ }
517
+ 100% {
518
+ transform: rotate(var(--rotation-angle)) translate(100px, 100px);
519
+ opacity: 0;
520
+ }
521
+ }
522
+
523
+ .particle.bonus {
524
+ --rotation-angle: 0deg;
525
+ position: absolute;
526
+ width: 10px;
527
+ height: 10px;
528
+ background: #ff0000;
529
+ border-radius: 50%;
530
+ opacity: 0;
531
+
532
+ transform: rotate(var(--rotation-angle));
533
+ animation: particle-bonus 1.0s;
534
+ }
535
+ ```
536
+
537
+ ## Step 6: Building the Skin Selection System
538
+
539
+ Let's add a way for players to choose different bird skins:
540
+
541
+ ```ruby
542
+ class SkinSelectionView < Live::View
543
+ SKINS = ['bird', 'gull', 'kiwi', 'owl']
544
+
545
+ def handle(event)
546
+ skin = event.dig(:detail, :skin) or return
547
+ @data[:skin] = skin
548
+ self.update!
549
+ end
550
+
551
+ def skin
552
+ @data[:skin] || SKINS.first
553
+ end
554
+
555
+ def render(builder)
556
+ builder.inline_tag(:ul, class: "skins") do
557
+ SKINS.each do |skin|
558
+ selected = (skin == self.skin ? "selected" : "")
559
+ builder.inline_tag(:li,
560
+ class: selected,
561
+ onClick: forward_event(skin: skin)
562
+ ) do
563
+ builder.inline_tag(:img,
564
+ src: "/assets/flappy-#{skin}.webp",
565
+ alt: skin
566
+ )
567
+ end
568
+ end
569
+ end
570
+ end
571
+ end
572
+ ```
573
+
574
+ Add CSS for skin selection:
575
+
576
+ ```css
577
+ .flappy ul.skins {
578
+ display: block;
579
+ text-align: center;
580
+ padding: 0;
581
+ margin: 0;
582
+ }
583
+
584
+ .flappy .skins li {
585
+ display: inline-block;
586
+ padding: 0.5rem;
587
+ margin: 0.5rem;
588
+ }
589
+
590
+ .flappy .skins img {
591
+ width: 34px;
592
+ vertical-align: middle;
593
+ }
594
+
595
+ .flappy .skins li.selected {
596
+ background-color: rgba(255, 255, 255, 0.5);
597
+ border-radius: 0.5rem;
598
+ }
599
+
600
+ .flappy .bird.gull {
601
+ background-image: url('/assets/flappy-gull.webp');
602
+ }
603
+
604
+ .flappy .bird.kiwi {
605
+ background-image: url('/assets/flappy-kiwi.webp');
606
+ }
607
+
608
+ .flappy .bird.owl {
609
+ background-image: url('/assets/flappy-owl.webp');
610
+ }
611
+ ```
612
+
613
+ ## Step 7: Implementing the Main Game Logic
614
+
615
+ Now let's put it all together in the main `FlappyView` class:
616
+
617
+ ```ruby
618
+ class FlappyView < Live::View
619
+ WIDTH = 420
620
+ HEIGHT = 640
621
+
622
+ def initialize(...)
623
+ super(...)
624
+
625
+ @game = nil
626
+ @bird = nil
627
+ @pipes = nil
628
+ @bonus = nil
629
+
630
+ @skin_selection = SkinSelectionView.mount(self, 'skin-selection')
631
+
632
+ # Game state
633
+ @score = 0
634
+ @count = 0
635
+ @scroll = 0
636
+ @prompt = "Press space or tap to start :)"
637
+
638
+ @random = nil
639
+ end
640
+
641
+ attr :bird
642
+
643
+ def bind(page)
644
+ super
645
+ page.attach(@skin_selection)
646
+ end
647
+
648
+ def close
649
+ if @game
650
+ @game.stop
651
+ @game = nil
652
+ end
653
+
654
+ page.detach(@skin_selection)
655
+ super
656
+ end
657
+
658
+ def jump
659
+ if (extreme = rand > 0.8)
660
+ play_sound(@bird.skin)
661
+ end
662
+
663
+ @bird&.jump(extreme)
664
+ end
665
+
666
+ def handle(event)
667
+ detail = event[:detail]
668
+
669
+ case event[:type]
670
+ when "keypress", "touchstart"
671
+ if @game.nil?
672
+ self.start_game!
673
+ elsif detail[:key] == " " || detail[:touch]
674
+ self.jump
675
+ end
676
+ end
677
+ end
678
+
679
+ def forward_keypress
680
+ "live.forwardEvent(#{JSON.dump(@id)}, event, {key: event.key})"
681
+ end
682
+
683
+ def reset!
684
+ @bird = Bird.new(skin: @skin_selection.skin)
685
+ @pipes = [
686
+ Pipe.new(WIDTH + WIDTH * 1/2, HEIGHT/2, random: @random),
687
+ Pipe.new(WIDTH + WIDTH * 2/2, HEIGHT/2, random: @random)
688
+ ]
689
+ @bonus = nil
690
+ @score = 0
691
+ @count = 0
692
+ @scroll = 0
693
+ end
694
+ end
695
+ ```
696
+
697
+ ## Step 8: Adding Sound Effects
698
+
699
+ Let's integrate audio feedback for a more immersive experience:
700
+
701
+ ```ruby
702
+ def play_sound(name)
703
+ self.script(<<~JAVASCRIPT)
704
+ if (!this.sounds) {
705
+ this.sounds = {};
706
+ }
707
+
708
+ if (!this.sounds[#{JSON.dump(name)}]) {
709
+ this.sounds[#{JSON.dump(name)}] = new Audio('/assets/#{name}.mp3');
710
+ }
711
+
712
+ this.sounds[#{JSON.dump(name)}].play();
713
+ JAVASCRIPT
714
+ end
715
+
716
+ def play_music
717
+ self.script(<<~JAVASCRIPT)
718
+ this.audioContext ||= new (window.AudioContext || window.webkitAudioContext)();
719
+
720
+ if (!this.source) {
721
+ let playAudioBuffer = (audioBuffer) => {
722
+ this.source = this.audioContext.createBufferSource();
723
+ this.source.buffer = audioBuffer;
724
+ this.source.connect(this.audioContext.destination);
725
+ this.source.loop = true;
726
+ this.source.loopStart = 32.0 * 60.0 / 80.0;
727
+ this.source.loopEnd = 96.0 * 60.0 / 80.0;
728
+ this.source.start(0, 0);
729
+ };
730
+
731
+ fetch('/assets/music.mp3')
732
+ .then(response => response.arrayBuffer())
733
+ .then(arrayBuffer => this.audioContext.decodeAudioData(arrayBuffer))
734
+ .then(playAudioBuffer);
735
+ }
736
+ JAVASCRIPT
737
+ end
738
+
739
+ def stop_music
740
+ self.script(<<~JAVASCRIPT)
741
+ if (this.source) {
742
+ this.source.stop();
743
+ this.source.disconnect();
744
+ this.source = null;
745
+ }
746
+ JAVASCRIPT
747
+ end
748
+ ```
749
+
750
+ **Audio concepts:**
751
+ - **Sound Effects**: Short audio clips for actions (jump, collect, death)
752
+ - **Background Music**: Looped music that starts after the player scores a few points
753
+ - **Web Audio API**: JavaScript API for precise audio control and timing
754
+
755
+ ## Step 9: Game Loop and Physics Simulation
756
+
757
+ The heart of our game is the main game loop that updates all objects:
758
+
759
+ ```ruby
760
+ def step(dt)
761
+ @scroll += dt
762
+
763
+ # Update bird physics
764
+ @bird.step(dt)
765
+
766
+ # Update pipes and handle scoring
767
+ @pipes.each do |pipe|
768
+ pipe.step(dt) do
769
+ # Pipe was reset - spawn bonus every 5 pipes
770
+ if @bonus.nil? and @count > 0 and (@count % 5).zero?
771
+ @bonus = Gemstone.new(*pipe.center)
772
+ end
773
+ end
774
+
775
+ # Check for scoring
776
+ if @bird.alive?
777
+ if pipe.right < @bird.x && !pipe.scored
778
+ @score += 1
779
+ @count += 1
780
+ pipe.scored = true
781
+
782
+ # Start music after 3 points
783
+ if @count == 3
784
+ play_music
785
+ end
786
+ end
787
+
788
+ # Check for collision
789
+ if pipe.intersect?(@bird)
790
+ Console.info(self, "Player has died.")
791
+ @bird.die
792
+ play_sound("death")
793
+ stop_music
794
+ end
795
+ end
796
+ end
797
+
798
+ # Update bonus gemstone
799
+ @bonus&.step(dt) do
800
+ @bonus = nil
801
+ end
802
+
803
+ if @bonus
804
+ if !@bonus.collected? and @bonus.intersect?(@bird)
805
+ play_sound("clink")
806
+ @score = @score * 2 # Double the score!
807
+ @bonus.collect!
808
+ elsif @bonus.right < 0
809
+ @bonus = nil
810
+ end
811
+ end
812
+
813
+ # Check for death by falling off screen
814
+ if @bird.top < -20
815
+ if @bird.alive?
816
+ @bird.die
817
+ play_sound("death")
818
+ end
819
+
820
+ stop_music
821
+ return game_over!
822
+ end
823
+ end
824
+
825
+ def run!(dt = 1.0/30.0)
826
+ Async do
827
+ start_time = Async::Clock.now
828
+
829
+ while true
830
+ self.step(dt)
831
+ self.update!
832
+
833
+ # Maintain consistent frame rate
834
+ duration = Async::Clock.now - start_time
835
+ if duration < dt
836
+ sleep(dt - duration)
837
+ else
838
+ Console.info(self, "Running behind by #{duration - dt} seconds")
839
+ end
840
+ start_time = Async::Clock.now
841
+ end
842
+ end
843
+ end
844
+ ```
845
+
846
+ **Game loop concepts:**
847
+ - **Fixed timestep**: Game updates 30 times per second for consistent physics
848
+ - **Frame rate independence**: Physics calculations use delta time
849
+ - **State management**: Track score, collisions, and game progression
850
+ - **Async execution**: Game loop runs independently of user interface
851
+
852
+ ## Step 10: High Score System and Game Over
853
+
854
+ Let's add persistence and competitive elements:
855
+
856
+ ```ruby
857
+ def game_over!
858
+ # Save high score to database
859
+ Highscore.create!(name: ENV.fetch("PLAYER", "Anonymous"), score: @score)
860
+
861
+ @prompt = "Game Over! Score: #{@score}. Press space or tap to restart."
862
+ @game = nil
863
+
864
+ self.update!
865
+
866
+ raise Async::Stop
867
+ end
868
+
869
+ def self.birdseed(time = Time.now)
870
+ time.year * 1000 + time.yday
871
+ end
872
+
873
+ def start_game!(seed = self.class.birdseed)
874
+ Console.info(self, "Starting game with seed: #{seed}")
875
+
876
+ if @game
877
+ @game.stop
878
+ @game = nil
879
+ end
880
+
881
+ @random = Random.new(seed) # Deterministic randomness for reproducible games
882
+
883
+ self.reset!
884
+ self.update!
885
+ self.script("this.querySelector('.flappy').focus()")
886
+ @game = self.run!
887
+ end
888
+ ```
889
+
890
+ ## Step 11: Complete Rendering System
891
+
892
+ Finally, let's implement the complete rendering system:
893
+
894
+ ```ruby
895
+ def render(builder)
896
+ builder.tag(:div,
897
+ class: "flappy",
898
+ tabIndex: 0,
899
+ onKeyPress: forward_keypress,
900
+ onTouchStart: forward_keypress
901
+ ) do
902
+ if @game
903
+ # Game is running - show score
904
+ builder.inline_tag(:div, class: "score") do
905
+ builder.text("Score: #{@score}")
906
+ end
907
+ else
908
+ # Game menu - show logo, skin selection, and high scores
909
+ builder.inline_tag(:div, class: "prompt") do
910
+ builder.inline_tag(:div, class: "logo")
911
+
912
+ builder << @skin_selection.to_html
913
+
914
+ builder.text(@prompt)
915
+
916
+ builder.inline_tag(:ol, class: "highscores") do
917
+ Highscore.top10.each do |highscore|
918
+ builder.inline_tag(:li) do
919
+ builder.text("#{highscore.name}: #{highscore.score}")
920
+ end
921
+ end
922
+ end
923
+ end
924
+ end
925
+
926
+ # Render game objects
927
+ @bird&.render(builder)
928
+
929
+ @pipes&.each do |pipe|
930
+ pipe.render(builder)
931
+ end
932
+
933
+ @bonus&.render(builder)
934
+ end
935
+ end
936
+ ```
937
+
938
+ ## Complete Implementation
939
+
940
+ Here's how all the pieces fit together in a complete game file:
941
+
942
+ ```ruby
943
+ #!/usr/bin/env lively
944
+ # frozen_string_literal: true
945
+
946
+ require 'live'
947
+
948
+ # [All the classes we've built: BoundingBox, Bird, Pipe, Gemstone, SkinSelectionView, FlappyView]
949
+ # Put them all together in a single file for easy execution
950
+
951
+ Application = Lively::Application[FlappyView]
952
+ ```
953
+
954
+ ## Key Live Framework Concepts
955
+
956
+ This tutorial demonstrated the advanced concepts needed for real-time games:
957
+
958
+ - **Physics Simulation**: Time-based movement and collision detection
959
+ - **Component Architecture**: Separating concerns into logical classes
960
+ - **Real-time Updates**: Smooth 30fps game loop with WebSocket communication
961
+ - **Event Handling**: Keyboard and touch input processing
962
+ - **Asset Management**: Images, sounds, and animations
963
+ - **State Management**: Game progression, scoring, and persistence
964
+ - **Performance Optimization**: Efficient rendering and memory management
965
+
966
+ ## Next Steps and Enhancements
967
+
968
+ Now that you understand how Live works for games, try these enhancements:
969
+
970
+ 1. **Power-ups**: Add temporary invincibility or slow-motion effects
971
+ 2. **Multiple Levels**: Different backgrounds and obstacle patterns
972
+ 3. **Multiplayer**: Real-time competitions between players
973
+ 4. **Mobile Optimization**: Touch gestures and responsive design
974
+ 5. **Analytics**: Track player behavior and difficulty balancing
975
+ 6. **Achievements**: Unlock systems and progression rewards
976
+ 7. **Custom Physics**: Experiment with different gravity or momentum
977
+ 8. **Procedural Content**: Dynamic obstacle generation algorithms
978
+
979
+ ## Performance Considerations
980
+
981
+ When building real-time games with Live:
982
+
983
+ - **Minimize DOM Updates**: Only update what has changed
984
+ - **Efficient Collision Detection**: Use spatial partitioning for many objects
985
+ - **Asset Preloading**: Load images and sounds before game starts
986
+ - **Memory Management**: Clean up particle effects and off-screen objects
987
+ - **Network Optimization**: Batch updates when possible
988
+
989
+ **Congratulations!** You've built a complete physics-based game that demonstrates the full power of Live for real-time interactive applications.
@@ -0,0 +1,137 @@
1
+ # Game Audio Tutorial
2
+
3
+ This guide shows you how to add audio to your Live.js games and applications using the `live-audio` library. You'll learn how to play sound effects, background music, and create dynamic audio experiences.
4
+
5
+ ## Overview
6
+
7
+ The `live-audio` library provides:
8
+
9
+ - **Synthesized sound effects** - Generated sounds like jumps, coins, explosions
10
+ - **Sample playback** - MP3/audio file playback for custom sounds and music
11
+ - **Real-time audio control** - Volume, playback control, and audio routing
12
+ - **Web Audio API integration** - Professional audio processing capabilities
13
+
14
+ ## Getting Started
15
+
16
+ ### Project Structure
17
+
18
+ Your Live.js project should have this basic structure:
19
+
20
+ ```
21
+ my-game/
22
+ application.rb # Your lively application file.
23
+ public/
24
+ application.js # Your main JavaScript file.
25
+ _static/
26
+ audio/ # Audio files directory.
27
+ ```
28
+
29
+ ### 1. Setting Up Audio in Your Application
30
+
31
+ First, create your main JavaScript file `public/application.js` and import the audio library:
32
+
33
+ ```javascript
34
+ import { Live, ViewElement } from 'live';
35
+ import { Audio, Library } from 'live-audio';
36
+
37
+ // Create a custom element with audio support:
38
+ customElements.define('live-game', class GameElement extends ViewElement {
39
+ #audio;
40
+
41
+ connectedCallback() {
42
+ super.connectedCallback();
43
+
44
+ // Initialize audio controller
45
+ this.#audio = Audio.start({
46
+ window,
47
+ onOutputCreated: (controller, output) => {
48
+ console.log('Audio system ready');
49
+ this.loadSounds();
50
+ }
51
+ });
52
+ }
53
+
54
+ disconnectedCallback() {
55
+ // Clean up audio
56
+ if (this.#audio) {
57
+ this.#audio.dispose();
58
+ }
59
+ super.disconnectedCallback();
60
+ }
61
+
62
+ loadSounds() {
63
+ // We'll add sounds here
64
+ }
65
+
66
+ get audio() {
67
+ return this.#audio;
68
+ }
69
+ });
70
+ ```
71
+
72
+ ### 2. Adding Synthesized Sound Effects
73
+
74
+ The library includes many pre-built synthesized sounds:
75
+
76
+ ```javascript
77
+ loadSounds() {
78
+ // Game action sounds
79
+ this.#audio.addSound('jump', new Library.JumpSound());
80
+ this.#audio.addSound('coin', new Library.CoinSound());
81
+ this.#audio.addSound('powerup', new Library.PowerUpSound());
82
+ this.#audio.addSound('death', new Library.DeathSound());
83
+
84
+ // Combat sounds
85
+ this.#audio.addSound('laser', new Library.LaserSound());
86
+ this.#audio.addSound('explosion', new Library.ExplosionSound());
87
+
88
+ // Interface sounds
89
+ this.#audio.addSound('beep', new Library.BeepSound());
90
+ this.#audio.addSound('blip', new Library.BlipSound());
91
+
92
+ // Animal sounds
93
+ this.#audio.addSound('meow', new Library.MeowSound());
94
+ this.#audio.addSound('bark', new Library.BarkSound());
95
+ this.#audio.addSound('duck', new Library.DuckSound());
96
+ this.#audio.addSound('roar', new Library.RoarSound());
97
+ this.#audio.addSound('howl', new Library.HowlSound());
98
+ this.#audio.addSound('chirp', new Library.ChirpSound());
99
+ }
100
+ ```
101
+
102
+ ### 3. Playing Sounds
103
+
104
+ To play sounds from your Ruby application, you need to connect your custom element to a Ruby view and use the `script(...)` method.
105
+
106
+ **Ruby Application (`application.rb`):**
107
+
108
+ ```ruby
109
+ require 'lively'
110
+
111
+ class GameView < Live::View
112
+ def tag_name
113
+ # This must correspond to the tag you defined in `application.js` since we are depending on client-side JavaScript.
114
+ "live-game"
115
+ end
116
+
117
+ def play_sound(sound_name)
118
+ # Use script() to call JavaScript methods on your custom element
119
+ script("this.audio.playSound('#{sound_name}')")
120
+ end
121
+
122
+ def player_jump
123
+ @player.jump
124
+ play_sound('jump') # Play jump sound
125
+ end
126
+
127
+ def collect_coin
128
+ @score += 10
129
+ play_sound('coin') # Play coin sound
130
+ end
131
+
132
+ def player_dies
133
+ @lives -= 1
134
+ play_sound('death') # Play death sound
135
+ end
136
+ end
137
+ ```
data/context/index.yaml CHANGED
@@ -14,3 +14,12 @@ files:
14
14
  title: Building a Worms Game with Lively
15
15
  description: This tutorial will guide you through creating a Worms-style game using
16
16
  Lively, a Ruby framework for building real-time applications.
17
+ - path: flappy-bird-tutorial.md
18
+ title: Building a Flappy Bird Game with Live Views
19
+ description: This tutorial will guide you through creating a complete Flappy Bird-style
20
+ game using Live Views, a Ruby framework for building real-time interactive applications.
21
+ - path: game-audio-tutorial.md
22
+ title: Game Audio Tutorial
23
+ description: This guide shows you how to add audio to your Live.js games and applications
24
+ using the `live-audio` library. You'll learn how to play sound effects, background
25
+ music, and create dynamic audio experiences.
@@ -4,6 +4,8 @@ This tutorial will guide you through creating a Worms-style game using Lively, a
4
4
 
5
5
  We'll build the game step by step, starting with simple concepts and gradually adding complexity.
6
6
 
7
+ > **Complete Working Example**: For the complete working implementation with all assets, code, and resources, see the `examples/worms` directory in this repository. This tutorial walks through the concepts step by step, while the example directory contains the final working game.
8
+
7
9
  ## What You'll Build
8
10
 
9
11
  By the end of this tutorial, you'll have created:
@@ -5,5 +5,5 @@
5
5
 
6
6
  # @namespace
7
7
  module Lively
8
- VERSION = "0.14.0"
8
+ VERSION = "0.14.1"
9
9
  end
data/readme.md CHANGED
@@ -10,7 +10,11 @@ Please see the [project documentation](https://socketry.github.io/lively/) for m
10
10
 
11
11
  - [Getting Started](https://socketry.github.io/lively/guides/getting-started/index) - This guide will help you get started with Lively, a framework for building real-time applications in Ruby.
12
12
 
13
- - [Building a Worms Game with Lively](https://socketry.github.io/lively/guides/worms-tutorial/index) - This tutorial will guide you through creating a Worms-style game using Lively, a Ruby framework for building real-time applications. We'll build the game step by step, starting with simple concepts and gradually adding complexity.
13
+ - [Building a Worms Game with Lively](https://socketry.github.io/lively/guides/worms-tutorial/index) - This tutorial will guide you through creating a Worms-style game using Lively, a Ruby framework for building real-time applications.
14
+
15
+ - [Building a Flappy Bird Game with Live Views](https://socketry.github.io/lively/guides/flappy-bird-tutorial/index) - This tutorial will guide you through creating a complete Flappy Bird-style game using Live Views, a Ruby framework for building real-time interactive applications.
16
+
17
+ - [Game Audio Tutorial](https://socketry.github.io/lively/guides/game-audio-tutorial/index) - This guide shows you how to add audio to your Live.js games and applications using the `live-audio` library. You'll learn how to play sound effects, background music, and create dynamic audio experiences.
14
18
 
15
19
  ## Contributing
16
20
 
@@ -30,6 +34,12 @@ In order to protect users of this project, we require all contributors to comply
30
34
 
31
35
  This project is best served by a collaborative and respectful environment. Treat each other professionally, respect differing viewpoints, and engage constructively. Harassment, discrimination, or harmful behavior is not tolerated. Communicate clearly, listen actively, and support one another. If any issues arise, please inform the project maintainers.
32
36
 
37
+ ## Releases
38
+
39
+ Please see the [project releases](https://socketry.github.io/lively/releases/index) for all releases.
40
+
41
+ ### v0.14.1
42
+
33
43
  ## See Also
34
44
 
35
45
  - [live](https://github.com/socketry/live) — Provides client-server communication using websockets.
data/releases.md ADDED
@@ -0,0 +1,3 @@
1
+ # Releases
2
+
3
+ ## v0.14.1
data.tar.gz.sig CHANGED
Binary file
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: lively
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.14.0
4
+ version: 0.14.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Samuel Williams
@@ -86,6 +86,8 @@ extensions: []
86
86
  extra_rdoc_files: []
87
87
  files:
88
88
  - bin/lively
89
+ - context/flappy-bird-tutorial.md
90
+ - context/game-audio-tutorial.md
89
91
  - context/getting-started.md
90
92
  - context/index.yaml
91
93
  - context/worms-tutorial.md
@@ -120,6 +122,7 @@ files:
120
122
  - public/_static/site.css
121
123
  - public/application.js
122
124
  - readme.md
125
+ - releases.md
123
126
  homepage: https://github.com/socketry/lively
124
127
  licenses:
125
128
  - MIT
metadata.gz.sig CHANGED
Binary file