ブラウザの「ヒーローズオブマイトアンドマジック」:長く、難しく、耐えられないほど興味深い

何年も前にブラウザなしで動かなくなったゲームをブラウザに実装する方法は? その過程でどのような困難に遭遇し、どのように解決できますか? そして最後に、なぜこれを行うのですか?



12月、HolyJSカンファレンスで、 Alexander Korotaev (Tinkoff.ru)は、彼がどのようにしてブラウザバージョンのHeroesを作成したかを説明しました。 以前はビデオレポートが登場しましたが、Habr向けにテキスト版も作成しました。 ビデオの方が便利な方-ビデオを開始し、テキストの方-カットの下でそれを読んでください:





私は、あなたがたの多くが子供の頃に演じた非常に3番目の「ヒーロー」をブラウザでどのように作成したかについてお話ししたいと思います。



興味深い長い旅に出かける前に、ルートを見てください。 GitHubに行って、2か月ごとにヒーローの新しいクローンが表示されるのを見ました。 これらは、コミットが2つまたは3つのリポジトリであり、文字通りいくつかの機能が追加され、実行が困難なために人がスローします。 彼は、これが完了した場合に彼に課せられる責任の全負担を理解しています。 ここでは、最も成功しているリポジトリへのリンクを提供しています。



  1. sfia-andreidaniel / heroes3
  2. mwardrop / HOMM3Clone
  3. potmdehex / homm3tools
  4. openhomm / openhomm
  5. vcmi / vcmi


コミュニティにとっての重要性を強調するために最後に強調したのは、Cで「ヒーロー」の完全に記述された唯一のクローンであり、元のリソースの配布を使用できるためです。 これが、Androidデバイスで3番目の「ヒーロー」を起動する唯一の方法です。 エミュレーターを介して実行されますが、問題は非常に遅いことです。タッチインターフェイスが使用できないため、マウスを動かす必要があります。一般に、これは非常に大きなファン専用です。



これを取り上げたとき、自分自身にどのような目標を設定しましたか?





最初は元の画像を繰り返してみました:







以下に、元のエディターとその単純なレンダー、およびその時点でフラグが不要な私の単純なレンダーを示します。 これは、ゲーム開発のほぼ最初のスクリーンショットです。 ちなみに、プロジェクトのスクリーンショットを撮っておくと便利かもしれません。いつか必要になるかもしれないので、誰かを殺してしまうでしょう。 レポートにはスクリーンショットが必要でしたが、当初は計画していませんでしたが、ストーリーを残したかっただけです。 ストーリーは長く、写真に収めるべきだと思いました。



それで、私は実際にオリジナルのゲームの絵を繰り返しましたが、先に進まなければなりませんでした。



まず、JavaScriptでgamedevを知らない人のために、通常のゲームが何で構成されるかを説明します。





これをコードの形で想像すると、すべてが簡単です。



01. const me = {name: 'Alex', left: 0} 02. ... 03. setInterval(() => update(), 1000) 04. ... 05. window.addEventListener('keyup', () => me.left++) 06. ... 07. requestAnimationFrame(() => draw())
      
      





この背後にあるもの:





JSのゲーム開発者の詳細については、 「Surrealism in JavaScript」という本を読んでください。少なくともこのような素晴らしい写真のために開いてください。










ゲーム開発の簡単な歴史



「ヒーロー」の作成を開始する場合は、次のものがあります。



  1. オリジナルゲーム
  2. マップエディター。 開発者は最初、ゲームが最大2年間存続できると考えていました。
  3. FizMigは、すべてのゲームメカニクスの優れたリファレンスです。 その驚くべき点は、人々がスキル、呪文、あらゆる損害の損失の確率をすべて経験的に計算し、それを式と表でパーセント比で提示したことです。 人々は10年間働いています。つまり、彼らは非常に大きな狂信者であり、私は彼らと比較することさえできません。
  4. 長年にわたって「英雄」を掘り下げてきた人たちとの多くのフォーラムがあります。 ちなみに、フォーラムはロシア語を話します:英語を話す人はほとんど掘りませんでした。
  5. リソースのアンパッカー。写真、データなどを取得できます。


最初の写真のように、通常の緑のフィールドをレンダリングすることから始めました。







ここでは、緑のフィールドにオブジェクトを描画し、重要なポイントをディバズした方法を見ることができます。 赤い点は障害物であり、黄色の点はこの時点での何らかのアクションです。 アクションキャッスルで、ヒーロー全体で、モデル全体で行くことができる場所にのみ。



