diff --git a/astro.config.mjs b/astro.config.mjs index 943009d..20738f2 100644 --- a/astro.config.mjs +++ b/astro.config.mjs @@ -71,10 +71,28 @@ export default defineConfig({ label: "About", link: 'game-design/index' }, { - label: "Godot", - autogenerate: { - directory: 'game-design/godot' - }, + label: "Godot", items: [{ + label: "Godot Basics", + link: "game-design/godot/basics" + },{ + label: "Universal Features", + link: "game-design/godot/universal" + },{ + label: "Survivors-Like", + link: "game-design/godot/survivors" + },{ + label: "Top-down Dungeon Crawler", + link: "game-design/godot/dungeoncrawler/0-scenesetup/" + },{ + label: "3D Intro", + link: "game-design/godot/3d" + },{ + label: "3D Game", + link: "game-design/godot/3dgame" + },{ + label: "Setting up C# For Godot", + link: "game-design/godot/projectsetup" + }], collapsed: true }], }, diff --git a/src/assets/godot/dotNet/DevKit.PNG b/src/assets/godot/dotNet/DevKit.png similarity index 100% rename from src/assets/godot/dotNet/DevKit.PNG rename to src/assets/godot/dotNet/DevKit.png diff --git a/src/assets/godot/dotNet/GDDownload.PNG b/src/assets/godot/dotNet/GDDownload.png similarity index 100% rename from src/assets/godot/dotNet/GDDownload.PNG rename to src/assets/godot/dotNet/GDDownload.png diff --git a/src/assets/godot/dotNet/editor.PNG b/src/assets/godot/dotNet/editor.png similarity index 100% rename from src/assets/godot/dotNet/editor.PNG rename to src/assets/godot/dotNet/editor.png diff --git a/src/assets/godot/dotNet/godotbuild.PNG b/src/assets/godot/dotNet/godotbuild.png similarity index 100% rename from src/assets/godot/dotNet/godotbuild.PNG rename to src/assets/godot/dotNet/godotbuild.png diff --git a/src/assets/godot/dotNet/typeSelect.PNG b/src/assets/godot/dotNet/typeSelect.png similarity index 100% rename from src/assets/godot/dotNet/typeSelect.PNG rename to src/assets/godot/dotNet/typeSelect.png diff --git a/src/assets/godot/dungeonCrawler/TextureFilter.png b/src/assets/godot/dungeonCrawler/TextureFilter.png new file mode 100644 index 0000000..6ead191 Binary files /dev/null and b/src/assets/godot/dungeonCrawler/TextureFilter.png differ diff --git a/src/assets/godot/dungeonCrawler/addanim.png b/src/assets/godot/dungeonCrawler/addanim.png new file mode 100644 index 0000000..ea851de Binary files /dev/null and b/src/assets/godot/dungeonCrawler/addanim.png differ diff --git a/src/assets/godot/dungeonCrawler/animlength.png b/src/assets/godot/dungeonCrawler/animlength.png new file mode 100644 index 0000000..3c7aa1e Binary files /dev/null and b/src/assets/godot/dungeonCrawler/animlength.png differ diff --git a/src/assets/godot/dungeonCrawler/animtimeline.png b/src/assets/godot/dungeonCrawler/animtimeline.png new file mode 100644 index 0000000..22d14c1 Binary files /dev/null and b/src/assets/godot/dungeonCrawler/animtimeline.png differ diff --git a/src/assets/godot/dungeonCrawler/assets.png b/src/assets/godot/dungeonCrawler/assets.png new file mode 100644 index 0000000..9026add Binary files /dev/null and b/src/assets/godot/dungeonCrawler/assets.png differ diff --git a/src/assets/godot/dungeonCrawler/autoloop.png b/src/assets/godot/dungeonCrawler/autoloop.png new file mode 100644 index 0000000..7d82530 Binary files /dev/null and b/src/assets/godot/dungeonCrawler/autoloop.png differ diff --git a/src/assets/godot/dungeonCrawler/basicroomimg.png b/src/assets/godot/dungeonCrawler/basicroomimg.png new file mode 100644 index 0000000..c7840e5 Binary files /dev/null and b/src/assets/godot/dungeonCrawler/basicroomimg.png differ diff --git a/src/assets/godot/dungeonCrawler/coinScene.png b/src/assets/godot/dungeonCrawler/coinScene.png new file mode 100644 index 0000000..cf92b11 Binary files /dev/null and b/src/assets/godot/dungeonCrawler/coinScene.png differ diff --git a/src/assets/godot/dungeonCrawler/collshapeimg.png b/src/assets/godot/dungeonCrawler/collshapeimg.png new file mode 100644 index 0000000..4d76457 Binary files /dev/null and b/src/assets/godot/dungeonCrawler/collshapeimg.png differ diff --git a/src/assets/godot/dungeonCrawler/createdLevel.png b/src/assets/godot/dungeonCrawler/createdLevel.png new file mode 100644 index 0000000..fb2a252 Binary files /dev/null and b/src/assets/godot/dungeonCrawler/createdLevel.png differ diff --git a/src/assets/godot/dungeonCrawler/enemyscene.png b/src/assets/godot/dungeonCrawler/enemyscene.png new file mode 100644 index 0000000..c78694f Binary files /dev/null and b/src/assets/godot/dungeonCrawler/enemyscene.png differ diff --git a/src/assets/godot/dungeonCrawler/eraseTool.png b/src/assets/godot/dungeonCrawler/eraseTool.png new file mode 100644 index 0000000..e374590 Binary files /dev/null and b/src/assets/godot/dungeonCrawler/eraseTool.png differ diff --git a/src/assets/godot/dungeonCrawler/folders.png b/src/assets/godot/dungeonCrawler/folders.png new file mode 100644 index 0000000..cfab0df Binary files /dev/null and b/src/assets/godot/dungeonCrawler/folders.png differ diff --git a/src/assets/godot/dungeonCrawler/groups.png b/src/assets/godot/dungeonCrawler/groups.png new file mode 100644 index 0000000..ce724e0 Binary files /dev/null and b/src/assets/godot/dungeonCrawler/groups.png differ diff --git a/src/assets/godot/dungeonCrawler/idleanim.png b/src/assets/godot/dungeonCrawler/idleanim.png new file mode 100644 index 0000000..da802cc Binary files /dev/null and b/src/assets/godot/dungeonCrawler/idleanim.png differ diff --git a/src/assets/godot/dungeonCrawler/inittestscene.png b/src/assets/godot/dungeonCrawler/inittestscene.png new file mode 100644 index 0000000..3f94c1b Binary files /dev/null and b/src/assets/godot/dungeonCrawler/inittestscene.png differ diff --git a/src/assets/godot/dungeonCrawler/inputmappreinputs.png b/src/assets/godot/dungeonCrawler/inputmappreinputs.png new file mode 100644 index 0000000..571ea91 Binary files /dev/null and b/src/assets/godot/dungeonCrawler/inputmappreinputs.png differ diff --git a/src/assets/godot/dungeonCrawler/inputswithbuttons.png b/src/assets/godot/dungeonCrawler/inputswithbuttons.png new file mode 100644 index 0000000..482e710 Binary files /dev/null and b/src/assets/godot/dungeonCrawler/inputswithbuttons.png differ diff --git a/src/assets/godot/dungeonCrawler/layersimg.png b/src/assets/godot/dungeonCrawler/layersimg.png new file mode 100644 index 0000000..af1a7e0 Binary files /dev/null and b/src/assets/godot/dungeonCrawler/layersimg.png differ diff --git a/src/assets/godot/dungeonCrawler/movementscriptsetpimage.png b/src/assets/godot/dungeonCrawler/movementscriptsetpimage.png new file mode 100644 index 0000000..e13c06f Binary files /dev/null and b/src/assets/godot/dungeonCrawler/movementscriptsetpimage.png differ diff --git a/src/assets/godot/dungeonCrawler/playerscenebasic.png b/src/assets/godot/dungeonCrawler/playerscenebasic.png new file mode 100644 index 0000000..de305b3 Binary files /dev/null and b/src/assets/godot/dungeonCrawler/playerscenebasic.png differ diff --git a/src/assets/godot/dungeonCrawler/recttoolimg.png b/src/assets/godot/dungeonCrawler/recttoolimg.png new file mode 100644 index 0000000..3f91051 Binary files /dev/null and b/src/assets/godot/dungeonCrawler/recttoolimg.png differ diff --git a/src/assets/godot/dungeonCrawler/screenshot.png b/src/assets/godot/dungeonCrawler/screenshot.png new file mode 100644 index 0000000..65a65f9 Binary files /dev/null and b/src/assets/godot/dungeonCrawler/screenshot.png differ diff --git a/src/assets/godot/dungeonCrawler/swordsceneinspector.png b/src/assets/godot/dungeonCrawler/swordsceneinspector.png new file mode 100644 index 0000000..ea0b8ab Binary files /dev/null and b/src/assets/godot/dungeonCrawler/swordsceneinspector.png differ diff --git a/src/assets/godot/dungeonCrawler/tilesetphysicslayer.png b/src/assets/godot/dungeonCrawler/tilesetphysicslayer.png new file mode 100644 index 0000000..32d72c1 Binary files /dev/null and b/src/assets/godot/dungeonCrawler/tilesetphysicslayer.png differ diff --git a/src/assets/godot/dungeonCrawler/walkanim.png b/src/assets/godot/dungeonCrawler/walkanim.png new file mode 100644 index 0000000..d5d2fbc Binary files /dev/null and b/src/assets/godot/dungeonCrawler/walkanim.png differ diff --git a/src/assets/godot/dungeonCrawler/wallcolliders.png b/src/assets/godot/dungeonCrawler/wallcolliders.png new file mode 100644 index 0000000..71b1a81 Binary files /dev/null and b/src/assets/godot/dungeonCrawler/wallcolliders.png differ diff --git a/src/assets/godot/dungeonCrawler/weaponscenetree.png b/src/assets/godot/dungeonCrawler/weaponscenetree.png new file mode 100644 index 0000000..642f8c5 Binary files /dev/null and b/src/assets/godot/dungeonCrawler/weaponscenetree.png differ diff --git a/src/assets/godot/dungeonCrawler/worldScene.png b/src/assets/godot/dungeonCrawler/worldScene.png new file mode 100644 index 0000000..a7ae843 Binary files /dev/null and b/src/assets/godot/dungeonCrawler/worldScene.png differ diff --git a/src/components/starlight/Pagination.astro b/src/components/starlight/Pagination.astro index a73b17c..d7ac82c 100644 --- a/src/components/starlight/Pagination.astro +++ b/src/components/starlight/Pagination.astro @@ -1,13 +1,15 @@ --- import Default from '@astrojs/starlight/components/Pagination.astro'; import type { Props } from '@astrojs/starlight/props'; +import { isTutorialEntry } from '../../content/config'; import { getTutorialPages } from '../../util/getTutorialPages'; -import { pages } from '../tutorial/TutorialNav.astro'; +import { allPages } from '../tutorial/TutorialNav.astro'; -const { entry, pagination } = Astro.props; +const { entry, pagination, id } = Astro.props; const { type } = Astro.props.entry.data; let { prev, next } = pagination; +const pages = allPages.filter((page) => isTutorialEntry(page, id)); const tutorialPages = getTutorialPages(pages); if (type === 'tutorial') { diff --git a/src/components/tutorial/ProgressStore.ts b/src/components/tutorial/ProgressStore.ts index cba5c1c..7be16f2 100644 --- a/src/components/tutorial/ProgressStore.ts +++ b/src/components/tutorial/ProgressStore.ts @@ -207,8 +207,6 @@ export class ProgressStore { } private static slugFromPathname(pathname: string) { - // Remove the language segment from the path, - // and strip a trailing slash, if present. - return pathname.split('/').slice(2).join('/').replace(/\/$/, ''); + return pathname.replace(/\/$/, ''); } } \ No newline at end of file diff --git a/src/components/tutorial/TutorialNav.astro b/src/components/tutorial/TutorialNav.astro index c91b716..b066a6f 100644 --- a/src/components/tutorial/TutorialNav.astro +++ b/src/components/tutorial/TutorialNav.astro @@ -1,4 +1,5 @@ --- +import type { Props } from '@astrojs/starlight/props'; import { getCollection } from "astro:content"; import { isTutorialEntry } from '../../content/config'; import { getTutorialPages, getTutorialUnits } from '../../util/getTutorialPages'; @@ -8,13 +9,11 @@ import TabPanel from '../tabs/TabPanel.astro'; import Progress from './Progress.astro'; import UnitProgressIcon from './UnitProgressIcon.astro'; -export interface Props { - id: string; -} - const currentUrl = Astro.url.pathname.replace(/\/$/, ''); +const { id } = Astro.props; + export const allPages = await getCollection('docs'); -export const pages = allPages.filter(isTutorialEntry); +const pages = allPages.filter((page) => isTutorialEntry(page, id)); const tutorialPages = getTutorialPages(pages); const units = getTutorialUnits(tutorialPages); diff --git a/src/content/config.ts b/src/content/config.ts index 847f1f6..a727a0a 100644 --- a/src/content/config.ts +++ b/src/content/config.ts @@ -1,5 +1,6 @@ import { docsSchema, i18nSchema } from "@astrojs/starlight/schema"; import { type CollectionEntry, defineCollection, z } from "astro:content"; +import path from 'node:path'; // find all tutorial pages const baseSchema = z.object({ @@ -24,11 +25,22 @@ export type DocsEntry = CollectionEntry<'docs'> & { type DocsEntryType = DocsEntryData['type']; function createIsDocsEntry(type: T) { - return (entry: CollectionEntry<'docs'>): entry is DocsEntry => entry.data.type === type; + return (entry: CollectionEntry<'docs'>, id: string): entry is DocsEntry => { + if (entry.data.type !== type) { + return false; + } + const currentPath = path.parse(id); + const currentDir = path.dirname(currentPath.dir); + + const pagePath = path.parse(entry.id); + const pageDir = path.dirname(pagePath.dir); + + return pageDir === currentDir; + }; } export type TutorialEntry = DocsEntry<'tutorial'>; -export const isTutorialEntry = createIsDocsEntry('tutorial'); +export const isTutorialEntry = createIsDocsEntry<'tutorial'>('tutorial'); export const collections = { docs: defineCollection({ schema: docsSchema({ extend: docsCollectionSchema }) }), diff --git a/src/content/docs/game-design/godot/dungeoncrawler/0-scenesetup/index.mdx b/src/content/docs/game-design/godot/dungeoncrawler/0-scenesetup/index.mdx new file mode 100644 index 0000000..bb212d0 --- /dev/null +++ b/src/content/docs/game-design/godot/dungeoncrawler/0-scenesetup/index.mdx @@ -0,0 +1,75 @@ +--- +type: tutorial +unitTitle: Making a 2D top-down dungeon crawler +title: Setting up our scene +description: This page works through 2D top-down dungeon crawler step-by-step +sidebar: + order: 2 +--- + +import Checklist from '/src/components/tutorial/Checklist.astro'; +import Box from '/src/components/tutorial/Box.astro'; +import { Steps } from '@astrojs/starlight/components'; + +This is a guide to making a 2-dimensional dungeon crawling game in [Godot](https://godotengine.org/). If you are unfamiliar with Godot, check out the [Godot basics](/game-design/godot/basics) doc as this tutorial assumes basic knowledge of navigating and using the Godot Engine. + +:::note[Version] +This guide is up-to-date with Godot 4.3 stable official release, and will likely work with any version of Godot newer than 4.3. Due to the use of **TileMapLayers** which were introduced in 4.3, this tutorial isn't compatible with any version pre 4.3 +::: + +## What you'll be making + +![Screenshot preview of the game](/src/assets/godot/dungeonCrawler/screenshot.png) + +In this tutorial, you'll work step by step through creating your very own Dungeon Crawler! In this game, the player will navigate through a multi-level dungeon of your design, full of enemies to fight, and treasure to collect! + +You'll learn to: +- Create an animated player character +- Create enemies that chase and attack the player +- Create a User Interface that tracks health and points +- Switch levels +- Design 2D Combat + +Let's jump right in! + +## Making the project + +:::note[Godot Documentation] +Godot Documentation for nodes discussed in this section: + +[Importing](https://docs.godotengine.org/en/stable/tutorials/assets_pipeline/import_process.html) +::: + + +We won't be creating our own assets as part of this project, we'll instead be using a free asset pack by Ox72 on Itch.IO which [can be found here](https://0x72.itch.io/dungeontileset-ii) Just click *Download now* followed by *No thanks, just take me to the download* and download the file called *0x72_DungeonTilesetII_v1.7.zip* + +Create a new 2D project, using the Forward+ Renderer. + +Let's start by importing our assets! + + + +1. First let's create a new folder, and call it 'Assets' + Then, let's extract the assets from the folder we downloaded, and at them into our Assets folder. Mine looks like this, but it's fine if yours looks slightly different. + + ![Assets](/src/assets/godot/dungeonCrawler/assets.png) + +2. Let's also create two new top level folders called 'Scripts' and 'Scenes' + + ![Folders](/src/assets/godot/dungeonCrawler/folders.png) + +3. To ensure our pixel art assets look crisp and not blurred we'll want to make one quick change. + Using the buttons in the top left of the screen, select **Project -> Project settings** In this menu, select the **General** tab, and scroll until you see the **Rendering** header. Under this, select **Textures** + Change **Default Texture Filter** from **Linear** to **Nearest** + + ![Folders](/src/assets/godot/dungeonCrawler/TextureFilter.png) + + + +## Checklist + +- [ ] I have imported the assets +- [ ] I have setup my folders and changed the settings +- [ ] I'm ready to make a game! + + \ No newline at end of file diff --git a/src/content/docs/game-design/godot/dungeoncrawler/1-player/index.mdx b/src/content/docs/game-design/godot/dungeoncrawler/1-player/index.mdx new file mode 100644 index 0000000..a15dc3d --- /dev/null +++ b/src/content/docs/game-design/godot/dungeoncrawler/1-player/index.mdx @@ -0,0 +1,149 @@ +--- +type: tutorial +unitTitle: Creating our player +title: Setting up our player. +description: Creating our player scene and scripts +--- + +import Checklist from '/src/components/tutorial/Checklist.astro'; +import Box from '/src/components/tutorial/Box.astro'; +import { Steps } from '@astrojs/starlight/components'; + +## Creating the Player + +:::note[Godot Documentation] +Godot Documentation for nodes discussed in this section: + +[Collision Shapes](https://docs.godotengine.org/en/stable/tutorials/physics/collision_shapes_2d.html) [CharacterBody2D](https://docs.godotengine.org/en/stable/classes/class_characterbody2d.html) [AnimatedSprite2D](https://docs.godotengine.org/en/stable/classes/class_animatedsprite2d.html) [Area2D](https://docs.godotengine.org/en/stable/classes/class_area2d.html) +::: + + +Great! Let's start by making a basic version of our player character that will let us move around. We'll worry about more complicated things like attacking later. + + + + +1. Start by creating a new 2D Scene, with a **CharacterBody2D** as the root node. Call it 'Player' + +2. Give it two children: + A **CollisionShape2D** and an **AnimatedSprite2D** + + + +Here's how my scene looks with no other modifications: + +![Basic player scene](/src/assets/godot/dungeonCrawler/playerscenebasic.png) + +Let's hit **Ctrl + S** and save this scene in our **Scenes** folder, call it **Player.tscn** + +## Animations + +Let's give ourselves something to look at! + +1. Click on the **AnimatedSprite2D** and in the inspector, under **Animation** you'll see **\** in the **Spriteframes** field. + +2. Click on **\** and create a new **Spriteframes** Click on the **Spriteframes** you created. This will open a new window at the bottom of the screen. + This is where we'll create our player's animations. Rename the *General* animation to *Idle* and click the *Add frames from File* Button (The folder icon) + +3. Navigate to your *assets/frames* folder, and decide which character you want to be your player. I'll be using the Plague Doctor. Using Shift + Click select all the frames for your character labeled *Idle* (This should be four frames) then open them. + + ![Idle Animation Frames](/src/assets/godot/dungeonCrawler/idleanim.png) + + You'll see them added to the animation timeline. + +4. We'll want to select two things in the timeline. The **Loop** Button (To ensure the animation loops) and the **Play on start button** (To ensure the animation plays automatically) + Let's also increase the FPS to 8 so that the animation plays a little faster. + + ![Idle Animation Frames](/src/assets/godot/dungeonCrawler/autoloop.png) + + Hit play to test! You'll see the player now has an idle animation that loops! + +5. Let's add our character's walking animation. Add a new animation using the **Add animation** Button and call it "Walk" + + ![Idle Animation Frames](/src/assets/godot/dungeonCrawler/addanim.png) + + Do the same thing we did to grab the frames for the idle animation, but this time, grab all the frames labeled "Run" it should again be 4 frames. We want this animation to loop, but we **don't** want it to autoplay. Let's also give this a framerate of 8 FPS. + + ![Walk Animation Frames](/src/assets/godot/dungeonCrawler/walkanim.png) + + Finally, just select your idle animation again, to make sure this is what our player will start on. + + Great! That's our animations all done! + + +## Collision + +Now that we have something to look at, let's give our player a hitbox. + +1. Open the inspector for the **CollisionShape2D** we added, and add a new shape in the **empty** shape field. + It's a good idea to use a **CapsuleShape** as it will make us less likely to get stuck on corners. Position and adjust the capsule so that it's *slightly* smaller than the sprite for our player. + Mine looks like this: + + ![CollissionShape](/src/assets/godot/dungeonCrawler/collshapeimg.png) + +2. Great! Our player now has collision. We'll do one more thing while we're here, which is give our player a script to handle movement. Right click on the **CharacterBody2D** and Attach a script. Call it something like "player.gd" and make sure we're saving it in our Scripts folder. + We also need to make sure we **untick** the **template** box as we will not be using the template! This is because the template is designed for gravity based platformers. + + +With our script created and attached, let's get to programming our movement! + +## Movement + + + +1. First, let's set up a variable to control our speed. + + ```gdscript + extends CharacterBody2D + + @export var speed = 200 + ``` + + The **\@export** tag will allow us to easily edit our speed variable, without needing to open the script! + +2. Then, we'll want to use Godots built-in **_physics_process(delta):** function for our movement logic. Inside that we'll want to get the combined vector of all the inputs the player is pressing. + + ```gdscript + func _physics_process(delta): + var direction = Input.get_vector("ui_left", "ui_right", "ui_up", "ui_down") + ``` + +3. We'll then add some lines to multiply this vector by our speed, and then invoke Godots built-in **move_and_slide()** function, which actually does the moving! + + ```gdscript + func _physics_process(delta): + var direction = Input.get_vector("ui_left", "ui_right", "ui_up", "ui_down") + + velocity = direction * speed + + move_and_slide() + ``` + +4. giving us a final script that looks like this: + + ```gdscript + extends CharacterBody2D + + @export var speed = 200 + + func _physics_process(delta): + var direction = Input.get_vector("ui_left", "ui_right", "ui_up", "ui_down") + velocity = direction * speed + + move_and_slide() + ``` + +For now our movement is controlled using the arrow keys, but we'll go over how to map it to whatever we want later in the guide. + +Switch back to 2D view at the top of the screen. + +And that's our player ready to go for now! + + +## Checklist + +- [ ] I've created the player scene +- [ ] I've setup the animations +- [ ] I've attached the script + + \ No newline at end of file diff --git a/src/content/docs/game-design/godot/dungeoncrawler/2-level/index.mdx b/src/content/docs/game-design/godot/dungeoncrawler/2-level/index.mdx new file mode 100644 index 0000000..1ae5792 --- /dev/null +++ b/src/content/docs/game-design/godot/dungeoncrawler/2-level/index.mdx @@ -0,0 +1,135 @@ +--- +type: tutorial +unitTitle: Creating our level +title: Building a level +description: Setting up a basic test level +--- + +import Checklist from '/src/components/tutorial/Checklist.astro'; +import Box from '/src/components/tutorial/Box.astro'; + +## Level Scene + +:::note[Godot Documentation] +Godot Documentation for nodes discussed in this section: + +[TileMapLayer](https://docs.godotengine.org/en/stable/classes/class_tilemaplayer.html) +::: + +import { Steps } from '@astrojs/starlight/components'; + +Let's move onto giving us something to walk around on! + + +1. Create a new scene. Give it a node2D as its root node, and call it "World" + + Save the scene in our Scenes folder, calling it "world.tscn". + +2. Give our root node a child of type **Node2D** and call it "level" + +3. To this node, add two children of type **TilemapLayer** + + call the top one "floor" and the bottom one "walls" + +4. Let's start with the "Floor" layer, as it'll be slightly more simple, and we'll redo all the steps for the "walls" layer. + + Click on the "floor" Node, and in the inspector, you'll see **Tile Set - Empty** click on **Empty** and create a **new Tileset** then click on the newly created **Tileset** + + You'll notice two new tabs have appeared at the bottom of the screen **TileSet** and **TileMap** we'll be working with both of these, but open the **TileSet** tab first. + + using the **+** button in the lower left, navigate to your assets, and load in the "atlas_floor-16x16.png" hit **Yes** when prompted. + + You can think of this as a palette we'll use to pain our level! Our dungeon will be made up from a series of tiles that we can arrange however we want! + + + +:::note[Decorations] + We won't be implementing the spikes/buttons/levers. So if you add these to your level they'll be purely decorative.** +::: + + +Now, click on the **floor** layer and follow the same steps, except this time loading in the "atlas_walls_low-16x16.png" file. + +With our **Tilesets** setup we can now go to the **TileMap** tab. + +From here, if you click on a tile and then in the scene, you'll notice you're able to paint them into the scene! This is how we'll create our levels! you can click between the floor and wall **Tilemaps** to get access to the different tiles and to paint the different layers. + +:::note[Layers] +Why do we have a different layer for the two? We'll want our walls to have collision, but not our floors! So we have them on different **TilemapLayer** nodes. +::: + +Spend some time and get familiar with painting and removing tiles! Aim to create a single room that we can use to test our game. + +:::tip +If you find you can't paint, make sure you're on the **TileMap** tab and NOT the **TileSet** tab (confusing, I know) +::: + +:::tip + You can use the Rect tool to draw a rectangle of tiles all at once + +![Rect tool](/src/assets/godot/dungeonCrawler/recttoolimg.png) + +You can use the Erase tool to remove tiles, just remember to click it again when you want to start painting again + +![Rect tool](/src/assets/godot/dungeonCrawler/eraseTool.png) +::: + +Here's what my room looks like: + +![Basic Level](/src/assets/godot/dungeonCrawler/basicroomimg.png) + +:::note[Testing] +Don't go too crazy on your level design just now! This first area will just be to test! +::: + +## Adding our player to the level + +Now that we have a basic level set up, let's add our player to it so we can start to properly test our game! To do this, we'll just drag our player scene from the **Scenes** folder +in the file browser, into our **scene tree** as a child of the root node! Finally, we'll give the player a child of type **Camera2D.** here's what my scene looks like: + +![Initial Scene](/src/assets/godot/dungeonCrawler/inittestscene.png) + +Don't worry if the names of your nodes are different, leave them as they are for now! + +You should be able to move around! Though you'll quickly notice you're able to walk through walls, which isn't ideal, so let's fix that! + +## Level collision + +1. Click on your **Walls TilemapLayer** Node, Then, navigate to the inspector. Click the **TileSet** Object at the top of the inspector. + +2. Under the **Physics Layers** drop-down, click **Add Element.** + +3. Here you'll see the **Collision Layer** section, make sure **1** and **3** are selected. For the **Collision Mask** section, only **1** should be selected. + +4. Now, back in the section at the bottom of the screen, navigate to the **Paint** tab, and using the drop-down, select **Physics Layer 0.** Here's how things look for me! + + ![Physics Layers](/src/assets/godot/dungeonCrawler/tilesetphysicslayer.png) + + Think of the blue square that has appeared under the **Paint** tab as our brush that we'll use to paint collisions onto our **Tileset** with the blue square representing where exactly our player will collide. + + + You can click and drag the white diamonds to move them, and click on the edges to add new points. pressing F (Full) will make the collider occupy the whole square, and pressing C (Clear) will make it occupy none of it. + +5. using this, try to create colliders for each tile that closely match their sprite. + + :::tip + It'll be easiest to do them in batches of tiles that have the same collision! + ::: + + Here's how mine look: + + ![Wall colliders](/src/assets/godot/dungeonCrawler/wallcolliders.png) + + + +Now try playing your game again! You'll notice you actually collide with the walls! Great! + + +## Checklist + +- [ ] I've set up the Tilemaps +- [ ] I've added collision +- [ ] I've made a basic room +- [ ] I've added my player to the game + + \ No newline at end of file diff --git a/src/content/docs/game-design/godot/dungeoncrawler/3-playerimprovement/index.mdx b/src/content/docs/game-design/godot/dungeoncrawler/3-playerimprovement/index.mdx new file mode 100644 index 0000000..e85740d --- /dev/null +++ b/src/content/docs/game-design/godot/dungeoncrawler/3-playerimprovement/index.mdx @@ -0,0 +1,199 @@ +--- +type: tutorial +unitTitle: Improving our player +title: Player Improvement +description: Upgrading our player scene +--- + +import Checklist from '/src/components/tutorial/Checklist.astro'; +import Box from '/src/components/tutorial/Box.astro'; +import { Steps } from '@astrojs/starlight/components'; + + +:::note[Godot Documentation] +Godot Documentation for nodes discussed in this section: + +[Input Map](https://docs.godotengine.org/en/stable/classes/class_inputmap.html) [AnimatedSprite2D](https://docs.godotengine.org/en/stable/classes/class_animatedsprite2d.html) +::: + + +Let's spend some time further improving our player. Let's implement some different controls (Well be doing WASD, but you can using any controls you like) +make sure our animation changes to our walking animation when we move, and make sure our sprite faces in the direction we're moving. + +We'll also be implementing attacking, but we'll do that in its own section! + +## Input mapping + + +1. To change our inputs, we'll first need to set up our **Input Map** to do this, we'll navigate to **Project -> Project Settings** in the top left of the workspace. + +2. Then, open the **Input Map** tab. + +3. We'll add our new Inputs by typing a name for each of them in the **Add New Action** box and hitting add. + + I'll be calling mine "Up" "Down" "Left" "Right" and i'll also add one called "Attack" + + ![Input Actions](/src/assets/godot/dungeonCrawler/inputmappreinputs.png) + +4. To add buttons to these, we'll hit the "+" to the right of each action, we'll then physically press the key we want to assign on our keyboard, and hit "Ok" + + For my **Attack** action i'll be using **Left Click* for which you'll want to physically click *once* inside the **Listening for input** box. + + Here's what my inputs look like: + + ![Input Actions with buttons](/src/assets/godot/dungeonCrawler/inputswithbuttons.png) + + If you're happy with your inputs, you can then hit **Close** at the bottom of the screen. Let's navigate back to our **player_movement.gd** script. This can be done by opening our **player** scene, clicking on the **CharacterBody2D** and clicking the **Script** tab at the top of the screen. + +5. To change our inputs, all we need to do is change the predefined inputs to what we called ours! + + :::Note[Case Sensitivity] + What you named your input actions *is* case sensitive + ::: + + So, "ui_left" becomes "Left" + "ui_right" becomes "Right" and so on! + + leaving that line in our script looking like this: + + ```gdscript + var direction = Input.get_vector("Left", "Right", "Up", "Down") + ``` + +if you run your game, you'll notice your inputs are now changed! + +We won't assign our Attack option just yet, but it's good we've created it already. + + +## Animation switching + +To change our animation depending on if we're moving or not, we'll only need to add a few lines to our script. But first, we'll need a reference to our **AnimatedSprite2D** in our script. + +We can do this easily, by **Clicking** and **Dragging** our **AnimatedSprite2D** into our script, making sure we hold **Ctrl** on the keyboard after grabbing it, but before dropping it. + +We'll want to drop it below the speed variable declaration + +```gdscript +@export var speed = 200 +@onready var animated_sprite_2d = $AnimatedSprite2D +``` + +if it doesn't look like this, delete the line and try again, making sure you're holding **Ctrl** on your keyboard after picking it up. + +Great! Let's get to changing our animation! + + + +1. Under where we set our velocity, we'll simply add a check to see if our velocity is anything other than 0 and change our animation based on that! + + We can check this simply with: + + ```gdscript + if velocity != Vector2.ZERO: + ``` + :::note[Vector2s] + Why can't we just do velocity != 0? This is because velocity actually contains both our X (Horizontal) and Y (Vertical) velocity, so we need to make sure **Both** aren't zero. + ::: + +2. Then, we can play our run animation! + + ```gdscript + if velocity != Vector2.ZERO: + animated_sprite_2d.play("walk") + ``` + +3. If we're not moving, let's play our idle + + ```gdscript + else: + animated_sprite_2d.play("idle") + ``` + +4. Giving us a movement script that looks like this: + + ```gdscript + extends CharacterBody2D + + @export var speed = 200 + @onready var animated_sprite_2d = $AnimatedSprite2D + + func _physics_process(delta): + var direction = Input.get_vector("Left", "Right", "Up", "Down") + velocity = direction * speed + if velocity != Vector2.ZERO: + animated_sprite_2d.play("walk") + else: + animated_sprite_2d.play("idle") + + move_and_slide() + ``` + + +Play your game, and you'll notice your animation changes when you move! + +## Sprite facing + +To flip our player based on the direction we're moving, all we really need to do is add another If to check only the **Horizontal** part of our velocity, as thankfully the **AnimatedSprite2D** node has a built-in way to flip our sprite! + +I'll be adding this section **beneath** the animation section, but before the **move_and_slide()** function call. + + + +1. Let's start by checking the Horizontal portion of our movement: + + ```gdscript + if(velocity.x < 0): + ``` +2. If our x velocity is less than 0 (moving to the left) we want to flip our sprite. + + ```gdscript + animated_sprite_2d.flip_h = true + ``` + +3. And if it's greater than 0, we'll unflip it + + ```gdscript + elif(velocity.x > 0): + animated_sprite_2d.flip_h = false + ``` + +4. Giving us a movement script that looks like this: + + ```gdscript + extends CharacterBody2D + + @export var speed = 200 + @onready var animated_sprite_2d = $AnimatedSprite2D + + func _physics_process(delta): + var direction = Input.get_vector("Left", "Right", "Up", "Down") + velocity = direction * speed + + if velocity != Vector2.ZERO: + animated_sprite_2d.play("walk") + else: + animated_sprite_2d.play("idle") + + if(velocity.x < 0): + animated_sprite_2d.flip_h = true + elif(velocity.x > 0): + animated_sprite_2d.flip_h = false + + move_and_slide() + ``` + :::note[Flipping] + Why didn't we juse use **else:**? - If we'd just used **Else:** our sprite would have jarringly flipped to face left whenever we stopped moving, or if we were just moving up and down. + This way the direction our sprite is facing doesn't change unless we move either left or right! + ::: + + +Test your game, and you'll notice your sprite now faces left or right depending on the direction we're facing! + + +## Checklist + +- [ ] I've setup the Inputs +- [ ] My sprite flips from left to right +- [ ] My sprite's animations change + + \ No newline at end of file diff --git a/src/content/docs/game-design/godot/dungeoncrawler/4-weapon/index.mdx b/src/content/docs/game-design/godot/dungeoncrawler/4-weapon/index.mdx new file mode 100644 index 0000000..181bcd8 --- /dev/null +++ b/src/content/docs/game-design/godot/dungeoncrawler/4-weapon/index.mdx @@ -0,0 +1,164 @@ +--- +type: tutorial +unitTitle: Adding a weapon +title: The Weapon +description: Implementing the weapon scene and script +--- + +import Checklist from '/src/components/tutorial/Checklist.astro'; +import Box from '/src/components/tutorial/Box.astro'; +import { Steps } from '@astrojs/starlight/components'; + + +:::note[Godot Documentation] +Godot Documentation for nodes discussed in this section: + +[Animation Player](https://docs.godotengine.org/en/stable/classes/class_animationplayer.html) [Sprites](https://docs.godotengine.org/en/stable/classes/class_sprite2d.html) [Area2D](https://docs.godotengine.org/en/stable/classes/class_area2d.html) +::: + +Let's give our player something to attack with! + +## Weapon Scene + +Let's make our weapon! + +1. Create a new scene for our weapon! + Give it a Root node of a **Node2D** and name it "Weapon" + +2. Give it two children, a **Sprite2D** and a **Animationplayer** + Give the **Sprite2D** a child of type **Area2D** and give the **Area2D** a child of **CollisionShape2D** + + Here's how my scenetree looks: + + ![Weapon scenetree](/src/assets/godot/dungeonCrawler/weaponscenetree.png) + + Make sure you save the scene, calling it "Weapon.tscn" + +3. Let's start by picking a sprite for our weapon, so we can see what we're working with! + Select the sprite node, and in the **Empty** texture field, select Load. Navigate to your assets folder, and select a weapon that you like! + + Don't worry that it's pointing up, we'll rotate it in a minute. + +4. Let's open the **CollisionShape2D** node and assign a shape, you'll probably just want to use a rectangle shape. This will be the hitbox of the sword, and determine whether an enemy has been hit! Adjust its bounds so that it vaguely matches the sprite. + Although you'll likely want to make it a little bigger than the sprite, as we don't want our game to feel like the player has to be too precise. + +5. let's rotate the **Sprite2D** node. Do this by selecting the node in the scenetree, navigating to the inspector, opening the **Transform** tab and changing the rotation to **90** + + Let's also offset it's position a little, to help it rotate around our player smoothly. Set its x-position to **20px** + + Here's how my sword scene and inspector look: + + ![Sword Scene Inspector](/src/assets/godot/dungeonCrawler/swordsceneinspector.png) + +6. Before we move on, navigate to the inspector tab of the **Area2D** node and find the **Collision** tab, under **Layer** make sure nothing is selected. Under **Mask** Make sure *only* 2 is selected. + + +## Weapon Script + +1. Let's create a script to control our weapon, create it using the default template and attach it to the **root node2d,** call it "weapon.gd" + + We'll need this script to do two things: + + 1. Rotate around our player, facing the mouse. + 2. Play the animation when we press our attack input. + +2. The first step will be nice and easy! In the **process** function, add the line + ```gdscript + look_at(get_global_mouse_position()) + ``` + This will cause this node to always face toward the position of the mouse! + +3. Next, we'll need to get a reference to our **AnimatedSprite2D** node, we'll do this the same way we did for the **animated_sprite_2d** node for the player' by clicking, dragging, and then holding ctrl before we let go of the click. + + We'll also want a variable that keeps track of if we're *currently* attacking, as we don't want the sword to destroy enemies when we haven't attacked. + + your script should look like this: + + ```gdscript + extends Node2D + + @onready var animation_player = $AnimationPlayer + @export var attacking : bool = false + + func _process(delta): + look_at(get_global_mouse_position()) + ``` + +4. Now, let's add a check to see if we've just used the **Attack** input action we created earlier, and play the animation we'll create next. This is all stuff we've done earlier, so this should be pretty easy. Here's what it'll look like! + + Making sure that the Input action name, and animation name match, including case sensitivity. + + + ```gdscript + extends Node2D + + @onready var animation_player = $AnimationPlayer + + @export var attacking : bool = false + + func _process(delta): + look_at(get_global_mouse_position()) + + if Input.is_action_just_pressed("Attack"): + animation_player.play("Attack") + ``` + + +## Sword Animation + +Now, let's start creating our attack animation! + +1. Navigate to the **Animationplayer** and click **Animation** and create a new animation. Call it "Attack" + +2. Now that we've created a new animation, we'll need to create an **Animation Track** which you can do by clicking **Add track.** This will ask us what type of Animation Track we want to create. + + We want to animation the **Position** of our weapon, this is a **Property** so we'll create a **Property** Track. + + Then, when prompted, we'll select the **Sprite2D** as this is what we want to change the position of! + + Finally, scroll until you see the **Position** property. + + Phew! That was quite a few steps, but our animation track is created! + +3. Now we need to determine the keyframes our sword will animate between. We'll need three: The first being the start position, the second being the extent of the attack, and the third being returning to the start position. + + To add a keyframe, right click on the animation track. (In the **Position** Row) and press **Insert Key** do this until you have three keyframes. If we select each keyframe, we can modify their values! + + The first and last keyframe, we want to be at **Time** 0 and 1.0 respectively, with their values being unchanged (Remember, we want both of these to represent the sword at rest) + + Our second keyframe, we'll for now put at a **Time** of 0.5. Let's however, set its **x-value** to 30. + +4. Great! Let's hit the play button on the animation, and you'll notice we have a simple stabbing animation. But it's a little slow... We can fix this by adjusting the total length of the animation, this can be done over on the right. + + Change the length from 1.0 to 0.5. We'll then need to adjust our keyframes, adjusting the middle one to be at a **Time** of 0.25, and the last to be at 0.5. + + Play it again, and you'll notice it's much faster! + +5. We'll want to add another **Property Animation Track** this time to modify the **attacking** variable, to keep track of if our weapon is "Active" or not. + + Add a new animation track, select property, and you should see "attacking" right at the top! Add two keyframes, one right at the start, and one right at the end. The first we'll want to set the value to "on" (as we're now attacking) and the one at the end will set the property back to "off" (or unticked) + + +And that's it! We'll come back later to add the code for destroying enemies. + +Great! Let's get our weapon added to our player! + +## Adding our weapon to our player + +Navigate to your player scene. From the filesystem, drag in your weapon scene (called **weapon.tscn**) and attach it as a child of the main +**CharacterBody2D** node. + +Play your game, and you should hopefully have a weapon that rotates around the player and stabs when you click it! If it doesn't seem to be rotating around the middle of the player sprite, feel free to adjust its position within the Player scene. + +You're also welcome to adjust the size of the weapon (Although, you're best to do this within the weapon scene itself) it's your game after all! + + + +## Checklist + +- [ ] I've created the weapon scene +- [ ] I've created the weapon script +- [ ] My sword sprite has an animation +- [ ] I've added the sword to my player scene + + \ No newline at end of file diff --git a/src/content/docs/game-design/godot/dungeoncrawler/5-health/index.mdx b/src/content/docs/game-design/godot/dungeoncrawler/5-health/index.mdx new file mode 100644 index 0000000..3e8f1eb --- /dev/null +++ b/src/content/docs/game-design/godot/dungeoncrawler/5-health/index.mdx @@ -0,0 +1,369 @@ +--- +type: tutorial +unitTitle: Health System +title: Player Health +description: Implementing the player health system +--- + +import Box from '/src/components/tutorial/Box.astro'; +import Checklist from '/src/components/tutorial/Checklist.astro'; +import { Steps } from '@astrojs/starlight/components'; + + +:::note[Godot Documentation] +Godot Documentation for nodes discussed in this section: + +[Signals](https://docs.godotengine.org/en/stable/getting_started/step_by_step/signals.html) [User Interface](https://docs.godotengine.org/en/stable/tutorials/ui/index.html) [Timer](https://docs.godotengine.org/en/stable/classes/class_timer.html) [Labels](https://docs.godotengine.org/en/stable/classes/class_label.html#class-label) [Groups](https://docs.godotengine.org/en/stable/tutorials/scripting/groups.html) +::: + + +## Giving our player Health + +Let's start setting up our health system on the player side! + +1. We'll start by adding a new Node to our player Scene, as a child of the root **CharacterBody2D** node. The node should be of type **Area2D** + give the **Area2D** a child of type **CollisionShape2D** rename the Area2D to "Hitbox." + + In the **Inspector** of the **Area2D** Navigate to the **Collision** section. Deselect all numbers under the **Layer** Section, and ensure *only* 2 is selected under the **Mask** Section. + + Give the **CollisionShape2D** a shape, ideally a rectangle or circle, and make it *slightly* bigger than the shape for the **CharacterBody2D's** **CollisionShape2D** + +2. Let's give the **Area2D** one more child of type **Timer** and name it "damageTimer" this timer will be used to determine how quickly we can take damage again after being hurt, think of it as invulnerability time! + In the timer's inspector, set its **Wait Time** field to 0.5s + + Great! this **Area2D** will be used to detect collision with enemies, potions, and coins! For now we'll just be setting it up to handle health, both healing and damage. + +3. Let's attach a script to the **Area2D** (that we named "Hitbox") and call it "hitbox.gd" + +4. This script is going to get a little complicated, as it's going to have to handle quite a few things. In a bigger project we would want to break its functionality up into multiple scripts, but for our scope this is fine! + Let's break down what we need it to do: + + 1. Track our health + 2. Detect collisions with potions/enemies/coins + 3. Change our health value + 4. Update the UI + +5. Let's start by creating the variables we'll need, those being: + + 1. The signal we'll use to talk to the UI when our health has changed + 2. While we're here, a signal for when we've collected a coin, as it'll also talk to the UI + 3. A max health, and current health value + 4. A reference to the timer we created. + 5. A boolean determining if we can take damage + + These should look something like this, using the same click + drag + ctrl method to get the reference to the **Timer** + + ```gdscript + signal on_health_changed(new_health : int) + signal on_point_gained + @export var max_health : int = 6 + @onready var damage_timer = $damageTimer + var health : int + var can_take_damage : bool = true + ``` + + :::note[\@export] + We can use the **\@export** to modify the max health value, without editing the script! + ::: + + :::note[Health] + Why are we starting at 6 health? We're doing this because each point of health will represent half a heart to the UI, for a total of 3 full hearts! Using ints like this is safer than using a float, + because what if we somehow end up with 0.001 health! + ::: + +6. In our **_ready** function we'll want to set some default values, and emit the health_changed signal to send our starting health to the UI. + + ```gdscript + func _ready(): + damage_timer.connect("timeout", allow_damage) + health = max_health + emit_signal("on_health_changed", health) + ``` + + + +1. Right, let's get onto the most complicated function in the script, the function for taking damage! In this function, there'll be a few different possible outcomes. + + 1. We can't take damage as we're currently immune. In this case. Nothing happens + 2. Otherwise we'll take damage. Emitting the signal to change the UI. + 3. If we do take damage, we might be reduced to 0 hitpoints + 4. If we are, we die! If we're not. We start our immunity timer. + +2. Great! Let's write that in gdscript: + + ```gdscript + func take_damage(): + if can_take_damage: + health = health - 1 + emit_signal("on_health_changed", health) + + if health <= 0: + get_tree().paused = true + else: + can_take_damage = false + damage_timer.start() + ``` + +2. You'll notice we connected our timer to a function called **allow_damage()** which doesn't exist, let's create that now. All it's going to do is set the **can_take_damage** boolean to **True** as unfortunately Godot doesn't let you assign a value to a variable directly via the **connect()** function. + + ```gdscript + func allow_damage(): + can_take_damage = true + ``` + +3. Next we'll do healing! This one is *much* easier. We just need to check if we have room to be healed (Our health is less than our max health). If we do, increase our health by 1. Then emit the signal to update the UI! We'll also want to delete the potion, so it can no longer be used. + + ```gdscript + func heal(body): + if health < max_health: + health = health + 1 + emit_signal("on_health_changed", health) + body.queue_free() + ``` + +4. Finally, we need a function that calls our **heal** and **damage** functions based on what we've collided with. To check what type of object we've collided with, we'll be using **Groups!** These are something we'll assign to our enemies/potions/coins later. + + To check if something has collided with us, we'll need to the **on_body_entered** signal! To connect this, swap to the **Node** tab of the **Inspector** and click on the **Area2D** node in the **SceneTree** again. You'll see a list of all the signals we have available to us! + Click the **on_body_entered** signal and press **Connect** select the **Area2D (hitbox)** node and click **Connect** + + You'll see a new function appear in our script! On that'll be called whenever something enters this **Area2D** + + In here, we can check the **Group** of what we've collided with! Let's also add a check to see if we've collected a coin here, to save us some time later! + + ```gdscript + func _on_body_entered(body): + if body.is_in_group("enemy"): + take_damage() + elif body.is_in_group("health"): + heal(body) + elif body.is_in_group("coin"): + emit_signal("on_point_gained") + body.queue_free() + + ``` +5. that's it! Giving us a full script that looks something like this: + + ```gdscript + extends Area2D + + signal on_health_changed(new_health : int) + signal on_point_gained + @export var max_health : int = 6 + @onready var damage_timer = $damageTimer + var health : int + var can_take_damage : bool = true + + # Called when the node enters the scene tree for the first time. + func _ready(): + damage_timer.connect("timeout", allow_damage) + health = max_health + emit_signal("on_health_changed", health) + + func _on_body_entered(): + if body.is_in_group("enemy"): + take_damage() + elif body.is_in_group("health"): + heal(body) + elif body.is_in_group("coin"): + emit_signal("on_point_gained") + body.queue_free() + + + func take_damage(): + if can_take_damage: + health = health - 1 + emit_signal("on_health_changed", health) + + if health <= 0: + get_tree().paused = true + else: + can_take_damage = false + damage_timer.start() + + func heal(body): + if health < max_health: + health = health + 1 + emit_signal("on_health_changed", health) + body.queue_free() + + func allow_damage(): + can_take_damage = true + ``` + + :::note[Signals] + If you copy and paste the above, you'll still need to manually connect the **on_body_entered** signal! + ::: + + + +And that's the player side of health done! Let's move onto the UI side! + +## Health UI + +### UI Setup + +Time to start making some UI! + +1. Let's make a new scene, of, as you may have guessed, type **User Interface.** call the Root node "UI" + +2. Add a child of type **HBoxContainer** This is a UI element that will neatly arrange our UI elements, in this case our hearts, horizontally! + + Let's open its inspector, navigate to the **Layout** tab and change it to "Anchors." Then change the **Anchor Preset** to "top left." This will make sure that whatever the size of our screen is, the health will always be pinned to the top left! + + Rename the node to "healthContainer." When we add hearts, this will be their parent Node, controlling their position on the screen and in relation to one another. (Like making sure they don't overlap) + +3. Let's also, to the **Root** node, and a child of type **Label** call it "diedLabel" + + In the inspector for this label, in the **Text** box, write "You Died!" then look for the **Theme Overrides** section, open this, find **Font Size** and change this to about 35px. + +4. Next, in the 2D view of the UI, move the Text so that it's where you want it, I placed mine in the middle of the screen. Then, as we want this to be hidden by default, click the little 'eye' icon next to the label in the **SceneTree** to hide it! + + +Great! That's all the UI setup we'll need to do for now (Though we'll come back to it later for points) + +### UI Scripting + + +1. Add a script to the root node, calling it "ui.gd" + + Let's think about what we need this script to do: + 1. Store our three different heart images + 2. Recieve signals from our player when we take damage + 3. Update our health UI. + + We'll set this up so that it automatically adjusts depending on the players **max_health** when the game is run, so you can easily have more (or less) than three hearts! + (Or, you could implement an item that increases your max hp!) + +2. Thankfully, we can reference images in our filesystem the same way we can reference nodes, with the **Drag + Ctrl + Release** technique we've been using! We'll want: The full heart image, the half heart image, and the empty heart image + Find these in your filesystem, and drag in the references, it should look something like this: + + ```gdscript + const UI_HEART_EMPTY = preload("res://Assets/frames/ui_heart_empty.png") + const UI_HEART_FULL = preload("res://Assets/frames/ui_heart_full.png") + const UI_HEART_HALF = preload("res://Assets/frames/ui_heart_half.png") + ``` + +3. Let's also get a reference to our **healthContainer** node, our **diedLabel** node, and create a variable to keep track of the most health we've had so far (This lets us know how many empty hearts to have!) + We won't set this variable here, as it'll be set by whatever the most health we've had so far has been. + + ```gdscript + @onready var health_cont = $healthContainer + @onready var died_label = $diedLabel + var maxHealth : int = 0 + ``` +4. Next will be the function that our signal will call, where most of the logic will happen, so let's think about what we need it to do! + + If our health is set to 0, show the "You died text" Otherwise, If the health received is bigger than our highest health so far, make that our new highest health. Easy enough! + + ```gdscript + func changed_health(newHealth : int): + if(newHealth == 0): + died_label.visible = true + + if newHealth > maxHealth: + maxHealth = newHealth + ``` + +5. We'll want to check if we have enough hearts currently to represent that, if we don't, we'll need to add some more. (We'll create the function for this last) + + ```gdscript + if(maxHealth/2 > health_cont.get_child_count()): + for h in (maxHealth/2) - health_cont.get_child_count(): + add_heart() + ``` + +6. We'll iterate through all the children our **healthContainer** node has, and assign an image based on the current health. + + This section may look complicated, but once you get your head around it, it's fairly simple! Spend some time looking over it, and thinking about the conditions for each heart to be drawn. + When I was figuring out how to program this, I found it useful to draw out the hearts on paper, at different levels of health! + + ```gdscript + for i in health_cont.get_child_count(): + if (i * 2) + 1 < newHealth: + health_cont.get_child(i).texture = UI_HEART_FULL + elif (i * 2) < newHealth: + health_cont.get_child(i).texture = UI_HEART_HALF + else: + health_cont.get_child(i).texture = UI_HEART_EMPTY + ``` + +7. Giving us a full **changed_health** function that looks like this: + + ```gdscript + func changed_health(newHealth : int): + if newHealth > maxHealth: + maxHealth = newHealth + + if(maxHealth/2 > health_cont.get_child_count()): + for h in (maxHealth/2) - health_cont.get_child_count(): + add_heart() + + for i in health_cont.get_child_count(): + if (i * 2) + 1 < newHealth: + health_cont.get_child(i).texture = UI_HEART_FULL + elif (i * 2) < newHealth: + health_cont.get_child(i).texture = UI_HEART_HALF + else: + health_cont.get_child(i).texture = UI_HEART_EMPTY + ``` + + Not too bad! + +8. Let's add that **add_heart** function, which just creates and configures another child if we need one. + + ```gdscript + func add_heart(): + var img : TextureRect = TextureRect.new() + img.expand_mode = TextureRect.EXPAND_FIT_WIDTH + health_cont.add_child(img) + ``` + +9. Let's also while we're here, add an empty function for our point system, which we'll come back to later! + + ```gdscript + func add_point(): + pass + ``` + +10. Save the scene as "UI.tscn" + + + +### Getting things connected + + +1. Go back to your main level scene. Add a new child of type **CanvasLayer** and add your new UI scene as a child of this! (This ensures that the UI 'sticks' to the camera, rather than existing within the game) + +2. *Finally,* to get everything hooked up, we just need to connect that signal! Open up **hitbox.gd** + +3. First, we'll get a reference to our new **UI** scene. Put this with the other variable declarations. + + ```gdscript + @onready var ui : Control = $"../../CanvasLayer/UI" + ``` + +4. Then, finally, before we call the signal the first time, connect the signal with: + + ```gdscript + on_health_changed.connect(ui.changed_health) + ``` + we'll also connect our point signal with: + + ```gdscript + on_point_gained.connect(ui.add_point) + ``` + + +Run your game! And you should have 3 hearts! Great! + + + +## Checklist + +- [ ] I've given the player a hitbox +- [ ] I've created the health UI +- [ ] I've created the UI script +- [ ] I've added the UI scene to the World scene, as a child of a CanvasLayer +- [ ] My health displays! + + \ No newline at end of file diff --git a/src/content/docs/game-design/godot/dungeoncrawler/6-pickups/index.mdx b/src/content/docs/game-design/godot/dungeoncrawler/6-pickups/index.mdx new file mode 100644 index 0000000..b8e03fa --- /dev/null +++ b/src/content/docs/game-design/godot/dungeoncrawler/6-pickups/index.mdx @@ -0,0 +1,106 @@ +--- +type: tutorial +unitTitle: Pickups +title: Potions & Coins +description: Implementing pickups for the player to collect +--- + +import Checklist from '/src/components/tutorial/Checklist.astro'; +import Box from '/src/components/tutorial/Box.astro'; +import { Steps } from '@astrojs/starlight/components'; + + +:::note[Godot Documentation] +Godot Documentation for nodes discussed in this section: + +[Static Body 2D](https://docs.godotengine.org/en/3.2/classes/class_staticbody2d.html) [Groups](https://docs.godotengine.org/en/stable/tutorials/scripting/groups.html) [Collision Shapes](https://docs.godotengine.org/en/stable/tutorials/physics/collision_shapes_2d.html) [Area2D](https://docs.godotengine.org/en/stable/classes/class_area2d.html) [Labels](https://docs.godotengine.org/en/stable/classes/class_label.html#class-label) [User Interface](https://docs.godotengine.org/en/stable/tutorials/ui/index.html) +::: + + +Let's get onto pickups! Our game will have two types of pickups: Coins, and Potions. Coins will give us a point, and Potions will heal us for half a heart! + +Thankfully the setup for the two will be extremely simple, and they won't even need a script! As all logic is handled by our hitbox script! + +## Coins + + + +1. For coins, we'll create a new scene with a root node of **Staticbody2D** calling it "Coin" + + It should have a child of type **CollisionShape2D** + +2. We'll also want to add an **AnimatedSprite2D** to the root node. + + Here's how my scene looks: + + ![Coin Scene](/src/assets/godot/dungeonCrawler/coinScene.png) + +3. We'll add the animation the same way we did with our player! Click on the **AnimatedSprite2D** and in the inspector, under **Animation** create a new **Spriteframes** + +4. Click on the new **Spriteframes** and add sprites from file. (The coin has four frames) Make sure to click **autoplay** (The 'A' in the pointy box) + and then assign a shape to the **CollisionShape2D** that loosely matches the coin. (I just used a circle) + +5. The coin as it is very small, so open the inspector for the root **Staticbody2D** and change the **Scale** values to 2. + +6. Now, all that's left to do is create a **Group.** With the root **Staticbody2D** still selected, navigate to the **Node** tab of the inspector, then to the **Groups** tab. + + ![Groups](/src/assets/godot/dungeonCrawler/groups.png) + + In the box, type "coin" and click **Add** + +7. Then, click **Manage Groups** and select each Node, followed by **Add** to ensure that each node in the scene is in the group. + +8. Finally, we'll want to open the **Collision** tab on the inspector of the **Staticbody2D** setting the **Layer** to *only* 2, and deselecting all numbers under the **Mask** as we don't want our coin to be looking for collisions, or to be physically collided with! + +9. Save the scene as "Coin.tscn" + + +And that's our coin! The only script work we need to do is in our **UI** script! But first we'll need to add a UI element to track our points! + +Don't worry, we've done all the hard work of connecting signals earlier! This'll be nice and easy! + + + +1. Let's head to our UI Scene. To the root node, add a new child, of type **HBoxContainer** call it "pointContainer" + +2. Give it two children, a **TextureRect** and a **Label,** name the label "pointsLabel" + +3. In the inspector of the **TextureRect** set the **Expand Mode** to "Fit Width" and assign the first coin image to the **Texture** field using the **Load** Option + +4. Finally for UI setup, in the inspector of the **Label** write "0" in the **Text** field. You'll notice this is pretty small! Scroll down in the **Inspector** until you see **Theme Overrides.** in this section you'll find **Font Sizes.** Set this to something you think looks good! I went with 40px. + +5. Great! Now we just need to add two lines of code to our UI. and then our points are done! + + First, a reference to our label, same way we've been doing, this should be second nature by now! + + ```gdscript + @onready var points_label = $pointContainer/pointsLabel + ``` + + then, we'll finish the function we created earlier. + + + ```gdscript + func add_point(): + points_label.text = str(int(points_label.text) + 1) + ``` + This looks a little silly, but what we're doing is taking the current text in the label, converting it to a number, adding 1 to it, and then converting to *back* to a string. + + + +If you've set it all up right, you should notice this number going up each time you pick up a coin! Add a few instances of the coin scene to your level to test! + +## Potions + +For potions the process is exactly the same! You should be able to do it on your own! Just follow the steps for creating the coin scene. Except in this case we'll just want a regular **Sprite2D** As the potion sprite isn't animated, and we'll want the group to be called "health" (make sure it matches the line we wrote in **hitbox.gd**) + +Don't worry if you don't seem to be able to pick up the potions! It's probably because you have full health! + + +## Checklist + +- [ ] I've created a coin pickup +- [ ] I've created a potion pickup +- [ ] I've tested them in my game + + \ No newline at end of file diff --git a/src/content/docs/game-design/godot/dungeoncrawler/7-enemy/index.mdx b/src/content/docs/game-design/godot/dungeoncrawler/7-enemy/index.mdx new file mode 100644 index 0000000..fadd790 --- /dev/null +++ b/src/content/docs/game-design/godot/dungeoncrawler/7-enemy/index.mdx @@ -0,0 +1,197 @@ +--- +type: tutorial +unitTitle: The Enemy +title: Adding an Enemy +description: Adding an enemy for the player to fight +--- +import Checklist from '/src/components/tutorial/Checklist.astro'; +import Box from '/src/components/tutorial/Box.astro'; +import { Steps } from '@astrojs/starlight/components'; + + +:::note[Godot Documentation] +Godot Documentation for nodes discussed in this section: + +[CharacterBody2D](https://docs.godotengine.org/en/stable/classes/class_characterbody2d.html) [Area2D](https://docs.godotengine.org/en/stable/classes/class_area2d.html) [User Interface](https://docs.godotengine.org/en/stable/tutorials/ui/index.html) [Collision Shapes](https://docs.godotengine.org/en/stable/tutorials/physics/collision_shapes_2d.html) [Animated Sprite](https://docs.godotengine.org/en/stable/classes/class_animatedsprite2d.html) +::: + +We've got a weapon, we've got health, we've got treasure to collect. What's missing? + +Something to fight! + +We'll be keeping our enemy fairly simple, it'll operate off a circular detection range, and walk toward the player if it enters that range, without taking into account walls (Though it will of course still collide with walls) + +## Enemy Scene + +Let's get to making the scene! + + + +1. It'll need: a root **CharacterBody2D** (Called "Enemy") with three children: a **CollisionShape2D,** an **Area2D** (named "range") and an **AnimatedSprite2D** + + Additionally we'll want to give the **Area2D** a **CollisionShape2D** as a child. + + ![Enemy Scene](/src/assets/godot/dungeonCrawler/enemyScene.png) + +2. First, on the **CharacterBody2D** under the **Collision** section, we'll want **Layer** **2** and **3** selected. With only **3** selected under **Mask** + +3. Let's skip down to setting up the **AnimatedSprite2D.** Create two animations, call the first "idle" and the second "move". Then, pick something to be your enemy! Selecting both the idle and move animation as we did for the player, using the **Add from file** button. + + You may want to speed the animations up as they're fairly slow by default, I found 10 fps to be good for both! You'll also want to make sure you set the **idle** animation to be autoplay! + +4. Set the **CollisionShape2D** that's a child of the Root node to have a shape that generally matches the sprite. + +5. Set the shape of the **CollisionShape2D** child of the **Area2D** to be a circle, making it however large you want the 'detection' range of the enemy to be! + +6. Finally, let's create a new **Group** and call it "enemy", and assign it to the root node of the scene. + + + +## Enemy Scripting + +Now let's get the enemy moving! + +1. Let's create some variables: + 1. a boolean to keep track of if the player is in range + 2. a reference to the player + 3. a variable to control our speed + + It should look something like this, again getting the reference to the **AnimatedSprite2D** the usual way: + ```gdscript + var in_range : bool = false + var target + + @export var speed : float = 50.0 + @onready var animated_sprite_2d = $AnimatedSprite2D + ``` + +2. Then, we'll want to connect two signals from the "range" **Area2D** node, we'll want to connect both the "on_body_entered" and "on_body_exited" signals to the script we created! + + Let's write those functions. When the player enters the range, we'll want to set our **in_range** boolean, and play our move animation + + ``` gdscript + func _on_range_body_entered(body): + if(body.is_in_group("player")): + in_range = true + target = body + animated_sprite_2d.play("move") + ``` + +3. In the exited function, we'll want to do the inverse! + + ```gdscript + func _on_range_body_exited(body): + if(body.is_in_group("player")): + in_range = false + animated_sprite_2d.play("idle") + ``` + +4. Now, in the **_process()** function, we'll want to check if our player is in range, if they are, move toward them. We'll also want to flip our sprite based on where the player is in relation to the enemy. + + ```gdscript + func _process(delta): + if(in_range): + + velocity = (target.global_position - global_position).normalized() * speed + + if(target.global_position.x < global_position.x): + animated_sprite_2d.flip_h = true + else: + animated_sprite_2d.flip_h = false + + move_and_slide() + ``` + +5. For a final script that looks like this: + + ```gdscript + + extends CharacterBody2D + + var in_range : bool = false + var target + + @export var speed : float = 50.0 + @onready var animated_sprite_2d = $AnimatedSprite2D + + # Called every frame. 'delta' is the elapsed time since the previous frame. + func _process(delta): + if(in_range): + + velocity = (target.global_position - global_position).normalized() * speed + + if(target.global_position.x < global_position.x): + animated_sprite_2d.flip_h = true + else: + animated_sprite_2d.flip_h = false + move_and_slide() + + + func _on_range_body_entered(body): + if(body.is_in_group("player")): + in_range = true + target = body + animated_sprite_2d.play("move") + + func _on_range_body_exited(body): + if(body.is_in_group("player")): + in_range = false + animated_sprite_2d.play("idle") + + + ``` + + +and that's it! The last thing we need to do is head over to our **Player** scene, create a group called "player" and add the root node of the scene to it. + +Add an enemy to the scene, and you should notice that if you get close to it, it'll walk toward you, and take health away whenever it touches you! + +Although there's one problem... We can't destroy it! + +## Destroying the enemy! + +Head to your Weapon Scene and open the **weapon.gd** script. We'll just need to make some slight modifications to check if the sword is colliding with enemies when we attack. + + +1. First, let's get a reference to the **Area2D** node. + + ```gdscript + @onready var area_2d = $Sprite2D/Area2D + ``` +2. Then, in our **_process()** function, if we're attacking, we'll want to get all the bodies we're colliding with and check if they're enemies. + + ```gdscript + if attacking: + for body in area_2d.get_overlapping_bodies(): + if body.is_in_group("enemy"): + body.queue_free() + ``` + +3. Leaving the **_process():** function looking like this: + + ```gdscript + func _process(delta): + look_at(get_global_mouse_position()) + + if Input.is_action_just_pressed("Attack"): + animation_player.play("Attack") + + if attacking: + for body in area_2d.get_overlapping_bodies(): + if body.is_in_group("enemy"): + body.queue_free() + ``` + + +Test it out, and hopefully you'll find you can now destroy the enemies! + + +## Checklist + +- [ ] I've created the enemy scene +- [ ] I've created the enemy script +- [ ] The enemy damages me +- [ ] The enemy moves toward me +- [ ] I'm able to destroy the enemy + + \ No newline at end of file diff --git a/src/content/docs/game-design/godot/dungeoncrawler/8-levels/index.mdx b/src/content/docs/game-design/godot/dungeoncrawler/8-levels/index.mdx new file mode 100644 index 0000000..853222e --- /dev/null +++ b/src/content/docs/game-design/godot/dungeoncrawler/8-levels/index.mdx @@ -0,0 +1,130 @@ +--- +type: tutorial +unitTitle: More Levels +title: Adding more levels +description: Adding a level-switching system to our game +--- + +import Checklist from '/src/components/tutorial/Checklist.astro'; +import Box from '/src/components/tutorial/Box.astro'; +import { Steps } from '@astrojs/starlight/components'; + + +:::note[Godot Documentation] +Godot Documentation for nodes discussed in this section: + +[PackedScene](https://docs.godotengine.org/en/stable/classes/class_packedscene.html) +::: + +Until now, our game has only had one level. Let's change that! Thankfully, Godot makes this fairly easy! There's only a couple of things we'll need to do. + +1. Actually create and store multiple levels. +2. Load the level when we enter a specific spot. + +Let's get straight into it! + +## Creating Multiple Levels + +To create multiple levels, we'll want to save each level and *everything* in the level, as a **Scene.** A level should contain **Everything** that we want to be part of the level, such as enemies, coins, and potions. However we *don't* want the player or the UI to be saved in the level. + +An easy way to remember what we want to store in a level, is by thinking of what we want to *belong* or exist as part of the level! + +Now, for our levels, we'll obviously need our **TileMapLayers!** But we don't want to be redoing **all** of the tileset settings each time... So what we'll want to do, is save a blank version of our current **TileMapLayers** so that we can reuse them! + +1. Let's go to what is currently our testing scene, go to your **TileMapLayers** and erase all the tiles you've painted, so it's just two blank **TileMapLayers** with all of our settings and our assets loaded. Now, let's right-click the parent node of **both** **TileMapLayers** and click **Save Branch as Scene** calling it "Level_Template.tscn" + +2. Great! Now we can get to actually setting up our first level! A level will only *Need* a couple of things to function: + + 1. The **TileMapLayers** + 2. A node that represents the players spawn + + Anything else, like enemies or coins, are optional + +3. Let's create a new scene with a root **Node2D** and call it "Level 1" drag in your **Level_Template.tscn** as a child of this. Also giving the root node another child of type **Node2D** calling "player_spawn" + +4. To edit the two **TileMapLayers** we'll need to right click on the **Level_Template** we imported, and tick **editable children** which makes this a unique instance of the scene, which we want anyway! + + And that's the most basic form of a level setup! From here, paint the level using the **TileMap** (Remembering to use the right layers for walls and floors) and adding enemies, potions, and coins, all as children of the **Root** node. + +5. Once you've made a level, save the scene. Creating a new folder called "Levels" to store it in! Try creating two levels "level_1" and "level_2" making sure they're somewhat different, so you can tell when you've changed levels. + +6. You'll also want to make sure you place the "player_spawn" node in the position you want the player to spawn in the level. + +7. Save the scene as "level_1.tscn" + + + +Here's an example of a level I made, take note of the order of the nodes in the SceneTree: + +![Initial Scene](/src/assets/godot/dungeonCrawler/createdLevel.png) + +## Level Loader + +The very last step for our game, is creating something to actually transition us between levels! + +1. But first, let's quickly clean up our main "World" scene. We'll want to delete everything from this scene *except* for the **Player & Camera2D** and the **CanvasLayer** that holds the **UI** (And obviously keeping the root node) + +2. We'll add one additional node as a child of the **root** node called "levelHolder" + +3. From your filesystem, drag your newly created "level_1.tscn" onto the **levelHolder** so that it becomes its child, as we want level 1 to be loaded from the start! + + + +Here's how my **World** scene looks: + +![World Scene](/src/assets/godot/dungeonCrawler/worldScene.png) + +We'll make that **level_transition** node now! + + + +1. For the final scene of our project, let's create a new scene, with a root **Node2D** called "levelTransition" with a child **Area2D** and give that a child **CollisionShape2D** + +2. Set the shape of the **CollisionShape2D** to be a square. + +3. let's create a script attached to the root node, called "level_transition.gd" + +4. Let's think about the variables for this script: + + 1. The level we want to load (We'll use an \@export for this) + 2. A reference to our "levelHolder" node + 3. A reference to our player, to set their position + + ```gdscript + @export var to_load : PackedScene + + @onready var levelHolder : Node2D = $"../../../levelHolder" + @onready var player : CharacterBody2D = $"../../../player" + ``` + + Great! The \@export will allow us to assign what level want to load from each level, within the editor! + +5. Now, if the player enters this region, we'll + + 1. Remove the last Level + 2. Load the new level + 3. Set the players position + + ```gdscript + func _on_area_2d_body_entered(body): + if(body.is_in_group("player")): + levelHolder.get_child(0).queue_free() + var loaded = to_load.instantiate() + levelHolder.call_deferred("add_child",loaded) + player.global_position = loaded.get_node("player_spawn").position + ``` + +6. Save the scene as "level_transition.tscn" + +7. Now to each of our level scenes that we've created, add in the **level_transition** scene by dragging it from the filesystem. Place it somewhere you want the level to end (Ideally on some stairs, so the player expects the change) + And in the inspector of the **level_transition,** *after* you've added an instance to a scene, find the level you want it to open in your filesystem, and drag it into the "to_load" slot. + + +### Checklist + +- [ ] I've at least two levels +- [ ] I've created the level loader +- [ ] I've added the level loader to my levels +- [ ] I'm able to transition between levels + + \ No newline at end of file diff --git a/src/content/docs/game-design/godot/dungeoncrawler/9-finish/index.mdx b/src/content/docs/game-design/godot/dungeoncrawler/9-finish/index.mdx new file mode 100644 index 0000000..75e2fc6 --- /dev/null +++ b/src/content/docs/game-design/godot/dungeoncrawler/9-finish/index.mdx @@ -0,0 +1,29 @@ +--- +type: tutorial +unitTitle: The End +title: The End +description: Some final comments on our dungeon crawler game +--- + +import Checklist from '/src/components/tutorial/Checklist.astro'; +import Box from '/src/components/tutorial/Box.astro'; +import { Steps } from '@astrojs/starlight/components'; + +## Finishing up! + +And that's it! Congratulations! You've just made your very own dungeon crawler! It's now all yours to do with as you wish! But here's some ideas for what you could add: + +1. Enemies having health +2. A title screen +3. Even more levels +4. An item that increases your max health + +Whatever you decide to add, I can't wait to see it! + + + +### Checklist + +- [ ] I've created a full game! + +