LibGDX + Scene2d(Kotlinでプログラム)。 パート2

みなさんこんにちは。 今日は、テクスチャ、スキンのアトラスについてお話します。また、レイアウトを使用して作業を繰り返します。 さらに国際化され、最後に色を扱う際の微妙な点がいくつかあります。 次のレッスンでは、ゲームモデルに進み、ゲームロジックとUI要素をリンクします。



前のパーツ
パート0

パート1

リポジトリ





テクスチャアトラス



アプリケーションの「快適さ」の最も重要なパラメーターの1つは、ダウンロード時間です。 これに関するボトルネックは、ドライブからの読み取りです。 そのようなデザインをどこでも使うなら
Image(Texture("backgrounds/main-screen-background.png"))
      
      



その後、余分なレイテンシを作成します。 この場合、テクスチャ「backgrounds / main-screen-background.png」が同期モードでドライブから読み取られます。 これは常に悪いことではありません。 原則として、1つの背景画像を読み込んでも、プログラムを操作する印象を損なうことはありません。 しかし、この方法でシーンの各要素を読み取ると、アプリケーションの速度と滑らかさが大幅に低下する可能性があります。



テクスチャを使用して作業を最適化するには、1つの大きな画像を一度アップロードして、そのフラグメントを作業で使用する方がはるかに安価です。 このアプローチはテクスチャアトラスと呼ばれます。

アトラスの例


私は時期尚早な最適化の大きな反対者ですが、テクスチャアトラスを使用すると、アプリケーションの速度と読みやすさの両方の点で大きな利点があります。 テクスチャアトラスを無視することはより高価です。 プロジェクトにはすでにAtlasGeneratorクラスがあり、それ自体がフォルダーの画像をアトラスに結合できます。 彼のコードは次のとおりです。

 object AtlasGenerator { @JvmStatic fun main(args: Array<String>) { val settings = TexturePacker.Settings() settings.maxWidth = 2048 settings.maxHeight = 2048 TexturePacker.process(settings, "images", "atlas", "game") } }
      
      



原則として、すべてがシンプルです。 パラメーター:ソースフォルダーの名前、アトラスを配置するフォルダーの名前、およびアトラス自体の名前。 大規模なアプリケーションでは、複数のアトラスを作成することが理にかなっています。 たとえば、「古代エジプト」のレベル-一部の写真、「宇宙」のレベル-その他。 それらは同時に使用されません。 現時点で必要な部分のみをロードするほうが、時間的にはるかに高速です。 しかし、このアプリケーションでは、少なくともグラフィックスがあり、1つのアトラスで実行できます。 アトラスの読み込みとテクスチャの読み取りは次のようになります。
 val atlas = TextureAtlas(Gdx.files.internal("atlas/game.atlas")) atlas.findRegion("texture-name")
      
      





このアプリケーションでは、AtlasのロードはAssetManagerを使用してわずかに異なる方法で実装されていますが、現時点では重要ではありません。



スキン



LibGDXライブラリの機能の1つは、ロジックとプレゼンテーションコードの密結合です。 要素を作成し、サイズ、位置、色をコードで直接指定します。 同時に、視覚スタイルでは、同じコード行を複数回繰り返す必要があります(DRY原則に違反します)。 とても高価です。 コピー&ペースト自体ではなく、変更の同期化です。 たとえば、テキストの色を黒から青銅に変更するとします。 また、ハードコードの場合は、アプリケーション全体を調べて、ある色を別の色に変更する必要があります。 一部をスキップし、変更すべきでない箇所を変更します。 この問題を解決するために、LibGDXはスキンメカニズムを実装しています。 これが私たちの例です:

 { "com.badlogic.gdx.scenes.scene2d.ui.Label$LabelStyle": { "default": { "font": "regular-font" }, "large": { "font": "large-font" }, "small": { "font": "small-font" }, "pane-caption": { "font": "large-font", "fontColor": "color-mongoose" } } }
      
      