次に、データを操作しました。 データは、すべてのスキル、モンスター、キャラクター、カード、読み取りと何らかの方法で蓄積する必要があるテキストファイルとバイナリファイルに関するすべてのリストです。







その後、アルゴリズムを使用しました。 アルゴリズムをすぐに取得できませんでした。 ここで、パス検索アルゴリズムを作成しようとしました。







しかし、すべてがスムーズに機能したわけではなく、これはおそらく彼の最高の実行の1つです。



自分で書き込もうとすると、どれだけ間違っているかを実感しました。 しかし、私にとってスムーズなことは何もありませんでした。実際、私は熊手畑をほぼ横切って歩きました。 幸いなことに、私はあきらめずにこの状況から抜け出す方法を見つけようとしましたが、どうにかしてそれを行うことができました。






解析カード



最初は非常に重要な段階があり、それは比較的退屈で複雑で、これは解析カードです。 事実は、それが彼のためでなければ、何もないということです。 何らかのオフセットを使用して互いに重ね合わせたオブジェクトを含むフィールドを描画することに興味がなかったので、ゲームの変更をすぐに確認できる便利なエディターを使用するために、元のマップを読みたいと思いました。







このエディターでマップを開くと、建物、オブジェクトなどを編集するための優れたビジュアルインターフェイスが表示されます。 それは便利で、明確で直感的です。 「英雄」のために何千または何万ものカードがすでに作られていますが、それらはまだ非常に多くあります。



しかし、開発者としてそれを読みたい場合は、読むのが難しいのは単なるバイナリコードであることがわかります。







私はこのコードを黙想し、それがどのように機能し、内部に何が含まれているかについていくつかの貧弱な仕様を見つけました。 私は彼を2週間見ており、すでにいくつかのパターンを見始めています!



それから私は何かがおかしいことに気づき、掘り始めて、普通の人がテンプレートエディタでこれを読んでいることがわかりました:







カードの場合、010エディターで解析できるテンプレートが既に作成されています。 その中で、ブラウザのように開きます。 dev-toolsに似たものが表示され、コードの一部にカーソルを合わせると、その中にあるものが表示されます。 これは、以前に使用しようとしたものよりもはるかに便利です。



スクリプトがあると仮定すると、コードを書くことは残っています。 最初は、これを処理できる別の言語を知らなかったため、PHPでこれを実行しようとしましたが、時間が経つにつれてhomm3toolsに出会いました 。 これは、「ヒーロー」のさまざまなデータを操作するためのライブラリのセットです。 基本的に、さまざまなカード形式のパーサー、マップジェネレーター、木からの碑文のレンダリング、さらにはゲームオブジェクトからのゲーム「スネーク」です。 このクラフトを見たとき、homm3toolsを使えば何でもできることを実感し、この人の熱狂に火がつきました。 私は彼と話し始めました、そして彼は私がCを学び、私自身のコンバーターを書くべきだと私に確信させました。







実際、私のコンバーターを使用すると、ヒーロー用の通常のマップファイルを取得し、読み取り可能なJSONに変換できます。 JavaScriptと人間の両方が読み取り可能。 つまり、このマップにあるもの、そこにあるデータを確認し、その操作方法をすばやく理解できます。



より多くのデータがあり、オブジェクトの数が増加し、すべての大きなマップを実行し、リソースがどこかで漏れていることがわかりました。 彼らはどんどん小さくなり、この地図上の小さな動きでさえフリーズとブレーキを引き起こしました。 それは非常にプレイ不能でいものでした。






すべてが遅くなります!



これで何をすべきですか? 私はこれに出会ったことは一度もなく、最初に地図の描画を見に行きました。 大きなカード、おそらく、それは遅くなります。



しかし、最初に、少し理論。 すべてがCanvasに描画されているため、DOMとの違いを説明したいと思います。 DOMでは、要素を取得するだけで、要素を移動できます。要素がどのように描画されるかを考えずに、移動するだけです。 キャンバス上で何かを移動して描画するには、毎回それを消去する必要があります。



 01. const ctx = canvas.getContext('2d') 02. 03. ctx.drawImage(hero, 0, 0) 04. ctx.clearRect(0, 0, 100, 100) 05. ctx.drawImage(hero, 100, 0) 06. ctx.clearRect(0, 0, 100, 100) 07. ctx.drawImage(hero, 200, 0)
      
      





