├── demo.gif
├── src
├── style.scss
├── index.html
└── index.js
├── README.md
├── webpack.config.js
├── config
├── webpack.prod.js
├── webpack.dev.js
└── base.js
├── package.json
└── LICENSE
/demo.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zedwang/RxJS-Breakout/HEAD/demo.gif
--------------------------------------------------------------------------------
/src/style.scss:
--------------------------------------------------------------------------------
1 | #stage {
2 | background: salmon;
3 | }
4 |
5 | html,
6 | body {
7 | height: 100%;
8 | }
9 |
10 | body {
11 | display: flex;
12 | align-items: center;
13 | justify-content: center;
14 | }
15 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # RxJS-Breakout
2 | A game into to RxJS
3 | Like this:
4 | 
5 | ### Install
6 | ```
7 | npm install
8 | ```
9 | ### Run
10 | ```
11 | npm start
12 | ```
13 |
14 | 打开浏览器输入```http://localhost:8000```
15 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | let env = 'dev'
2 | console.log(process.env.NODE_ENV)
3 | if (process.env.NODE_ENV === 'production') {
4 | env = 'prod'
5 | module.exports = require('./config/webpack.'+ env)
6 | } else {
7 | module.exports = require('./config/webpack.'+ env)
8 | }
--------------------------------------------------------------------------------
/config/webpack.prod.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Created by Administrator on 2017/12/29 0029.
3 | */
4 | const Webpack = require('webpack')
5 | const config = require('./base')
6 | const UglifyJsPlugin = require('uglifyjs-webpack-plugin')
7 |
8 | config.devtool = 'source-map'
9 | config.plugins.push(new UglifyJsPlugin({
10 | sourceMap: true
11 | }))
12 |
13 | module.exports = config
--------------------------------------------------------------------------------
/config/webpack.dev.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Created by Administrator on 2017/12/29 0029.
3 | */
4 | const Webpack = require('webpack')
5 | const config = require('./base')
6 |
7 | config.devtool = 'inline-source-map'
8 | config.devServer = {
9 | contentBase: './dist',
10 | hot: true
11 | }
12 | config.plugins.push(new Webpack.NamedModulesPlugin())
13 | config.plugins.push(new Webpack.HotModuleReplacementPlugin())
14 |
15 | module.exports = config
--------------------------------------------------------------------------------
/src/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | A game intro to RxJS
8 |
9 |
10 |
11 |
A game intro to RxJS
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "AGameIntroToRxJS",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "index.js",
6 | "scripts": {
7 | "start": "webpack-dev-server",
8 | "build": "cross-env NODE_ENV=production webpack --config webpack.config.js",
9 | "test": "echo \"Error: no test specified\" && exit 1"
10 | },
11 | "author": "zed",
12 | "license": "MIT",
13 | "devDependencies": {
14 | "babel-core": "^6.26.0",
15 | "babel-loader": "^7.1.2",
16 | "babel-preset-env": "^1.6.1",
17 | "clean-webpack-plugin": "^0.1.17",
18 | "cross-env": "^5.1.3",
19 | "css-loader": "^0.28.8",
20 | "html-webpack-plugin": "^2.30.1",
21 | "node-sass": "^4.7.2",
22 | "sass-loader": "^6.0.6",
23 | "style-loader": "^0.19.1",
24 | "uglifyjs-webpack-plugin": "^1.1.5",
25 | "webpack": "^3.10.0",
26 | "webpack-dev-middleware": "^2.0.2",
27 | "webpack-dev-server": "^2.10.1",
28 | "webpack-hot-middleware": "^2.21.0"
29 | },
30 | "dependencies": {
31 | "koa": "^2.4.1",
32 | "rxjs": "^5.5.6"
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 Zed
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 |
--------------------------------------------------------------------------------
/config/base.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Created by Administrator on 2017/12/29 0029.
3 | */
4 | const path = require('path')
5 | const Webpack = require('webpack')
6 | const HtmlWebpackPlugin = require('html-webpack-plugin')
7 | const CleanWebpackPlugin = require('clean-webpack-plugin')
8 | module.exports = {
9 |
10 | entry: {
11 | "app": "./src/index.js"
12 | },
13 |
14 | output: {
15 | filename: '[name].boundle.js',
16 | path: path.resolve(__dirname,'../dist'),
17 | publicPath:'/'
18 | },
19 |
20 | module: {
21 | rules: [
22 | {
23 | test: /\.scss$/,
24 | use: ["style-loader","css-loader","sass-loader"]
25 |
26 | },
27 | {
28 | test: /\.js$/,
29 | exclude: /node_modules/,
30 | use: {
31 | loader: 'babel-loader',
32 | options: {
33 | babelrc: false,
34 | presets: ['babel-preset-env']
35 | }
36 | }
37 | }
38 | ]
39 | },
40 | plugins: [
41 | new CleanWebpackPlugin('../dist'),
42 | new HtmlWebpackPlugin({
43 | title:'A game intro to RxJS',
44 | template: 'src/index.html'
45 | })
46 | ]
47 | }
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import Rx from 'rxjs/Rx'
2 | import './style.scss'
3 |
4 | /* Graphics */
5 |
6 | const canvas = document.getElementById('stage');
7 | const context = canvas.getContext('2d');
8 | context.fillStyle = 'pink';
9 |
10 | const PADDLE_WIDTH = 100;
11 | const PADDLE_HEIGHT = 20;
12 |
13 | const BALL_RADIUS = 10;
14 |
15 | const BRICK_ROWS = 5;
16 | const BRICK_COLUMNS = 7;
17 | const BRICK_HEIGHT = 20;
18 | const BRICK_GAP = 3;
19 |
20 | function drawTitle() {
21 | context.textAlign = 'center';
22 | context.font = '24px Courier New';
23 | context.fillText('rxjs breakout', canvas.width / 2, canvas.height / 2 - 24);
24 | }
25 |
26 | function drawControls() {
27 | context.textAlign = 'center';
28 | context.font = '16px Courier New';
29 | context.fillText('press [<] and [>] to play', canvas.width / 2, canvas.height / 2);
30 | }
31 |
32 | function drawGameOver(text) {
33 | context.clearRect(canvas.width / 4, canvas.height / 3, canvas.width / 2, canvas.height / 3);
34 | context.textAlign = 'center';
35 | context.font = '24px Courier New';
36 | context.fillText(text, canvas.width / 2, canvas.height / 2);
37 | }
38 |
39 | function drawAuthor() {
40 | context.textAlign = 'center';
41 | context.font = '16px Courier New';
42 | context.fillText('by Manuel Wieser', canvas.width / 2, canvas.height / 2 + 24);
43 | }
44 |
45 | function drawScore(score) {
46 | context.textAlign = 'left';
47 | context.font = '16px Courier New';
48 | context.fillText(score, BRICK_GAP, 16);
49 | }
50 |
51 | function drawPaddle(position) {
52 | context.beginPath();
53 | context.rect(
54 | position - PADDLE_WIDTH / 2,
55 | context.canvas.height - PADDLE_HEIGHT,
56 | PADDLE_WIDTH,
57 | PADDLE_HEIGHT
58 | );
59 | context.fill();
60 | context.closePath();
61 | }
62 |
63 | function drawBall(ball) {
64 | context.beginPath();
65 | context.arc(ball.position.x, ball.position.y, BALL_RADIUS, 0, Math.PI * 2);
66 | context.fill();
67 | context.closePath();
68 | }
69 |
70 | function drawBrick(brick) {
71 | context.beginPath();
72 | context.rect(
73 | brick.x - brick.width / 2,
74 | brick.y - brick.height / 2,
75 | brick.width,
76 | brick.height
77 | );
78 | context.fill();
79 | context.closePath();
80 | }
81 |
82 | function drawBricks(bricks) {
83 | bricks.forEach((brick) => drawBrick(brick));
84 | }
85 |
86 |
87 | /* Sounds */
88 |
89 | const audio = new (window.AudioContext || window.webkitAudioContext)();
90 | const beeper = new Rx.Subject();
91 | beeper.subscribe((key) => {
92 |
93 | let oscillator = audio.createOscillator();
94 | oscillator.connect(audio.destination);
95 | // 设置音频影调
96 | oscillator.type = 'square';
97 |
98 | // https://en.wikipedia.org/wiki/Piano_key_frequencies
99 | // 设置音频频率
100 | oscillator.frequency.value = Math.pow(2, (key - 49) / 12) * 440;
101 |
102 | oscillator.start();
103 | oscillator.stop(audio.currentTime + 0.100);
104 |
105 | });
106 |
107 |
108 | /* Ticker */
109 |
110 | const TICKER_INTERVAL = 17;
111 |
112 | const ticker$ = Rx.Observable
113 | .interval(TICKER_INTERVAL, Rx.Scheduler.requestAnimationFrame)
114 | .map(() => ({
115 | time: Date.now(),
116 | deltaTime: null
117 | }))
118 | .scan(
119 | (previous, current) => ({
120 | time: current.time,
121 | deltaTime: (current.time - previous.time) / 1000
122 | })
123 | );
124 |
125 |
126 | /* Paddle */
127 |
128 | const PADDLE_SPEED = 240;
129 | const PADDLE_KEYS = {
130 | left: 37,
131 | right: 39
132 | };
133 |
134 | const input$ = Rx.Observable
135 | .merge(
136 | Rx.Observable.fromEvent(document, 'keydown', event => {
137 | switch (event.keyCode) {
138 | case PADDLE_KEYS.left:
139 | return -1;
140 | case PADDLE_KEYS.right:
141 | return 1;
142 | default:
143 | return 0;
144 | }
145 | }),
146 | Rx.Observable.fromEvent(document, 'keyup', event => 0)
147 | )
148 | .distinctUntilChanged();
149 |
150 | const paddle$ = ticker$
151 | .withLatestFrom(input$)
152 | .scan((position, [ticker, direction]) => {
153 |
154 | let next = position + direction * ticker.deltaTime * PADDLE_SPEED;
155 | return Math.max(Math.min(next, canvas.width - PADDLE_WIDTH / 2), PADDLE_WIDTH / 2);
156 |
157 | }, canvas.width / 2)
158 | .distinctUntilChanged();
159 |
160 |
161 | /* Ball */
162 |
163 | const BALL_SPEED = 60;
164 | const INITIAL_OBJECTS = {
165 | ball: {
166 | position: {
167 | x: canvas.width / 2,
168 | y: canvas.height / 2
169 | },
170 | direction: {
171 | x: 2,
172 | y: 2
173 | }
174 | },
175 | bricks: factory(),
176 | score: 0
177 | };
178 |
179 | function hit(paddle, ball) {
180 | return ball.position.x > paddle - PADDLE_WIDTH / 2
181 | && ball.position.x < paddle + PADDLE_WIDTH / 2
182 | && ball.position.y > canvas.height - PADDLE_HEIGHT - BALL_RADIUS / 2;
183 | }
184 |
185 | const objects$ = ticker$
186 | .withLatestFrom(paddle$)
187 | .scan(({ball, bricks, collisions, score}, [ticker, paddle]) => {
188 |
189 | let survivors = [];
190 | collisions = {
191 | paddle: false,
192 | floor: false,
193 | wall: false,
194 | ceiling: false,
195 | brick: false
196 | };
197 |
198 | ball.position.x = ball.position.x + ball.direction.x * ticker.deltaTime * BALL_SPEED;
199 | ball.position.y = ball.position.y + ball.direction.y * ticker.deltaTime * BALL_SPEED;
200 |
201 | bricks.forEach((brick) => {
202 | if (!collision(brick, ball)) {
203 | survivors.push(brick);
204 | } else {
205 | collisions.brick = true;
206 | score = score + 10;
207 | }
208 | });
209 |
210 | collisions.paddle = hit(paddle, ball);
211 |
212 | if (ball.position.x < BALL_RADIUS || ball.position.x > canvas.width - BALL_RADIUS) {
213 | ball.direction.x = -ball.direction.x;
214 | collisions.wall = true;
215 | }
216 |
217 | collisions.ceiling = ball.position.y < BALL_RADIUS;
218 |
219 | if (collisions.brick || collisions.paddle || collisions.ceiling ) {
220 | ball.direction.y = -ball.direction.y;
221 | }
222 |
223 | return {
224 | ball: ball,
225 | bricks: survivors,
226 | collisions: collisions,
227 | score: score
228 | };
229 |
230 | }, INITIAL_OBJECTS);
231 |
232 |
233 | /* Bricks */
234 |
235 | function factory() {
236 | let width = (canvas.width - BRICK_GAP - BRICK_GAP * BRICK_COLUMNS) / BRICK_COLUMNS;
237 | let bricks = [];
238 |
239 | for (let i = 0; i < BRICK_ROWS; i++) {
240 | for (let j = 0; j < BRICK_COLUMNS; j++) {
241 | bricks.push({
242 | x: j * (width + BRICK_GAP) + width / 2 + BRICK_GAP,
243 | y: i * (BRICK_HEIGHT + BRICK_GAP) + BRICK_HEIGHT / 2 + BRICK_GAP + 20,
244 | width: width,
245 | height: BRICK_HEIGHT
246 | });
247 | }
248 | }
249 |
250 | return bricks;
251 | }
252 |
253 | function collision(brick, ball) {
254 | return ball.position.x + ball.direction.x > brick.x - brick.width / 2
255 | && ball.position.x + ball.direction.x < brick.x + brick.width / 2
256 | && ball.position.y + ball.direction.y > brick.y - brick.height / 2
257 | && ball.position.y + ball.direction.y < brick.y + brick.height / 2;
258 | }
259 |
260 |
261 | /* Game */
262 |
263 | drawTitle();
264 | drawControls();
265 | drawAuthor();
266 |
267 | function update([ticker, paddle, objects]) {
268 |
269 | context.clearRect(0, 0, canvas.width, canvas.height);
270 |
271 | drawPaddle(paddle);
272 | drawBall(objects.ball);
273 | drawBricks(objects.bricks);
274 | drawScore(objects.score);
275 |
276 | if (objects.ball.position.y > canvas.height - BALL_RADIUS) {
277 | beeper.next(28);
278 | drawGameOver('GAME OVER');
279 | game.unsubscribe();
280 | }
281 |
282 | if (!objects.bricks.length) {
283 | beeper.next(52);
284 | drawGameOver('CONGRATULATIONS');
285 | game.unsubscribe();
286 | }
287 |
288 | if (objects.collisions.paddle) beeper.next(40);
289 | if (objects.collisions.wall || objects.collisions.ceiling) beeper.next(45);
290 | if (objects.collisions.brick) beeper.next(47 + Math.floor(objects.ball.position.y % 12));
291 |
292 | }
293 |
294 | const game = Rx.Observable
295 | .combineLatest(ticker$, paddle$, objects$)
296 | .subscribe(update);
297 |
--------------------------------------------------------------------------------