そして、これがスキンの使用例です
 Label("some text here", uiSkin, "pane-caption")
      
      





内部ではどのように機能しますか? 当たり前のことは簡単です。 スキンの内部には、ObjectMap <Class、ObjectMap <String、Object >> resources = new ObjectMap();があります。 クラスごとに、インスタンスの名前付きセットが保存されます。 上記のJSONは、このマップに値を設定するだけです。 リフレクションによって、オブジェクトが作成され、フィールドもリフレクションによって埋められます。 スキンの作成と作業の例を次に示します。

 val atlas = TextureAtlas(Gdx.files.internal("atlas/game.atlas")) val skin = Skin(atlas) skin.getDrawable("texture-name") skin.get("default", Label.LabelStyle::class.java) Label("some text here", skin , "pane-caption")
      
      







レイアウト



今日の作業の結果は、「ブート」ボタンをクリックすると、遠征パネルが表示されます。 この例では、基本的な考え方を維持しながらアプリケーションのレイアウトを拡張する方法、シーンへのアクタの追加/削除、いくつかの新しいレイアウトコンテナを確認します。 過去のコード:

 row().let { add(Image(Texture("backgrounds/main-screen-background.png")).apply { setScaling(Scaling.fill) }).expand() }
      
      



ウィンドウの中央に写真を配置しました。 ここで、この中央部分をコンテナとして使用します。 2つのオプションがあります。 バックグラウンドでコンテナを使用するか、スタックを使用します。 Stackは、すべての子を追加された順序でその上に描画するレイアウトコンテナーです。 要素のサイズは常にスタックの寸法として設定されます。 最初のオプションに焦点を当てます 画像は再びスタブです。 最終バージョンでは、TiledMapRendererを使用してマップを描画します。

 val centralPanel = Container<WidgetGroup>() row().let { add(centralPanel.apply { background = TextureRegionDrawable(TextureRegion(Texture("backgrounds/main-screen-background.png"))) fill() pad(AppConstants.PADDING * 2) }).expand() }
      
      