この方法でアニメーション化するヒーローの下に草がある場合、草を描く必要があります。



 01. const ctx = canvas.getContext('2d') 02. 03. ctx.drawImage(hero, 0, 0) 04. ctx.drawImage(grass, 0, 0) 05. ctx.drawImage(hero, 100, 0) 06. ctx.drawImage(grass, 100, 0) 07. ctx.drawImage(hero, 200, 0)
      
      





これはさらに高価で、さらに困難であり、非常に複雑な背景の場合、一般的に不可能な困難な作業です。



したがって、レイヤーでペイントすることをお勧めします。







レイヤーを取得するだけで、ビデオカードがそれらをミックスします。 したがって、私は再描画を大幅に節約し、各レイヤーは異なる時間に描画される独自の順序で更新されます。 多かれ少なかれレンダリングが高速になったので、本当に複雑なことをすることができました。



互いに重ねられた3つのCanvasを使用します。



 <canvas id=”terrain”> <canvas id=”objects”> <canvas id=”ui”>
      
      





彼らの名前は彼ら自身のために語っています。 地形-草、道路、川。



地形描画アルゴリズムを見ると、かなりリソースがロードされているように見えるかもしれません。



  1. 土壌型タイルを取る
  2. オリジナルのゲームの開発者は多くのリソースを節約したため、ディスプレイスメントと回転で描画します
  3. オーバーレイ川
  4. 敷設道路
  5. そして、まだ特別な土壌タイプがあります。


そして、これらはすべて描画する必要があり、できれば実行時ではありません。 したがって、マップの最初のレンダリングを行ったらすぐにそれを描画し、キャッシュに入れることをお勧めします。 既製の絵を描くことは、必要なたびに道路を新しく描くよりもはるかに安価です。



マップをスムーズに移動する方法は? 私はこれに問題がありましたが、Yandex.Mapsから解決策に出くわしました:







実際、マップを移動すると、その変換が変化します。 多くの人が知っているように、この操作はビデオカードでのみ実行され、Repaintを呼び出しません。 かなり大きな画像を移動するためのかなり安価な操作。 しかし、32ピクセルごとに、このマップの左を補正します。実際、単に再描画しますが、ユーザーはマップの連続的な動きの印象を持っています。 私が達成したかったのはYandex.Mapsに実装されていたので、気づきました。



1つのマップに対して十分な最適化ができなかったため、オブジェクトの描画を開始しました。 しかし、最初に、少し理論。 実際には、「ヒーロー」の描画オブジェクトの軸は反転しています。 実際、オブジェクトは右下隅から描かれます。 なぜこれが行われるのですか? 実際には、マップを上から見ていますが、プレーヤーに側面から4分の3を眺めているような印象を与えるために、オブジェクトは下から上に描画され、互いに重なり合っています。







