Pjesa 1 e Zhvillimit të Lojërave In Rust: Making A Strategy Game, ne krijuam një fushë beteje bazë për të na nxitur. Është koha për të shtuar disa njësi në mënyrë që të mos duket aq e vetmuar.

Ky artikull synon të ngarkojë një njësi dhe ta shfaqë atë në fushën e betejës dhe të përmirësojë bazën tonë të kodeve për të përshtatur këtë veçori të re.

Ne do të përballemi me disa vendime interesante gjatë rrugës:

  • Po ngarkohen spritet që nuk janë kompakte brenda fletës sprite.
  • Si të strukturojmë sistemet tona.
  • Varësitë ndërmjet subjekteve në ECS tonë.

Zhvillimi i lojës In Rust: Making A Strategy Game (Pjesa 2 - Shtimi i Njësisë së Parë)

Siç u diskutua, ne do të shtojmë disa njësi për lojën tonë strategjike në Rust. Ne duhet të fillojmë me shtimin e një njësie të vetme. Në artikullin tjetër, ne do të përpiqemi të rrëmojmë fushën e betejës me të gjitha llojet e luftëtarëve dhe magjistarëve.

Renderimi i njësisë së parë

Në artikullin tonë të mëparshëm, ne krijuam një sistem fillestar për të krijuar fushën e betejës. Ne mund të bëjmë diçka të ngjashme dhe të krijojmë një sistem fillestar për të krijuar njësitë tona. Një create_units_system tingëllon e përshtatshme për këtë detyrë.

fn main() {
    App::new()
        .insert_resource(Msaa::Off)
        .add_plugins(
            DefaultPlugins
                .set(WindowPlugin {
                    primary_window: Some(Window {
                        title: "Strategy Game in Rust".to_string(),
                        resolution: WindowResolution::new(960.0, 540.0)
                            .with_scale_factor_override(4.0),
                        ..default()
                    }),
                    ..default()
                })
                .set(ImagePlugin::default_nearest()),
        )
        .add_startup_system(create_battlefield_system)
        .add_startup_system(create_units_system)
        .run();
}
fn create_units_system(
    mut commands: Commands,
    asset_server: Res<AssetServer>,
    mut texture_atlases: ResMut<Assets<TextureAtlas>>,
) {
}

Mënyra e pjelljes së njësive është e ngjashme me mënyrën e pjelljes së pllakave. Ne duhet te:

  • Ngarko imazhin e fletës sprite.
  • Krijo një atlas teksture.
  • Pjellë pako e fletës sprite në pozicionin e dëshiruar.

Për momentin, ne do të japim vetëm një sprit statik pa asnjë animacion. Mos u shqetësoni, ne do ta mbulojmë këtë në një artikull të mëvonshëm.

Ju mund të shtoni çdo njësi që ju pëlqen nga aktivet. Për ta mbajtur atë të thjeshtë dhe standard, do të shkoj te varianti i parë i shigjetës (I dua harkëtarët), i vendosur në assets/Sprite Sheets/Archer/Archer_Blue1.png. Vjen në një fletë sprite me tre grupe sprites për drejtime dhe animacione të ndryshme. Tani për tani do të jap vetëm sprite në krye majtas.

Nëse inspektojmë fletën sprite, do të gjejmë diçka interesante. Gjëja e parë që duhet vënë re është se, në krahasim me fletën sprite të pllakave, spritet nuk janë të rreshtuara me skajet e imazhit, por kanë një zhvendosje. Spritet gjithashtu kanë disa mbushje midis tyre. Kjo është për t'u dhënë hapësirë ​​disa animacioneve dhe llojeve të njësive që kërkojnë sprite më të gjera ose më të larta.

Për shembull, kalorësi heshta është mjaft i rëndë dhe ka... mirë, një shtizë, kështu që ka nevojë për më shumë hapësirë ​​se shigjetari ynë. Ne do të duhet ta marrim parasysh këtë kur të ngarkojmë fletën.

Edhe nëse na duhet vetëm një sprite për momentin, ne ende duhet të ngarkojmë grupin e parë prej 12 sprite. Ne do të përdorim më shumë nga këto sprite më vonë, kështu që ato do të jenë tashmë të aksesueshme. Ne mund t'i injorojmë me siguri dy grupet e tjera me fytyrë poshtë e lart, pasi nuk do t'i përdorim në këtë lojë.

Brenda create_units_system, mund të fillojmë duke ngarkuar imazhin e fletës sprite:

fn create_units_system(
    mut commands: Commands,
    asset_server: Res<AssetServer>,
    mut texture_atlases: ResMut<Assets<TextureAtlas>>,
) {
    let archer_blue_light_handle = asset_server.load("Sprite Sheets/Archer/Archer_Blue1.png");
}

Kodi i mësipërm është i ngjashëm me mënyrën se si kemi ngarkuar fletën sprite të pllakave. Ju lutemi vini re se e kam testuar këtë në Linux dhe Windows, por nuk e kam provuar në MacOS. Unë pres që Bevy të jetë mjaft i zgjuar për të trajtuar hapësirën e bardhë në fillim të shtegut të skedarit. Dhe MacOS është gjithsesi i ngjashëm me Linux. Nëse kjo nuk funksionon për ju, mos ngurroni të riemërtoni dosjen Sprite Sheets në diçka si Sprite_Sheets.

Më pas, duhet të krijojmë atlasin e teksturës nga imazhi. Këtu hyjnë në vend offset dhe mbushja. Unë sugjeroj që të marrim parasysh sa vijon:

  • Fleta sprite është 256x448 piksele. Është një rrjet 8x14 me madhësi 32x32 piksele.
  • Spritet ndryshojnë në madhësi, por ne duhet të jemi të sigurt nëse supozojmë se janë 32x32 piksele. Kompensimi është midis 4 dhe 8 pikselë, por ne mund ta injorojmë atë. E njëjta gjë vlen edhe për mbushjen.
  • Gjysma e fletës sprite është bosh (në anën e djathtë). Ne mund ta injorojmë atë, si dhe dy grupet e fundit me nga 12 sprites secili.

Kështu që ne do të ngarkojmë një rrjet 4x4 me sprites 32x32. Tani për tani, ne mund të japim sprite 32x32, pasi ato kanë transparencë. Disa sprite janë në qendër dhe pak më të vogla (pothuajse 16x16), ndërsa të tjerët përdorin më shumë hapësirë. Pra, ne duhet t'i trajtojmë ato në mënyrë të përgjithshme.

Ne mund të krijojmë disa konstante me këtë informacion dhe të thërrasim TextureAtlas::from_grid me parametrat e duhur.

fn create_units_system(
    mut commands: Commands,
    asset_server: Res<AssetServer>,
    mut texture_atlases: ResMut<Assets<TextureAtlas>>,
) {
    const SPRITE_SIZE: f32 = 32.0;
    const NUM_COLUMNS: usize = 4;
    const NUM_ROWS: usize = 4;

    let archer_blue_light_handle = asset_server.load("Sprite Sheets/Archer/Archer_Blue1.png");
    let archer_atlas = TextureAtlas::from_grid(
        archer_blue_light_handle,
        Vec2::new(SPRITE_SIZE, SPRITE_SIZE),
        NUM_COLUMNS,
        NUM_ROWS,
        None,
        None,
    );
    let archer_atlas_handle = texture_atlases.add(archer_atlas);
}

Ne mund të indeksojmë spritet në mënyrë sekuenciale siç bëmë me pllakat. Unë kam shtuar gjithashtu dorezën e atlasit, e cila na duhet për të krijuar sprite(s).

Hapi i fundit është pjellja eSpriteSheetBundle:

  • Ne duam të japim spritin e parë. Indeksi do të jetë 0.
  • Ne mund të përcaktojmë çdo pozicion që duam. Për shembull, ne mund ta vendosim harkëtarin në (0, 0) brenda rrjetës së fushëbetejës.
  • Meqenëse spritet janë 32x32 dhe janë pak të mbushura, ne mund të rregullojmë pozicionin e tyre duke shtuar një zhvendosje. Unë kam zgjedhur një të katërtën e madhësisë së spritit për çdo dimension (mund ta rregullojmë këtë në rrugë).
  • Duhet i japimznjë vlerë më të lartë se pllakat. Normalisht, spritet e paraqitura pas pllakave duhet të shfaqen sipër. Megjithatë, pashë disa sjellje të çuditshme tek Bevy ndërsa shkrova këtë artikull. Disa sprites nuk do të paraqiteshin në majë të pllakave. Duket sikur Bevy nuk i jep spritet në të njëjtin rend që janë pjellë, kështu që ne duhet të mbajmë një sy në këtë. Nuk është një problem i madh pasi është një ide e mirë që të jepen njësitë në një shtresë të ndryshme z pavarësisht.