この場合、行()の外側でcentralPanel変数を宣言します。{...} パラメータとして渡します。 アイデアは、CommandPanel(下部にボタンがあるパネル)は、それがどこにあるのか、一般的なシーンのどこに新しい要素を挿入するのかを知らないということです。 したがって、centralPanelをコンストラクターに渡し、CommandPanel内でボタンにハンドラーを掛けます。

 class CommandPanel(val centralPanel: Container<WidgetGroup>) : Table() { ... add(Button(uiSkin.getDrawable("command-move")).apply { addListener(object : ChangeListener() { override fun changed(event: ChangeEvent?, actor: Actor?) { when (isChecked) { false -> centralPanel.actor = null true -> centralPanel.actor = ExplorePanel() } } }) })
      
      



パラメーターのコンストラクターにはキーワードvalがあるため、この最後のフィールドはクラス内のどこでも使用できます。 そうでない場合、このパラメーターはinit {...}ブロックでのみ使用できます。 if-thenの代わりにwhen(java-switchのアナログ)when より読みやすくなります。 ボタンが押されると、ExplorePanelがパネルに埋め込まれ、押されると中央のパネルがクリアされます。

レイアウトサイコロ地形




遠征パネルのレイアウト




テレインプレートのレイアウトに2つの新しいレイアウトコンテナーを使用します。 VerticalGroupおよびHorizo​​ntalGroup。 これらは、テーブルの「ライト」バージョンであり、特に利点が1つあります。 それらからアイテムを削除すると、行/列が削除されます。 これはテーブルには当てはまりません。 単一行のテーブルがある場合でも、列の要素を削除するとセルが空になります。 また、Container、VerticalGroup、Horizo​​ntalGroupの展開/塗りつぶし/スペース/パッド修飾子は、すべての要素にすぐに適用されます。 テーブルの場合、これらの値は各セルに適用されます。

 class ExplorePanel : Table() { init { background = uiSkin.getDrawable("panel-background") pad(AppConstants.PADDING) row().let { add(TerrainPane()) } row().let { add(SearchPane()) } row().let { add(MovePane()) } row().let { add(TownPortalPane()) } row().let { add().expand() //    } } }
      
      



この場合、ExplorePanelはテーブルを介して実装されますが、VerticalGroupを介してそれを行うことはありません。 これは基本的に好みの問題です。 最下位の要素は、展開修飾子を使用して空のセルを追加しています。 このセルは最大スペースを占有しようとするため、最初の要素が「湧き」ます。



そして、これが地形プレートです。

 class TerrainPane : WoodenPane() { init { add(Image(uiSkin.getDrawable("terrain-meadow"))).width(160f).height(160f).top() add(VerticalGroup().apply { space(AppConstants.PADDING) addActor(Label(i18n["terrain.meadow"], uiSkin, "pane-caption")) addActor(HorizontalGroup().apply { space(AppConstants.PADDING) addActor(Image(uiSkin.getDrawable("herbs-01"))) addActor(Image(uiSkin.getDrawable("herbs-unidentified"))) addActor(Image(uiSkin.getDrawable("herbs-unidentified"))) addActor(Image(uiSkin.getDrawable("herbs-unidentified"))) addActor(Image(uiSkin.getDrawable("herbs-unidentified"))) }) }).expandX().fill() } }
      
      



とりあえず、「見る」国際化(i18n)を行い、レイアウトに注意を払ってください。 WoodenPaneは実際にはテーブルです(実際にはボタンです。これは、前述のとおり、Tableの子孫です)。 2人の俳優を追加します。 地形画像と垂直グループ。 垂直グループには1つのセルテキストがあり、2番目のセルは5つの画像の水平グループです。 同様に作られたアクションサイコロ-検索、移動、都市への戻り。 すでに述べたように、次のパートでロジックをアタッチしてデータモデルにバインドします。



国際化



何らかの形で国際化に取り組んできた人は誰も新しいことではありません。 国際化はまったく同じように機能します。 キーと値のペアが保存される基本的な.propertiesファイルがあります。 補助ファイルxxx_ru.properties、xxx_en.properties、xxx_fr.propertiesがあります。 デバイスのロケールに応じて、適切な補助ファイル(定義されている場合)またはベースファイル(一致するものがない場合)がロードされます。 私たちの場合、国際化ファイルは次のようになります。

medieval-tycoon.properties

medieval-tycoon_en.properties

medieval-tycoon_ru.properties

... ...

explore.move=

explore.search=

explore.town-portal=

terrain.forest=

terrain.meadow=

terrain.swamp=







i18nという名前をグローバル名前空間に入れます

 val i18n: I18NBundle get() = assets.i18n class MedievalTycoonGame : Game() { lateinit var assets: Assets
      
      





 class Assets { val i18n: I18NBundle by lazy { manager.get(i18nDescriptor) }
      
      



繰り返しますが、ダウンロードはアセットマネージャーを経由します。 従来のI18NBundleブートオプションは次のようになります。
 val i18n = I18NBundle.createBundle(Gdx.files.internal("i18n/fifteen-puzzle"), Locale.getDefault())
      
      



さらに、テキストの代わりに、単にi18n.get( "key.name")を挿入します



色を扱う際の微妙な点



スキンでは、色定数を本当に使いたいです。 しかし、このように書き込もうとすると、プログラムはエラーでクラッシュします。

 { "com.badlogic.gdx.scenes.scene2d.ui.Label$LabelStyle": { "pane-caption": { "font": "large-font", "fontColor": "color-mongoose" } } }
      
      



LibGDXが「マングース」の色について何も知らないことさえありません。デフォルトでは、スキンは「黒」と「白」についても知りません。 ただし、スキンを作成するときに、パラメーターObjectMap <String、Any>()を渡すことができます。このパラメーターには、アプリケーションパレットの実行色と基本色を入れます。 次のようになります。

テキストの色識別子を追加する
 private val skinResources = ObjectMap<String, Any>() private val skinDescriptor = AssetDescriptor("default-ui-skin.json", Skin::class.java, SkinLoader.SkinParameter("atlas/game.atlas", skinResources)) ... loadColors() manager.load(skinDescriptor) ... private fun loadColors() { skinResources.put("color-mongoose", Color.valueOf("BAA083")) skinResources.put("clear", Color.CLEAR) skinResources.put("black", Color.BLACK) skinResources.put("white", Color.WHITE) skinResources.put("light_gray", Color.LIGHT_GRAY) skinResources.put("gray", Color.GRAY) skinResources.put("dark_gray", Color.DARK_GRAY) skinResources.put("blue", Color.BLUE) skinResources.put("navy", Color.NAVY) skinResources.put("royal", Color.ROYAL) skinResources.put("slate", Color.SLATE) skinResources.put("sky", Color.SKY) skinResources.put("cyan", Color.CYAN) skinResources.put("teal", Color.TEAL) skinResources.put("green", Color.GREEN) skinResources.put("chartreuse", Color.CHARTREUSE) skinResources.put("lime", Color.LIME) skinResources.put("forest", Color.FOREST) skinResources.put("olive", Color.OLIVE) skinResources.put("yellow", Color.YELLOW) skinResources.put("gold", Color.GOLD) skinResources.put("goldenrod", Color.GOLDENROD) skinResources.put("orange", Color.ORANGE) skinResources.put("brown", Color.BROWN) skinResources.put("tan", Color.TAN) skinResources.put("firebrick", Color.FIREBRICK) skinResources.put("red", Color.RED) skinResources.put("scarlet", Color.SCARLET) skinResources.put("coral", Color.CORAL) skinResources.put("salmon", Color.SALMON) skinResources.put("pink", Color.PINK) skinResources.put("magenta", Color.MAGENTA) skinResources.put("purple", Color.PURPLE) skinResources.put("violet", Color.VIOLET) skinResources.put("maroon", Color.MAROON) }
      
      







これはAssetManagerを使用した例です。 これも行うことができます(主なことは、skin.jsonファイルをロードする前に行うことです)。

 uiSkin.add("black", Color.BLACK) uiSkin.load(Gdx.files.internal("uiskin.json"))
      
      







そして最後に。 ラベルは2つの方法で「ペイント」できます。 善悪。

 color = Color.BLACK //  style.fontColor = Color.BLACK // 
      
      



レンダリングのメカニズムを説明するのに十分な知識がありません。 指では、このようなものです。どんな俳優でもタッチで描くことができます。 白灰色の色合いで作られた写真を撮り、色を設定し、取得する白灰色の画像の代わりに、たとえば黄暗黄色や赤暗赤などを取得します。 問題は、最終シェードが「乗算」であることです。 また、白灰色のベースの代わりに赤の絵があり、色相が青の場合、結果は黒になります。 実際、これは良い結果を得るための非常に悪い時間のかかるオプションです。 赤、緑、黄、青のオプションが確実に非常に難しく見えるように、グレーの強度を選択します。 さらに、私が間違っていなければ、透明性を維持することには何らかの問題があります。



2番目のオプションは正常に機能します。 私の場合、半透明の暗いストロークで、フォントは白で生成されます。

 val largeFont = FreetypeFontLoader.FreeTypeFontLoaderParameter() largeFont.fontFileName = "fonts/Merriweather-Bold.ttf" ... largeFont.fontParameters.borderColor = Color.valueOf("00000080") largeFont.fontParameters.borderWidth = 4f ...
      
      







結果






最後の例では、アクションダイスの通常のレイアウトはありません。 TerrainPaneとの類推により、自分で実装してみることができます。



更新:

ちょっとおかしいオフトピック
HEXカラー名



カラーピッカー

クーラー。 ジャンルの古典

Paletton.com-気に入った

Habrからの記事




All Articles