$ mol_app_life:日曜大工の神のシミュレーター

こんにちは、私の名前はDmitry Karlovskyです。 最近、私は死にかけていました、そして私はどれほど人生を愛しているかに気付きました。 これはソシオパスにとって理想的なゲームであり、あなたは誰の手で誰が生き、誰が死に、誰が落ちるかを全会一致で決める神の役割を果たす。 新しいセルは、3人の他の同性の隣人の共存の結果として現れ、3人以上の群衆によって踏みにじられ、単独で、または1人だけの会社で放置されると死にます。 誰がそのような単純な法律が非常に多種多様なゲーム体験を生み出し、それらが策定されてから50年後にLifeをプレイするだろうと思ったでしょう。


グライダー


以前に$ molを使用したことがない場合は、読む前に初心者向けのガイド$ mol_app_calc:スプレッドシートパーティーを読むことをお勧めします。 そして、既にマスターされている場合は、次のことがわかります。


  1. 無限の生命分野で働く方法。
  2. 高速ベクトルグラフィックスを描画する方法。
  3. $ molのように、指の制御とグラフィックスの描画を組み合わせるのは簡単で簡単です。

ベクターグラフィックス


$ molは、コンパクトさ、コード効率、使いやすさを考慮して開発されました。 つまり、アプリケーションプログラマーは、最適化について考えることなく、レンダリング先を指定するだけでよく、グラフィックモジュール自体がこれを改善する方法を見つけます。 $ mol_plotモジュールのコレクションは、まさにそのような実装です。 最も単純なケースでは、数値のベクトルとグラフのタイプを彼女に与え、彼女自身がそれらを適切に配置します。


<= Plot $mol_plot_pane graphs / <= Trend $mol_plot_line series <= trend / 1 2 5 4 

折れ線グラフ


次のタイプがサポートされるようになりました:線形、円柱、スポット、および塗りつぶし。 さらに、垂直および水平ルーラー。 また、特別な種類のグラフで複数の他の種類を1つに結合できる場合、既存の種類を組み合わせて新しい種類のグラフを設計できます。 たとえば、「塗りのあるロープ型のグラフ」を作成できます。


 $my_plot_rope $mol_plot_group graphs / <= Line $mol_plot_line <= Dot $mol_plot_dot <= Fill $mol_plot_fill 

ロープスケジュール


もちろん、異なるベクトルに複数のグラフを描くことができますが、$ mol_plot_paneは各グラフに基本色から始まる一意の色を与えることができます:


 <= Plot $mol_plot_pane hue_base 206 graphs / <= Fact $mol_plot_bar series <= fact / 1000 2000 4000 9000 <= Plan $mol_plot_line series <= plan / 1000 3000 5000 7000 type \dashed 

事実計画


しかし、これだけではありません。グラフの種類は、凡例のサンプルを返すことができます。 同時に、実際のグラフィックのようにサンプルを組み合わせることができます。 そのため、あるタイプの線がグラフに表示され、別のタイプの線が凡例に表示される場合、バグは発生しません。 チャートから自動的に生成される凡例を見てください:


さまざまなスタイルのグラフを使用したグラフ。


グラフィックス速度


グラフの実装は非常にコンパクトであるだけでなく、非常に効果的です。


ベンチマークロープ図
ロープダイアグラムのベンチマーク結果


ベンチマーク棒グラフ
棒グラフのベンチマーク結果


この効率は多くの要因により達成されます:


  1. 実装は単純で、オーバーヘッドが少なくて済みます。
  2. リアクティブプログラミングは、状態を効果的に更新するために使用されます。 非常に効果的であるため、システムの負荷をほとんどかけずに、リアルタイムレンダリングのあらゆる側面を簡単に変更できます。 例: グラフィックディスコ
  3. 視覚的に見分けがつかないほど近くにあるポイントは、1つになります。 レンダリングでどのくらいのデータを投げても、システムは同じピクセルの複数の再レンダリングを行いません。
  4. ビューポートの外側にあるポイントはレンダリングから除外されます。 大量のデータをパンするときの実際。 レンダリングが少ないほど、レンダリングは高速になります。
  5. チャートは、最小限のSVG要素で描画されます。