fn create_units_system(
    mut commands: Commands,
    asset_server: Res<AssetServer>,
    mut texture_atlases: ResMut<Assets<TextureAtlas>>,
) {
    ...
    
    let archer_atlas_handle = texture_atlases.add(archer_atlas);

    commands.spawn(SpriteSheetBundle {
        texture_atlas: archer_atlas_handle,
        sprite: TextureAtlasSprite::new(0),
        transform: Transform {
            translation: Vec3 {
                x: 0.0 * SPRITE_SIZE + SPRITE_SIZE / 4.0 - WIDTH_CENTER_OFFSET,
                y: 0.0 * SPRITE_SIZE + SPRITE_SIZE / 4.0 - HEIGHT_CENTER_OFFSET,
                z: 1.0,
            },
            ..default()
        },
        ..default()
    });
}

Nëse përpiqemi të përpilojmë lojën tonë, nuk do të funksionojë. Përpiluesi ankohet për WIDTH_CENTER_OFFSET dhe HEIGHT_CENTER_OFFSET. Nëse keni ndjekur së bashku me Pjesën 1, ne kemi përdorur këto variabla për të përqendruar pllakat në ekran. Me fjalë të tjera, ne i përdorëm këto variabla për të krijuar koordinata relative për fushën e betejës.

Më vonë, do të kemi një UI me kthesën e luajtësit dhe elementë të tjerë. Fusha e betejës do të jetë ende e përqendruar në ekran, por nga këndvështrimi i logjikës sonë të lojës, është elementi kryesor ku ndodh i gjithë veprimi. Pra, ne duam që njësitë tona, ose çdo element tjetër i lojës, të jenë brenda fushës së betejës. Kjo do të thotë që ne mund ta bëjmë jetën tonë më të lehtë nëse marrim pozicionin e fushëbetejës si pozicion relativ për pjesën tjetër të elementeve të lojës.

Ka një problem me idenë e mësipërme. Për momentin, ne duhet të ndajmë njohuritë se ku është pozicioni i fushëbetejës. Kjo nuk është ideale sepse do të na duhej të modifikonim çdo element në fushën e betejës nëse e japim fushën e betejës diku tjetër. Sigurisht, ne mund ta bëjmë pozicionin (ose rregullimin në këtë rast) global, kështu që çdo gjë mund ta përdorë atë për të pozicionuar veten. Por kjo krijon bashkim që as na duhet dhe as nuk duhet ta kemi.

Tani për tani, ne mund të marrim rrugën e lehtë dhe t'i bëjmë WIDTH_CENTER_OFFSET dhe HEIGHT_CENTER_OFFSET globale, në mënyrë që të mos qëndrojmë në "të kuqe" (gabimet e përpiluesit) për një kohë të gjatë. Por ne do të rifaktojmë së shpejti fushën e betejës për të akomoduar për vendosjen e njësive në lidhje me pozicionin e saj.

Le të shkojmë përpara dhe të zhvendosim konstantat WIDTH_CENTER_OFFSET dhe HEIGHT_CENTER_OFFSET, dhe të gjitha varësitë e tyre në create_battlefield_system në shtrirjen më të jashtme në krye të skedarit. Ne mund t'i vendosim ato nën konstantat NUM_COLUMNS dhe NUM_ROWS për t'i mbajtur ato së bashku.

const NUM_COLUMNS: usize = 20;
const NUM_ROWS: usize = 20;
const BATTLEFIELD_WIDTH_IN_TILES: usize = 13;
const BATTLEFIELD_HEIGHT_IN_TILES: usize = 6;
const TILE_SIZE: f32 = 16.0;
const HALF_TILE_SIZE: f32 = TILE_SIZE / 2.0;
const HALF_BATTLEFIELD_WIDTH_IN_PIXELS: f32 = BATTLEFIELD_WIDTH_IN_TILES as f32 * TILE_SIZE / 2.0;
const HALF_BATTLEFIELD_HEIGHT_IN_PIXELS: f32 = BATTLEFIELD_HEIGHT_IN_TILES as f32 * TILE_SIZE / 2.0;
const WIDTH_CENTER_OFFSET: f32 = HALF_BATTLEFIELD_WIDTH_IN_PIXELS - HALF_TILE_SIZE;
const HEIGHT_CENTER_OFFSET: f32 = HALF_BATTLEFIELD_HEIGHT_IN_PIXELS - HALF_TILE_SIZE;

Nëse kemi bërë gjithçka në mënyrë korrekte, duhet të shohim shigjetarin tonë blu të pozicionuar në kolonën e parë dhe në rreshtin e parë (duke filluar nga poshtë majtas, sipas sistemit të koordinatave të Bevy) në fushën e betejës.

Këtu është "ndryshimi" për këtë seksion.

Rifaktorimi i fushës së betejës

