使用Angular+Rxjs製作王思聰吃熱狗遊戲
最近被王思聰吃熱狗,王思聰打飛機等遊戲所啟發。故實驗了下使用rxjs 實現 王思聰吃熱狗
首先放上最終實現圖:

地址:
ofollow,noindex">Angular 7 貪玩熱狗先說下實現思路,螢幕上掉熱狗動畫 使用css屬性 transform:translateY(Xpx) 實現
並且用流控制熱狗的位置以及熱狗的建立和銷燬。
其他一些流用於控制王思聰滑鼠移動,熱狗根據時間下掉並且到底部消失:
開始動手實踐:
首先使用cli 建立一個Angular 專案
ng new wsc-hotdog
然後開啟我們的 app.component.ts
//設定一個變數來記錄滑鼠位置 mouse_position: { x: number } = {x: 0};
在 ngOnInit 中建立 mouse move 的監聽事件,故此來做王思聰頭像的移動流
let mouseMove = fromEvent(document, 'mousemove'); mouseMove.subscribe((event: MouseEvent) => { this.mouse_position = {x: event.x}; });
當滑鼠移動的時候使用一個變數 mouse_position 記錄下滑鼠的x軸位置。
把我們的wsc頭像放入html模板中:
app.component.html:
<div #wangsicong class="wsc" [style.left.px]="mouse_position.x"> <img width="80px" height="80px" src="./assets/wangsicong.png"> </div>
設定wsc的樣式:
.wsc { position: absolute; bottom: 20px }
接下來在元件中放入我們的熱狗:
因為熱狗數量是不確定的 所以拿一個數組來記錄:
interface HotDog { x: number y?: number } hot_dogs: HotDog[] = [];
然後我們需要根據時間動態建立熱狗。所以假設每0.5s 建立一枚熱狗在陣列中,位置隨機
在 ngOnInit 中
this.observable = interval(500).pipe(map(index => { //由於ngZone會捕獲代理大部分事件,所以此處為提高效能需要runOutsideAngular, // 否則會導致熱狗異常卡頓 this.ngZone.runOutsideAngular(_ => { this.hot_dogs.push({ //隨機建立x x: Math.random() * document.body.offsetWidth, y: 0 } ); } ); // 返回建立熱狗的index,便於銷燬 return index; }), //此處轉換為每隔0.01秒移動下熱狗的x位置,依然是runOutsideAngular map((index) => interval(10).pipe(map(_ => { //因為時序的問題,熱狗index可能會在這段時間內被銷燬, // 所以此處需要做判斷或者使用takeUntil()去實現 if (this.hot_dogs[index]) { this.ngZone.runOutsideAngular(_ => { this.hot_dogs[index].y += 10; }); } return index; }))), //合併所有流發出的值。 mergeAll()); }
觀察者流已經建立好,接下來就是訂閱並且判斷熱狗和wsc頭像是否重疊。
在ngAfterViewInit hook中訂閱他
this.subscription = this.observable.subscribe(data => { this.ngZone.runOutsideAngular(_ => { //判斷在熱狗下雨的過程中是否體積相碰 if (this.hot_dogs[data] && this.hot_dogs[data].y > document.body.offsetHeight - 100) { if (this.mouse_position.x - 10 <= this.hot_dogs[data].x && this.hot_dogs[data].x < this.mouse_position.x + 80) { this.score += 1; this.loadComponent(); } this.hot_dogs.splice(data, 1); } }); });
至此核心邏輯已完成。完整的app.component 和app.component.html
import {ChangeDetectionStrategy, Component, ComponentFactoryResolver, ElementRef, NgZone, OnInit, ViewChild} from '@angular/core'; import {from, fromEvent, interval, merge, Observable, Subscription, timer} from 'rxjs'; import {concatAll, concatMap, filter, map, mergeAll, mergeMap, switchMap, take, takeUntil} from 'rxjs/operators'; import {PointContainerComponent} from './point-container/point-container.component'; import {PointBonusComponent} from './point-bonus/point-bonus.component'; interface HotDog { x: number y?: number } @Component({ selector: 'app-root', templateUrl: './app.component.html', styleUrls: ['./app.component.scss'] }) export class AppComponent implements OnInit { mouse_position: { x: number } = {x: 0}; hot_dogs: HotDog[] = []; score = 0; next_time = 30; started = false; subscription: Subscription = Subscription.EMPTY; observable: Observable<any>; next_sub: Subscription = Subscription.EMPTY; @ViewChild('wangsicong') wangsicong: ElementRef; @ViewChild(PointContainerComponent) pc: PointContainerComponent; ngOnInit(): void { let mouseMove = fromEvent(document, 'mousemove'); let touchMove = fromEvent(this.wangsicong.nativeElement, 'touchmove'); merge(touchMove, mouseMove).subscribe((event: MouseEvent) => { this.mouse_position = {x: event.x}; }); this.observable = interval(500).pipe(map(index => { this.ngZone.runOutsideAngular(_ => { this.hot_dogs.push({ x: Math.random() * document.body.offsetWidth, y: 0 } ); } ); return index; }), map((index) => interval(10).pipe(map(_ => { if (this.hot_dogs[index]) { this.ngZone.runOutsideAngular(_ => { this.hot_dogs[index].y += 10; }); } return index; }))), mergeAll()); } constructor(private componentFactoryResolver: ComponentFactoryResolver, private ngZone: NgZone) { } viewContainerRef; currentAdIndex = 0; loadComponent() { let componentFactory = this.componentFactoryResolver.resolveComponentFactory(PointBonusComponent); this.viewContainerRef = this.pc.viewContainerRef; let componentRef = this.viewContainerRef.createComponent(componentFactory); (<PointBonusComponent>componentRef.instance).x = this.mouse_position.x + 90; this.currentAdIndex += 1; let index = this.currentAdIndex; setTimeout(_ => { this.removeComponent(index); }, 1000); return this.currentAdIndex; } removeComponent(index) { this.viewContainerRef.remove(index); } startGame() { this.score = 0; this.next_time = 30; this.started = true; this.subscription = this.observable.subscribe(data => { this.ngZone.runOutsideAngular(_ => { if (this.hot_dogs[data] && this.hot_dogs[data].y > document.body.offsetHeight - 100) { if (this.mouse_position.x - 10 <= this.hot_dogs[data].x && this.hot_dogs[data].x < this.mouse_position.x + 80) { this.score += 1; this.loadComponent(); } this.hot_dogs.splice(data, 1); } }); }); this.next_sub = interval(1000).pipe(filter(x => x < 30)).subscribe(data => { if (data < 30) { this.next_time -= 1; } if (data == 29) { this.finishGame(); } }); } finishGame() { this.next_sub.unsubscribe(); this.subscription.unsubscribe(); this.started = false; } goGitHub() { window.location.href = 'https://github.com/100cm/wsc-eat-hotdog'; } }
<div class="tanwan-wsc"> <div class="score-bar"> 當前得分:{{score}} </div> <div class="剩餘時間"> 剩餘時間:{{next_time}} </div> <div *ngIf="!started" class="start-game"> <p>總得分:{{score}}</p> <button (click)="startGame()" class="start-button">開始遊戲</button> <button (click)=" goGitHub()" class="github">GitHub</button> </div> <ng-container *ngFor="let item of hot_dogs"> <div class="wsc-hot-dogs" [style.left.px]="item.x" [ngStyle]="{transform: 'translateY('+item.y +'px)'}"> <img src="./assets/hot-dog.png" width="40px" height="70px"> </div> </ng-container> <div #wangsicong class="wsc" [style.left.px]="mouse_position.x"> <img width="80px" height="80px" src="./assets/wangsicong.png"> </div> <app-point-container></app-point-container> </div>
第一次寫文章。如有建議,歡迎指正。