オブジェクトを描画するためのアルゴリズム:



  1. 各オブジェクトの下枠のYで配列をソートします(異なる高さのテクスチャ、これを考慮する必要があります)
  2. 私たちは窓に落ちないものをフィルタリングします(人が見えないものを描くのは高価です)
  3. さまざまなチェック
  4. オブジェクトテクスチャを描画します
  5. 必要に応じて、プレーヤーの旗を描きます

    そして、オブジェクトの数が9000を超える可能性があるという事実にもかかわらず、これらはすべて! 何をすべきか、実行時にそれをどのように描くか? 実行時にこれを描画しない方が良いと思います。次に、その方法を説明します。







    はじめに、renderTreeのような描画アルゴリズムを見つけました。 たとえば、ブラウザで使用され、Z-indexで互いに重なり合うDOM要素を描画します。 そして、このツリーにある各ブランチは、オブジェクトがソートされるY軸です。 次に、各ブランチで、すべてのオブジェクトがX軸に沿ってソートされます。



    これから何が得られますか? 画面にヒットしないブランチをすぐに切り取ることができるため、より安価なイテレーションが得られます。 そして、ブランチの各反復で、Xオブジェクトを確認し、マップに収まらないオブジェクトに出会うとすぐに、このオブジェクトの繰り返しを停止します。 したがって、配列を通過するだけの場合よりも少ないオブジェクトが影響を受けます。 また、オブジェクトは既にソートされているため、オブジェクトの正しいオーバーラップがすぐに提供されます。 したがって、有能なデータストレージが取得されます。



    次に、描画機能に行きました。



     01. const object = getObject(id) 02. const {x, y} = getAnimationFrame(object) 03. const offsetleft = getMapLeft() 04. const offsetTop = getMapTop() 05. 06. context.drawImage(object.texture, x - offsetleft, …
          
          





    各関数は、オブジェクトの最終オフセット、アニメーション用のフレーム、そして最も重要なこととしてdrawImage関数を決定するという事実から成り立っていることがわかります。 すべてはこの機能に帰着し、どういうわけかそれを最適化する必要がありました。



    必要なパラメーターとバインドしてこの関数を作成し、renderTreeに直接保存できることに気付きました。 つまり、オブジェクトの保存を停止し、描画関数のみを保存し始めました。 そこにはこれ以上何も必要ないので、パフォーマンスが大幅に向上しました。







    しかし、問題はオブジェクトだけではありません。実際、ゲームはアニメーションの観点から速度を落とすべきではありません。 Konyashkaは画面上で完全に実行されるはずです。そうでなければ、ゲームに何か問題があるという印象を受けます。



    ジオメトリを少し掘り下げて、何をしなければならないかを理解しましょう。 そこでは、任意の方向(少なくとも水平方向、少なくとも斜め方向)に一定の距離でセグメントを描画すると、それらは等しくなります。







    しかし、これはジオメトリです。 そして、「ヒーロー計測」があります。 そこに問題があるのは、これが実際に対角線と水平方向の変位が等しくないグリッド上のゲームであるが、ゲームはこれが同じであり、すべてが正常であると信じているということです。







    それと一緒に暮らすには? カウントする場合、水平方向の動きについては4つのアニメーションステップ、対角線については約6つ行います。 このアニメーションを本当にスムーズにする方法についての解決策を探し始めました。



    JavaScriptの問題は、シングルスレッドであり、タスクで動作することです。 設定した各setTimeoutは個別のタスクを作成し、たとえば他のsetTimeoutと競合する他のタスクと競合します。 この点で、私たちを救うものは何もありません。



    私はsetTimeout、setInterval、requestAnimationFrameを介してそれを実行しようとしました-すべてが互いに競合するタスクを作成します。







    また、プレイヤーが移動する際の多数の計算により、競合するタスクがアニメーション全体を台無しにしました。



    さらに検索を続けたところ、JavaScriptにはタスクの一部であるマイクロタスクがあることがわかりました。 たとえば、Promiseなどに渡すコールバックが、マイクロタスクを実行する唯一のオブジェクトを即座に、または非同期に実行できる場合に必要になります。 したがって、念のため、タスクよりも優先度の高いマイクロタスクを実装しました。







    実際、アニメーションに使用できる非ブロッキングループを取得します。 詳細については、Jake Archibaldの記事をご覧ください。



    最初に、すべてを取り上げてPromiseでラップしました。



     01. new Promise(resolve => { 02. setTimeout(() => { 03. //    04. requestAnimationFrame(() => /*  */) 05. resolve() 06. }) 07. })
          
          





    アニメーションを実行するにはsetTimeoutが必要でしたが、すでにPromiseにありました。 アニメーションの計算を行い、これらの計算の結果に基づいて描画するために必要なものをrequestAnimationFrame関数に入力して、計算が描画をブロックしないようにし、本当に必要なときに行った。



    したがって、アニメーションステップのシーケンス全体を構築できました。



     01. startAnimation() 02. .then(step) 03. .then(step) 04. .then(step) 05. .then(step) 06. .then(doAction) 07. .then(endAnimation)
          
          





    しかし、このオブジェクトはあまり設定可能ではなく、私が望むものを強く反映していないことに気付きました。 そして、AsyncSequenceと呼ばれるオブジェクトにアニメーションを保存することを思いつきました。



     01. AsyncSequence([ 02. startAnimation, [ 03. step 04. step 05. ...], 06. doAction, 07. endAnimation 08. ])
          
          





    実際、これは一種のリデュースであり、Promiseを通過し、それらを順次呼び出します。 しかし、見た目ほど単純ではありません。実際には、ネストされたアニメーションループもあります。 つまり、startAnimationの後、1つのステップから配列を配置できました。 ヒーローの最大の対角線アニメーションに必要なものが7つまたは8つあるとします。



    ヒーローが特定のポイントに到達するとすぐに、このアニメーションに拒否が表示され、アニメーションが停止し、AsyncSequenceは親ブランチに移動する必要があることを理解し、そこでdoActionとendAnimationが既に呼び出されています。 複雑なアニメーションを宣言的に作成すると非常に便利です。






    データ保存



    しかし、レンダリングのおかげだけでなく、生産性を高めることができます。 データの遅延が最も遅いことがわかりました。これは、JavaScriptで私にとって最大の驚きでした。



    まず、最も遅くなるデータを見つけます。これがマップです。 マップ全体はグリッドであり、タイルで構成されています。 タイルは、独自のテクスチャ、独自のデータセットを持つグリッド内のある種の条件付き正方形であり、すべての古いゲームが行ったように、限られた数のテクスチャからマップを構築できます。



    このデータセットには以下が含まれます。



    1. タイルの種類(水、土、木材)
    2. タイルの開通性/コスト
    3. イベントの可用性
    4. 「使用中」フラグ
    5. エンジンの実装に応じたその他のフィールド


    コードでは、これはグリッドとして表すことができます。



     01.const map = [ 02. [{...}, {...}, {...}, {...}, {...}, {...}], 03. [{...}, {...}, {...}, {...}, {...}, {...}], 04. [{...}, {...}, {...}, {...}, {...}, {...}], 05. ... 06. ] 07.const tile = map[1][3]
          
          





    タイルグリッドと同じ視覚デザイン。 配列の配列。各配列には、タイルの何かを含むオブジェクトがあります。 XとYをオフセットすることで特定のタイルを取得できます。このコードは機能し、正常なようです。



    しかし。 パスを見つけるためのアルゴリズムがありますが、それ自体は非常に高価であり、タイルだけでなくオブジェクトにもある多くの詳細を考慮する必要があります。 そして、マウスを動かすと、カーソルがこのポイントに到達できるかどうか、敵またはアクションがこのポイントにあるかどうかに応じて変化します。







    たとえば、彼らは木を指しています-あなたはこの点に行くことができないので、通常のカーソルが現れました。 そして、到達するまでにかかる日数を示す必要があります。 つまり、実際には、常にパスを見つけるためのアルゴリズムを駆動し、同じグリッドの動作は非常に遅くなります。



    タイルプロパティを取得するには、次のものが必要です。



    1. タイルの配列をリクエストする
    2. 文字列のクエリ配列配列
    3. 要求タイルオブジェクト
    4. オブジェクトプロパティをリクエストする


    判明したように、4回のヒープ呼び出しは、パス検索アルゴリズムのためにマップを何度も要求する必要がある場合、非常に遅くなります。



    そして、それについて何ができますか? 最初に、データを見ました:



     01. const tile = { 02. //    03. render: {...}, 04. //     05. passability: {...}, 06. //      07. otherStuff: {...}, 08. }
          
          





    各タイルオブジェクトは、レンダリングに必要なもの、パス検索アルゴリズムに必要なもの、およびそれほど頻繁に必要とされない他のデータで構成されていることがわかりました。 それらは必要ではないという事実にもかかわらず、毎回呼び出されました。 このデータを破棄し、保存方法を把握する必要がありました。



    そして、このデータを読み取る最も速い方法は配列からであることがわかりました。







    結局のところ、タイルオブジェクトは配列に分割できます。 もちろん、職場でこのビジネスコードを作成する場合は、質問があります。 しかし、パフォーマンスについて話しているので、ここではすべてのツールが優れています。 オブジェクトのタイプをタイルに保存するか、タイルが空であるという別の配列を取得し、単純な単位/ゼロの「セルが通過可能かどうか」というパス検索アルゴリズムの数値の配列を取得します。



    しかし、パス検索アルゴリズムの場合、オブジェクトがあるかどうかを調べるだけでなく、1または0を入力する必要もありません。 異なる種類の土壌は異なる通過性を持ち、異なるヒーローは異なる方法で歩きます。これはすべて考慮しなければなりません。







    この単純な配列は、2つの大きな配列(タイルとオブジェクト)からの複雑なアルゴリズムによって考慮されます。 したがって、パス検索アルゴリズムですぐに使用できる計算済みの数値を取得します。 オブジェクトが更新され、値が更新されると、事前にカウントされます。



    その結果、何かをキャッシュしてバインドする多くの配列があります。



    • レンダリングループのレンダリング関数の配列
    • パスを見つけるための数値の配列
    • オブジェクトをタイルに関連付ける文字列の配列
    • 追加のタイルプロパティの数値の配列
    • オブジェクトをゲームロジック用のIDでマップする


    残っているのは、遅いストレージから速いストレージへのデータのタイムリーな更新です。



    もちろん、この質問は、配列の配列から抜け出す方法を熟成させました。これは、通常の配列よりもはるかに遅く動作します。



    実際、通常の配列に切り替えて、配列の配列を拡張するだけで、これは50%高速になります。







    配列内のデータオフセットの取得は簡単です。 Y, , , X.



    — . , X Y . - , , X Y, - :



     01. const map = [{...}, {...}, {...}, {...}, ...] 02. 03. const tile = map[y * width + x] 04. map.forEach((value, index) => { 05. const y = Math.floor(index / width) 06. const x = index - (y * width) 07. })
          
          





    , , . , , , .



    :







    «Power of 2», « » « », , . , , .



    , -, , , , . , .



    , , , , , , , , , , .



    , , , , , , - , .



     01. const map = [{...}, {...}, {...}, {...}, ...] 02. const powerOfTwo = Math.ceil(Math.log2(width)) 03. 04. const tile = map[y << powerOfTwo + x] 05. map.forEach((value, index) => { 06. const y = index >> powerOfTwo 07. const x = index - (y << powerOfTwo) 08. })
          
          





    , 50x50, 50 ( X Y, ).



    , :







    , MIP-, - , . , , , .



    Grid. Grid — , , X Y , , , X Y.



     01. const grid = new Grid(32) 02. 03. const tile = grid.get(x, y) 04. grid.forEach((value, x, y) => {})
          
          





    , , , . , , 256: , , . , . , 256x256, .






    UI Canvas



    UI Canvas. , , , UI HTML. , , . .



    , - , eventListener. , - .



     01. const okButton = new Buttton(0, 10, 'Ok') 02. okButton.addEventListener('click', () => { ... }) 03. const cancellButton = new Buttton(0, 10, 'Cancel') 04.cancellButton.addEventListener('click', () => { ... })
          
          





    , , . «» , .



     01. const okButton = new Buttton({ 02. left: 0, 03. top: 10, 04. onClick: () => { ... } 05. }) 06. const cancellButton = new Buttton({...})
          
          





    , , JSON.



     01. [ 02. { 03. id: 'okButton', 04. options: { 05. left: 0, 06. top: 10, 07. onClick: () => { ... } 08. }, 09. },
          
          





    , , . , . , . , , , .



    , XML. XML — , HTML, , JSON, , .



     01. <button id="okButton" 02. left="0" 03. top="10" 04. onClick="{doSomething()}" 05. />
          
          





    , . , .



    , , , , . , .



     01. <group id="main" ... > 02. <group id="header" ... > 03. <text-block ... /> 04. <button ... /> 05. </group> 06. <group id="footer" ... > 07. <any-component ... /> 08. <button ... /> 09. </group> 10. </group>
          
          





    , , — XML - Canvas. — react-canvas , , , - , - , .








    , , , … , : ? - :







    , , , , - , - , — . , - , .



    . — , . , , , . , , , . , .



    , , . - , . ?



    :







    A*. . — , , , , . , — , . «» ( , , , , ).



    , , . . なぜこれが必要なのですか? , , , , , :







    , , .



    , , , :



    • ID
    • : ,
    • ID
    • ,


    , , PubSub events:



     01. const objectInAction = Objects.get(ID) 02.const hero = Player.activeHero 03. objectInAction.events.dispatch('action', hero) 04. ... 05. this.events.on('action', hero => { 06. hero.owner.resources.set('gems', this.value) 07. this.remove() 08. })
          
          





    , , callback ( «action» , — . , ).



    , , , , :



    • -,
    • ( , save/load, , )


    , . , , — 10%, , , . , 90% , , - . , , , .



    , . , , , , . . , ? , , - , ?



    ? , :



     01. //        02. @Mixin(Attacable) 03. class TownObject extends OwnershipObject {...} 04. //     ,    .. 05. class OwnershipObject extends MapObject {...} 06. //        07.class MapObject {...}
          
          





    , - . , - . , TownObject, , Attacable, . , , , , , , ( , , , ).



    TownObject OwnershipObject, , , . , , , , - . , , MapObject, , .






    結論



    ? . , , , (, ). , , . , , , - , .



    : ? . , - ? , , , , : « webpack - , ». , , , . , !



    :



    • . , , , web-, , , , , , .
    • , , .
    • , - «». , - . - , - . , , , , . .


    :



    • , , , ,
    • , . , , , , , , , , .
    • . , , .


    , , . , , , , . , . , , 50, , .



    , , . , , , - . , . ́ .



    , :





    , , , . .

    広告の分。 HolyJS, : 19-20 HolyJS 2018 Piter . , , !



All Articles