Initial commit
147
.cargo/config_fast_builds.toml
Normal file
@ -0,0 +1,147 @@
|
||||
# Copy this file to `config.toml` to speed up your builds.
|
||||
#
|
||||
# # Faster linker
|
||||
#
|
||||
# One of the slowest aspects of compiling large Rust programs is the linking time. This file configures an
|
||||
# alternate linker that may improve build times. When choosing a new linker, you have two options:
|
||||
#
|
||||
# ## LLD
|
||||
#
|
||||
# LLD is a linker from the LLVM project that supports Linux, Windows, MacOS, and WASM. It has the greatest
|
||||
# platform support and the easiest installation process. It is enabled by default in this file for Linux
|
||||
# and Windows. On MacOS, the default linker yields higher performance than LLD and is used instead.
|
||||
#
|
||||
# To install, please scroll to the corresponding table for your target (eg. `[target.x86_64-pc-windows-msvc]`
|
||||
# for Windows) and follow the steps under `LLD linker`.
|
||||
#
|
||||
# For more information, please see LLD's website at <https://lld.llvm.org>.
|
||||
#
|
||||
# ## Mold
|
||||
#
|
||||
# Mold is a newer linker written by one of the authors of LLD. It boasts even greater performance, specifically
|
||||
# through its high parallelism, though it only supports Linux.
|
||||
#
|
||||
# Mold is disabled by default in this file. If you wish to enable it, follow the installation instructions for
|
||||
# your corresponding target, disable LLD by commenting out its `-Clink-arg=...` line, and enable Mold by
|
||||
# *uncommenting* its `-Clink-arg=...` line.
|
||||
#
|
||||
# There is a fork of Mold named Sold that supports MacOS, but it is unmaintained and is about the same speed as
|
||||
# the default ld64 linker. For this reason, it is not included in this file.
|
||||
#
|
||||
# For more information, please see Mold's repository at <https://github.com/rui314/mold>.
|
||||
#
|
||||
# # Nightly configuration
|
||||
#
|
||||
# Be warned that the following features require nightly Rust, which is expiremental and may contain bugs. If you
|
||||
# are having issues, skip this section and use stable Rust instead.
|
||||
#
|
||||
# There are a few unstable features that can improve performance. To use them, first install nightly Rust
|
||||
# through Rustup:
|
||||
#
|
||||
# ```
|
||||
# rustup toolchain install nightly
|
||||
# ```
|
||||
#
|
||||
# Finally, uncomment the lines under the `Nightly` heading for your corresponding target table (eg.
|
||||
# `[target.x86_64-unknown-linux-gnu]` for Linux) to enable the following features:
|
||||
#
|
||||
# ## `share-generics`
|
||||
#
|
||||
# Usually rustc builds each crate separately, then combines them all together at the end. `share-generics` forces
|
||||
# crates to share monomorphized generic code, so they do not duplicate work.
|
||||
#
|
||||
# In other words, instead of crate 1 generating `Foo<String>` and crate 2 generating `Foo<String>` separately,
|
||||
# only one crate generates `Foo<String>` and the other adds on to the pre-exiting work.
|
||||
#
|
||||
# Note that you may have some issues with this flag on Windows. If compiling fails due to the 65k symbol limit,
|
||||
# you may have to disable this setting. For more information and possible solutions to this error, see
|
||||
# <https://github.com/bevyengine/bevy/issues/1110>.
|
||||
#
|
||||
# ## `threads`
|
||||
#
|
||||
# This option enables rustc's parallel frontend, which improves performance when parsing, type checking, borrow
|
||||
# checking, and more. We currently set `threads=0`, which defaults to the amount of cores in your CPU.
|
||||
#
|
||||
# For more information, see the blog post at <https://blog.rust-lang.org/2023/11/09/parallel-rustc.html>.
|
||||
|
||||
[target.x86_64-unknown-linux-gnu]
|
||||
linker = "clang"
|
||||
rustflags = [
|
||||
# LLD linker
|
||||
#
|
||||
# You may need to install it:
|
||||
#
|
||||
# - Ubuntu: `sudo apt-get install lld clang`
|
||||
# - Fedora: `sudo dnf install lld clang`
|
||||
# - Arch: `sudo pacman -S lld clang`
|
||||
"-Clink-arg=-fuse-ld=lld",
|
||||
|
||||
# Mold linker
|
||||
#
|
||||
# You may need to install it:
|
||||
#
|
||||
# - Ubuntu: `sudo apt-get install mold clang`
|
||||
# - Fedora: `sudo dnf install mold clang`
|
||||
# - Arch: `sudo pacman -S mold clang`
|
||||
# "-Clink-arg=-fuse-ld=/usr/bin/mold",
|
||||
|
||||
# Nightly
|
||||
# "-Zshare-generics=y",
|
||||
# "-Zthreads=0",
|
||||
]
|
||||
|
||||
[target.x86_64-apple-darwin]
|
||||
rustflags = [
|
||||
# LLD linker
|
||||
#
|
||||
# The default ld64 linker is faster, you should continue using it instead.
|
||||
#
|
||||
# You may need to install it:
|
||||
#
|
||||
# Brew: `brew install llvm`
|
||||
# Manually: <https://lld.llvm.org/MachO/index.html>
|
||||
# "-Clink-arg=-fuse-ld=/usr/local/opt/llvm/bin/ld64.lld",
|
||||
|
||||
# Nightly
|
||||
# "-Zshare-generics=y",
|
||||
# "-Zthreads=0",
|
||||
]
|
||||
|
||||
[target.aarch64-apple-darwin]
|
||||
rustflags = [
|
||||
# LLD linker
|
||||
#
|
||||
# The default ld64 linker is faster, you should continue using it instead.
|
||||
#
|
||||
# You may need to install it:
|
||||
#
|
||||
# Brew: `brew install llvm`
|
||||
# Manually: <https://lld.llvm.org/MachO/index.html>
|
||||
# "-Clink-arg=-fuse-ld=/opt/homebrew/opt/llvm/bin/ld64.lld",
|
||||
|
||||
# Nightly
|
||||
# "-Zshare-generics=y",
|
||||
# "-Zthreads=0",
|
||||
]
|
||||
|
||||
[target.x86_64-pc-windows-msvc]
|
||||
# LLD linker
|
||||
#
|
||||
# You may need to install it:
|
||||
#
|
||||
# ```
|
||||
# cargo install -f cargo-binutils
|
||||
# rustup component add llvm-tools
|
||||
# ```
|
||||
linker = "rust-lld.exe"
|
||||
rustdocflags = ["-Clinker=rust-lld.exe"]
|
||||
rustflags = [
|
||||
# Nightly
|
||||
# "-Zshare-generics=n", # This needs to be off if you use dynamic linking on Windows.
|
||||
# "-Zthreads=0",
|
||||
]
|
||||
|
||||
# Optional: Uncommenting the following improves compile times, but reduces the amount of debug info to 'line number tables only'
|
||||
# In most cases the gains are negligible, but if you are on macos and have slow compile times you should see significant gains.
|
||||
# [profile.dev]
|
||||
# debug = 1
|
||||
84
.github/workflows/ci.yaml
vendored
Normal file
@ -0,0 +1,84 @@
|
||||
name: CI
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
RUSTFLAGS: --deny warnings
|
||||
RUSTDOCFLAGS: --deny warnings
|
||||
jobs:
|
||||
# Run tests.
|
||||
test:
|
||||
name: Tests
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
- name: Install Rust toolchain
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
- name: Install dependencies
|
||||
run: sudo apt-get update; sudo apt-get install --no-install-recommends libasound2-dev libudev-dev libwayland-dev libxkbcommon-dev
|
||||
- name: Populate target directory from cache
|
||||
uses: Leafwing-Studios/cargo-cache@v2
|
||||
with:
|
||||
sweep-cache: true
|
||||
- name: Run tests
|
||||
run: |
|
||||
cargo test --locked --workspace --all-features --all-targets
|
||||
# Workaround for https://github.com/rust-lang/cargo/issues/6669
|
||||
cargo test --locked --workspace --all-features --doc
|
||||
# Run clippy lints.
|
||||
clippy:
|
||||
name: Clippy
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
- name: Install Rust toolchain
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
components: clippy
|
||||
- name: Install dependencies
|
||||
run: sudo apt-get update; sudo apt-get install --no-install-recommends libasound2-dev libudev-dev libwayland-dev libxkbcommon-dev
|
||||
- name: Populate target directory from cache
|
||||
uses: Leafwing-Studios/cargo-cache@v2
|
||||
with:
|
||||
sweep-cache: true
|
||||
- name: Run clippy lints
|
||||
run: cargo clippy --locked --workspace --all-targets --all-features -- --deny warnings
|
||||
# Check formatting.
|
||||
format:
|
||||
name: Format
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
- name: Install Rust toolchain
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
components: rustfmt
|
||||
- name: Run cargo fmt
|
||||
run: cargo fmt --all -- --check
|
||||
# Check documentation.
|
||||
doc:
|
||||
name: Docs
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
- name: Install Rust toolchain
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
- name: Install dependencies
|
||||
run: sudo apt-get update; sudo apt-get install --no-install-recommends libasound2-dev libudev-dev libwayland-dev libxkbcommon-dev
|
||||
- name: Populate target directory from cache
|
||||
uses: Leafwing-Studios/cargo-cache@v2
|
||||
with:
|
||||
sweep-cache: true
|
||||
- name: Check documentation
|
||||
run: cargo doc --locked --workspace --all-features --document-private-items --no-deps
|
||||
273
.github/workflows/release.yaml
vendored
Normal file
@ -0,0 +1,273 @@
|
||||
name: Release
|
||||
on:
|
||||
# Trigger this workflow when a tag is pushed in the format `v1.2.3`.
|
||||
push:
|
||||
tags:
|
||||
# Pattern syntax: https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#filter-pattern-cheat-sheet
|
||||
- "v[0-9]+.[0-9]+.[0-9]+*"
|
||||
# Trigger this workflow manually via workflow dispatch.
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: 'Version number in the format `v1.2.3`'
|
||||
required: true
|
||||
type: string
|
||||
# Configure the release workflow by editing these values.
|
||||
env:
|
||||
# The base filename of the binary produced by `cargo build`.
|
||||
cargo_build_binary_name: the-labyrinth-of-echoes
|
||||
# The path to the assets directory.
|
||||
assets_path: assets
|
||||
# Whether to upload the packages produced by this workflow to a GitHub release.
|
||||
upload_to_github: true
|
||||
# The itch.io project to upload to in the format `user-name/project-name`.
|
||||
# There will be no upload to itch.io if this is commented out.
|
||||
upload_to_itch: kristoferssolo/https://kristoferssolo.itch.io/the-labyrinth-of-echoes
|
||||
############
|
||||
# ADVANCED #
|
||||
############
|
||||
|
||||
# The ID of the app produced by this workflow.
|
||||
# Applies to macOS releases.
|
||||
# Must contain only A-Z, a-z, 0-9, hyphen, and period: https://developer.apple.com/documentation/bundleresources/information_property_list/cfbundleidentifier
|
||||
app_id: kristoferssolo.the-labyrinth-of-echoes
|
||||
# The base filename of the binary in the package produced by this workflow.
|
||||
# Applies to Windows, macOS, and Linux releases.
|
||||
# Defaults to `cargo_build_binary_name` if commented out.
|
||||
#app_binary_name: the-labyrinth-of-echoes
|
||||
|
||||
# The name of the `.zip` or `.dmg` file produced by this workflow.
|
||||
# Defaults to `app_binary_name` if commented out.
|
||||
#app_package_name: the-labyrinth-of-echoes
|
||||
|
||||
# The display name of the app produced by this workflow.
|
||||
# Applies to macOS releases.
|
||||
# Defaults to `app_package_name` if commented out.
|
||||
#app_display_name: The Labyrinth Of Echoes
|
||||
|
||||
# The short display name of the app produced by this workflow.
|
||||
# Applies to macOS releases.
|
||||
# Must be 15 or fewer characters: https://developer.apple.com/documentation/bundleresources/information_property_list/cfbundlename
|
||||
# Defaults to `app_display_name` if commented out.
|
||||
#app_short_name: The Labyrint…
|
||||
|
||||
# Before enabling LFS, please take a look at GitHub's documentation for costs and quota limits:
|
||||
# https://docs.github.com/en/repositories/working-with-files/managing-large-files/about-storage-and-bandwidth-usage
|
||||
git_lfs: false
|
||||
jobs:
|
||||
# Determine the version number for this workflow.
|
||||
get-version:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Get version number from tag
|
||||
id: tag
|
||||
run: echo "tag=${GITHUB_REF#refs/tags/}" >> "${GITHUB_OUTPUT}"
|
||||
outputs:
|
||||
# Use the input from workflow dispatch, or fall back to the git tag.
|
||||
version: ${{ inputs.version || steps.tag.outputs.tag }}
|
||||
# Build and package a release for each platform.
|
||||
build:
|
||||
needs:
|
||||
- get-version
|
||||
env:
|
||||
version: ${{ needs.get-version.outputs.version }}
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- platform: web
|
||||
targets: wasm32-unknown-unknown
|
||||
profile: release
|
||||
binary_ext: .wasm
|
||||
package_ext: .zip
|
||||
runner: ubuntu-latest
|
||||
- platform: linux
|
||||
targets: x86_64-unknown-linux-gnu
|
||||
profile: release-native
|
||||
features: bevy/wayland
|
||||
package_ext: .zip
|
||||
runner: ubuntu-latest
|
||||
- platform: windows
|
||||
targets: x86_64-pc-windows-msvc
|
||||
profile: release-native
|
||||
binary_ext: .exe
|
||||
package_ext: .zip
|
||||
runner: windows-latest
|
||||
- platform: macos
|
||||
targets: x86_64-apple-darwin aarch64-apple-darwin
|
||||
profile: release-native
|
||||
app_suffix: .app/Contents/MacOS
|
||||
package_ext: .dmg
|
||||
runner: macos-latest
|
||||
runs-on: ${{ matrix.runner }}
|
||||
permissions:
|
||||
# Required to create a GitHub release: https://docs.github.com/en/rest/releases/releases#create-a-release
|
||||
contents: write
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
steps:
|
||||
- name: Set up environment
|
||||
run: |
|
||||
# Default values:
|
||||
echo "app_binary_name=${app_binary_name:=${{ env.cargo_build_binary_name }}}" >> "${GITHUB_ENV}"
|
||||
echo "app_package_name=${app_package_name:=${app_binary_name}}" >> "${GITHUB_ENV}"
|
||||
echo "app_display_name=${app_display_name:=${app_package_name}}" >> "${GITHUB_ENV}"
|
||||
echo "app_short_name=${app_short_name:=${app_display_name}}" >> "${GITHUB_ENV}"
|
||||
|
||||
# File paths:
|
||||
echo "app=tmp/app/${app_package_name}"'${{ matrix.app_suffix }}' >> "${GITHUB_ENV}"
|
||||
echo "package=${app_package_name}-"'${{ matrix.platform }}${{ matrix.package_ext }}' >> "${GITHUB_ENV}"
|
||||
|
||||
# macOS environment:
|
||||
if [ '${{ matrix.platform }}' == 'macos' ]; then
|
||||
echo 'MACOSX_DEPLOYMENT_TARGET=11.0' >> "${GITHUB_ENV}" # macOS 11.0 Big Sur is the first version to support universal binaries.
|
||||
echo "SDKROOT=$(xcrun --sdk macosx --show-sdk-path)" >> "${GITHUB_ENV}"
|
||||
fi
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
lfs: ${{ env.git_lfs }}
|
||||
- name: Install Rust toolchain
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
targets: ${{ matrix.targets }}
|
||||
- name: Populate cargo cache
|
||||
uses: Leafwing-Studios/cargo-cache@v2
|
||||
with:
|
||||
sweep-cache: true
|
||||
- name: Install dependencies (Linux)
|
||||
if: ${{ matrix.platform == 'linux' }}
|
||||
run: sudo apt-get update; sudo apt-get install --no-install-recommends libasound2-dev libudev-dev libwayland-dev libxkbcommon-dev
|
||||
- name: Prepare output directories
|
||||
run: rm -rf tmp; mkdir -p tmp/binary '${{ env.app }}'
|
||||
- name: Install cargo-binstall (Web)
|
||||
if: ${{ matrix.platform == 'web' }}
|
||||
uses: cargo-bins/cargo-binstall@v1.9.0
|
||||
- name: Install and run trunk (Web)
|
||||
if: ${{ matrix.platform == 'web' }}
|
||||
run: |
|
||||
cargo binstall --no-confirm trunk wasm-bindgen-cli wasm-opt
|
||||
trunk build --locked --release --dist '${{ env.app }}'
|
||||
- name: Build binaries (non-Web)
|
||||
if: ${{ matrix.platform != 'web' }}
|
||||
run: |
|
||||
for target in ${{ matrix.targets }}; do
|
||||
cargo build --locked --profile='${{ matrix.profile }}' --target="${target}" --no-default-features --features='${{ matrix.features }}'
|
||||
mv target/"${target}"/'${{ matrix.profile }}/${{ env.cargo_build_binary_name }}${{ matrix.binary_ext }}' tmp/binary/"${target}"'${{ matrix.binary_ext }}'
|
||||
done
|
||||
- name: Add binaries to app (non-Web)
|
||||
if: ${{ matrix.platform != 'web' }}
|
||||
run: |
|
||||
if [ '${{ matrix.platform }}' == 'macos' ]; then
|
||||
lipo tmp/binary/*'${{ matrix.binary_ext }}' -create -output '${{ env.app }}/${{ env.app_binary_name }}${{ matrix.binary_ext }}'
|
||||
else
|
||||
mv tmp/binary/*'${{ matrix.binary_ext }}' '${{ env.app }}/${{ env.app_binary_name }}${{ matrix.binary_ext }}'
|
||||
fi
|
||||
- name: Add assets to app (non-Web)
|
||||
if: ${{ matrix.platform != 'web' }}
|
||||
run: cp -r ./'${{ env.assets_path }}' '${{ env.app }}' || true # Ignore error if assets folder does not exist
|
||||
- name: Add metadata to app (macOS)
|
||||
if: ${{ matrix.platform == 'macos' }}
|
||||
run: |
|
||||
cat >'${{ env.app }}/../Info.plist' <<EOF
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>en</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>${{ env.app_display_name }}</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>${{ env.app_binary_name }}</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>${{ env.app_id }}</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>${{ env.app_short_name }}</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>${{ env.version }}</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>${{ env.version }}</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleSupportedPlatforms</key>
|
||||
<array>
|
||||
<string>MacOSX</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
EOF
|
||||
- name: Package app (non-Windows)
|
||||
if: ${{ matrix.platform != 'windows' }}
|
||||
working-directory: tmp/app
|
||||
run: |
|
||||
if [ '${{ matrix.platform }}' == 'macos' ]; then
|
||||
ln -s /Applications .
|
||||
hdiutil create -fs HFS+ -volname '${{ env.app_package_name }}' -srcfolder . '${{ env.package }}'
|
||||
else
|
||||
zip --recurse-paths '${{ env.package }}' '${{ env.app_package_name }}'
|
||||
fi
|
||||
- name: Package app (Windows)
|
||||
if: ${{ matrix.platform == 'windows' }}
|
||||
working-directory: tmp/app
|
||||
shell: pwsh
|
||||
run: Compress-Archive -Path '${{ env.app_package_name }}' -DestinationPath '${{ env.package }}'
|
||||
- name: Upload package to workflow artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
path: tmp/app/${{ env.package }}
|
||||
name: package-${{ matrix.platform }}
|
||||
retention-days: 1
|
||||
- name: Upload package to GitHub release
|
||||
if: ${{ env.upload_to_github == 'true' }}
|
||||
uses: svenstaro/upload-release-action@v2
|
||||
with:
|
||||
repo_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
file: tmp/app/${{ env.package }}
|
||||
asset_name: ${{ env.package }}
|
||||
release_name: ${{ env.version }}
|
||||
tag: ${{ env.version }}
|
||||
overwrite: true
|
||||
# Check if upload to itch.io is enabled.
|
||||
# This is needed because the `env` context can't be used in the `if:` condition of a job:
|
||||
# https://docs.github.com/en/actions/learn-github-actions/contexts#context-availability
|
||||
check-upload-to-itch:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Do nothing
|
||||
run: 'true'
|
||||
outputs:
|
||||
target: ${{ env.upload_to_itch }}
|
||||
# Upload all packages to itch.io.
|
||||
upload-to-itch:
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- get-version
|
||||
- check-upload-to-itch
|
||||
- build
|
||||
if: ${{ needs.check-upload-to-itch.outputs.target != '' }}
|
||||
steps:
|
||||
- name: Download all packages
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
pattern: package-*
|
||||
path: tmp
|
||||
- name: Install butler
|
||||
run: |
|
||||
curl -L -o butler.zip 'https://broth.itch.zone/butler/linux-amd64/LATEST/archive/default'
|
||||
unzip butler.zip
|
||||
chmod +x butler
|
||||
./butler -V
|
||||
- name: Upload all packages to itch.io
|
||||
env:
|
||||
BUTLER_API_KEY: ${{ secrets.BUTLER_CREDENTIALS }}
|
||||
run: |
|
||||
for channel in $(ls tmp); do
|
||||
./butler push \
|
||||
--fix-permissions \
|
||||
--userversion='${{ needs.get-version.outputs.version }}' \
|
||||
tmp/"${channel}"/* \
|
||||
'${{ env.upload_to_itch }}':"${channel#package-}"
|
||||
done
|
||||
4
.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
# Rust builds
|
||||
/target
|
||||
# This file contains environment-specific configuration like linker settings
|
||||
.cargo/config.toml
|
||||
4716
Cargo.lock
generated
Normal file
88
Cargo.toml
Normal file
@ -0,0 +1,88 @@
|
||||
[package]
|
||||
name = "the-labyrinth-of-echoes"
|
||||
authors = ["Kristofers Solo <dev@kristofers.xyz>"]
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
bevy = { version = "0.14", features = ["wayland"] }
|
||||
rand = "0.8"
|
||||
# Compile low-severity logs out of native builds for performance.
|
||||
log = { version = "0.4", features = [
|
||||
"max_level_debug",
|
||||
"release_max_level_warn",
|
||||
] }
|
||||
# Compile low-severity logs out of web builds for performance.
|
||||
tracing = { version = "0.1", features = [
|
||||
"max_level_debug",
|
||||
"release_max_level_warn",
|
||||
] }
|
||||
|
||||
[features]
|
||||
default = [
|
||||
# Default to a native dev build.
|
||||
"dev_native",
|
||||
]
|
||||
dev = [
|
||||
# Improve compile times for dev builds by linking Bevy as a dynamic library.
|
||||
"bevy/dynamic_linking",
|
||||
"bevy/bevy_dev_tools",
|
||||
]
|
||||
dev_native = [
|
||||
"dev",
|
||||
# Enable asset hot reloading for native dev builds.
|
||||
"bevy/file_watcher",
|
||||
# Enable embedded asset hot reloading for native dev builds.
|
||||
"bevy/embedded_watcher",
|
||||
]
|
||||
|
||||
|
||||
# Idiomatic Bevy code often triggers these lints, and the CI workflow treats them as errors.
|
||||
# In some cases they may still signal poor code quality however, so consider commenting out these lines.
|
||||
[lints.clippy]
|
||||
# Bevy supplies arguments to systems via dependency injection, so it's natural for systems to
|
||||
# request more than 7 arguments -- which triggers this lint.
|
||||
too_many_arguments = "allow"
|
||||
# Queries that access many components may trigger this lint.
|
||||
type_complexity = "allow"
|
||||
|
||||
|
||||
# Compile with Performance Optimizations:
|
||||
# https://bevyengine.org/learn/quick-start/getting-started/setup/#compile-with-performance-optimizations
|
||||
|
||||
# Enable a small amount of optimization in the dev profile.
|
||||
[profile.dev]
|
||||
opt-level = 1
|
||||
|
||||
# Enable a large amount of optimization in the dev profile for dependencies.
|
||||
[profile.dev.package."*"]
|
||||
opt-level = 3
|
||||
|
||||
# Remove expensive debug assertions due to <https://github.com/bevyengine/bevy/issues/14291>
|
||||
[profile.dev.package.wgpu-types]
|
||||
debug-assertions = false
|
||||
|
||||
# The default profile is optimized for Wasm builds because
|
||||
# that's what [Trunk reads](https://github.com/trunk-rs/trunk/issues/605).
|
||||
# Optimize for size in the wasm-release profile to reduce load times and bandwidth usage on web.
|
||||
[profile.release]
|
||||
# Compile the entire crate as one unit.
|
||||
# Slows compile times, marginal improvements.
|
||||
codegen-units = 1
|
||||
# Do a second optimization pass over the entire program, including dependencies.
|
||||
# Slows compile times, marginal improvements.
|
||||
lto = "thin"
|
||||
# Optimize with size in mind (also try "z", sometimes it is better).
|
||||
# Slightly slows compile times, great improvements to file size and runtime performance.
|
||||
opt-level = "s"
|
||||
# Strip all debugging information from the binary to slightly reduce file size.
|
||||
strip = "debuginfo"
|
||||
|
||||
# Override some settings for native builds.
|
||||
[profile.release-native]
|
||||
# Default to release profile values.
|
||||
inherits = "release"
|
||||
# Optimize with performance in mind.
|
||||
opt-level = 3
|
||||
# Keep debug information in the binary.
|
||||
strip = "none"
|
||||
131
QUICKSTART.md
Normal file
@ -0,0 +1,131 @@
|
||||
_Brought to you by the Bevy Jam working group._
|
||||
|
||||
# Bevy Quickstart
|
||||
|
||||
This template is a great way to get started on a new [Bevy](https://bevyengine.org/) game—especially for a game jam!
|
||||
Start with a [basic project structure](#write-your-game) and [CI / CD](#release-your-game) that can deploy to [itch.io](https://itch.io).
|
||||
You can [try this template in your web browser!](https://the-bevy-flock.itch.io/bevy-quickstart)
|
||||
|
||||
[@ChristopherBiscardi](https://github.com/ChristopherBiscardi) made a video on how to use this template from start to finish:
|
||||
|
||||
[<img src="./docs/img/thumbnail.png" width=40% height=40% alt="A video tutorial for bevy_quickstart"/>](https://www.youtube.com/watch?v=ESBRyXClaYc)
|
||||
|
||||
## Prerequisites
|
||||
|
||||
We assume that you know how to use Bevy already and have seen the [official Quick Start Guide](https://bevyengine.org/learn/quick-start/introduction/).
|
||||
|
||||
If you're new to Bevy, the patterns used in this template may look a bit weird at first glance.
|
||||
See our [Design Document](./docs/design.md) for more information on how we structured the code and why.
|
||||
|
||||
## Create a new game
|
||||
|
||||
Install [`cargo-generate`](https://github.com/cargo-generate/cargo-generate) and run the following command:
|
||||
|
||||
```sh
|
||||
cargo generate TheBevyFlock/bevy_quickstart --branch cargo-generate
|
||||
```
|
||||
|
||||
Then navigate to the newly generated directory and run the following commands:
|
||||
|
||||
```sh
|
||||
git branch --move main
|
||||
cargo update
|
||||
git commit -am 'Initial commit'
|
||||
```
|
||||
|
||||
Then [create a GitHub repository](https://github.com/new) and push your local repository to it.
|
||||
|
||||
<details>
|
||||
<summary>This template can also be set up manually.</summary>
|
||||
|
||||
Navigate to the top of [this GitHub repository](https://github.com/TheBevyFlock/bevy_quickstart/) and select `Use this template > Create a new repository`:
|
||||
|
||||

|
||||
|
||||
Clone your new Github repository to a local repository and push a commit with the following changes:
|
||||
|
||||
- Delete `LICENSE`, `README`, and `docs/` files.
|
||||
- Search for and replace instances of `bevy_quickstart` with the name of your project.
|
||||
- Adjust the `env` variables in [`.github/workflows/release.yaml`](./.github/workflows/release.yaml).
|
||||
|
||||
</details>
|
||||
|
||||
## Write your game
|
||||
|
||||
The best way to get started is to play around with what you find in [`src/demo/`](./src/demo).
|
||||
|
||||
This template comes with a basic project structure that you may find useful:
|
||||
|
||||
| Path | Description |
|
||||
| -------------------------------------------------- | ------------------------------------------------------------------ |
|
||||
| [`src/lib.rs`](./src/lib.rs) | App setup |
|
||||
| [`src/asset_tracking.rs`](./src/asset_tracking.rs) | A high-level way to load collections of asset handles as resources |
|
||||
| [`src/audio/`](./src/audio) | Marker components for sound effects and music |
|
||||
| [`src/demo/`](./src/demo) | Example game mechanics & content (replace with your own code) |
|
||||
| [`src/dev_tools.rs`](./src/dev_tools.rs) | Dev tools for dev builds (press \` aka backtick to toggle) |
|
||||
| [`src/screens/`](./src/screens) | Splash screen, title screen, gameplay screen, etc. |
|
||||
| [`src/theme/`](./src/theme) | Reusable UI widgets & theming |
|
||||
|
||||
Feel free to move things around however you want, though.
|
||||
|
||||
> [!Tip]
|
||||
> Be sure to check out the [3rd-party tools](./docs/tooling.md) we recommend!
|
||||
|
||||
## Run your game
|
||||
|
||||
Running your game locally is very simple:
|
||||
|
||||
- Use `cargo run` to run a native dev build.
|
||||
- Use [`trunk serve`](https://trunkrs.dev/) to run a web dev build.
|
||||
|
||||
If you're using [VS Code](https://code.visualstudio.com/), this template comes with a [`.vscode/tasks.json`](./.vscode/tasks.json) file.
|
||||
|
||||
<details>
|
||||
<summary>Run release builds</summary>
|
||||
|
||||
- Use `cargo run --profile release-native --no-default-features` to run a native release build.
|
||||
- Use `trunk serve --release --no-default-features` to run a web release build.
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Linux dependencies</summary>
|
||||
|
||||
If you are using Linux, make sure you take a look at Bevy's [Linux dependencies](https://github.com/bevyengine/bevy/blob/main/docs/linux_dependencies.md).
|
||||
Note that this template enables Wayland support, which requires additional dependencies as detailed in the link above.
|
||||
Wayland is activated by using the `bevy/wayland` feature in the [`Cargo.toml`](./Cargo.toml).
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>(Optional) Improve your compile times</summary>
|
||||
|
||||
[`.cargo/config_fast_builds.toml`](./.cargo/config_fast_builds.toml) contains documentation on how to set up your environment to improve compile times.
|
||||
After you've fiddled with it, rename it to `.cargo/config.toml` to enable it.
|
||||
|
||||
</details>
|
||||
|
||||
## Release your game
|
||||
|
||||
This template uses [GitHub workflows](https://docs.github.com/en/actions/using-workflows) to run tests and build releases.
|
||||
See [Workflows](./docs/workflows.md) for more information.
|
||||
|
||||
## Known Issues
|
||||
|
||||
There are some known issues in Bevy that require some arcane workarounds.
|
||||
To keep this template simple, we have opted not to include those workarounds.
|
||||
You can read about them in the [Known Issues](./docs/known-issues.md) document.
|
||||
|
||||
## License
|
||||
|
||||
The source code in this repository is licensed under any of the following at your option:
|
||||
|
||||
- [CC0-1.0 License](./LICENSE-CC0-1.0.txt)
|
||||
- [MIT License](./LICENSE-MIT.txt)
|
||||
- [Apache License, Version 2.0](./LICENSE-Apache-2.0.txt)
|
||||
|
||||
The CC0 license explicitly does not waive patent rights, but we confirm that we hold no patent rights to anything presented in this repository.
|
||||
|
||||
## Credits
|
||||
|
||||
The [assets](./assets) in this repository are all 3rd-party. See the [credits screen](./src/screens/credits.rs) for more information.
|
||||
12
README.md
Normal file
@ -0,0 +1,12 @@
|
||||
# Maze Ascension: The Labyrinth of Echoes
|
||||
|
||||
"Maze Ascension: The Labyrinth of Echoes" is a minimalist maze exploration
|
||||
game built using the Bevy engine. The game features simple visuals with
|
||||
hexagonal tiles forming the maze structure on a white background with black
|
||||
borders, and a stickman-style player character. Players navigate through
|
||||
multiple levels of increasing difficulty, progressing vertically as they
|
||||
climb up through levels. The game includes power-ups and abilities hidden
|
||||
throughout the maze, and later introduces the ability to move between levels
|
||||
freely. This unique blend of puzzle-solving, exploration, and vertical
|
||||
progression offers a fresh twist on traditional maze gameplay, presented in
|
||||
an accessible and clean visual style.
|
||||
11
README.norg
Normal file
@ -0,0 +1,11 @@
|
||||
* Maze Ascension: The Labyrinth of Echoes
|
||||
"Maze Ascension: The Labyrinth of Echoes" is a minimalist maze exploration
|
||||
game built using the Bevy engine. The game features simple visuals with
|
||||
hexagonal tiles forming the maze structure on a white background with black
|
||||
borders, and a stickman-style player character. Players navigate through
|
||||
multiple levels of increasing difficulty, progressing vertically as they
|
||||
climb up through levels. The game includes power-ups and abilities hidden
|
||||
throughout the maze, and later introduces the ability to move between levels
|
||||
freely. This unique blend of puzzle-solving, exploration, and vertical
|
||||
progression offers a fresh twist on traditional maze gameplay, presented in
|
||||
an accessible and clean visual style.
|
||||
13
Trunk.toml
Normal file
@ -0,0 +1,13 @@
|
||||
[build]
|
||||
# Point to our `index.html`.
|
||||
target = "web/index.html"
|
||||
# Set the output directory for the web build.
|
||||
dist = "target/trunk"
|
||||
# This is needed in order to host the game on itch.io.
|
||||
public_url = "./"
|
||||
|
||||
[serve]
|
||||
# Required in order to receive 404s for missing assets, which is what Bevy expects.
|
||||
no_spa = true
|
||||
# Open a browser tab once the initial build is complete.
|
||||
open = true
|
||||
BIN
assets/audio/music/Fluffing A Duck.ogg
Normal file
BIN
assets/audio/music/Monkeys Spinning Monkeys.ogg
Normal file
BIN
assets/audio/sound_effects/button_hover.ogg
Normal file
BIN
assets/audio/sound_effects/button_press.ogg
Normal file
BIN
assets/audio/sound_effects/step1.ogg
Normal file
BIN
assets/audio/sound_effects/step2.ogg
Normal file
BIN
assets/audio/sound_effects/step3.ogg
Normal file
BIN
assets/audio/sound_effects/step4.ogg
Normal file
BIN
assets/images/ducky.png
Normal file
|
After Width: | Height: | Size: 956 B |
BIN
assets/images/splash.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
354
docs/design.md
Normal file
@ -0,0 +1,354 @@
|
||||
# Design philosophy
|
||||
|
||||
The high-level goal of this template is to feel like the official template that is currently missing from Bevy.
|
||||
The exists an [official CI template](https://github.com/bevyengine/bevy_github_ci_template), but, in our opinion,
|
||||
that one is currently more of an extension to the [Bevy examples](https://bevyengine.org/examples/) than an actual template.
|
||||
We say this because it is extremely bare-bones and as such does not provide things that in practice are necessary for game development.
|
||||
|
||||
## Principles
|
||||
|
||||
So, how would an official template that is built for real-world game development look like?
|
||||
The Bevy Jam working group has agreed on the following guiding design principles:
|
||||
|
||||
- Show how to do things in pure Bevy. This means using no 3rd-party dependencies.
|
||||
- Have some basic game code written out already.
|
||||
- Have everything outside of code already set up.
|
||||
- Nice IDE support.
|
||||
- `cargo-generate` support.
|
||||
- Workflows that provide CI and CD with an auto-publish to itch.io.
|
||||
- Builds configured for performance by default.
|
||||
- Answer questions that will quickly come up when creating an actual game.
|
||||
- How do I structure my code?
|
||||
- How do I preload assets?
|
||||
- What are best practices for creating UI?
|
||||
- etc.
|
||||
|
||||
The last point means that in order to make this template useful for real-life projects,
|
||||
we have to make some decisions that are necessarily opinionated.
|
||||
|
||||
These opinions are based on the experience of the Bevy Jam working group and
|
||||
what we have found to be useful in our own projects.
|
||||
If you disagree with any of these, it should be easy to change them.
|
||||
|
||||
Bevy is still young, and many design patterns are still being discovered and refined.
|
||||
Most do not even have an agreed name yet. For some prior work in this area that inspired us,
|
||||
see [the Unofficial Bevy Cheatbook](https://bevy-cheatbook.github.io/) and [bevy_best_practices](https://github.com/tbillington/bevy_best_practices).
|
||||
|
||||
## Pattern Table of Contents
|
||||
|
||||
- [Plugin Organization](#plugin-organization)
|
||||
- [Widgets](#widgets)
|
||||
- [Asset Preloading](#asset-preloading)
|
||||
- [Spawn Commands](#spawn-commands)
|
||||
- [Interaction Callbacks](#interaction-callbacks)
|
||||
- [Dev Tools](#dev-tools)
|
||||
- [Screen States](#screen-states)
|
||||
|
||||
When talking about these, use their name followed by "pattern",
|
||||
e.g. "the widgets pattern", or "the plugin organization pattern".
|
||||
|
||||
## Plugin Organization
|
||||
|
||||
### Pattern
|
||||
|
||||
Structure your code into plugins like so:
|
||||
|
||||
```rust
|
||||
// game.rs
|
||||
mod player;
|
||||
mod enemy;
|
||||
mod powerup;
|
||||
|
||||
use bevy::prelude::*;
|
||||
|
||||
pub(super) fn plugin(app: &mut App) {
|
||||
app.add_plugins((player::plugin, enemy::plugin, powerup::plugin));
|
||||
}
|
||||
```
|
||||
|
||||
```rust
|
||||
// player.rs / enemy.rs / powerup.rs
|
||||
use bevy::prelude::*;
|
||||
|
||||
pub(super) fn plugin(app: &mut App) {
|
||||
app.add_systems(Update, (your, systems, here));
|
||||
}
|
||||
```
|
||||
|
||||
### Reasoning
|
||||
|
||||
Bevy is great at organizing code into plugins. The most lightweight way to do this is by using simple functions as plugins.
|
||||
By splitting your code like this, you can easily keep all your systems and resources locally grouped. Everything that belongs to the `player` is only in `player.rs`, and so on.
|
||||
|
||||
A good rule of thumb is to have one plugin per file,
|
||||
but feel free to leave out a plugin if your file does not need to do anything with the `App`.
|
||||
|
||||
## Widgets
|
||||
|
||||
### Pattern
|
||||
|
||||
Spawn your UI elements by extending the [`Widgets` trait](../src/theme/widgets.rs):
|
||||
|
||||
```rust
|
||||
pub trait Widgets {
|
||||
fn button(&mut self, text: impl Into<String>) -> EntityCommands;
|
||||
fn header(&mut self, text: impl Into<String>) -> EntityCommands;
|
||||
fn label(&mut self, text: impl Into<String>) -> EntityCommands;
|
||||
fn text_input(&mut self, text: impl Into<String>) -> EntityCommands;
|
||||
fn image(&mut self, texture: Handle<Texture>) -> EntityCommands;
|
||||
fn progress_bar(&mut self, progress: f32) -> EntityCommands;
|
||||
}
|
||||
```
|
||||
|
||||
### Reasoning
|
||||
|
||||
This pattern is inspired by [sickle_ui](https://github.com/UmbraLuminosa/sickle_ui).
|
||||
`Widgets` is implemented for `Commands` and similar, so you can easily spawn UI elements in your systems.
|
||||
By encapsulating a widget inside a function, you save on a lot of boilerplate code and can easily change the appearance of all widgets of a certain type.
|
||||
By returning `EntityCommands`, you can easily chain multiple widgets together and insert children into a parent widget.
|
||||
|
||||
## Asset Preloading
|
||||
|
||||
### Pattern
|
||||
|
||||
Define your assets with a resource that maps asset paths to `Handle`s.
|
||||
If you're defining the assets in code, add their paths as constants.
|
||||
Otherwise, load them dynamically from e.g. a file.
|
||||
|
||||
```rust
|
||||
#[derive(Resource, Debug, Deref, DerefMut, Reflect)]
|
||||
#[reflect(Resource)]
|
||||
pub struct ImageHandles(HashMap<String, Handle<Image>>);
|
||||
|
||||
impl ImageHandles {
|
||||
pub const PATH_PLAYER: &'static str = "images/player.png";
|
||||
pub const PATH_ENEMY: &'static str = "images/enemy.png";
|
||||
pub const PATH_POWERUP: &'static str = "images/powerup.png";
|
||||
}
|
||||
|
||||
impl FromWorld for ImageHandles {
|
||||
fn from_world(world: &mut World) -> Self {
|
||||
let asset_server = world.resource::<AssetServer>();
|
||||
|
||||
let paths = [
|
||||
ImageHandles::PATH_PLAYER,
|
||||
ImageHandles::PATH_ENEMY,
|
||||
ImageHandles::PATH_POWERUP,
|
||||
];
|
||||
let map = paths
|
||||
.into_iter()
|
||||
.map(|path| (path.to_string(), asset_server.load(path)))
|
||||
.collect();
|
||||
|
||||
Self(map)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Then start preloading in the `assets::plugin`:
|
||||
|
||||
```rust
|
||||
pub(super) fn plugin(app: &mut App) {
|
||||
app.register_type::<ImageHandles>();
|
||||
app.init_resource::<ImageHandles>();
|
||||
}
|
||||
```
|
||||
|
||||
And finally add a loading check to the `screens::loading::plugin`:
|
||||
|
||||
```rust
|
||||
fn all_assets_loaded(
|
||||
image_handles: Res<ImageHandles>,
|
||||
) -> bool {
|
||||
image_handles.all_loaded(&asset_server)
|
||||
}
|
||||
```
|
||||
|
||||
### Reasoning
|
||||
|
||||
This pattern is inspired by [bevy_asset_loader](https://github.com/NiklasEi/bevy_asset_loader).
|
||||
By preloading your assets, you can avoid hitches during gameplay.
|
||||
We start loading as soon as the app starts and wait for all assets to be loaded in the loading screen.
|
||||
|
||||
By using strings as keys, you can dynamically load assets based on input data such as a level file.
|
||||
If you prefer a purely static approach, you can also use an `enum YourAssetHandleKey` and `impl AsRef<str> for YourAssetHandleKey`.
|
||||
You can also mix the dynamic and static approach according to your needs.
|
||||
|
||||
## Spawn Commands
|
||||
|
||||
### Pattern
|
||||
|
||||
Spawn a game object by using a custom command. Inside the command,
|
||||
run the spawning code with `world.run_system_once` or `world.run_system_once_with`:
|
||||
|
||||
```rust
|
||||
// monster.rs
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct SpawnMonster {
|
||||
pub health: u32,
|
||||
pub transform: Transform,
|
||||
}
|
||||
|
||||
impl Command for SpawnMonster {
|
||||
fn apply(self, world: &mut World) {
|
||||
world.run_system_once_with(self, spawn_monster);
|
||||
}
|
||||
}
|
||||
|
||||
fn spawn_monster(
|
||||
spawn_monster: In<SpawnMonster>,
|
||||
mut commands: Commands,
|
||||
) {
|
||||
commands.spawn((
|
||||
Name::new("Monster"),
|
||||
Health::new(spawn_monster.health),
|
||||
SpatialBundle::from_transform(spawn_monster.transform),
|
||||
// other components
|
||||
));
|
||||
}
|
||||
```
|
||||
|
||||
And then to use a spawn command, add it to `Commands`:
|
||||
|
||||
```rust
|
||||
// dangerous_forest.rs
|
||||
|
||||
fn spawn_forest_goblin(mut commands: Commands) {
|
||||
commands.add(SpawnMonster {
|
||||
health: 100,
|
||||
transform: Transform::from_xyz(10.0, 0.0, 0.0),
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### Reasoning
|
||||
|
||||
By encapsulating the spawning of a game object in a custom command,
|
||||
you save on boilerplate code and can easily change the behavior of spawning.
|
||||
We use `world.run_system_once_with` to run the spawning code with the same syntax as a regular system.
|
||||
That way you can easily add system parameters to access things like assets and resources while spawning the entity.
|
||||
|
||||
A limitation of this approach is that calling code cannot extend the spawn call with additional components or children,
|
||||
as custom commands don't return `Entity` or `EntityCommands`. This kind of usage will be possible in future Bevy versions.
|
||||
|
||||
## Interaction Callbacks
|
||||
|
||||
### Pattern
|
||||
|
||||
When spawning an entity that can be interacted with, such as a button that can be pressed,
|
||||
use an observer to handle the interaction:
|
||||
|
||||
```rust
|
||||
fn spawn_button(mut commands: Commands) {
|
||||
// See the Widgets pattern for information on the `button` method
|
||||
commands.button("Pay up!").observe(pay_money);
|
||||
}
|
||||
|
||||
fn pay_money(_trigger: Trigger<OnPress>, mut money: ResMut<Money>) {
|
||||
money.0 -= 10.0;
|
||||
}
|
||||
```
|
||||
|
||||
The event `OnPress`, which is [defined in this template](../src/theme/interaction.rs),
|
||||
is triggered when the button is [`Interaction::Pressed`](https://docs.rs/bevy/latest/bevy/prelude/enum.Interaction.html#variant.Pressed).
|
||||
|
||||
If you have many interactions that only change a state, consider using the following helper function:
|
||||
|
||||
```rust
|
||||
fn spawn_button(mut commands: Commands) {
|
||||
commands.button("Play the game").observe(enter_state(Screen::Gameplay));
|
||||
}
|
||||
|
||||
fn enter_state<S: FreelyMutableState>(
|
||||
new_state: S,
|
||||
) -> impl Fn(Trigger<OnPress>, ResMut<NextState<S>>) {
|
||||
move |_trigger, mut next_state| next_state.set(new_state.clone())
|
||||
}
|
||||
```
|
||||
|
||||
### Reasoning
|
||||
|
||||
This pattern is inspired by [bevy_mod_picking](https://github.com/aevyrie/bevy_mod_picking).
|
||||
By pairing the system handling the interaction with the entity as an observer,
|
||||
the code running on interactions can be scoped to the exact context of the interaction.
|
||||
|
||||
For example, the code for what happens when you press a *specific* button is directly attached to that exact button.
|
||||
|
||||
This also keeps the interaction logic close to the entity that is interacted with,
|
||||
allowing for better code organization.
|
||||
|
||||
## Dev Tools
|
||||
|
||||
### Pattern
|
||||
|
||||
Add all systems that are only relevant while developing the game to the [`dev_tools` plugin](../src/dev_tools.rs):
|
||||
|
||||
```rust
|
||||
// dev_tools.rs
|
||||
pub(super) fn plugin(app: &mut App) {
|
||||
app.add_systems(Update, (draw_debug_lines, show_debug_console, show_fps_counter));
|
||||
}
|
||||
```
|
||||
|
||||
### Reasoning
|
||||
|
||||
The `dev_tools` plugin is only included in dev builds.
|
||||
By adding your dev tools here, you automatically guarantee that they are not included in release builds.
|
||||
|
||||
## Screen States
|
||||
|
||||
### Pattern
|
||||
|
||||
Use the [`Screen`](../src/screen/mod.rs) enum to represent your game's screens as states:
|
||||
|
||||
```rust
|
||||
#[derive(States, Debug, Hash, PartialEq, Eq, Clone, Default)]
|
||||
pub enum Screen {
|
||||
#[default]
|
||||
Splash,
|
||||
Loading,
|
||||
Title,
|
||||
Credits,
|
||||
Gameplay,
|
||||
Victory,
|
||||
Leaderboard,
|
||||
MultiplayerLobby,
|
||||
SecretMinigame,
|
||||
}
|
||||
```
|
||||
|
||||
Constrain entities that should only be present in a certain screen to that screen by adding a
|
||||
[`StateScoped`](https://docs.rs/bevy/latest/bevy/prelude/struct.StateScoped.html) component to them.
|
||||
Transition between screens by setting the [`NextState<Screen>`](https://docs.rs/bevy/latest/bevy/prelude/enum.NextState.html) resource.
|
||||
|
||||
For each screen, create a plugin that handles the setup and teardown of the screen with `OnEnter` and `OnExit`:
|
||||
|
||||
```rust
|
||||
// game_over.rs
|
||||
pub(super) fn plugin(app: &mut App) {
|
||||
app.add_systems(OnEnter(Screen::Victory), show_victory_screen);
|
||||
app.add_systems(OnExit(Screen::Victory), reset_highscore);
|
||||
}
|
||||
|
||||
fn show_victory_screen(mut commands: Commands) {
|
||||
commands.
|
||||
.ui_root()
|
||||
.insert((Name::new("Victory screen"), StateScoped(Screen::Victory)))
|
||||
.with_children(|parent| {
|
||||
// Spawn UI elements.
|
||||
});
|
||||
}
|
||||
|
||||
fn reset_highscore(mut highscore: ResMut<Highscore>) {
|
||||
*highscore = default();
|
||||
}
|
||||
```
|
||||
|
||||
### Reasoning
|
||||
|
||||
"Screen" is not meant as a physical screen, but as "what kind of screen is the game showing right now", e.g. the title screen, the loading screen, the credits screen, the victory screen, etc.
|
||||
These screens usually correspond to different logical states of your game that have different systems running.
|
||||
|
||||
By using dedicated `State`s for each screen, you can easily manage systems and entities that are only relevant for a certain screen.
|
||||
This allows you to flexibly transition between screens whenever your game logic requires it.
|
||||
BIN
docs/img/readme-manual-setup.png
Normal file
|
After Width: | Height: | Size: 21 KiB |
BIN
docs/img/thumbnail.png
Normal file
|
After Width: | Height: | Size: 23 KiB |
BIN
docs/img/workflow-dispatch-release.png
Normal file
|
After Width: | Height: | Size: 51 KiB |
BIN
docs/img/workflow-itch-release.png
Normal file
|
After Width: | Height: | Size: 6.0 KiB |
BIN
docs/img/workflow-ruleset.png
Normal file
|
After Width: | Height: | Size: 36 KiB |
BIN
docs/img/workflow-secrets.png
Normal file
|
After Width: | Height: | Size: 73 KiB |
27
docs/known-issues.md
Normal file
@ -0,0 +1,27 @@
|
||||
# Known Issues
|
||||
|
||||
## My audio is stuttering on web
|
||||
|
||||
There are a number of issues with audio on web, so this is not an exhaustive list. The short version is that you can try the following:
|
||||
|
||||
- If you use materials, make sure to force render pipelines to [load at the start of the game](https://github.com/rparrett/bevy_pipelines_ready/blob/main/src/lib.rs).
|
||||
- Keep the FPS high.
|
||||
- Advise your users to play on Chromium-based browsers.
|
||||
- Apply the suggestions from the blog post [Workaround for the Choppy Music in Bevy Web Builds](https://necrashter.github.io/bevy-choppy-music-workaround).
|
||||
|
||||
## My game window is flashing white for a split second when I start the game on native
|
||||
|
||||
The game window is created before the GPU is ready to render everything.
|
||||
This means that it will start with a white screen for a little bit.
|
||||
The workaround is to [spawn the Window hidden](https://github.com/bevyengine/bevy/blob/release-0.14.0/examples/window/window_settings.rs#L29-L32)
|
||||
and then [make it visible after a few frames](https://github.com/bevyengine/bevy/blob/release-0.14.0/examples/window/window_settings.rs#L56-L64).
|
||||
|
||||
## My character or camera is not moving smoothly
|
||||
|
||||
Choppy movement is often caused by movement updates being tied to the frame rate.
|
||||
See the [physics_in_fixed_timestep](https://github.com/bevyengine/bevy/blob/main/examples/movement/physics_in_fixed_timestep.rs) example
|
||||
for how to fix this.
|
||||
|
||||
A camera not moving smoothly is pretty much always caused by the camera position being tied too tightly to the character's position.
|
||||
To give the camera some inertia, use the [`smooth_nudge`](https://github.com/bevyengine/bevy/blob/main/examples/movement/smooth_follow.rs#L127-L142)
|
||||
to interpolate the camera position towards its target position.
|
||||
64
docs/tooling.md
Normal file
@ -0,0 +1,64 @@
|
||||
# Recommended 3rd-party tools
|
||||
|
||||
Check out the [Bevy Assets](https://bevyengine.org/assets/) page for more great options.
|
||||
|
||||
## Libraries
|
||||
|
||||
A few libraries that the authors of this template have vetted and think you might find useful:
|
||||
|
||||
| Name | Category | Description |
|
||||
| -------------------------------------------------------------------------------------- | -------------- | ------------------------------------- |
|
||||
| [`leafwing-input-manager`](https://github.com/Leafwing-Studios/leafwing-input-manager) | Input | Input -> Action mapping |
|
||||
| [`bevy_mod_picking`](https://github.com/aevyrie/bevy_mod_picking) | Input | Advanced mouse interaction |
|
||||
| [`bevy-inspector-egui`](https://github.com/jakobhellermann/bevy-inspector-egui) | Debugging | Live entity inspector |
|
||||
| [`bevy_mod_debugdump`](https://github.com/jakobhellermann/bevy_mod_debugdump) | Debugging | Schedule inspector |
|
||||
| [`avian`](https://github.com/Jondolf/avian) | Physics | Physics engine |
|
||||
| [`bevy_rapier`](https://github.com/dimforge/bevy_rapier) | Physics | Physics engine (not ECS-driven) |
|
||||
| [`bevy_common_assets`](https://github.com/NiklasEi/bevy_common_assets) | Asset loading | Asset loaders for common file formats |
|
||||
| [`bevy_asset_loader`](https://github.com/NiklasEi/bevy_asset_loader) | Asset loading | Asset management tools |
|
||||
| [`iyes_progress`](https://github.com/IyesGames/iyes_progress) | Asset loading | Progress tracking |
|
||||
| [`bevy_kira_audio`](https://github.com/NiklasEi/bevy_kira_audio) | Audio | Advanced audio |
|
||||
| [`sickle_ui`](https://github.com/UmbraLuminosa/sickle_ui) | UI | UI widgets |
|
||||
| [`bevy_egui`](https://github.com/mvlabat/bevy_egui) | UI / Debugging | UI framework (great for debug UI) |
|
||||
| [`tiny_bail`](https://github.com/benfrankel/tiny_bail) | Error handling | Error handling macros |
|
||||
|
||||
In particular:
|
||||
|
||||
- `leafwing-input-manager` and `bevy_mod_picking` are very likely to be upstreamed into Bevy in the near future.
|
||||
- `bevy-inspector-egui` and `bevy_mod_debugdump` help fill the gap until Bevy has its own editor.
|
||||
- `avian` or `bevy_rapier` helps fill the gap until Bevy has its own physics engine. `avian` is easier to use, while `bevy_rapier` is more performant.
|
||||
- `sickle_ui` is well-aligned with `bevy_ui` and helps fill the gap until Bevy has a full collection of UI widgets.
|
||||
|
||||
None of these are necessary, but they can save you a lot of time and effort.
|
||||
|
||||
## VS Code extensions
|
||||
|
||||
If you're using [VS Code](https://code.visualstudio.com/), the following extensions are highly recommended:
|
||||
|
||||
| Name | Description |
|
||||
|-----------------------------------------------------------------------------------------------------------|-----------------------------------|
|
||||
| [rust-analyzer](https://marketplace.visualstudio.com/items?itemName=rust-lang.rust-analyzer) | Rust support |
|
||||
| [Even Better TOML](https://marketplace.visualstudio.com/items?itemName=tamasfe.even-better-toml) | TOML support |
|
||||
| [vscode-ron](https://marketplace.visualstudio.com/items?itemName=a5huynh.vscode-ron) | RON support |
|
||||
| [Dependi](https://marketplace.visualstudio.com/items?itemName=fill-labs.dependi) | `crates.io` dependency resolution |
|
||||
| [EditorConfig for VS Code](https://marketplace.visualstudio.com/items?itemName=EditorConfig.EditorConfig) | `.editorconfig` support |
|
||||
|
||||
> [!Note]
|
||||
> <details>
|
||||
> <summary>About the included rust-analyzer settings</summary>
|
||||
>
|
||||
> This template sets [`rust-analyzer.cargo.targetDir`](https://rust-analyzer.github.io/generated_config.html#rust-analyzer.cargo.targetDir)
|
||||
> to `true` in [`.vscode/settings.json`](../.vscode/settings.json).
|
||||
>
|
||||
> This makes `rust-analyzer` use a different `target` directory than `cargo`,
|
||||
> which means that you can run commands like `cargo run` even while `rust-analyzer` is still indexing.
|
||||
> As a trade-off, this will use more disk space.
|
||||
>
|
||||
> If that is an issue for you, you can set it to `false` or remove the setting entirely.
|
||||
> </details>
|
||||
|
||||
## Other templates
|
||||
|
||||
There are many other Bevy templates out there.
|
||||
Check out the [templates category](https://bevyengine.org/assets/#templates) on Bevy Assets for more options.
|
||||
Even if you don't end up using them, they are a great way to learn how to implement certain features you might be interested in.
|
||||
125
docs/workflows.md
Normal file
@ -0,0 +1,125 @@
|
||||
# Workflows
|
||||
|
||||
This template uses [GitHub workflows](https://docs.github.com/en/actions/using-workflows) for [CI / CD](https://www.redhat.com/en/topics/devops/what-is-ci-cd), defined in [`.github/workflows/`](../.github/workflows).
|
||||
|
||||
## CI (testing)
|
||||
|
||||
The [CI workflow](.github/workflows/ci.yaml) will trigger on every commit or PR to `main`, and do the following:
|
||||
|
||||
- Run tests.
|
||||
- Run Clippy lints.
|
||||
- Check formatting.
|
||||
- Check documentation.
|
||||
|
||||
> [!Tip]
|
||||
> <details>
|
||||
> <summary>You may want to set up a <a href="https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/managing-rulesets/about-rulesets">GitHub ruleset</a> to require that all commits to <code>main</code> pass CI.</summary>
|
||||
>
|
||||
> <img src="img/workflow-ruleset.png" alt="A screenshot showing a GitHub ruleset with status checks enabled" width="100%">
|
||||
> </details>
|
||||
|
||||
## CD (releasing)
|
||||
|
||||
The [CD workflow](../.github/workflows/release.yaml) will trigger on every pushed tag in the format `v1.2.3`, and do the following:
|
||||
|
||||
- Create a release build for Windows, macOS, Linux, and web.
|
||||
- (Optional) Upload to [GitHub releases](https://docs.github.com/en/repositories/releasing-projects-on-github).
|
||||
- (Optional) Upload to [itch.io](https://itch.io).
|
||||
|
||||
<details>
|
||||
<summary>This workflow can also be triggered manually.</summary>
|
||||
|
||||
In your GitHub repository, navigate to `Actions > Release > Run workflow`:
|
||||
|
||||

|
||||
|
||||
Enter a version number in the format `v1.2.3`, then hit the green `Run workflow` button.
|
||||
</details>
|
||||
|
||||
> [!Important]
|
||||
> Using this workflow requires some setup. We will go through this now.
|
||||
|
||||
### Configure environment variables
|
||||
|
||||
The release workflow can be configured by tweaking the environment variables in [`.github/workflows/release.yaml`](../.github/workflows/release.yaml).
|
||||
|
||||
<details>
|
||||
<summary>Click here for a list of variables and how they're used.</summary>
|
||||
|
||||
```yaml
|
||||
# The base filename of the binary produced by `cargo build`.
|
||||
cargo_build_binary_name: bevy_quickstart
|
||||
|
||||
# The path to the assets directory.
|
||||
assets_path: assets
|
||||
|
||||
# Whether to upload the packages produced by this workflow to a GitHub release.
|
||||
upload_to_github: true
|
||||
|
||||
# The itch.io project to upload to in the format `user-name/project-name`.
|
||||
# There will be no upload to itch.io if this is commented out.
|
||||
upload_to_itch: the-bevy-flock/bevy-quickstart
|
||||
|
||||
############
|
||||
# ADVANCED #
|
||||
############
|
||||
|
||||
# The ID of the app produced by this workflow.
|
||||
# Applies to macOS releases.
|
||||
# Must contain only A-Z, a-z, 0-9, hyphens, and periods: https://developer.apple.com/documentation/bundleresources/information_property_list/cfbundleidentifier
|
||||
app_id: the-bevy-flock.bevy-quickstart
|
||||
|
||||
# The base filename of the binary in the package produced by this workflow.
|
||||
# Applies to Windows, macOS, and Linux releases.
|
||||
# Defaults to `cargo_build_binary_name` if commented out.
|
||||
app_binary_name: bevy_quickstart
|
||||
|
||||
# The name of the `.zip` or `.dmg` file produced by this workflow.
|
||||
# Defaults to `app_binary_name` if commented out.
|
||||
app_package_name: bevy_quickstart
|
||||
|
||||
# The display name of the app produced by this workflow.
|
||||
# Applies to macOS releases.
|
||||
# Defaults to `app_package_name` if commented out.
|
||||
app_display_name: Bevy Quickstart
|
||||
|
||||
# The short display name of the app produced by this workflow.
|
||||
# Applies to macOS releases.
|
||||
# Must be 15 or fewer characters: https://developer.apple.com/documentation/bundleresources/information_property_list/cfbundlename
|
||||
# Defaults to `app_display_name` if commented out.
|
||||
app_short_name: Bevy Quickstart
|
||||
|
||||
# Before enabling LFS, please take a look at GitHub's documentation for costs and quota limits:
|
||||
# https://docs.github.com/en/repositories/working-with-files/managing-large-files/about-storage-and-bandwidth-usage
|
||||
git_lfs: false
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
The values are set automatically by `cargo generate`, or you can edit them yourself and push a commit.
|
||||
|
||||
### Set up itch.io upload
|
||||
|
||||
#### Add butler credentials
|
||||
|
||||
<details>
|
||||
<summary>In your GitHub repository, navigate to <code>Settings > Secrets and variables > Actions</code>.</summary>
|
||||
|
||||

|
||||
</details>
|
||||
|
||||
Hit `New repository secret` and enter the following values, then hit `Add secret`:
|
||||
|
||||
- **Name:** `BUTLER_CREDENTIALS`
|
||||
- **Secret:** Your [itch.io API key](https://itch.io/user/settings/api-keys) (create a new one if necessary)
|
||||
|
||||
#### Create itch.io project
|
||||
|
||||
Create a new itch.io project with the same user and project name as in the `upload_to_itch` variable in [`.github/workflows/release.yaml`](../.github/workflows/release.yaml).
|
||||
Hit `Save & view page` at the bottom of the page.
|
||||
|
||||
[Trigger the release workflow](#cd-releasing) for the first time. Once it's done, go back to itch.io and hit `Edit game` in the top left.
|
||||
|
||||
Set `Kind of project` to `HTML`, then find the newly uploaded `web` build and tick the box that says "This file will be played in the browser".
|
||||
|
||||

|
||||
64
src/asset_tracking.rs
Normal file
@ -0,0 +1,64 @@
|
||||
//! A high-level way to load collections of asset handles as resources.
|
||||
|
||||
use std::collections::VecDeque;
|
||||
|
||||
use bevy::prelude::*;
|
||||
|
||||
pub(super) fn plugin(app: &mut App) {
|
||||
app.init_resource::<ResourceHandles>();
|
||||
app.add_systems(PreUpdate, load_resource_assets);
|
||||
}
|
||||
|
||||
pub trait LoadResource {
|
||||
/// This will load the [`Resource`] as an [`Asset`]. When all of its asset dependencies
|
||||
/// have been loaded, it will be inserted as a resource. This ensures that the resource only
|
||||
/// exists when the assets are ready.
|
||||
fn load_resource<T: Resource + Asset + Clone + FromWorld>(&mut self) -> &mut Self;
|
||||
}
|
||||
|
||||
impl LoadResource for App {
|
||||
fn load_resource<T: Resource + Asset + Clone + FromWorld>(&mut self) -> &mut Self {
|
||||
self.init_asset::<T>();
|
||||
let world = self.world_mut();
|
||||
let value = T::from_world(world);
|
||||
let assets = world.resource::<AssetServer>();
|
||||
let handle = assets.add(value);
|
||||
let mut handles = world.resource_mut::<ResourceHandles>();
|
||||
handles
|
||||
.waiting
|
||||
.push_back((handle.untyped(), |world, handle| {
|
||||
let assets = world.resource::<Assets<T>>();
|
||||
if let Some(value) = assets.get(handle.id().typed::<T>()) {
|
||||
world.insert_resource(value.clone());
|
||||
}
|
||||
}));
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// A function that inserts a loaded resource.
|
||||
type InsertLoadedResource = fn(&mut World, &UntypedHandle);
|
||||
|
||||
#[derive(Resource, Default)]
|
||||
struct ResourceHandles {
|
||||
// Use a queue for waiting assets so they can be cycled through and moved to
|
||||
// `finished` one at a time.
|
||||
waiting: VecDeque<(UntypedHandle, InsertLoadedResource)>,
|
||||
finished: Vec<UntypedHandle>,
|
||||
}
|
||||
|
||||
fn load_resource_assets(world: &mut World) {
|
||||
world.resource_scope(|world, mut resource_handles: Mut<ResourceHandles>| {
|
||||
world.resource_scope(|world, assets: Mut<AssetServer>| {
|
||||
for _ in 0..resource_handles.waiting.len() {
|
||||
let (handle, insert_fn) = resource_handles.waiting.pop_front().unwrap();
|
||||
if assets.is_loaded_with_dependencies(&handle) {
|
||||
insert_fn(world, &handle);
|
||||
resource_handles.finished.push(handle);
|
||||
} else {
|
||||
resource_handles.waiting.push_back((handle, insert_fn));
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
37
src/audio.rs
Normal file
@ -0,0 +1,37 @@
|
||||
use bevy::prelude::*;
|
||||
|
||||
/// An organizational marker component that should be added to a spawned [`AudioBundle`] if it is in the
|
||||
/// general "music" category (ex: global background music, soundtrack, etc).
|
||||
///
|
||||
/// This can then be used to query for and operate on sounds in that category. For example:
|
||||
///
|
||||
/// ```
|
||||
/// use bevy::prelude::*;
|
||||
/// use the-labyrinth-of-echoes::audio::Music;
|
||||
///
|
||||
/// fn set_music_volume(sink_query: Query<&AudioSink, With<Music>>) {
|
||||
/// for sink in &sink_query {
|
||||
/// sink.set_volume(0.5);
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
#[derive(Component, Default)]
|
||||
pub struct Music;
|
||||
|
||||
/// An organizational marker component that should be added to a spawned [`AudioBundle`] if it is in the
|
||||
/// general "sound effect" category (ex: footsteps, the sound of a magic spell, a door opening).
|
||||
///
|
||||
/// This can then be used to query for and operate on sounds in that category. For example:
|
||||
///
|
||||
/// ```
|
||||
/// use bevy::prelude::*;
|
||||
/// use the-labyrinth-of-echoes::audio::SoundEffect;
|
||||
///
|
||||
/// fn set_sound_effect_volume(sink_query: Query<&AudioSink, With<SoundEffect>>) {
|
||||
/// for sink in &sink_query {
|
||||
/// sink.set_volume(0.5);
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
#[derive(Component, Default)]
|
||||
pub struct SoundEffect;
|
||||
177
src/demo/animation.rs
Normal file
@ -0,0 +1,177 @@
|
||||
//! Player sprite animation.
|
||||
//! This is based on multiple examples and may be very different for your game.
|
||||
//! - [Sprite flipping](https://github.com/bevyengine/bevy/blob/latest/examples/2d/sprite_flipping.rs)
|
||||
//! - [Sprite animation](https://github.com/bevyengine/bevy/blob/latest/examples/2d/sprite_animation.rs)
|
||||
//! - [Timers](https://github.com/bevyengine/bevy/blob/latest/examples/time/timers.rs)
|
||||
|
||||
use bevy::prelude::*;
|
||||
use rand::prelude::*;
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::{
|
||||
audio::SoundEffect,
|
||||
demo::{movement::MovementController, player::PlayerAssets},
|
||||
AppSet,
|
||||
};
|
||||
|
||||
pub(super) fn plugin(app: &mut App) {
|
||||
// Animate and play sound effects based on controls.
|
||||
app.register_type::<PlayerAnimation>();
|
||||
app.add_systems(
|
||||
Update,
|
||||
(
|
||||
update_animation_timer.in_set(AppSet::TickTimers),
|
||||
(
|
||||
update_animation_movement,
|
||||
update_animation_atlas,
|
||||
trigger_step_sound_effect,
|
||||
)
|
||||
.chain()
|
||||
.run_if(resource_exists::<PlayerAssets>)
|
||||
.in_set(AppSet::Update),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Update the sprite direction and animation state (idling/walking).
|
||||
fn update_animation_movement(
|
||||
mut player_query: Query<(&MovementController, &mut Sprite, &mut PlayerAnimation)>,
|
||||
) {
|
||||
for (controller, mut sprite, mut animation) in &mut player_query {
|
||||
let dx = controller.intent.x;
|
||||
if dx != 0.0 {
|
||||
sprite.flip_x = dx < 0.0;
|
||||
}
|
||||
|
||||
let animation_state = if controller.intent == Vec2::ZERO {
|
||||
PlayerAnimationState::Idling
|
||||
} else {
|
||||
PlayerAnimationState::Walking
|
||||
};
|
||||
animation.update_state(animation_state);
|
||||
}
|
||||
}
|
||||
|
||||
/// Update the animation timer.
|
||||
fn update_animation_timer(time: Res<Time>, mut query: Query<&mut PlayerAnimation>) {
|
||||
for mut animation in &mut query {
|
||||
animation.update_timer(time.delta());
|
||||
}
|
||||
}
|
||||
|
||||
/// Update the texture atlas to reflect changes in the animation.
|
||||
fn update_animation_atlas(mut query: Query<(&PlayerAnimation, &mut TextureAtlas)>) {
|
||||
for (animation, mut atlas) in &mut query {
|
||||
if animation.changed() {
|
||||
atlas.index = animation.get_atlas_index();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// If the player is moving, play a step sound effect synchronized with the
|
||||
/// animation.
|
||||
fn trigger_step_sound_effect(
|
||||
mut commands: Commands,
|
||||
player_assets: Res<PlayerAssets>,
|
||||
mut step_query: Query<&PlayerAnimation>,
|
||||
) {
|
||||
for animation in &mut step_query {
|
||||
if animation.state == PlayerAnimationState::Walking
|
||||
&& animation.changed()
|
||||
&& (animation.frame == 2 || animation.frame == 5)
|
||||
{
|
||||
let rng = &mut rand::thread_rng();
|
||||
let random_step = player_assets.steps.choose(rng).unwrap();
|
||||
commands.spawn((
|
||||
AudioBundle {
|
||||
source: random_step.clone(),
|
||||
settings: PlaybackSettings::DESPAWN,
|
||||
},
|
||||
SoundEffect,
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Component that tracks player's animation state.
|
||||
/// It is tightly bound to the texture atlas we use.
|
||||
#[derive(Component, Reflect)]
|
||||
#[reflect(Component)]
|
||||
pub struct PlayerAnimation {
|
||||
timer: Timer,
|
||||
frame: usize,
|
||||
state: PlayerAnimationState,
|
||||
}
|
||||
|
||||
#[derive(Reflect, PartialEq)]
|
||||
pub enum PlayerAnimationState {
|
||||
Idling,
|
||||
Walking,
|
||||
}
|
||||
|
||||
impl PlayerAnimation {
|
||||
/// The number of idle frames.
|
||||
const IDLE_FRAMES: usize = 2;
|
||||
/// The duration of each idle frame.
|
||||
const IDLE_INTERVAL: Duration = Duration::from_millis(500);
|
||||
/// The number of walking frames.
|
||||
const WALKING_FRAMES: usize = 6;
|
||||
/// The duration of each walking frame.
|
||||
const WALKING_INTERVAL: Duration = Duration::from_millis(50);
|
||||
|
||||
fn idling() -> Self {
|
||||
Self {
|
||||
timer: Timer::new(Self::IDLE_INTERVAL, TimerMode::Repeating),
|
||||
frame: 0,
|
||||
state: PlayerAnimationState::Idling,
|
||||
}
|
||||
}
|
||||
|
||||
fn walking() -> Self {
|
||||
Self {
|
||||
timer: Timer::new(Self::WALKING_INTERVAL, TimerMode::Repeating),
|
||||
frame: 0,
|
||||
state: PlayerAnimationState::Walking,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new() -> Self {
|
||||
Self::idling()
|
||||
}
|
||||
|
||||
/// Update animation timers.
|
||||
pub fn update_timer(&mut self, delta: Duration) {
|
||||
self.timer.tick(delta);
|
||||
if !self.timer.finished() {
|
||||
return;
|
||||
}
|
||||
self.frame = (self.frame + 1)
|
||||
% match self.state {
|
||||
PlayerAnimationState::Idling => Self::IDLE_FRAMES,
|
||||
PlayerAnimationState::Walking => Self::WALKING_FRAMES,
|
||||
};
|
||||
}
|
||||
|
||||
/// Update animation state if it changes.
|
||||
pub fn update_state(&mut self, state: PlayerAnimationState) {
|
||||
if self.state != state {
|
||||
match state {
|
||||
PlayerAnimationState::Idling => *self = Self::idling(),
|
||||
PlayerAnimationState::Walking => *self = Self::walking(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether animation changed this tick.
|
||||
pub fn changed(&self) -> bool {
|
||||
self.timer.finished()
|
||||
}
|
||||
|
||||
/// Return sprite index in the atlas.
|
||||
pub fn get_atlas_index(&self) -> usize {
|
||||
match self.state {
|
||||
PlayerAnimationState::Idling => self.frame,
|
||||
PlayerAnimationState::Walking => 6 + self.frame,
|
||||
}
|
||||
}
|
||||
}
|
||||
20
src/demo/level.rs
Normal file
@ -0,0 +1,20 @@
|
||||
//! Spawn the main level.
|
||||
|
||||
use bevy::{ecs::world::Command, prelude::*};
|
||||
|
||||
use crate::demo::player::SpawnPlayer;
|
||||
|
||||
pub(super) fn plugin(_app: &mut App) {
|
||||
// No setup required for this plugin.
|
||||
// It's still good to have a function here so that we can add some setup
|
||||
// later if needed.
|
||||
}
|
||||
|
||||
/// A [`Command`] to spawn the level.
|
||||
/// Functions that accept only `&mut World` as their parameter implement [`Command`].
|
||||
/// We use this style when a command requires no configuration.
|
||||
pub fn spawn_level(world: &mut World) {
|
||||
// The only thing we have in our level is a player,
|
||||
// but add things like walls etc. here.
|
||||
SpawnPlayer { max_speed: 400.0 }.apply(world);
|
||||
}
|
||||
20
src/demo/mod.rs
Normal file
@ -0,0 +1,20 @@
|
||||
//! Demo gameplay. All of these modules are only intended for demonstration
|
||||
//! purposes and should be replaced with your own game logic.
|
||||
//! Feel free to change the logic found here if you feel like tinkering around
|
||||
//! to get a feeling for the template.
|
||||
|
||||
use bevy::prelude::*;
|
||||
|
||||
mod animation;
|
||||
pub mod level;
|
||||
mod movement;
|
||||
pub mod player;
|
||||
|
||||
pub(super) fn plugin(app: &mut App) {
|
||||
app.add_plugins((
|
||||
animation::plugin,
|
||||
movement::plugin,
|
||||
player::plugin,
|
||||
level::plugin,
|
||||
));
|
||||
}
|
||||
84
src/demo/movement.rs
Normal file
@ -0,0 +1,84 @@
|
||||
//! Handle player input and translate it into movement through a character
|
||||
//! controller. A character controller is the collection of systems that govern
|
||||
//! the movement of characters.
|
||||
//!
|
||||
//! In our case, the character controller has the following logic:
|
||||
//! - Set [`MovementController`] intent based on directional keyboard input.
|
||||
//! This is done in the `player` module, as it is specific to the player
|
||||
//! character.
|
||||
//! - Apply movement based on [`MovementController`] intent and maximum speed.
|
||||
//! - Wrap the character within the window.
|
||||
//!
|
||||
//! Note that the implementation used here is limited for demonstration
|
||||
//! purposes. If you want to move the player in a smoother way,
|
||||
//! consider using a [fixed timestep](https://github.com/bevyengine/bevy/blob/main/examples/movement/physics_in_fixed_timestep.rs).
|
||||
|
||||
use bevy::{prelude::*, window::PrimaryWindow};
|
||||
|
||||
use crate::AppSet;
|
||||
|
||||
pub(super) fn plugin(app: &mut App) {
|
||||
app.register_type::<(MovementController, ScreenWrap)>();
|
||||
|
||||
app.add_systems(
|
||||
Update,
|
||||
(apply_movement, apply_screen_wrap)
|
||||
.chain()
|
||||
.in_set(AppSet::Update),
|
||||
);
|
||||
}
|
||||
|
||||
/// These are the movement parameters for our character controller.
|
||||
/// For now, this is only used for a single player, but it could power NPCs or
|
||||
/// other players as well.
|
||||
#[derive(Component, Reflect)]
|
||||
#[reflect(Component)]
|
||||
pub struct MovementController {
|
||||
/// The direction the character wants to move in.
|
||||
pub intent: Vec2,
|
||||
|
||||
/// Maximum speed in world units per second.
|
||||
/// 1 world unit = 1 pixel when using the default 2D camera and no physics
|
||||
/// engine.
|
||||
pub max_speed: f32,
|
||||
}
|
||||
|
||||
impl Default for MovementController {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
intent: Vec2::ZERO,
|
||||
// 400 pixels per second is a nice default, but we can still vary this per character.
|
||||
max_speed: 400.0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_movement(
|
||||
time: Res<Time>,
|
||||
mut movement_query: Query<(&MovementController, &mut Transform)>,
|
||||
) {
|
||||
for (controller, mut transform) in &mut movement_query {
|
||||
let velocity = controller.max_speed * controller.intent;
|
||||
transform.translation += velocity.extend(0.0) * time.delta_seconds();
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Component, Reflect)]
|
||||
#[reflect(Component)]
|
||||
pub struct ScreenWrap;
|
||||
|
||||
fn apply_screen_wrap(
|
||||
window_query: Query<&Window, With<PrimaryWindow>>,
|
||||
mut wrap_query: Query<&mut Transform, With<ScreenWrap>>,
|
||||
) {
|
||||
let Ok(window) = window_query.get_single() else {
|
||||
return;
|
||||
};
|
||||
let size = window.size() + 256.0;
|
||||
let half_size = size / 2.0;
|
||||
for mut transform in &mut wrap_query {
|
||||
let position = transform.translation.xy();
|
||||
let wrapped = (position + half_size).rem_euclid(size) - half_size;
|
||||
transform.translation = wrapped.extend(transform.translation.z);
|
||||
}
|
||||
}
|
||||
153
src/demo/player.rs
Normal file
@ -0,0 +1,153 @@
|
||||
//! Plugin handling the player character in particular.
|
||||
//! Note that this is separate from the `movement` module as that could be used
|
||||
//! for other characters as well.
|
||||
|
||||
use bevy::{
|
||||
ecs::{system::RunSystemOnce as _, world::Command},
|
||||
prelude::*,
|
||||
render::texture::{ImageLoaderSettings, ImageSampler},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
asset_tracking::LoadResource,
|
||||
demo::{
|
||||
animation::PlayerAnimation,
|
||||
movement::{MovementController, ScreenWrap},
|
||||
},
|
||||
screens::Screen,
|
||||
AppSet,
|
||||
};
|
||||
|
||||
pub(super) fn plugin(app: &mut App) {
|
||||
app.register_type::<Player>();
|
||||
app.load_resource::<PlayerAssets>();
|
||||
|
||||
// Record directional input as movement controls.
|
||||
app.add_systems(
|
||||
Update,
|
||||
record_player_directional_input.in_set(AppSet::RecordInput),
|
||||
);
|
||||
}
|
||||
|
||||
#[derive(Component, Debug, Clone, Copy, PartialEq, Eq, Default, Reflect)]
|
||||
#[reflect(Component)]
|
||||
pub struct Player;
|
||||
|
||||
/// A command to spawn the player character.
|
||||
#[derive(Debug)]
|
||||
pub struct SpawnPlayer {
|
||||
/// See [`MovementController::max_speed`].
|
||||
pub max_speed: f32,
|
||||
}
|
||||
|
||||
impl Command for SpawnPlayer {
|
||||
fn apply(self, world: &mut World) {
|
||||
world.run_system_once_with(self, spawn_player);
|
||||
}
|
||||
}
|
||||
|
||||
fn spawn_player(
|
||||
In(config): In<SpawnPlayer>,
|
||||
mut commands: Commands,
|
||||
player_assets: Res<PlayerAssets>,
|
||||
mut texture_atlas_layouts: ResMut<Assets<TextureAtlasLayout>>,
|
||||
) {
|
||||
// A texture atlas is a way to split one image with a grid into multiple
|
||||
// sprites. By attaching it to a [`SpriteBundle`] and providing an index, we
|
||||
// can specify which section of the image we want to see. We will use this
|
||||
// to animate our player character. You can learn more about texture atlases in
|
||||
// this example: https://github.com/bevyengine/bevy/blob/latest/examples/2d/texture_atlas.rs
|
||||
let layout = TextureAtlasLayout::from_grid(UVec2::splat(32), 6, 2, Some(UVec2::splat(1)), None);
|
||||
let texture_atlas_layout = texture_atlas_layouts.add(layout);
|
||||
let player_animation = PlayerAnimation::new();
|
||||
|
||||
commands.spawn((
|
||||
Name::new("Player"),
|
||||
Player,
|
||||
SpriteBundle {
|
||||
texture: player_assets.ducky.clone(),
|
||||
transform: Transform::from_scale(Vec2::splat(8.0).extend(1.0)),
|
||||
..Default::default()
|
||||
},
|
||||
TextureAtlas {
|
||||
layout: texture_atlas_layout.clone(),
|
||||
index: player_animation.get_atlas_index(),
|
||||
},
|
||||
MovementController {
|
||||
max_speed: config.max_speed,
|
||||
..default()
|
||||
},
|
||||
ScreenWrap,
|
||||
player_animation,
|
||||
StateScoped(Screen::Gameplay),
|
||||
));
|
||||
}
|
||||
|
||||
fn record_player_directional_input(
|
||||
input: Res<ButtonInput<KeyCode>>,
|
||||
mut controller_query: Query<&mut MovementController, With<Player>>,
|
||||
) {
|
||||
// Collect directional input.
|
||||
let mut intent = Vec2::ZERO;
|
||||
if input.pressed(KeyCode::KeyW) || input.pressed(KeyCode::ArrowUp) {
|
||||
intent.y += 1.0;
|
||||
}
|
||||
if input.pressed(KeyCode::KeyS) || input.pressed(KeyCode::ArrowDown) {
|
||||
intent.y -= 1.0;
|
||||
}
|
||||
if input.pressed(KeyCode::KeyA) || input.pressed(KeyCode::ArrowLeft) {
|
||||
intent.x -= 1.0;
|
||||
}
|
||||
if input.pressed(KeyCode::KeyD) || input.pressed(KeyCode::ArrowRight) {
|
||||
intent.x += 1.0;
|
||||
}
|
||||
|
||||
// Normalize so that diagonal movement has the same speed as
|
||||
// horizontal and vertical movement.
|
||||
// This should be omitted if the input comes from an analog stick instead.
|
||||
let intent = intent.normalize_or_zero();
|
||||
|
||||
// Apply movement intent to controllers.
|
||||
for mut controller in &mut controller_query {
|
||||
controller.intent = intent;
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Resource, Asset, Reflect, Clone)]
|
||||
pub struct PlayerAssets {
|
||||
// This #[dependency] attribute marks the field as a dependency of the Asset.
|
||||
// This means that it will not finish loading until the labeled asset is also loaded.
|
||||
#[dependency]
|
||||
pub ducky: Handle<Image>,
|
||||
#[dependency]
|
||||
pub steps: Vec<Handle<AudioSource>>,
|
||||
}
|
||||
|
||||
impl PlayerAssets {
|
||||
pub const PATH_DUCKY: &'static str = "images/ducky.png";
|
||||
pub const PATH_STEP_1: &'static str = "audio/sound_effects/step1.ogg";
|
||||
pub const PATH_STEP_2: &'static str = "audio/sound_effects/step2.ogg";
|
||||
pub const PATH_STEP_3: &'static str = "audio/sound_effects/step3.ogg";
|
||||
pub const PATH_STEP_4: &'static str = "audio/sound_effects/step4.ogg";
|
||||
}
|
||||
|
||||
impl FromWorld for PlayerAssets {
|
||||
fn from_world(world: &mut World) -> Self {
|
||||
let assets = world.resource::<AssetServer>();
|
||||
Self {
|
||||
ducky: assets.load_with_settings(
|
||||
PlayerAssets::PATH_DUCKY,
|
||||
|settings: &mut ImageLoaderSettings| {
|
||||
// Use `nearest` image sampling to preserve the pixel art style.
|
||||
settings.sampler = ImageSampler::nearest();
|
||||
},
|
||||
),
|
||||
steps: vec![
|
||||
assets.load(PlayerAssets::PATH_STEP_1),
|
||||
assets.load(PlayerAssets::PATH_STEP_2),
|
||||
assets.load(PlayerAssets::PATH_STEP_3),
|
||||
assets.load(PlayerAssets::PATH_STEP_4),
|
||||
],
|
||||
}
|
||||
}
|
||||
}
|
||||
30
src/dev_tools.rs
Normal file
@ -0,0 +1,30 @@
|
||||
//! Development tools for the game. This plugin is only enabled in dev builds.
|
||||
|
||||
use bevy::{
|
||||
dev_tools::{
|
||||
states::log_transitions,
|
||||
ui_debug_overlay::{DebugUiPlugin, UiDebugOptions},
|
||||
},
|
||||
input::common_conditions::input_just_pressed,
|
||||
prelude::*,
|
||||
};
|
||||
|
||||
use crate::screens::Screen;
|
||||
|
||||
pub(super) fn plugin(app: &mut App) {
|
||||
// Log `Screen` state transitions.
|
||||
app.add_systems(Update, log_transitions::<Screen>);
|
||||
|
||||
// Toggle the debug overlay for UI.
|
||||
app.add_plugins(DebugUiPlugin);
|
||||
app.add_systems(
|
||||
Update,
|
||||
toggle_debug_ui.run_if(input_just_pressed(TOGGLE_KEY)),
|
||||
);
|
||||
}
|
||||
|
||||
const TOGGLE_KEY: KeyCode = KeyCode::Backquote;
|
||||
|
||||
fn toggle_debug_ui(mut options: ResMut<UiDebugOptions>) {
|
||||
options.toggle();
|
||||
}
|
||||
96
src/lib.rs
Normal file
@ -0,0 +1,96 @@
|
||||
mod asset_tracking;
|
||||
pub mod audio;
|
||||
mod demo;
|
||||
#[cfg(feature = "dev")]
|
||||
mod dev_tools;
|
||||
mod screens;
|
||||
mod theme;
|
||||
|
||||
use bevy::{
|
||||
asset::AssetMetaCheck,
|
||||
audio::{AudioPlugin, Volume},
|
||||
prelude::*,
|
||||
};
|
||||
|
||||
pub struct AppPlugin;
|
||||
|
||||
impl Plugin for AppPlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
// Order new `AppStep` variants by adding them here:
|
||||
app.configure_sets(
|
||||
Update,
|
||||
(AppSet::TickTimers, AppSet::RecordInput, AppSet::Update).chain(),
|
||||
);
|
||||
|
||||
// Spawn the main camera.
|
||||
app.add_systems(Startup, spawn_camera);
|
||||
|
||||
// Add Bevy plugins.
|
||||
app.add_plugins(
|
||||
DefaultPlugins
|
||||
.set(AssetPlugin {
|
||||
// Wasm builds will check for meta files (that don't exist) if this isn't set.
|
||||
// This causes errors and even panics on web build on itch.
|
||||
// See https://github.com/bevyengine/bevy_github_ci_template/issues/48.
|
||||
meta_check: AssetMetaCheck::Never,
|
||||
..default()
|
||||
})
|
||||
.set(WindowPlugin {
|
||||
primary_window: Window {
|
||||
title: "The Labyrinth Of Echoes".to_string(),
|
||||
canvas: Some("#bevy".to_string()),
|
||||
fit_canvas_to_parent: true,
|
||||
prevent_default_event_handling: true,
|
||||
..default()
|
||||
}
|
||||
.into(),
|
||||
..default()
|
||||
})
|
||||
.set(AudioPlugin {
|
||||
global_volume: GlobalVolume {
|
||||
volume: Volume::new(0.3),
|
||||
},
|
||||
..default()
|
||||
}),
|
||||
);
|
||||
|
||||
// Add other plugins.
|
||||
app.add_plugins((
|
||||
asset_tracking::plugin,
|
||||
demo::plugin,
|
||||
screens::plugin,
|
||||
theme::plugin,
|
||||
));
|
||||
|
||||
// Enable dev tools for dev builds.
|
||||
#[cfg(feature = "dev")]
|
||||
app.add_plugins(dev_tools::plugin);
|
||||
}
|
||||
}
|
||||
|
||||
/// High-level groupings of systems for the app in the `Update` schedule.
|
||||
/// When adding a new variant, make sure to order it in the `configure_sets`
|
||||
/// call above.
|
||||
#[derive(SystemSet, Debug, Clone, Copy, Eq, PartialEq, Hash, PartialOrd, Ord)]
|
||||
enum AppSet {
|
||||
/// Tick timers.
|
||||
TickTimers,
|
||||
/// Record player input.
|
||||
RecordInput,
|
||||
/// Do everything else (consider splitting this into further variants).
|
||||
Update,
|
||||
}
|
||||
|
||||
fn spawn_camera(mut commands: Commands) {
|
||||
commands.spawn((
|
||||
Name::new("Camera"),
|
||||
Camera2dBundle::default(),
|
||||
// Render all UI to this camera.
|
||||
// Not strictly necessary since we only use one camera,
|
||||
// but if we don't use this component, our UI will disappear as soon
|
||||
// as we add another camera. This includes indirect ways of adding cameras like using
|
||||
// [ui node outlines](https://bevyengine.org/news/bevy-0-14/#ui-node-outline-gizmos)
|
||||
// for debugging. So it's good to have this here for future-proofing.
|
||||
IsDefaultUiCamera,
|
||||
));
|
||||
}
|
||||
9
src/main.rs
Normal file
@ -0,0 +1,9 @@
|
||||
// Disable console on Windows for non-dev builds.
|
||||
#![cfg_attr(not(feature = "dev"), windows_subsystem = "windows")]
|
||||
|
||||
use bevy::prelude::*;
|
||||
use the_labyrinth_of_echoes::AppPlugin;
|
||||
|
||||
fn main() -> AppExit {
|
||||
App::new().add_plugins(AppPlugin).run()
|
||||
}
|
||||
73
src/screens/credits.rs
Normal file
@ -0,0 +1,73 @@
|
||||
//! A credits screen that can be accessed from the title screen.
|
||||
|
||||
use bevy::prelude::*;
|
||||
|
||||
use crate::{asset_tracking::LoadResource, audio::Music, screens::Screen, theme::prelude::*};
|
||||
|
||||
pub(super) fn plugin(app: &mut App) {
|
||||
app.add_systems(OnEnter(Screen::Credits), spawn_credits_screen);
|
||||
|
||||
app.load_resource::<CreditsMusic>();
|
||||
app.add_systems(OnEnter(Screen::Credits), play_credits_music);
|
||||
app.add_systems(OnExit(Screen::Credits), stop_music);
|
||||
}
|
||||
|
||||
fn spawn_credits_screen(mut commands: Commands) {
|
||||
commands
|
||||
.ui_root()
|
||||
.insert(StateScoped(Screen::Credits))
|
||||
.with_children(|children| {
|
||||
children.header("Made by");
|
||||
children.label("Joe Shmoe - Implemented aligator wrestling AI");
|
||||
children.label("Jane Doe - Made the music for the alien invasion");
|
||||
|
||||
children.header("Assets");
|
||||
children.label("Bevy logo - All rights reserved by the Bevy Foundation. Permission granted for splash screen use when unmodified.");
|
||||
children.label("Ducky sprite - CC0 by Caz Creates Games");
|
||||
children.label("Button SFX - CC0 by Jaszunio15");
|
||||
children.label("Music - CC BY 3.0 by Kevin MacLeod");
|
||||
|
||||
children.button("Back").observe(enter_title_screen);
|
||||
});
|
||||
}
|
||||
|
||||
fn enter_title_screen(_trigger: Trigger<OnPress>, mut next_screen: ResMut<NextState<Screen>>) {
|
||||
next_screen.set(Screen::Title);
|
||||
}
|
||||
|
||||
#[derive(Resource, Asset, Reflect, Clone)]
|
||||
pub struct CreditsMusic {
|
||||
#[dependency]
|
||||
music: Handle<AudioSource>,
|
||||
entity: Option<Entity>,
|
||||
}
|
||||
|
||||
impl FromWorld for CreditsMusic {
|
||||
fn from_world(world: &mut World) -> Self {
|
||||
let assets = world.resource::<AssetServer>();
|
||||
Self {
|
||||
music: assets.load("audio/music/Monkeys Spinning Monkeys.ogg"),
|
||||
entity: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn play_credits_music(mut commands: Commands, mut music: ResMut<CreditsMusic>) {
|
||||
music.entity = Some(
|
||||
commands
|
||||
.spawn((
|
||||
AudioBundle {
|
||||
source: music.music.clone(),
|
||||
settings: PlaybackSettings::LOOP,
|
||||
},
|
||||
Music,
|
||||
))
|
||||
.id(),
|
||||
);
|
||||
}
|
||||
|
||||
fn stop_music(mut commands: Commands, mut music: ResMut<CreditsMusic>) {
|
||||
if let Some(entity) = music.entity.take() {
|
||||
commands.entity(entity).despawn_recursive();
|
||||
}
|
||||
}
|
||||
67
src/screens/gameplay.rs
Normal file
@ -0,0 +1,67 @@
|
||||
//! The screen state for the main gameplay.
|
||||
|
||||
use bevy::{input::common_conditions::input_just_pressed, prelude::*};
|
||||
|
||||
use crate::{
|
||||
asset_tracking::LoadResource, audio::Music, demo::level::spawn_level as spawn_level_command,
|
||||
screens::Screen,
|
||||
};
|
||||
|
||||
pub(super) fn plugin(app: &mut App) {
|
||||
app.add_systems(OnEnter(Screen::Gameplay), spawn_level);
|
||||
|
||||
app.load_resource::<GameplayMusic>();
|
||||
app.add_systems(OnEnter(Screen::Gameplay), play_gameplay_music);
|
||||
app.add_systems(OnExit(Screen::Gameplay), stop_music);
|
||||
|
||||
app.add_systems(
|
||||
Update,
|
||||
return_to_title_screen
|
||||
.run_if(in_state(Screen::Gameplay).and_then(input_just_pressed(KeyCode::Escape))),
|
||||
);
|
||||
}
|
||||
|
||||
fn spawn_level(mut commands: Commands) {
|
||||
commands.add(spawn_level_command);
|
||||
}
|
||||
|
||||
#[derive(Resource, Asset, Reflect, Clone)]
|
||||
pub struct GameplayMusic {
|
||||
#[dependency]
|
||||
handle: Handle<AudioSource>,
|
||||
entity: Option<Entity>,
|
||||
}
|
||||
|
||||
impl FromWorld for GameplayMusic {
|
||||
fn from_world(world: &mut World) -> Self {
|
||||
let assets = world.resource::<AssetServer>();
|
||||
Self {
|
||||
handle: assets.load("audio/music/Fluffing A Duck.ogg"),
|
||||
entity: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn play_gameplay_music(mut commands: Commands, mut music: ResMut<GameplayMusic>) {
|
||||
music.entity = Some(
|
||||
commands
|
||||
.spawn((
|
||||
AudioBundle {
|
||||
source: music.handle.clone(),
|
||||
settings: PlaybackSettings::LOOP,
|
||||
},
|
||||
Music,
|
||||
))
|
||||
.id(),
|
||||
);
|
||||
}
|
||||
|
||||
fn stop_music(mut commands: Commands, mut music: ResMut<GameplayMusic>) {
|
||||
if let Some(entity) = music.entity.take() {
|
||||
commands.entity(entity).despawn_recursive();
|
||||
}
|
||||
}
|
||||
|
||||
fn return_to_title_screen(mut next_screen: ResMut<NextState<Screen>>) {
|
||||
next_screen.set(Screen::Title);
|
||||
}
|
||||
47
src/screens/loading.rs
Normal file
@ -0,0 +1,47 @@
|
||||
//! A loading screen during which game assets are loaded.
|
||||
//! This reduces stuttering, especially for audio on WASM.
|
||||
|
||||
use bevy::prelude::*;
|
||||
|
||||
use crate::{
|
||||
demo::player::PlayerAssets,
|
||||
screens::{credits::CreditsMusic, gameplay::GameplayMusic, Screen},
|
||||
theme::{interaction::InteractionAssets, prelude::*},
|
||||
};
|
||||
|
||||
pub(super) fn plugin(app: &mut App) {
|
||||
app.add_systems(OnEnter(Screen::Loading), spawn_loading_screen);
|
||||
|
||||
app.add_systems(
|
||||
Update,
|
||||
continue_to_title_screen.run_if(in_state(Screen::Loading).and_then(all_assets_loaded)),
|
||||
);
|
||||
}
|
||||
|
||||
fn spawn_loading_screen(mut commands: Commands) {
|
||||
commands
|
||||
.ui_root()
|
||||
.insert(StateScoped(Screen::Loading))
|
||||
.with_children(|children| {
|
||||
children.label("Loading...").insert(Style {
|
||||
justify_content: JustifyContent::Center,
|
||||
..default()
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
fn continue_to_title_screen(mut next_screen: ResMut<NextState<Screen>>) {
|
||||
next_screen.set(Screen::Title);
|
||||
}
|
||||
|
||||
fn all_assets_loaded(
|
||||
player_assets: Option<Res<PlayerAssets>>,
|
||||
interaction_assets: Option<Res<InteractionAssets>>,
|
||||
credits_music: Option<Res<CreditsMusic>>,
|
||||
gameplay_music: Option<Res<GameplayMusic>>,
|
||||
) -> bool {
|
||||
player_assets.is_some()
|
||||
&& interaction_assets.is_some()
|
||||
&& credits_music.is_some()
|
||||
&& gameplay_music.is_some()
|
||||
}
|
||||
33
src/screens/mod.rs
Normal file
@ -0,0 +1,33 @@
|
||||
//! The game's main screen states and transitions between them.
|
||||
|
||||
mod credits;
|
||||
mod gameplay;
|
||||
mod loading;
|
||||
mod splash;
|
||||
mod title;
|
||||
|
||||
use bevy::prelude::*;
|
||||
|
||||
pub(super) fn plugin(app: &mut App) {
|
||||
app.init_state::<Screen>();
|
||||
app.enable_state_scoped_entities::<Screen>();
|
||||
|
||||
app.add_plugins((
|
||||
credits::plugin,
|
||||
gameplay::plugin,
|
||||
loading::plugin,
|
||||
splash::plugin,
|
||||
title::plugin,
|
||||
));
|
||||
}
|
||||
|
||||
/// The game's main screen states.
|
||||
#[derive(States, Debug, Hash, PartialEq, Eq, Clone, Default)]
|
||||
pub enum Screen {
|
||||
#[default]
|
||||
Splash,
|
||||
Loading,
|
||||
Title,
|
||||
Credits,
|
||||
Gameplay,
|
||||
}
|
||||
153
src/screens/splash.rs
Normal file
@ -0,0 +1,153 @@
|
||||
//! A splash screen that plays briefly at startup.
|
||||
|
||||
use bevy::{
|
||||
input::common_conditions::input_just_pressed,
|
||||
prelude::*,
|
||||
render::texture::{ImageLoaderSettings, ImageSampler},
|
||||
};
|
||||
|
||||
use crate::{screens::Screen, theme::prelude::*, AppSet};
|
||||
|
||||
pub(super) fn plugin(app: &mut App) {
|
||||
// Spawn splash screen.
|
||||
app.insert_resource(ClearColor(SPLASH_BACKGROUND_COLOR));
|
||||
app.add_systems(OnEnter(Screen::Splash), spawn_splash_screen);
|
||||
|
||||
// Animate splash screen.
|
||||
app.add_systems(
|
||||
Update,
|
||||
(
|
||||
tick_fade_in_out.in_set(AppSet::TickTimers),
|
||||
apply_fade_in_out.in_set(AppSet::Update),
|
||||
)
|
||||
.run_if(in_state(Screen::Splash)),
|
||||
);
|
||||
|
||||
// Add splash timer.
|
||||
app.register_type::<SplashTimer>();
|
||||
app.add_systems(OnEnter(Screen::Splash), insert_splash_timer);
|
||||
app.add_systems(OnExit(Screen::Splash), remove_splash_timer);
|
||||
app.add_systems(
|
||||
Update,
|
||||
(
|
||||
tick_splash_timer.in_set(AppSet::TickTimers),
|
||||
check_splash_timer.in_set(AppSet::Update),
|
||||
)
|
||||
.run_if(in_state(Screen::Splash)),
|
||||
);
|
||||
|
||||
// Exit the splash screen early if the player hits escape.
|
||||
app.add_systems(
|
||||
Update,
|
||||
continue_to_loading_screen
|
||||
.run_if(input_just_pressed(KeyCode::Escape).and_then(in_state(Screen::Splash))),
|
||||
);
|
||||
}
|
||||
|
||||
const SPLASH_BACKGROUND_COLOR: Color = Color::srgb(0.157, 0.157, 0.157);
|
||||
const SPLASH_DURATION_SECS: f32 = 1.8;
|
||||
const SPLASH_FADE_DURATION_SECS: f32 = 0.6;
|
||||
|
||||
fn spawn_splash_screen(mut commands: Commands, asset_server: Res<AssetServer>) {
|
||||
commands
|
||||
.ui_root()
|
||||
.insert((
|
||||
Name::new("Splash screen"),
|
||||
BackgroundColor(SPLASH_BACKGROUND_COLOR),
|
||||
StateScoped(Screen::Splash),
|
||||
))
|
||||
.with_children(|children| {
|
||||
children.spawn((
|
||||
Name::new("Splash image"),
|
||||
ImageBundle {
|
||||
style: Style {
|
||||
margin: UiRect::all(Val::Auto),
|
||||
width: Val::Percent(70.0),
|
||||
..default()
|
||||
},
|
||||
image: UiImage::new(asset_server.load_with_settings(
|
||||
// This should be an embedded asset for instant loading, but that is
|
||||
// currently [broken on Windows Wasm builds](https://github.com/bevyengine/bevy/issues/14246).
|
||||
"images/splash.png",
|
||||
|settings: &mut ImageLoaderSettings| {
|
||||
// Make an exception for the splash image in case
|
||||
// `ImagePlugin::default_nearest()` is used for pixel art.
|
||||
settings.sampler = ImageSampler::linear();
|
||||
},
|
||||
)),
|
||||
..default()
|
||||
},
|
||||
UiImageFadeInOut {
|
||||
total_duration: SPLASH_DURATION_SECS,
|
||||
fade_duration: SPLASH_FADE_DURATION_SECS,
|
||||
t: 0.0,
|
||||
},
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
#[derive(Component, Reflect)]
|
||||
#[reflect(Component)]
|
||||
struct UiImageFadeInOut {
|
||||
/// Total duration in seconds.
|
||||
total_duration: f32,
|
||||
/// Fade duration in seconds.
|
||||
fade_duration: f32,
|
||||
/// Current progress in seconds, between 0 and [`Self::total_duration`].
|
||||
t: f32,
|
||||
}
|
||||
|
||||
impl UiImageFadeInOut {
|
||||
fn alpha(&self) -> f32 {
|
||||
// Normalize by duration.
|
||||
let t = (self.t / self.total_duration).clamp(0.0, 1.0);
|
||||
let fade = self.fade_duration / self.total_duration;
|
||||
|
||||
// Regular trapezoid-shaped graph, flat at the top with alpha = 1.0.
|
||||
((1.0 - (2.0 * t - 1.0).abs()) / fade).min(1.0)
|
||||
}
|
||||
}
|
||||
|
||||
fn tick_fade_in_out(time: Res<Time>, mut animation_query: Query<&mut UiImageFadeInOut>) {
|
||||
for mut anim in &mut animation_query {
|
||||
anim.t += time.delta_seconds();
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_fade_in_out(mut animation_query: Query<(&UiImageFadeInOut, &mut UiImage)>) {
|
||||
for (anim, mut image) in &mut animation_query {
|
||||
image.color.set_alpha(anim.alpha())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Resource, Debug, Clone, PartialEq, Reflect)]
|
||||
#[reflect(Resource)]
|
||||
struct SplashTimer(Timer);
|
||||
|
||||
impl Default for SplashTimer {
|
||||
fn default() -> Self {
|
||||
Self(Timer::from_seconds(SPLASH_DURATION_SECS, TimerMode::Once))
|
||||
}
|
||||
}
|
||||
|
||||
fn insert_splash_timer(mut commands: Commands) {
|
||||
commands.init_resource::<SplashTimer>();
|
||||
}
|
||||
|
||||
fn remove_splash_timer(mut commands: Commands) {
|
||||
commands.remove_resource::<SplashTimer>();
|
||||
}
|
||||
|
||||
fn tick_splash_timer(time: Res<Time>, mut timer: ResMut<SplashTimer>) {
|
||||
timer.0.tick(time.delta());
|
||||
}
|
||||
|
||||
fn check_splash_timer(timer: ResMut<SplashTimer>, mut next_screen: ResMut<NextState<Screen>>) {
|
||||
if timer.0.just_finished() {
|
||||
next_screen.set(Screen::Loading);
|
||||
}
|
||||
}
|
||||
|
||||
fn continue_to_loading_screen(mut next_screen: ResMut<NextState<Screen>>) {
|
||||
next_screen.set(Screen::Loading);
|
||||
}
|
||||
35
src/screens/title.rs
Normal file
@ -0,0 +1,35 @@
|
||||
//! The title screen that appears when the game starts.
|
||||
|
||||
use bevy::prelude::*;
|
||||
|
||||
use crate::{screens::Screen, theme::prelude::*};
|
||||
|
||||
pub(super) fn plugin(app: &mut App) {
|
||||
app.add_systems(OnEnter(Screen::Title), spawn_title_screen);
|
||||
}
|
||||
|
||||
fn spawn_title_screen(mut commands: Commands) {
|
||||
commands
|
||||
.ui_root()
|
||||
.insert(StateScoped(Screen::Title))
|
||||
.with_children(|children| {
|
||||
children.button("Play").observe(enter_gameplay_screen);
|
||||
children.button("Credits").observe(enter_credits_screen);
|
||||
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
children.button("Exit").observe(exit_app);
|
||||
});
|
||||
}
|
||||
|
||||
fn enter_gameplay_screen(_trigger: Trigger<OnPress>, mut next_screen: ResMut<NextState<Screen>>) {
|
||||
next_screen.set(Screen::Gameplay);
|
||||
}
|
||||
|
||||
fn enter_credits_screen(_trigger: Trigger<OnPress>, mut next_screen: ResMut<NextState<Screen>>) {
|
||||
next_screen.set(Screen::Credits);
|
||||
}
|
||||
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
fn exit_app(_trigger: Trigger<OnPress>, mut app_exit: EventWriter<AppExit>) {
|
||||
app_exit.send(AppExit::Success);
|
||||
}
|
||||
104
src/theme/interaction.rs
Normal file
@ -0,0 +1,104 @@
|
||||
use bevy::prelude::*;
|
||||
|
||||
use crate::{asset_tracking::LoadResource, audio::SoundEffect};
|
||||
|
||||
pub(super) fn plugin(app: &mut App) {
|
||||
app.register_type::<InteractionPalette>();
|
||||
app.load_resource::<InteractionAssets>();
|
||||
app.add_systems(
|
||||
Update,
|
||||
(
|
||||
trigger_on_press,
|
||||
apply_interaction_palette,
|
||||
trigger_interaction_sound_effect,
|
||||
)
|
||||
.run_if(resource_exists::<InteractionAssets>),
|
||||
);
|
||||
}
|
||||
|
||||
/// Palette for widget interactions. Add this to an entity that supports
|
||||
/// [`Interaction`]s, such as a button, to change its [`BackgroundColor`] based
|
||||
/// on the current interaction state.
|
||||
#[derive(Component, Debug, Reflect)]
|
||||
#[reflect(Component)]
|
||||
pub struct InteractionPalette {
|
||||
pub none: Color,
|
||||
pub hovered: Color,
|
||||
pub pressed: Color,
|
||||
}
|
||||
|
||||
/// Event triggered on a UI entity when the [`Interaction`] component on the same entity changes to
|
||||
/// [`Interaction::Pressed`]. Observe this event to detect e.g. button presses.
|
||||
#[derive(Event)]
|
||||
pub struct OnPress;
|
||||
|
||||
fn trigger_on_press(
|
||||
interaction_query: Query<(Entity, &Interaction), Changed<Interaction>>,
|
||||
mut commands: Commands,
|
||||
) {
|
||||
for (entity, interaction) in &interaction_query {
|
||||
if matches!(interaction, Interaction::Pressed) {
|
||||
commands.trigger_targets(OnPress, entity);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_interaction_palette(
|
||||
mut palette_query: Query<
|
||||
(&Interaction, &InteractionPalette, &mut BackgroundColor),
|
||||
Changed<Interaction>,
|
||||
>,
|
||||
) {
|
||||
for (interaction, palette, mut background) in &mut palette_query {
|
||||
*background = match interaction {
|
||||
Interaction::None => palette.none,
|
||||
Interaction::Hovered => palette.hovered,
|
||||
Interaction::Pressed => palette.pressed,
|
||||
}
|
||||
.into();
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Resource, Asset, Reflect, Clone)]
|
||||
pub struct InteractionAssets {
|
||||
#[dependency]
|
||||
hover: Handle<AudioSource>,
|
||||
#[dependency]
|
||||
press: Handle<AudioSource>,
|
||||
}
|
||||
|
||||
impl InteractionAssets {
|
||||
pub const PATH_BUTTON_HOVER: &'static str = "audio/sound_effects/button_hover.ogg";
|
||||
pub const PATH_BUTTON_PRESS: &'static str = "audio/sound_effects/button_press.ogg";
|
||||
}
|
||||
|
||||
impl FromWorld for InteractionAssets {
|
||||
fn from_world(world: &mut World) -> Self {
|
||||
let assets = world.resource::<AssetServer>();
|
||||
Self {
|
||||
hover: assets.load(Self::PATH_BUTTON_HOVER),
|
||||
press: assets.load(Self::PATH_BUTTON_PRESS),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn trigger_interaction_sound_effect(
|
||||
interaction_query: Query<&Interaction, Changed<Interaction>>,
|
||||
interaction_assets: Res<InteractionAssets>,
|
||||
mut commands: Commands,
|
||||
) {
|
||||
for interaction in &interaction_query {
|
||||
let source = match interaction {
|
||||
Interaction::Hovered => interaction_assets.hover.clone(),
|
||||
Interaction::Pressed => interaction_assets.press.clone(),
|
||||
_ => continue,
|
||||
};
|
||||
commands.spawn((
|
||||
AudioBundle {
|
||||
source,
|
||||
settings: PlaybackSettings::DESPAWN,
|
||||
},
|
||||
SoundEffect,
|
||||
));
|
||||
}
|
||||
}
|
||||
23
src/theme/mod.rs
Normal file
@ -0,0 +1,23 @@
|
||||
//! Reusable UI widgets & theming.
|
||||
|
||||
// Unused utilities may trigger this lints undesirably.
|
||||
#![allow(dead_code)]
|
||||
|
||||
pub mod interaction;
|
||||
pub mod palette;
|
||||
mod widgets;
|
||||
|
||||
#[allow(unused_imports)]
|
||||
pub mod prelude {
|
||||
pub use super::{
|
||||
interaction::{InteractionPalette, OnPress},
|
||||
palette as ui_palette,
|
||||
widgets::{Containers as _, Widgets as _},
|
||||
};
|
||||
}
|
||||
|
||||
use bevy::prelude::*;
|
||||
|
||||
pub(super) fn plugin(app: &mut App) {
|
||||
app.add_plugins(interaction::plugin);
|
||||
}
|
||||
10
src/theme/palette.rs
Normal file
@ -0,0 +1,10 @@
|
||||
use bevy::prelude::*;
|
||||
|
||||
pub const BUTTON_HOVERED_BACKGROUND: Color = Color::srgb(0.186, 0.328, 0.573);
|
||||
pub const BUTTON_PRESSED_BACKGROUND: Color = Color::srgb(0.286, 0.478, 0.773);
|
||||
|
||||
pub const BUTTON_TEXT: Color = Color::srgb(0.925, 0.925, 0.925);
|
||||
pub const LABEL_TEXT: Color = Color::srgb(0.867, 0.827, 0.412);
|
||||
pub const HEADER_TEXT: Color = Color::srgb(0.867, 0.827, 0.412);
|
||||
|
||||
pub const NODE_BACKGROUND: Color = Color::srgb(0.286, 0.478, 0.773);
|
||||
154
src/theme/widgets.rs
Normal file
@ -0,0 +1,154 @@
|
||||
//! Helper traits for creating common widgets.
|
||||
|
||||
use bevy::{ecs::system::EntityCommands, prelude::*, ui::Val::*};
|
||||
|
||||
use crate::theme::{interaction::InteractionPalette, palette::*};
|
||||
|
||||
/// An extension trait for spawning UI widgets.
|
||||
pub trait Widgets {
|
||||
/// Spawn a simple button with text.
|
||||
fn button(&mut self, text: impl Into<String>) -> EntityCommands;
|
||||
|
||||
/// Spawn a simple header label. Bigger than [`Widgets::label`].
|
||||
fn header(&mut self, text: impl Into<String>) -> EntityCommands;
|
||||
|
||||
/// Spawn a simple text label.
|
||||
fn label(&mut self, text: impl Into<String>) -> EntityCommands;
|
||||
}
|
||||
|
||||
impl<T: Spawn> Widgets for T {
|
||||
fn button(&mut self, text: impl Into<String>) -> EntityCommands {
|
||||
let mut entity = self.spawn((
|
||||
Name::new("Button"),
|
||||
ButtonBundle {
|
||||
style: Style {
|
||||
width: Px(200.0),
|
||||
height: Px(65.0),
|
||||
justify_content: JustifyContent::Center,
|
||||
align_items: AlignItems::Center,
|
||||
..default()
|
||||
},
|
||||
background_color: BackgroundColor(NODE_BACKGROUND),
|
||||
..default()
|
||||
},
|
||||
InteractionPalette {
|
||||
none: NODE_BACKGROUND,
|
||||
hovered: BUTTON_HOVERED_BACKGROUND,
|
||||
pressed: BUTTON_PRESSED_BACKGROUND,
|
||||
},
|
||||
));
|
||||
entity.with_children(|children| {
|
||||
children.spawn((
|
||||
Name::new("Button Text"),
|
||||
TextBundle::from_section(
|
||||
text,
|
||||
TextStyle {
|
||||
font_size: 40.0,
|
||||
color: BUTTON_TEXT,
|
||||
..default()
|
||||
},
|
||||
),
|
||||
));
|
||||
});
|
||||
|
||||
entity
|
||||
}
|
||||
|
||||
fn header(&mut self, text: impl Into<String>) -> EntityCommands {
|
||||
let mut entity = self.spawn((
|
||||
Name::new("Header"),
|
||||
NodeBundle {
|
||||
style: Style {
|
||||
width: Px(500.0),
|
||||
height: Px(65.0),
|
||||
justify_content: JustifyContent::Center,
|
||||
align_items: AlignItems::Center,
|
||||
..default()
|
||||
},
|
||||
background_color: BackgroundColor(NODE_BACKGROUND),
|
||||
..default()
|
||||
},
|
||||
));
|
||||
entity.with_children(|children| {
|
||||
children.spawn((
|
||||
Name::new("Header Text"),
|
||||
TextBundle::from_section(
|
||||
text,
|
||||
TextStyle {
|
||||
font_size: 40.0,
|
||||
color: HEADER_TEXT,
|
||||
..default()
|
||||
},
|
||||
),
|
||||
));
|
||||
});
|
||||
entity
|
||||
}
|
||||
|
||||
fn label(&mut self, text: impl Into<String>) -> EntityCommands {
|
||||
let entity = self.spawn((
|
||||
Name::new("Label"),
|
||||
TextBundle::from_section(
|
||||
text,
|
||||
TextStyle {
|
||||
font_size: 24.0,
|
||||
color: LABEL_TEXT,
|
||||
..default()
|
||||
},
|
||||
)
|
||||
.with_style(Style {
|
||||
width: Px(500.0),
|
||||
..default()
|
||||
}),
|
||||
));
|
||||
entity
|
||||
}
|
||||
}
|
||||
|
||||
/// An extension trait for spawning UI containers.
|
||||
pub trait Containers {
|
||||
/// Spawns a root node that covers the full screen
|
||||
/// and centers its content horizontally and vertically.
|
||||
fn ui_root(&mut self) -> EntityCommands;
|
||||
}
|
||||
|
||||
impl Containers for Commands<'_, '_> {
|
||||
fn ui_root(&mut self) -> EntityCommands {
|
||||
self.spawn((
|
||||
Name::new("UI Root"),
|
||||
NodeBundle {
|
||||
style: Style {
|
||||
width: Percent(100.0),
|
||||
height: Percent(100.0),
|
||||
justify_content: JustifyContent::Center,
|
||||
align_items: AlignItems::Center,
|
||||
flex_direction: FlexDirection::Column,
|
||||
row_gap: Px(10.0),
|
||||
position_type: PositionType::Absolute,
|
||||
..default()
|
||||
},
|
||||
..default()
|
||||
},
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
/// An internal trait for types that can spawn entities.
|
||||
/// This is here so that [`Widgets`] can be implemented on all types that
|
||||
/// are able to spawn entities.
|
||||
/// Ideally, this trait should be [part of Bevy itself](https://github.com/bevyengine/bevy/issues/14231).
|
||||
trait Spawn {
|
||||
fn spawn<B: Bundle>(&mut self, bundle: B) -> EntityCommands;
|
||||
}
|
||||
|
||||
impl Spawn for Commands<'_, '_> {
|
||||
fn spawn<B: Bundle>(&mut self, bundle: B) -> EntityCommands {
|
||||
self.spawn(bundle)
|
||||
}
|
||||
}
|
||||
|
||||
impl Spawn for ChildBuilder<'_> {
|
||||
fn spawn<B: Bundle>(&mut self, bundle: B) -> EntityCommands {
|
||||
self.spawn(bundle)
|
||||
}
|
||||
}
|
||||
38
web/index.html
Normal file
@ -0,0 +1,38 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="description"
|
||||
content="Maze exploration game built using the Bevy engine">
|
||||
<meta name="keywords" content="game, bevy">
|
||||
<title>Maze Ascension: The Labyrinth of Echoes</title>
|
||||
<link data-trunk rel="copy-dir" href="../assets" />
|
||||
<link data-trunk rel="inline" href="style.css" />
|
||||
<link data-trunk rel="inline" type="module" href="restart-audio-context.js" />
|
||||
<link data-trunk
|
||||
rel="rust"
|
||||
data-cargo-no-default-features
|
||||
data-wasm-opt="s"
|
||||
href="../" />
|
||||
</head>
|
||||
<body>
|
||||
<div id="game" class="center">
|
||||
<div id="loading-screen" class="center">
|
||||
<span class="spinner"></span>
|
||||
</div>
|
||||
<canvas id="bevy"> Javascript and canvas support is required </canvas>
|
||||
</div>
|
||||
<script type="module">
|
||||
// Hide loading screen when the game starts.
|
||||
const loading_screen = document.getElementById("loading-screen");
|
||||
const bevy = document.getElementById("bevy");
|
||||
const observer = new MutationObserver(() => {
|
||||
if (bevy.height > 1) {
|
||||
loading_screen.style.display = "none";
|
||||
observer.disconnect();
|
||||
}
|
||||
});
|
||||
observer.observe(bevy, { attributeFilter: ["height"] });
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
57
web/restart-audio-context.js
Normal file
@ -0,0 +1,57 @@
|
||||
// taken from https://developer.chrome.com/blog/web-audio-autoplay/#moving-forward
|
||||
(() => {
|
||||
// An array of all contexts to resume on the page
|
||||
const audioContextList = [];
|
||||
|
||||
// An array of various user interaction events we should listen for
|
||||
const userInputEventNames = [
|
||||
"click",
|
||||
"contextmenu",
|
||||
"auxclick",
|
||||
"dblclick",
|
||||
"mousedown",
|
||||
"mouseup",
|
||||
"pointerup",
|
||||
"touchend",
|
||||
"keydown",
|
||||
"keyup",
|
||||
];
|
||||
|
||||
// A proxy object to intercept AudioContexts and
|
||||
// add them to the array for tracking and resuming later
|
||||
self.AudioContext = new Proxy(self.AudioContext, {
|
||||
construct(target, args) {
|
||||
const result = new target(...args);
|
||||
audioContextList.push(result);
|
||||
return result;
|
||||
},
|
||||
});
|
||||
|
||||
// To resume all AudioContexts being tracked
|
||||
function resumeAllContexts(_) {
|
||||
let count = 0;
|
||||
|
||||
for (const context of audioContextList) {
|
||||
if (context.state !== "running") {
|
||||
context.resume();
|
||||
} else {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
|
||||
// If all the AudioContexts have now resumed then we
|
||||
// unbind all the event listeners from the page to prevent
|
||||
// unnecessary resume attempts
|
||||
if (count === audioContextList.length) {
|
||||
for (const eventName of userInputEventNames) {
|
||||
document.removeEventListener(eventName, resumeAllContexts);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// We bind the resume function for each user interaction
|
||||
// event on the page
|
||||
for (const eventName of userInputEventNames) {
|
||||
document.addEventListener(eventName, resumeAllContexts);
|
||||
}
|
||||
})();
|
||||
56
web/style.css
Normal file
@ -0,0 +1,56 @@
|
||||
:root {
|
||||
/* Consider adjusting this color to match your splash screen! */
|
||||
--loading-screen-bg-color: #282828;
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.center {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
#loading-screen {
|
||||
background-color: var(--loading-screen-bg-color);
|
||||
}
|
||||
|
||||
.spinner {
|
||||
width: 128px;
|
||||
height: 128px;
|
||||
border: 64px solid transparent;
|
||||
border-bottom-color: #ececec;
|
||||
border-right-color: #b2b2b2;
|
||||
border-top-color: #787878;
|
||||
border-radius: 50%;
|
||||
box-sizing: border-box;
|
||||
animation: spin 1.2s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
#bevy {
|
||||
/* Hide Bevy app before it loads */
|
||||
height: 0;
|
||||
}
|
||||