最後の点では、一連の線要素ではなく1つのパス要素でレンダリングする正しく実装されたSVGチャートが、キャンバスでの手動レンダリングに比べてそれほど劣っていないことを示す別のベンチマークもあります。


SVG対Canvas
異なるレンダリング方法を比較した結果


ライフグラフィックス


左から右ではなく、任意の場所にあるポイントを描画する必要があります。 これを行うには、 seriesではなく、 points_rawを指定します。これは、数値のベクトルではなく、座標からのベクトルを返します。


 $mol_app_life_map $mol_plot_pane gap 0 graphs / <= Points $mol_plot_dot threshold 0 points_raw <= points / 

レンダリング領域の端からチャートのインデント( gap )と密集したポイントの折り畳み( threshold )が必要ないので削除したことに注意してください。


$mol_plot_paneは、チャートのサイズと位置を一元的に変更できるshiftscaleプロパティがあります。 ズーム係数を設定するzoomプロパティと、 shiftのエイリアスpanあるpan紹介しましょう。 私たちは両方とも変更可能です。


 $mol_app_life_map $mol_plot_pane gap 0 - pan?val / 0 0 zoom?val 16 scale / <= zoom - <= zoom - shift <= pan - - graphs / <= Points $mol_plot_dot threshold 0 diameter <= zoom - points_raw <= points / 

近似の程度に等しい点の直径を示したことに注意してください。 デフォルトでは、近似度は16です。したがって、フィールド全体は、最初は16ピクセルの円が存在するセル内の16ピクセルのグリッドを表します。


ズームとパン


$ Molには、独立したレンダリング用ではなく、他の機能を追加するための特別なコンポーネントがあります。 これらのプラグインの1つは$ mol_touchで 、これは指とマウスの入力イベントをキャプチャし、さまざまなジェスチャーを実装します。 セルのサイズを変更してフィールドを移動するだけでよいので、プラグインを追加して、以前に発表されたプロパティを編成します。


 plugins / <= Touch $mol_touch zoom?val <=> zoom?val - pan?val <=> pan?val - 

以上です。 真実は。 双方向バインディングは素晴らしいことです。 最小限のコードを記述しますが、同時にすべてが完全に制御されます。 ネストされたコンポーネントがプロパティ値を要求するか、何かを書き込もうとすると、関数が呼び出されます。 たとえば、近似の最小レベルを1に設定してみましょう。


 @ $mol_mem zoom( next = super.zoom() ) { return Math.max( 1 , next ) } 

デフォルトのオフセットとして、レンダリング領域の半分のサイズを設定して、座標[0,0]セル[0,0]最初に中央に配置されるようにします。


 @ $mol_mem pan( next? : number[] ) { return next || this.size_real().map( v => v / 2 ) } 

ゲームのルール


画面サイズの小さなフィールドに制限することもできますが、簡単な方法を探しているわけではないため、フィールドは無限になります。 まあ、どのように無限...トロイダル、しかし非常に大きい:64K * 64K = 4Gセル。


このような巨大なフィールドの各セルの状態を計算することは、長すぎる操作です。 生きている細胞の数は、死んだ細胞の数よりもかなり少ないことに注意してください。 そしてこれは、すべてのステップで、生きている細胞とその周辺の状態のみを更新することが理にかなっていることを意味します。


これを行うには、生細胞の座標を保存するSetという構造体が必要です。 はい、問題は次のとおりです。セル座標は2つの数値であり、セットへのキーは1つのプリミティブ値(またはオブジェクトへの参照ですが、これも実際にはプリミティブです)のみです。


数字を文字列にシリアル化し、キーを取得して連結することができます。 ただし、文字列の操作は比較的簡単です。 最も効果的なのは、ビット演算を使用して2つの数値を1つに結合することです。 JSのビット操作は、常に数値を32ビット表現にします。 つまり、各座標に最大16ビットが割り当てられます。したがって、フィールドのサイズは4ギガバイトに制限されます。


