From 827d817530501024f21a2d2c2b2dcbc12fb0cc8f Mon Sep 17 00:00:00 2001 From: kitsunecafe Date: Thu, 6 Jun 2024 20:45:29 -0400 Subject: [PATCH] sprite animations --- Cargo.lock | 10 ++ Cargo.toml | 1 + assets/level.ldtk | 26 +-- assets/player.toml | 0 crates/animation/.gitignore | 2 + crates/animation/Cargo.lock | 331 ++++++++++++++++++++++++++++++++++++ crates/animation/Cargo.toml | 9 + crates/animation/src/lib.rs | 165 ++++++++++++++++++ src/animation/mod.rs | 52 ++++++ src/camera/mod.rs | 33 +++- src/level/mod.rs | 38 +++-- src/lib.rs | 4 + src/player/mod.rs | 37 +--- 13 files changed, 648 insertions(+), 60 deletions(-) create mode 100644 assets/player.toml create mode 100644 crates/animation/.gitignore create mode 100644 crates/animation/Cargo.lock create mode 100644 crates/animation/Cargo.toml create mode 100644 crates/animation/src/lib.rs create mode 100644 src/animation/mod.rs diff --git a/Cargo.lock b/Cargo.lock index 5fec18d..2eb784c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1062,6 +1062,15 @@ dependencies = [ "uuid", ] +[[package]] +name = "bevy_smooth_pixel_camera" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b640e3e189e037377b260f5dc79c5ee4f51da09035390e3aacc9c02a2581f86" +dependencies = [ + "bevy", +] + [[package]] name = "bevy_sprite" version = "0.13.2" @@ -1423,6 +1432,7 @@ dependencies = [ "bevy_asset_loader", "bevy_ecs_ldtk", "bevy_editor_pls", + "bevy_smooth_pixel_camera", "bevy_xpbd_2d", "leafwing-input-manager", "toml", diff --git a/Cargo.toml b/Cargo.toml index a54eb23..821cac8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,6 +26,7 @@ bevy_ecs_ldtk = { git = "https://github.com/Trouv/bevy_ecs_ldtk.git" } bevy_xpbd_2d = "0.4.2" benimator = "4.1.3" toml = "0.8.13" +bevy_smooth_pixel_camera = "0.3.0" [patch.crates-io] # Patch unstable version to resolve conflicting dependencies from bevy_ecs_ldtk diff --git a/assets/level.ldtk b/assets/level.ldtk index 3a7086f..8f3dc0c 100644 --- a/assets/level.ldtk +++ b/assets/level.ldtk @@ -11,7 +11,7 @@ "iid": "e1f10300-fec0-11ee-aa65-4dc22f7ac0b0", "jsonVersion": "1.5.3", "appBuildId": 476670, - "nextUid": 84, + "nextUid": 85, "identifierStyle": "Capitalize", "toc": [], "worldLayout": "Free", @@ -730,8 +730,8 @@ "exportToToc": false, "allowOutOfBounds": false, "doc": null, - "width": 32, - "height": 32, + "width": 16, + "height": 16, "resizableX": false, "resizableY": false, "minWidth": null, @@ -890,7 +890,7 @@ "renderMode": "Tile", "showName": true, "tilesetId": 8, - "tileRenderMode": "Cover", + "tileRenderMode": "FitInside", "tileRect": { "tilesetUid": 8, "x": 16, "y": 0, "w": 32, "h": 16 }, "uiTileRect": null, "nineSliceBorders": [], @@ -967,7 +967,7 @@ "max": null, "regex": null, "acceptFileTypes": null, - "defaultOverride": null, + "defaultOverride": { "id": "V_Int", "params": [0] }, "textLanguageMode": null, "symmetricalRef": false, "autoChainRef": true, @@ -1346,10 +1346,10 @@ 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,0,0,0,0,0,0,0,1, 1,0,0,0,0,0,0,0,0,0,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,0,0,0,0, - 0,0,0,1,1,0,0,0,0,0,0,0,1,1,1,1,1,1,1,2,2,2,1,1,0,0,1,1,0,0,0,0,0,0,0, + 0,0,0,1,1,0,0,0,0,0,0,0,1,1,1,1,1,1,1,2,2,2,1,1,0,0,1,1,0,0,0,0,0,1,1, 0,0,0,0,0,0,0,1,1,1,1,0,0,0,0,0,1,1,1,1,1,1,1,0,0,0,1,1,0,0,1,1,0,0,0, - 0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,0,0,0,0,0,1,1,1,1,1,1,1,0,0,0,1,1,0,0,1, - 1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,1,1,0,0,0,0,0,0,0,1,1,1,1,0,0,0,0,0,1,1,1,1,1,1,1,0,0,0,1,1,0,0,1, + 1,0,0,0,0,0,1,1,0,0,0,0,0,0,0,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 ], @@ -1358,6 +1358,7 @@ { "px": [272,96], "src": [16,16], "f": 0, "t": 17, "d": [60,251], "a": 1 }, { "px": [336,128], "src": [16,16], "f": 0, "t": 17, "d": [60,333], "a": 1 }, { "px": [352,144], "src": [16,0], "f": 0, "t": 1, "d": [60,373], "a": 1 }, + { "px": [384,160], "src": [16,16], "f": 0, "t": 17, "d": [60,414], "a": 1 }, { "px": [80,176], "src": [16,16], "f": 0, "t": 17, "d": [60,434], "a": 1 }, { "px": [96,176], "src": [16,0], "f": 0, "t": 1, "d": [60,435], "a": 1 }, { "px": [544,176], "src": [16,16], "f": 0, "t": 17, "d": [60,463], "a": 1 }, @@ -1372,8 +1373,11 @@ { "px": [160,80], "src": [16,0], "f": 1, "t": 1, "d": [59,205], "a": 1 }, { "px": [80,160], "src": [16,16], "f": 0, "t": 17, "d": [59,395], "a": 1 }, { "px": [96,160], "src": [16,0], "f": 1, "t": 1, "d": [59,396], "a": 1 }, + { "px": [400,160], "src": [16,0], "f": 1, "t": 1, "d": [59,415], "a": 1 }, { "px": [528,160], "src": [16,16], "f": 0, "t": 17, "d": [59,423], "a": 1 }, { "px": [544,160], "src": [16,16], "f": 1, "t": 17, "d": [59,424], "a": 1 }, + { "px": [384,176], "src": [16,16], "f": 0, "t": 17, "d": [59,453], "a": 1 }, + { "px": [400,176], "src": [16,0], "f": 1, "t": 1, "d": [59,454], "a": 1 }, { "px": [528,176], "src": [0,16], "f": 0, "t": 16, "d": [59,462], "a": 1 }, { "px": [48,192], "src": [16,16], "f": 0, "t": 17, "d": [59,471], "a": 1 }, { "px": [144,192], "src": [16,16], "f": 1, "t": 17, "d": [59,477], "a": 1 }, @@ -1381,6 +1385,8 @@ { "px": [224,192], "src": [0,16], "f": 1, "t": 16, "d": [59,482], "a": 1 }, { "px": [272,192], "src": [0,16], "f": 0, "t": 16, "d": [59,485], "a": 1 }, { "px": [288,192], "src": [16,0], "f": 1, "t": 1, "d": [59,486], "a": 1 }, + { "px": [384,192], "src": [0,16], "f": 0, "t": 16, "d": [59,492], "a": 1 }, + { "px": [400,192], "src": [16,0], "f": 1, "t": 1, "d": [59,493], "a": 1 }, { "px": [528,192], "src": [16,16], "f": 0, "t": 17, "d": [59,501], "a": 1 }, { "px": [576,192], "src": [0,16], "f": 1, "t": 16, "d": [59,504], "a": 1 }, { "px": [160,96], "src": [0,16], "f": 0, "t": 16, "d": [58,244], "a": 1 }, @@ -1389,7 +1395,6 @@ { "px": [288,112], "src": [16,0], "f": 0, "t": 1, "d": [58,291], "a": 1 }, { "px": [320,128], "src": [16,16], "f": 0, "t": 17, "d": [58,332], "a": 1 }, { "px": [368,160], "src": [16,16], "f": 0, "t": 17, "d": [58,413], "a": 1 }, - { "px": [384,160], "src": [0,16], "f": 0, "t": 16, "d": [58,414], "a": 1 }, { "px": [64,208], "src": [0,16], "f": 0, "t": 16, "d": [58,511], "a": 1 }, { "px": [80,208], "src": [16,0], "f": 0, "t": 1, "d": [58,512], "a": 1 }, { "px": [96,208], "src": [16,16], "f": 0, "t": 17, "d": [58,513], "a": 1 }, @@ -1413,13 +1418,14 @@ { "px": [304,128], "src": [0,16], "f": 0, "t": 16, "d": [56,331], "a": 1 }, { "px": [336,144], "src": [0,16], "f": 0, "t": 16, "d": [56,372], "a": 1 }, { "px": [352,160], "src": [16,16], "f": 0, "t": 17, "d": [56,412], "a": 1 }, - { "px": [400,160], "src": [16,16], "f": 1, "t": 17, "d": [56,415], "a": 1 }, { "px": [48,208], "src": [0,16], "f": 0, "t": 16, "d": [56,510], "a": 1 }, { "px": [144,208], "src": [0,16], "f": 1, "t": 16, "d": [56,516], "a": 1 }, { "px": [208,208], "src": [16,0], "f": 0, "t": 1, "d": [56,520], "a": 1 }, { "px": [224,208], "src": [16,0], "f": 1, "t": 1, "d": [56,521], "a": 1 }, { "px": [272,208], "src": [0,16], "f": 0, "t": 16, "d": [56,524], "a": 1 }, { "px": [288,208], "src": [0,16], "f": 1, "t": 16, "d": [56,525], "a": 1 }, + { "px": [384,208], "src": [0,16], "f": 0, "t": 16, "d": [56,531], "a": 1 }, + { "px": [400,208], "src": [16,0], "f": 1, "t": 1, "d": [56,532], "a": 1 }, { "px": [528,208], "src": [16,0], "f": 0, "t": 1, "d": [56,540], "a": 1 }, { "px": [576,208], "src": [0,16], "f": 1, "t": 16, "d": [56,543], "a": 1 }, { "px": [144,64], "src": [0,0], "f": 0, "t": 0, "d": [55,165], "a": 1 }, diff --git a/assets/player.toml b/assets/player.toml new file mode 100644 index 0000000..e69de29 diff --git a/crates/animation/.gitignore b/crates/animation/.gitignore new file mode 100644 index 0000000..dc0d833 --- /dev/null +++ b/crates/animation/.gitignore @@ -0,0 +1,2 @@ +target/ + diff --git a/crates/animation/Cargo.lock b/crates/animation/Cargo.lock new file mode 100644 index 0000000..5fd6623 --- /dev/null +++ b/crates/animation/Cargo.lock @@ -0,0 +1,331 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "animation" +version = "0.1.0" +dependencies = [ + "rstest", +] + +[[package]] +name = "autocfg" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + +[[package]] +name = "futures" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" + +[[package]] +name = "futures-executor" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" + +[[package]] +name = "futures-macro" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" + +[[package]] +name = "futures-task" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" + +[[package]] +name = "futures-timer" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24" + +[[package]] +name = "futures-util" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "glob" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" + +[[package]] +name = "indexmap" +version = "2.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "memchr" +version = "2.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d" + +[[package]] +name = "pin-project-lite" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "proc-macro-crate" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d37c51ca738a55da99dc0c4a34860fd675453b8b36209178c2249bb13651284" +dependencies = [ + "toml_edit", +] + +[[package]] +name = "proc-macro2" +version = "1.0.85" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22244ce15aa966053a896d1accb3a6e68469b97c7f33f284b99f0d576879fc23" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "regex" +version = "1.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c117dbdfde9c8308975b6a18d71f3f385c89461f7b3fb054288ecf2a2058ba4c" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86b83b8b9847f9bf95ef68afb0b8e6cdb80f498442f5179a29fad448fcc1eaea" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adad44e29e4c806119491a7f06f03de4d1af22c3a680dd47f1e6e179439d1f56" + +[[package]] +name = "relative-path" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba39f3699c378cd8970968dcbff9c43159ea4cfbd88d43c00b22f2ef10a435d2" + +[[package]] +name = "rstest" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9afd55a67069d6e434a95161415f5beeada95a01c7b815508a82dcb0e1593682" +dependencies = [ + "futures", + "futures-timer", + "rstest_macros", + "rustc_version", +] + +[[package]] +name = "rstest_macros" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4165dfae59a39dd41d8dec720d3cbfbc71f69744efb480a3920f5d4e0cc6798d" +dependencies = [ + "cfg-if", + "glob", + "proc-macro-crate", + "proc-macro2", + "quote", + "regex", + "relative-path", + "rustc_version", + "syn", + "unicode-ident", +] + +[[package]] +name = "rustc_version" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" +dependencies = [ + "semver", +] + +[[package]] +name = "semver" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" + +[[package]] +name = "slab" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg", +] + +[[package]] +name = "syn" +version = "2.0.66" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c42f3f41a2de00b01c0aaad383c5a45241efc8b2d1eda5661812fda5f3cdcff5" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "toml_datetime" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4badfd56924ae69bcc9039335b2e017639ce3f9b001c393c1b2d1ef846ce2cbf" + +[[package]] +name = "toml_edit" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a8534fd7f78b5405e860340ad6575217ce99f38d4d5c8f2442cb5ecb50090e1" +dependencies = [ + "indexmap", + "toml_datetime", + "winnow", +] + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "winnow" +version = "0.5.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" +dependencies = [ + "memchr", +] diff --git a/crates/animation/Cargo.toml b/crates/animation/Cargo.toml new file mode 100644 index 0000000..6d799af --- /dev/null +++ b/crates/animation/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "animation" +version = "0.1.0" +edition = "2021" + +[dependencies] + +[dev-dependencies] +rstest = "0.21.0" diff --git a/crates/animation/src/lib.rs b/crates/animation/src/lib.rs new file mode 100644 index 0000000..b34f6fd --- /dev/null +++ b/crates/animation/src/lib.rs @@ -0,0 +1,165 @@ +use std::time::Duration; + +#[cfg(feature = "f32")] +pub type Float = f32; +#[cfg(feature = "f32")] +pub use std::f32 as floats; +#[cfg(not(feature = "f32"))] +pub type Float = f64; +#[cfg(not(feature = "f32"))] +pub use std::f64 as floats; + +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +pub(crate) enum Mode { + Once, + RepeatFrom(usize), + PingPong, +} + +impl Default for Mode { + fn default() -> Self { + Self::RepeatFrom(0usize) + } +} + +pub struct Frame { + pub index: usize, + pub duration: Duration, +} + +impl Frame { + pub fn new(index: usize, duration: Duration) -> Self { + assert!(!duration.is_zero(), "must be a non-zero duration"); + Self { index, duration } + } +} + +pub struct Animation { + pub(crate) frames: Vec, + pub(crate) mode: Mode, +} + +impl Animation { + pub fn len(&self) -> usize { + self.frames.len() + } + + pub fn is_empty(&self) -> bool { + self.len() == 0 + } + + pub fn from_indices(indices: impl Iterator, frame_rate: FrameRate) -> Self { + let duration = if frame_rate.is_total_duration { + frame_rate.frame_duration.div_f64(indices.cloned().count() as f64) + } else { + frame_rate.frame_duration + }; + + indices + .map(|index| Frame::new(index, duration)) + .collect() + } +} + +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +pub struct FrameRate { + frame_duration: Duration, + is_total_duration: bool, +} + +impl FrameRate { + pub fn from_fps(fps: Float) -> Self { + assert!(fps.is_finite(), "FPS must be finite"); + Self::from_total_duration(Duration::from_secs(1).div_f64(fps)) + } + + pub fn from_frame_duration(frame_duration: Duration) -> Self { + Self { + frame_duration, + is_total_duration: false, + } + } + + pub fn from_total_duration(frame_duration: Duration) -> Self { + Self { + frame_duration, + is_total_duration: true, + } + } +} + +#[derive(Debug, Default, Clone)] +pub struct State { + animation_index: usize, + sprite_index: usize, + elapsed_time: Duration, + is_reverse: bool, + is_complete: bool, +} + +impl State { + pub fn new() -> Self { + Self::default() + } + + pub fn frame<'a>(&self, animation: &'a Animation) -> &'a Frame { + &animation.frames[self.animation_index % animation.len()] + } + + pub fn update(&mut self, animation: &Animation, delta: Duration) { + let mut current_frame = self.frame(animation); + self.sprite_index = current_frame.index; + self.elapsed_time += delta; + while self.elapsed_time >= current_frame.duration { + let is_last_frame = self.animation_index >= animation.len(); + + match animation.mode { + Mode::Once if self.is_complete => {} + Mode::Once if is_last_frame => self.is_complete = true, + Mode::Once => self.animation_index += 1, + Mode::RepeatFrom(start_frame) if is_last_frame => { + self.animation_index = start_frame + } + Mode::RepeatFrom(_start_frame) => self.animation_index += 1, + Mode::PingPong if self.is_reverse && self.animation_index == 0 => { + self.is_reverse = false; + self.animation_index += 1; + } + Mode::PingPong if is_last_frame => { + self.is_reverse = true; + self.animation_index += 1; + } + Mode::PingPong => self.animation_index += 1, + } + + self.elapsed_time -= current_frame.duration; + current_frame = self.frame(animation); + self.sprite_index = current_frame.index; + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use rstest::{fixture, rstest}; + + #[rstest] + #[should_panic(expected = "must be a non-zero duration")] + fn should_panic_on_zero_duration_frame(#[values(0.0)] value: Float) { + let _ = Frame::new(0usize, Duration::from_secs_f64(value)); + } + + #[fixture] + fn state() -> State { + State::new() + } + + #[fixture] + fn sixy_fps_4_frame_animation() -> Animation { + Animation::f + } + + #[rstest] + fn update_sixty_fps(sixy_fps: Duration) {} +} diff --git a/src/animation/mod.rs b/src/animation/mod.rs new file mode 100644 index 0000000..056cf55 --- /dev/null +++ b/src/animation/mod.rs @@ -0,0 +1,52 @@ +use std::time::Duration; + +#[cfg(feature = "f32")] +pub type Float = f32; +#[cfg(feature = "f32")] +pub use std::f32 as floats; +#[cfg(not(feature = "f32"))] +pub type Float = f64; +#[cfg(not(feature = "f32"))] +pub use std::f64 as floats; + + +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +pub(crate) enum Mode { + Once, + RepeatFrom(usize), + PingPong +} + +impl Default for Mode { + fn default() -> Self { + Self::RepeatFrom(0usize) + } +} + +pub struct Frame { + pub index: usize, + pub duration: Duration +} + +impl Frame { + pub fn new(index: usize, duration: Duration) -> Self { + assert!(!duration.is_zero(), "must be a non-zero duration"); + Self { index, duration } + } +} + +pub struct Animation { + pub(crate) frames: Vec, + pub(crate) mode: Mode +} + +#[cfg(test)] +mod tests { + se rstest::rstest; + use super::Float; + + #[rstest] + fn test(#[values(0.0, -1.0)] time: Float) { + } +} + diff --git a/src/camera/mod.rs b/src/camera/mod.rs index 409ac9e..e89929f 100644 --- a/src/camera/mod.rs +++ b/src/camera/mod.rs @@ -1,5 +1,6 @@ use bevy::prelude::*; use bevy_ecs_ldtk::prelude::*; +use bevy_smooth_pixel_camera::{components::PixelCamera, viewport::ViewportSize}; #[derive(Component, Debug)] pub struct Follow { @@ -36,27 +37,47 @@ impl UnresolvedTargetRef { } } +#[derive(Bundle)] +pub struct PixelCameraBundle { + pixel_camera: PixelCamera, + camera_2d_bundle: Camera2dBundle, +} + +impl Default for PixelCameraBundle { + fn default() -> Self { + Self { + pixel_camera: PixelCamera::from_size(ViewportSize::PixelFixed(1)), + camera_2d_bundle: Camera2dBundle::default(), + } + } +} + #[derive(Bundle, Default, LdtkEntity)] pub struct CameraEntity { #[with(UnresolvedTargetRef::from_field)] unresolved_target_ref: UnresolvedTargetRef, - camera_2d_bundle: Camera2dBundle + pixel_camera_bundle: PixelCameraBundle, } pub struct CameraPlugin; impl Plugin for CameraPlugin { fn build(&self, app: &mut App) { - app.add_systems(Update, (follow_target, resolve_target_references, dynamic_scale)).register_ldtk_entity::("Camera"); + app.add_systems( + Update, + (follow_target, resolve_target_references, dynamic_scale), + ) + .register_ldtk_entity::("Camera"); } } fn follow_target( - mut follower_q: Query<(&mut Transform, &Follow)>, + mut follower_q: Query<(&mut PixelCamera, &Follow)>, target_q: Query<&GlobalTransform, Without>, ) { - for (mut transform, follow) in follower_q.iter_mut() { + for (mut camera, follow) in follower_q.iter_mut() { if let Ok(target) = target_q.get(follow.target) { - transform.translation = target.transform_point(follow.offset); + let result = target.transform_point(follow.offset); + camera.subpixel_pos = Vec2::new(result.x, result.y); } } } @@ -87,6 +108,6 @@ pub fn resolve_target_references( pub fn dynamic_scale(mut query: Query<&mut OrthographicProjection>) { for mut projection in query.iter_mut() { - projection.scale = 0.2; + projection.scale = 0.5; } } diff --git a/src/level/mod.rs b/src/level/mod.rs index 7a67706..44daf2b 100644 --- a/src/level/mod.rs +++ b/src/level/mod.rs @@ -10,7 +10,7 @@ impl ZIndex { Self( *instance .get_int_field("ZIndex") - .expect("expected entity to have Z Index field") + .expect("expected entity to have Z Index field"), ) } } @@ -25,44 +25,56 @@ impl> From for ZIndex { pub struct Tile; #[derive(Bundle)] -pub struct TileBundle { - tile: Tile, +pub struct ColliderBundle { collider: Collider, rigidbody: RigidBody, } -impl Default for TileBundle { +impl Default for ColliderBundle { fn default() -> Self { Self { - tile: Tile, collider: Collider::rectangle(16., 16.), rigidbody: RigidBody::Static, } } } -#[derive(Default, Bundle, LdtkIntCell)] -pub struct TileIntCell { - tile_bundle: TileBundle, +impl From<&EntityInstance> for ColliderBundle { + fn from(value: &EntityInstance) -> Self { + let rigidbody = match value.get_enum_field("Collider") { + Ok(_) => RigidBody::Kinematic, + _ => RigidBody::Static, + }; + + Self { + rigidbody, + collider: Collider::rectangle(value.width as f32, value.height as f32), + } + } } +#[derive(Default, Bundle, LdtkIntCell)] +pub struct TileIntCell { + tile: Tile, + collider_bundle: ColliderBundle, +} #[derive(Default, Bundle, LdtkEntity)] pub struct PlatformEntity { #[with(ZIndex::from_field)] z_index: ZIndex, + #[from_entity_instance] + collider_bundle: ColliderBundle, #[sprite_sheet_bundle] - sprite_sheet_bundle: SpriteSheetBundle + sprite_sheet_bundle: SpriteSheetBundle, } - pub struct LevelPlugin; impl Plugin for LevelPlugin { fn build(&self, app: &mut App) { - app - .register_type::() + app.register_type::() .add_systems(Last, update_z_index) - .register_ldtk_entity::("Platforms") + .register_ldtk_entity::("OneWayPlatform") .register_default_ldtk_int_cell_for_layer::("Ground"); } } diff --git a/src/lib.rs b/src/lib.rs index 72cb91d..e07b321 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,5 +1,6 @@ use assets::{ldtk::WorldConfig, AssetPlugin}; use bevy::prelude::*; +use bevy_smooth_pixel_camera::PixelCameraPlugin; use bevy_xpbd_2d::math::{Scalar, Vector}; use camera::CameraPlugin; use debug::DebugPlugin; @@ -13,6 +14,7 @@ mod debug; mod level; mod physics; mod player; +mod animation; #[derive(States, Default, Debug, Clone, PartialEq, Eq, Hash)] pub enum GameState { @@ -29,10 +31,12 @@ pub struct GamePlugin; impl Plugin for GamePlugin { fn build(&self, app: &mut bevy::prelude::App) { app.add_plugins(DefaultPlugins.set(ImagePlugin::default_nearest())) + .add_plugins(PixelCameraPlugin) .add_plugins(( AssetPlugin, PhysicsPlugin, LevelPlugin, + AnimationPlugin, PlayerPlugin, CameraPlugin, )) diff --git a/src/player/mod.rs b/src/player/mod.rs index 766e298..ce0ceca 100644 --- a/src/player/mod.rs +++ b/src/player/mod.rs @@ -2,22 +2,17 @@ mod controller; mod input; mod movement; -use bevy::{prelude::*, render::view::RenderLayers}; +use bevy::prelude::*; use bevy_ecs_ldtk::prelude::*; use leafwing_input_manager::{action_state::ActionState, InputManagerBundle}; use self::{ controller::{CharacterControllerBundle, CharacterControllerPlugin, Jumping}, input::{InputPlugin, PlayerAction}, - movement::MovementDirection + movement::MovementDirection, }; -use crate::level::ZIndex; +use crate::{animation::*, level::ZIndex}; -#[derive(Component, Deref)] -pub struct Animation(benimator::Animation); - -#[derive(Default, Component, Deref, DerefMut)] -pub struct AnimationState(benimator::State); #[derive(Component, Default)] pub struct Player; @@ -27,8 +22,7 @@ pub struct PlayerBundle { player: Player, input_manager_bundle: InputManagerBundle, character_controller_bundle: CharacterControllerBundle, - animation: Animation, - animation_state: AnimationState, + animation_bundle: AnimationBundle, } impl Default for PlayerBundle { @@ -37,11 +31,10 @@ impl Default for PlayerBundle { player: Player, input_manager_bundle: InputManagerBundle::with_map(PlayerAction::default_input_map()), character_controller_bundle: CharacterControllerBundle::default(), - animation: Animation(benimator::Animation::from_indices( + animation_bundle: AnimationBundle::from_animation(benimator::Animation::from_indices( 0..4, benimator::FrameRate::from_fps(12.), )), - animation_state: AnimationState::default(), } } } @@ -59,14 +52,7 @@ pub struct PlayerPlugin; impl Plugin for PlayerPlugin { fn build(&self, app: &mut App) { app.add_plugins((InputPlugin, CharacterControllerPlugin)) - .add_systems( - Update, - ( - apply_movement_input, - apply_jumping_input, - animate, - ), - ) + .add_systems(Update, (apply_movement_input, apply_jumping_input)) .register_ldtk_entity::("Player"); } } @@ -92,14 +78,3 @@ fn apply_jumping_input( } } } - -fn animate( - time: Res