Nëse jeni si unë, programuesi juaj OCD duhet të jetë në mes të nivelit të mesëm dhe të lartë tani. Ka disa probleme me kodin tonë:

  • Konstantet globale që lidhen me fushën e betejës përdoren gjithashtu për të krijuar njësi.
  • Përplasjet e emrave: NUM_COLUMNS dhe NUM_ROWS tona në create_units_system hijezojnë konstantet e jashtme për fushën e betejës.

Kjo na tregon diçka. Fusha e betejës dhe njësitë nuk kanë nevojë të dinë për njëri-tjetrin, edhe nëse janë të lidhur.

Plani im afatgjatë është të zhvendos sistemin e fushëbetejës në modulin e vet dhe të bëj të njëjtën gjë me njësitë një. Ne nuk duam që konstantet tona të fushës së betejës të rrjedhin dhe të jenë të disponueshme për konceptet e tjera të domenit sepse ato janë detaje të zbatimit. Megjithatë, ky mund të jetë një abstraksion i parakohshëm dhe ne mund ta ndryshojmë atë ndërsa shkojmë në artikujt e mëvonshëm. Por diçka po bërtet për mungesën e abstraksionit dhe mendoj se është e rëndësishme të fillojmë të mendojmë për strukturën tonë të domainit.

Për momentin, unë sugjeroj sa vijon:

  • Krijoni një abstraksion për fushën e betejës. Ne do ta diskutojmë këtë në detaje së shpejti.
  • Bëjeni këtë abstraksion të disponueshëm në ECS tonë.
  • Përdorni abstraksionin e fushëbetejës për të pozicionuar njësitë në lidhje me fushën e betejës, pa ditur njësitë për pozicionin e fushëbetejës.

Ka mënyra të ndryshme për t'iu qasur këtij problemi. Unë shkova për idenë e mësipërme, por YMMV. Ne mund të bëjmë që fusha e betejës të dijë për njësitë, ose mund t'i bëjmë njësitë të dinë për fushën e betejës. Dhe ne duhet ta bëjmë këtë në një mënyrë që detajet e zbatimit të asnjërit të mos ndahen.

Në një skenar jo-ECS, unë do të thosha se fusha e betejës duhet të dijë për njësitë. Kjo për shkak se fusha e betejës përfaqëson një rrjet ku vendosen njësitë dhe lëvizin përreth. Me këtë qasje, as fusha e betejës dhe as njësitë nuk varen nga njëra-tjetra. Ne thjesht përdorim fushën e betejës për të kthyer pozicionin e njësive.

Megjithatë, motori ynë ECS kërkon një qasje të ndryshme. Në mendjen time:

  • fusha e betejës po kërkon të jetë një burim(më shumë për këtë në një minutë).
  • njësitë dhe çdo entitet tjetër do të jenë globalisht të aksesueshëm nga sistemet, të cilat do të abonohen në komponentët që ata janë i i nteresuar në.
  • sistemet do të përdorin të gjitha këto të dhëna për të përcaktuar sjelljen të lojës sonë.

Burimet janë një element i ndryshëm nga entitetet në një ECS:

  • Subjektet janë thjesht një ID dhe nuk (ose, më mirë, nuk duhet) përmbajnë asnjë logjikë. Mund të ketë shumë entitete me të njëjtin lloj komponentësh.
  • Burimet janë zakonisht unike. Nuk do të ketë dy fusha beteje në një skenë të caktuar.

Mund të jem i keq dhe të shkel disa rregulla të "pastërtisë" së ECS, por do të sugjeroj ta bëjmë fushën e betejës një burim (që ndoshta duhet të jetë gjithsesi, pasi është një pjesë unike e të dhënave) dhe duke shtuar një logjikë në të. Një nga pjesët në enigmën tonë duhet të dijë për pozicionin e fushëbetejës dhe nuk mund të mendoj për një alternativë më të mirë se vetë fusha e betejës.

Koha (dhe artikujt e ardhshëm) do të thonë nëse ky ishte vendimi i duhur 🙂 Për momentin, ne mund ta bëjmë fushën e betejës të përmbledhë njohuritë për dimensionet e saj dhe të ofrojmë një mënyrë për të transformuar një pozicion në koordinata në lidhje me pozicionin e tij. Kjo do të na ndihmojë të heqim varësitë me konstante globale dhe të zhvendosim varësinë e njësisë/fushë betejës në ECS, ku duhet të jetojë.

Le t'i bëjmë duart pis. Ne duhet të krijojmë një strukturë të re Battlefield dhe ta deklarojmë atë si një burim Bevy.

#[derive(Resource)]
struct Battlefield {}