数値の結合は非常に簡単です-16ビットにトリミングし、異なるオフセットと結合します。


 function key( a : number , b : number ) { return a << 16 | b & 0xFFFF } 

また、セットを反復する座標を取得するために、それらを分離する必要があります。 最高のビットを詰めてシフトするだけで、最高の数を得るのは難しくありません。


 function x_of( key : number ) { return key >> 16 } 

しかし、最小の数値を得るには、上位ビットを切り取るだけでは十分ではありません。その場合、負の値が壊れ、上位ビットはゼロではなく1になります。 最下位ビットを最上位ビットにシフトする必要があり、その後タスクは前のビットに削減されます。


 function y_of( key : number ) { return key << 16 >> 16 } 

これで、セットを作成し、それらから座標を追加/削除できます。


 const state = new Set<[ number , number ]>() state.add( key( 1, 2 ) ) state.add( key( 3, 4 ) ) state.delete( key( 1, 4 ) ) for( let key of state ) { console.log( x_of( key ) , y_of( key ) ) } 

反応stateプロパティを取得して、現在の瞬間の宇宙の状態を保存し、シリアル化されたsnapshotビューに基づいて多くの生細胞を形成します。これにより、外部から初期状態を設定できます。


 @ $mol_mem state( next? : Set<number> ) { const snapshot = this.snapshot() if( next ) return next return new Set( snapshot.split( '~' ).map( v => parseInt( v , 16 ) ) ) } 

最初に現在のスナップショットを読み取り、それから再定義できるようにすることに注意してください。 これは、状態を変更した場合でも、スナップショットと同期するために必要です。


さらに、現在の変更された状態のスナップショットをコンポーネントから事後的に受信する機会を提供します。


 @ $mol_mem snapshot_current() { return [ ... this.state() ].map( key => key.toString( 16 ) ).join( '~' ) } 

view.treeで通信プロパティを宣言することを忘れないでください:


 snapshot \ snapshot_current \ - speed 0 population 0 

同時に、世界とpopulationを更新する頻度を設定するspeedプロパティを宣言しましたpopulationこれにより、現在の生細胞の数を取得できます。 後者は簡単に実装できます。


 @ $mol_mem population() { return this.state().size } 

