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;
|
||||||
|
}
|
||||||