const NUM_COLUMNS: usize = 20;
...

Siç u diskutua, ne duam të ofrojmë sa vijon:

  • Informacioni i nevojshëm për të krijuar fushën e betejës.
  • Një funksion për t'u kthyer nga koordinatat globale në ato të fushëbetejës. Ky funksion përmbledh detajet e pozicionit të fushëbetejës.

Mund të fillojmë duke zbatuar një metodë default për fushën tonë të betejës.

#[derive(Resource)]
struct Battlefield {}

impl Battlefield {
    pub fn default() -> Self {
        Self {}
    }
}

Nuk kemi ende të dhëna, por mund të fillojmë t'i zhvendosim ato në strukturë. Nëse shikojmë create_battlefield_system, na duhen:

  • Madhësia e pllakave
  • Harta e pllakave

Ne mund të fillojmë t'i zhvendosim këto të dhëna në strukturën tonë të re, e cila do të ketë efektin e këndshëm anësor të pastrimit të create_battlefield_system deri pak. Në Pjesën 1, ne grumbulluam gjithçka në këtë funksion për t'i mbajtur gjërat të thjeshta, por tani është koha për ta përmirësuar atë pak nga pak.

Pika më e drejtpërdrejtë e fillimit është madhësia e pllakës. Ne mund të shtojmë një fushë tile_size në strukturën tonë.

#[derive(Resource)]
struct Battlefield {
    tile_size: f32,
}

impl Battlefield {
    pub fn default() -> Self {
        Self { tile_size: 16.0 }
    }
}

Vini re se ne mund të heqim qafe konstantën origjinale TILE_SIZE (megjithatë mos e hiqni atë ende!). Kjo ka dy përfitime:

  • Kodi më i shkurtër.
  • Ne detyrojmë çdo pjesë tjetër të kodit të përdorë fushën e betejës nëse duan të dinë për madhësinë e pllakës, duke i bërë varësitë më të qarta.

Nëse jeni një purist i numrave magjikë, mos ngurroni të mbani konstante. Por unë mendoj se tile_size: 16.0 është vetë-shpjeguese në këtë rast të veçantë.

Hapi tjetër në rifaktorimin tonë epik është shtimi i fushës së betejës si një burimpër sistemin tonë create_battlefield_system. Ne tashmë kemi një burim në lojën tonë (Msaa::Off), kështu që duhet të bëjmë diçka të ngjashme për fushën tonë të re të betejës.

fn main() {
    App::new()
        .insert_resource(Msaa::Off)
        .insert_resource(Battlefield::default())
        .add_plugins(
            DefaultPlugins
                .set(WindowPlugin {
                    primary_window: Some(Window {
                        title: "Strategy Game in Rust".to_string(),
                        resolution: WindowResolution::new(960.0, 540.0)
                            .with_scale_factor_override(4.0),
                        ..default()
                    }),
                    ..default()
                })
                .set(ImagePlugin::default_nearest()),
        )
        .add_startup_system(create_battlefield_system)
        .add_startup_system(create_units_system)
        .run();
}

E mira e përdorimit të default është se ne mund të shtojmë më shumë fusha në strukturën tonë dhe të mos shqetësohemi për shtimin e tyre kur krijojmë shembullin. Meqenëse ne instantojmë fushën e betejës vetëm një herë, kjo është në rregull. Nëse do të krijonim shumë fusha beteje në disa faza, do të preferohej përdorimi i new dhe kalimi i disa parametrave.

Më pas, ne marrim burimin e ri të fushëbetejës në sistem dhe e përdorim atë për të hyrë në madhësinë e pllakës.

fn create_battlefield_system(
    battlefield: Res<Battlefield>,
    mut commands: Commands,
    asset_server: Res<AssetServer>,
    mut texture_atlases: ResMut<Assets<TextureAtlas>>,
) {
    commands.spawn(Camera2dBundle::default());

    let tiles_handle = asset_server.load("Tiles/FullTileset.png");
    let tiles_atlas = TextureAtlas::from_grid(
        tiles_handle,
        Vec2::new(battlefield.tile_size, battlefield.tile_size),
        NUM_COLUMNS,
        NUM_ROWS,
        None,
        None,
    );
    ...
    for (y, row) in tilemap.iter().enumerate() {
        for (x, col) in row.iter().enumerate() {
            commands.spawn(SpriteSheetBundle {
                texture_atlas: tiles_atlas_handle.clone(),
                sprite: TextureAtlasSprite::new(col.index),
                transform: Transform {
                    translation: Vec3 {
                        x: x as f32 * battlefield.tile_size - WIDTH_CENTER_OFFSET,
                        y: y as f32 * battlefield.tile_size - HEIGHT_CENTER_OFFSET,
                        z: 0.0,
                    },
                    ..default()
                },
                ..default()
            });
        }
    }
}

