lively 0.13.1 → 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 +4 -4
- checksums.yaml.gz.sig +0 -0
- data/context/flappy-bird-tutorial.md +989 -0
- data/context/game-audio-tutorial.md +137 -0
- data/context/index.yaml +9 -0
- data/context/worms-tutorial.md +2 -0
- data/lib/lively/version.rb +1 -1
- data/readme.md +11 -1
- data/releases.md +3 -0
- data.tar.gz.sig +0 -0
- metadata +4 -1
- metadata.gz.sig +0 -0
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 20a2334e950eae8e16b8e6e637e0b078dc78fa026d733161c619566f9491892d
|
4
|
+
data.tar.gz: b775297b7d46989e7ef63ac4b4a297ee1cd3e617da8e8e07d2be1c8a2cc56644
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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.
|
data/context/worms-tutorial.md
CHANGED
@@ -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:
|
data/lib/lively/version.rb
CHANGED
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.
|
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
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.
|
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
|