最後に、最も興味深いのは、指定された速度で状態を更新することです。 これを行うには、 futureプロパティを確立します。これは、状態に基づいて状態を読み取り、新しい状態を計算して書き戻します。


 @ $mol_mem future( next? : Set<number> ) { let prev = this.state() const state = new Set<number>() //  state   prev return this.state( state ) } 

このようなプロパティは一度計算されますが、これは定期的に行う必要があるため、現在の時刻に目的の頻度で追加します。


 @ $mol_mem future( next? : Set<number> ) { let prev = this.state() if( !this.speed() ) return prev this.$.$mol_state_time.now( 1000 / this.speed() ) const state = new Set<number>() //  state   prev return this.state( state ) } 

これで、Nミリ秒(16から1000)ごとに無効になり、メソッドの実行と世界の状態の更新につながります。 ところで、このアップデートのコードは次のとおりです。


 const state = new Set<number>() const skip = new Set<number>() for( let alive of prev ) { const ax = x_of( alive ) const ay = y_of( alive ) for( let ny = ay - 1 ; ny <= ay + 1 ; ++ny ) for( let nx = ax - 1 ; nx <= ax + 1 ; ++nx ) { const nkey = key( nx , ny ) if( skip.has( nkey ) ) continue skip.add( nkey ) let sum = 0 for( let y = -1 ; y <= 1 ; ++y ) for( let x = -1 ; x <= 1 ; ++x ) { if( !x && !y ) continue if( prev.has( key( nx + x , ny + y ) ) ) ++sum } if( sum != 3 && ( !prev.has( nkey ) || sum !== 2 ) ) continue state.add( nkey ) } } 

ここでは、基本的な最適化がすでに適用されています。 おそらくそれをさらに最適化できます。 がんばれ!


最後に、レンダリング用のポイントのリストを作成しましょう。


 points() { const points = [] as number[][] for( let key of this.future().keys() ) { points.push([ x_of( key ) , y_of( key ) ]) } return points } 

神の手


プレイヤーが単なる愚かな目撃者ではなく、運命の支配者であるように、インデックスのいくつかのイベントにサブスクライブします。


 event * ^ mousedown?event <=> draw_start?event null mouseup?event <=> draw_end?event null 

名前にもかかわらず、マウスと指の両方で動作します。 残念ながら、 clickイベントはここでは機能しません。パンするときでも発生するためです。これは絶対に必要ではありません。 したがって、ポインターをアクティブにすると、現在の位置が記憶されます。


 @ $mol_mem draw_start_pos( next? : number[] ) { return next } draw_start( event? : MouseEvent ) { this.draw_start_pos([ event.pageX , event.pageY ]) } 

そして、非アクティブ化の際、変位を確認し、それがあまり変化していない場合は、セルの生と死の切り替えを開始します。


 draw_end( event? : MouseEvent ) { const start_pos = this.draw_start_pos() const pos = [ event.pageX , event.pageY ] if( Math.abs( start_pos[0] - pos[0] ) > 4 ) return if( Math.abs( start_pos[1] - pos[1] ) > 4 ) return const zoom = this.zoom() const pan = this.pan() const cell = key( Math.round( ( event.offsetX - pan[0] ) / zoom ) , Math.round( ( event.offsetY - pan[1] ) / zoom ) , ) const state = new Set( this.state() ) if( state.has( cell ) ) state.delete( cell ) else state.add( cell ) this.state( state ) } 

制御インターフェース


プレイフィールド$mol_app_life_map準備$mol_app_life_mapできたら、それを管理するためのアプリケーションの作成を開始できます。 ヘッダーとプレイフィールドで構成される通常のページ$ mol_pageとして作成します。


 $mol_app_life $mol_page title @ \Life of {population} cells sub / <= Head - <= Map $mol_app_life_map speed <= speed - snapshot <= snapshot \ snapshot_current => snapshot_current population => population 

ここでは、速度と基本的なスナップショットをフィールドに設定し、生きている細胞の数と現在の状態のスナップショットを描画します。


ヘッダーで、プレースホルダーを特定の数の生細胞に置き換えます。


 title() { return super.title().replace( '{population}' , `${ this.population() }` ) } 

リンクから基本的なスナップショットを取得します。


 snapshot() { return this.$.$mol_state_arg.value( 'snapshot' ) || super.snapshot() } 

同時に、世界の現在の状態へのリンクを形成します。これにより、ブラウザの履歴に現在のスナップショットが追加されます。


 store_link() { return this.$.$mol_state_arg.make_link({ snapshot : this.snapshot_current() }) } 

速度スイッチとともに、ヘッダーのツールバーへのこのリンクを表示します。


 tools / <= Store_link $mol_link uri <= store_link?val \ hint <= store_link_hint @ \Store snapshot sub / <= Stored $mol_icon_stored <= Time $mol_switch value?val <=> speed?val 0 options * 1 <= time_slowest_label @ \Slowest 5 <= time_slow_label @ \Slow 25 <= time_fast_label @ \Fast 60 <= time_fastest_label @ \Fastest 

リンクが現在のスナップショットにつながる場合、それを散らかします:


 [mol_app_life_store_link][mol_link_current] { opacity: .5; } 

最後の仕上げ-ローカライズを追加:


 { "$mol_app_life_title": "  {population} ", "$mol_app_life_store_link_hint": " ", "$mol_app_life_time_slowest_label": "", "$mol_app_life_time_slow_label": "", "$mol_app_life_time_fast_label": "", "$mol_app_life_time_fastest_label": "" } 

さらにいくつかのマイナーな編集とアプリケーションの準備ができました:



貴重なフィードバックを寄せてくれたコメントのhabtraテスターに​​感謝します。 すべてのバグはすでに修正されています。


興味深い組み合わせ


グライダー工場
グライダー工場


舗装
Paverを起動します



Source: https://habr.com/ru/post/J342064/


All Articles