Ne mund të ekzekutojmë cargo run për t'u siguruar që jemi në rrugën e duhur dhe kodi ende përpilohet dhe sillet si më parë. Këtu është "ndryshimi" për këtë hap.

Nuk mund ta heqim ende TILE_SIZE, pasi përdoret nga konstante të tjera. Por tani, ky duhet të jetë përdorimi i vetëm i mbetur.

Hapi tjetër është i ngjashëm dhe ne do të pastrojmë një pjesë të madhe të të dhënave në create_battlefield_system tonë. Ne do të zhvendosim hartën e pllakave në strukturë. Shkoni përpara dhe shtoni një fushë të re dhe lëvizni konstantat përkatëse për bashkëvendosje më të mirë.

const BATTLEFIELD_WIDTH_IN_TILES: usize = 13;
const BATTLEFIELD_HEIGHT_IN_TILES: usize = 6;

#[derive(Resource)]
struct Battlefield {
    tile_size: f32,
    tilemap: [[Tile; BATTLEFIELD_WIDTH_IN_TILES]; BATTLEFIELD_HEIGHT_IN_TILES],
}

Lloji i hartës së pllakave është mjaft i gjatë, por ne do ta rregullojmë këtë në një sekondë.

Ne gjithashtu duhet të shtojmë hartën e pllakave në instancimin e fushës së betejës. Ja kur gjërat fillojnë të bëhen interesante.

impl Battlefield {
    pub fn default() -> Self {
        Self {
            tile_size: 16.0,
            tilemap: [
                [
                    ...
                ],
            ],
        }
    }
}

Për hir të shkurtësisë dhe një gjurmë të vogël, e kam lënë jashtë përmbajtjen e hartës së pllakave, por ato janë të njëjta si më parë.

Ne gjithashtu duhet të marrim hartën e pllakave nga burimi i fushëbetejës në sistem.

fn create_battlefield_system(
    battlefield: Res<Battlefield>,
    mut commands: Commands,
    asset_server: Res<AssetServer>,
    mut texture_atlases: ResMut<Assets<TextureAtlas>>,
) {
    ...

    for (y, row) in battlefield.tilemap.iter().enumerate() {
        for (x, col) in row.iter().enumerate() {
            commands.spawn(SpriteSheetBundle {
                ...
            });
        }
    }
}

Sistemi ynë ka filluar të duket shumë më i pastër tani që po lëvizim të dhënat. Nëse përpilojmë dhe ekzekutojmë, përsëri duhet të shohim paraqitjen e saktë të fushës së betejës. Këtu është "ndryshimi" i këtij rifaktorimi.

Një gjë tjetër që mund të bëjmë është të riemërtojmë konstantet NUM_COLUMNS dhe NUM_ROWS që lidhen me fletën sprite të fushëbetejës, në mënyrë që ato të mos bien ndesh me ato në sistemin e njësive. Ne nuk duam që këto të dhëna të jenë në strukturën e fushës së betejës, sepse nuk i përkasin fare atje.

Dimensionet aktuale të fushës së betejës jepen nga harta e pllakave, jo nga fleta sprite. Meqenëse sistemet janë përgjegjëse për ngarkimin e fletëve sprite, ne mund ta lëmë atë ashtu siç është tani për tani dhe të mbajmë një sy për mundësitë për ta hequr këtë.

Unë kam zgjedhur BATTLEFIELD_NUM_COLUMNS dhe BATTLEFIELD_NUM_ROWS. Unë do të ndaj "ndryshimin" në vend që të ngjit kodin, pasi ne thjesht po e riemërojmë këtë konstante.

SHËNIM: Unë jam duke bërë shumë rifaktorime pa teste njësie. Kjo është menduar. Ne po mbulojmë shumë përmbajtje këtu dhe shtimi i akoma më shumë gjërave për të mësuar ose për të kaluar në përzierje ndoshta do të ishte shumë. Të mësosh Bevy, të bësh një lojë strategjike dhe TDD menjëherë mund të sfidojë edhe zhvilluesit e mëdhenj. Në një moment, do të doja të trajtoja anën e testimit TDD / të automatizuar, por do ta lë për momentin. Hapat e bebes!

Përdorimi i fushës së betejës për të pozicionuar njësitë

Pas gjithë këtij rifaktorimi të mundimshëm, tani jemi gati të zhvendosim përgjegjësinë e shndërrimit të pozicionit të njësive në fushëbetejë. Ato konstante WIDTH_CENTER_OFFSET dhe HEIGHT_CENTER_OFFSET ende po më shqetësojnë shumë për t'i falur ato.

Ne do të krijojmë një funksion të quajtur to_battlefield_coordinates në zbatimin tonë Battlefield. Ky funksion do të marrë (x, y, z) dhe do të kthejë një Vec3 të pozicionuar në lidhje me koordinatat e fushës së betejës.

Pas new, brenda impl Battlefield, mund të shtojmë sa vijon:

pub fn to_battlefield_coordinates(&self, x: f32, y: f32, z: f32) -> Vec3 {
    let half_tile_size: f32 = self.tile_size / 2.0;
    let half_battlefield_width_in_pixels: f32 =
        BATTLEFIELD_WIDTH_IN_TILES as f32 * self.tile_size / 2.0;
    let half_battlefield_height_in_pixels: f32 =
        BATTLEFIELD_HEIGHT_IN_TILES as f32 * self.tile_size / 2.0;
    let width_center_offset: f32 = half_battlefield_width_in_pixels - half_tile_size;
    let height_center_offset: f32 = half_battlefield_height_in_pixels - half_tile_size;

    return Vec3::new(x - width_center_offset, y - height_center_offset, z);
}

Ne thjesht po kthejmë të njëjtat koordinata si më parë. Vini re se tani mund të përdorim self.tile_size, pasi është pjesë e strukturës. Ne duhet të përdorim emrat e variablave let dhe shkronja të vogla për të mbajtur Clippy të lumtur. Mund të kishim përdorur ende konstante dhe t'i kishim shtuar ato në implementimin Battlefield. Ju mund ta bëni këtë nëse preferoni.

Mos harroni të shtoni &self për t'i treguar kompajlerit se ky funksion i përket instancave Battlefield dhe nuk ndryshon strukturën (me fjalë të tjera, ai nuk përdor mut&).

Tani mund përfundimisht të zëvendësojmë koordinatat e pllakave në create_battlefield_system.

            transform: Transform {
                translation: battlefield.to_battlefield_coordinates(
                    x as f32 * battlefield.tile_size,
                    y as f32 * battlefield.tile_size,
                    0.0,
                ),
                ..default()
            },

Dhe ne mund të bëjmë të njëjtën gjë me koordinatat e shigjetarit. Mos harroni të shtoni burimin battlefield si një parametër të create_units_system.

fn create_units_system(
    battlefield: Res<Battlefield>,
    mut commands: Commands,
    asset_server: Res<AssetServer>,
    mut texture_atlases: ResMut<Assets<TextureAtlas>>,
) {
    ...

    commands.spawn(SpriteSheetBundle {
        texture_atlas: archer_atlas_handle,
        sprite: TextureAtlasSprite::new(0),
        transform: Transform {
            translation: battlefield.to_battlefield_coordinates(
                0.0 * SPRITE_SIZE + SPRITE_SIZE / 4.0,
                0.0 * SPRITE_SIZE + SPRITE_SIZE / 4.0,
                1.0,
            ),
            ..default()
        },
        ..default()
    });
}

Këtu është "ndryshimi" për këtë pjesë të fundit të rifaktorimit tonë.

SHËNIM: Ju mund të keni vënë re se kam krijuar një strukturë për fushën e betejës, por nuk kam krijuar një për njësitë. Ne do ta ndryshojmë këtë në artikujt e mëvonshëm, kur të shtojmë komponentët për të përcaktuar lloje të ndryshme të dhënash për entitetet në ECS. Tani për tani, ne mund të jetojmë me konstanten SPRITE_SIZE.

Krijimi i abstraksionit të Hartës së Tile

E mbani mend tilemap: [[Tile; BATTLEFIELD_WIDTH_IN_TILES]; BATTLEFIELD_HEIGHT_IN_TILES]? Unë duhet ta kisha rifaktoruar këtë shumë kohë më parë, por doja ta lija në një seksion të veçantë për diskutim. Këto dy konstante janë të varura përreth, dhe grupi shumëdimensional duket pak i pavend.

Ka mënyra të ndryshme për të modeluar hartën e pllakave.

Një opsion do të ishte krijimi i një lloj si ky:

type Tilemap = [[Tile; BATTLEFIELD_WIDTH_IN_TILES]; BATTLEFIELD_HEIGHT_IN_TILES];

Në varësi të kujt kërkoni, ky është një përmirësim i mirë ose indirekt i panevojshëm. Megjithatë, ajo nuk e zgjidh çështjen kryesore: ne po nxjerrim përmasat e hartës së pllakave.

Në vend që të ndalem në abstragimin e llojit të grupit, unë do të krijoj një strukturë të re që do të përmbajë dimensionet dhe të dhënat e hartës. Mendoj se kjo është një mënyrë më e mirë për t'u siguruar që ne mund t'i përmbledhim të dy konceptet. Ne do të kemi nevojë për sa vijon:

  • Një lloj për të përcaktuar dimensionet.
  • Një struktur për të përmbledhur të dhënat e hartës tilema. Ai do të përmbajë grupin e të dhënave dhe dy fusha për të aksesuar me lehtësi numrin e kolonave dhe rreshtave pa mbajtur konstante përreth.
type TilemapDimensions = [[Tile; 13]; 6];

struct Tilemap {
    data: TilemapDimensions,
    num_columns: usize,
    num_rows: usize,
}

impl Tilemap {
    pub fn new(data: TilemapDimensions) -> Self {
        let num_columns = data.get(0).unwrap().len();
        let num_rows = data.len();

        Self {
            data,
            num_columns,
            num_rows,
        }
    }
}

Ne kemi rreshtuar BATTLEFIELD_WIDTH_IN_TILES dhe BATTLEFIELD_HEIGHT_IN_TILES (mos ndjehuni të lirë t'i mbani nëse preferoni, por ato përcaktojnë një rrjet standard, kështu që mendoj se nuk janë rreptësisht të nevojshme), dhe i marrim dimensionet nga vetë të dhënat dhe i ruajmë në num_columns dhe num_rows .

Në vend të kësaj, ne mund të krijojmë dy funksione në zbatim dhe të lexojmë grupin data sa herë që duam të marrim dimensionet. Meqenëse të dhënat janë të pandryshueshme, kjo nuk është e nevojshme. Ne mund të memorizojmë dimensionet gjatë krijimit të shembullit. Sido që të jetë është mirë.

Ne gjithashtu duhet të ndryshojmë strukturën dhe zbatimin e Battlefield për të pasqyruar strukturën e re të hartës së pllakave.

#[derive(Resource)]
struct Battlefield {
    tile_size: f32,
    tilemap: Tilemap,
}

impl Battlefield {
    pub fn default() -> Self {
        Self {
            tile_size: 16.0,
            tilemap: Tilemap::new([
                [
                    ...
                ],
                ...
            ]),
        }
    }

    pub fn to_battlefield_coordinates(&self, x: f32, y: f32, z: f32) -> Vec3 {
        let half_tile_size: f32 = self.tile_size / 2.0;
        let half_battlefield_width_in_pixels: f32 =
            self.tilemap.num_columns as f32 * self.tile_size / 2.0;
        let half_battlefield_height_in_pixels: f32 =
            self.tilemap.num_rows as f32 * self.tile_size / 2.0;
        let width_center_offset: f32 = half_battlefield_width_in_pixels - half_tile_size;
        let height_center_offset: f32 = half_battlefield_height_in_pixels - half_tile_size;
        return Vec3::new(x - width_center_offset, y - height_center_offset, z);
    }
}

Më në fund, ne mund të marrim të dhënat e hartës së pllakave nga sistemi i fushëbetejës.

    for (y, row) in battlefield.tilemap.data.iter().enumerate() {
        ...
    }

Dhe këto janë të gjitha rifaktorimet për momentin. Ju mund t'i gjeni ndryshimet e mësipërme "këtu", dhe "ndryshimin e plotë" për këtë artikull këtu.

Ndoshta ka më shumë gjëra që mund të përmirësojmë, por jam mjaft i lumtur deri më tani 🙂 Nuk dua ta largoj shumë vëmendjen, pasi ne ende duhet të shtojmë më shumë njësi në fushën tonë të betejës.

konkluzioni

Ne kemi ndërtuar bazat për shtimin e njësive në lojën tonë. Ne kemi krijuar gjithashtu burimin Battlefield për të ndihmuar në strukturimin e të dhënave tona të lojës. Dhe ne krijuam një abstraksion të vogël për hartën e pllakave. Unë kam besim se kjo është e mjaftueshme për të vazhduar eksplorimin tonë se si të bëjmë një lojë strategjike.

Në "artikullin tjetër", ne do të ndërtojmë sipër dhe do të shtojmë disa njësi të ndryshme për secilin lojtar. Ne do të krijojmë disa funksione të ripërdorshme dhe do të argëtohemi me referenca. Qëndroni të sintonizuar!

Faleminderit që lexuat.

Ky artikull u botua fillimisht në "blogun tim".