584 lines
15 KiB
Markdown
584 lines
15 KiB
Markdown
---
|
|
allowed_tools:
|
|
- AskUserQuestion
|
|
- mcp__godot__*
|
|
- Write
|
|
- Read
|
|
---
|
|
|
|
Create common scene templates for 2D games, including characters, enemies, levels, and more.
|
|
|
|
# Process
|
|
|
|
1. **Ask the user what type of scene they want to create:**
|
|
|
|
Use AskUserQuestion with the following options:
|
|
|
|
Question: "What type of scene would you like to create?"
|
|
Header: "Scene Type"
|
|
Multi-select: false
|
|
Options:
|
|
- 2D Player Character: Platformer character with movement and collision
|
|
- 2D Enemy: Basic enemy with AI placeholder
|
|
- 2D Level: Level scene with tilemap and camera
|
|
- 2D Projectile: Bullet/projectile with movement
|
|
- Collectible: Coin/item pickup
|
|
- Interactable Object: Chest, door, switch, etc.
|
|
|
|
Question 2: "Where should the scene be created?"
|
|
Header: "Location"
|
|
Multi-select: false
|
|
Options:
|
|
- scenes/characters/: For player and NPCs
|
|
- scenes/enemies/: For enemy characters
|
|
- scenes/levels/: For level scenes
|
|
- scenes/objects/: For items and interactables
|
|
- Custom path: I'll specify the path
|
|
|
|
2. **If Custom path selected, ask for the specific path and scene name**
|
|
|
|
3. **Create the appropriate template based on selection**
|
|
|
|
## 2D Player Character Template
|
|
|
|
Create scene with:
|
|
```
|
|
CharacterBody2D (root)
|
|
├── Sprite2D (sprite)
|
|
├── CollisionShape2D (collision)
|
|
├── AnimationPlayer (animation_player)
|
|
└── Camera2D (camera)
|
|
```
|
|
|
|
Node details:
|
|
- CharacterBody2D: motion_mode = MOTION_MODE_GROUNDED
|
|
- Sprite2D: centered = true, texture = placeholder (32x32 white rect)
|
|
- CollisionShape2D: shape = RectangleShape2D (16x32)
|
|
- Camera2D: enabled = true, position_smoothing_enabled = true
|
|
|
|
Create accompanying script:
|
|
```gdscript
|
|
extends CharacterBody2D
|
|
|
|
# Movement parameters
|
|
@export var speed: float = 200.0
|
|
@export var jump_velocity: float = -400.0
|
|
@export var acceleration: float = 800.0
|
|
@export var friction: float = 1000.0
|
|
|
|
# Get the gravity from the project settings
|
|
var gravity = ProjectSettings.get_setting("physics/2d/default_gravity")
|
|
|
|
@onready var sprite = $Sprite2D
|
|
@onready var animation_player = $AnimationPlayer
|
|
|
|
func _physics_process(delta):
|
|
# Add gravity
|
|
if not is_on_floor():
|
|
velocity.y += gravity * delta
|
|
|
|
# Handle jump
|
|
if Input.is_action_just_pressed("ui_accept") and is_on_floor():
|
|
velocity.y = jump_velocity
|
|
|
|
# Get input direction
|
|
var direction = Input.get_axis("ui_left", "ui_right")
|
|
|
|
# Apply movement
|
|
if direction != 0:
|
|
velocity.x = move_toward(velocity.x, direction * speed, acceleration * delta)
|
|
|
|
# Flip sprite based on direction
|
|
sprite.flip_h = direction < 0
|
|
|
|
else:
|
|
# Apply friction when no input
|
|
velocity.x = move_toward(velocity.x, 0, friction * delta)
|
|
|
|
move_and_slide()
|
|
|
|
# Animation logic (requires animations to be set up)
|
|
_update_animation()
|
|
|
|
func _update_animation():
|
|
if not animation_player:
|
|
return
|
|
|
|
# TODO: Create animations in AnimationPlayer:
|
|
# - "idle": Standing still
|
|
# - "run": Running
|
|
# - "jump": Jumping up
|
|
# - "fall": Falling down
|
|
|
|
# Example animation logic:
|
|
# if not is_on_floor():
|
|
# if velocity.y < 0:
|
|
# animation_player.play("jump")
|
|
# else:
|
|
# animation_player.play("fall")
|
|
# elif abs(velocity.x) > 10:
|
|
# animation_player.play("run")
|
|
# else:
|
|
# animation_player.play("idle")
|
|
pass
|
|
```
|
|
|
|
## 2D Enemy Template
|
|
|
|
Create scene with:
|
|
```
|
|
CharacterBody2D (root)
|
|
├── Sprite2D (sprite)
|
|
├── CollisionShape2D (collision)
|
|
├── Area2D (detection_area)
|
|
│ └── CollisionShape2D (detection_collision)
|
|
├── AnimationPlayer (animation_player)
|
|
└── Timer (patrol_timer)
|
|
```
|
|
|
|
Node details:
|
|
- CharacterBody2D: motion_mode = MOTION_MODE_GROUNDED
|
|
- Sprite2D: centered = true, modulate = Color(1, 0.5, 0.5) (reddish tint)
|
|
- CollisionShape2D: shape = RectangleShape2D (16x24)
|
|
- Area2D/CollisionShape2D: shape = CircleShape2D (radius = 100) for player detection
|
|
- Timer: wait_time = 2.0, one_shot = false
|
|
|
|
Create accompanying script:
|
|
```gdscript
|
|
extends CharacterBody2D
|
|
|
|
enum State { IDLE, PATROL, CHASE, ATTACK }
|
|
|
|
# Movement parameters
|
|
@export var patrol_speed: float = 50.0
|
|
@export var chase_speed: float = 150.0
|
|
@export var detection_range: float = 100.0
|
|
@export var attack_range: float = 30.0
|
|
@export var health: int = 100
|
|
|
|
var current_state: State = State.IDLE
|
|
var player: Node2D = null
|
|
var patrol_direction: int = 1
|
|
var gravity = ProjectSettings.get_setting("physics/2d/default_gravity")
|
|
|
|
@onready var sprite = $Sprite2D
|
|
@onready var detection_area = $detection_area
|
|
@onready var patrol_timer = $patrol_timer
|
|
|
|
func _ready():
|
|
# Connect area signals for player detection
|
|
detection_area.body_entered.connect(_on_detection_area_entered)
|
|
detection_area.body_exited.connect(_on_detection_area_exited)
|
|
|
|
# Connect patrol timer
|
|
patrol_timer.timeout.connect(_on_patrol_timer_timeout)
|
|
patrol_timer.start()
|
|
|
|
current_state = State.PATROL
|
|
|
|
func _physics_process(delta):
|
|
# Apply gravity
|
|
if not is_on_floor():
|
|
velocity.y += gravity * delta
|
|
|
|
# State machine
|
|
match current_state:
|
|
State.IDLE:
|
|
velocity.x = 0
|
|
|
|
State.PATROL:
|
|
velocity.x = patrol_direction * patrol_speed
|
|
sprite.flip_h = patrol_direction < 0
|
|
|
|
# Turn around at ledges or walls
|
|
if is_on_wall() or not _check_floor_ahead():
|
|
patrol_direction *= -1
|
|
|
|
State.CHASE:
|
|
if player:
|
|
var direction = sign(player.global_position.x - global_position.x)
|
|
velocity.x = direction * chase_speed
|
|
sprite.flip_h = direction < 0
|
|
|
|
# Check if close enough to attack
|
|
var distance = global_position.distance_to(player.global_position)
|
|
if distance < attack_range:
|
|
current_state = State.ATTACK
|
|
else:
|
|
current_state = State.PATROL
|
|
|
|
State.ATTACK:
|
|
velocity.x = 0
|
|
# TODO: Implement attack logic
|
|
# For now, just return to chase after a moment
|
|
await get_tree().create_timer(0.5).timeout
|
|
current_state = State.CHASE
|
|
|
|
move_and_slide()
|
|
|
|
func _check_floor_ahead() -> bool:
|
|
# Raycast to check if there's floor ahead
|
|
var space_state = get_world_2d().direct_space_state
|
|
var query = PhysicsRayQueryParameters2D.create(
|
|
global_position,
|
|
global_position + Vector2(patrol_direction * 20, 30)
|
|
)
|
|
var result = space_state.intersect_ray(query)
|
|
return result.size() > 0
|
|
|
|
func _on_detection_area_entered(body):
|
|
if body.is_in_group("player"):
|
|
player = body
|
|
current_state = State.CHASE
|
|
|
|
func _on_detection_area_exited(body):
|
|
if body == player:
|
|
player = null
|
|
current_state = State.PATROL
|
|
|
|
func _on_patrol_timer_timeout():
|
|
if current_state == State.IDLE or current_state == State.PATROL:
|
|
patrol_direction *= -1
|
|
|
|
func take_damage(amount: int):
|
|
health -= amount
|
|
# TODO: Add damage flash/animation
|
|
|
|
if health <= 0:
|
|
die()
|
|
|
|
func die():
|
|
# TODO: Add death animation
|
|
queue_free()
|
|
```
|
|
|
|
## 2D Level Template
|
|
|
|
Create scene with:
|
|
```
|
|
Node2D (root)
|
|
├── TileMap (tilemap)
|
|
├── Node2D (spawn_points)
|
|
│ └── Marker2D (player_spawn)
|
|
├── Node2D (enemies)
|
|
├── Node2D (collectibles)
|
|
├── ParallaxBackground (background)
|
|
│ └── ParallaxLayer (layer1)
|
|
│ └── Sprite2D (bg_sprite)
|
|
└── Camera2D (camera)
|
|
```
|
|
|
|
Node details:
|
|
- TileMap: tile_set = null (needs to be set by user), layer 0 = "Ground", layer 1 = "Walls"
|
|
- Camera2D: enabled = true, limit_left = 0, limit_top = 0
|
|
|
|
Create accompanying script:
|
|
```gdscript
|
|
extends Node2D
|
|
|
|
@onready var tilemap = $TileMap
|
|
@onready var player_spawn = $spawn_points/player_spawn
|
|
@onready var camera = $Camera2D
|
|
|
|
# Preload player scene
|
|
const PLAYER_SCENE = preload("res://scenes/characters/player.tscn")
|
|
|
|
func _ready():
|
|
# Spawn player
|
|
spawn_player()
|
|
|
|
# Set camera limits based on level bounds
|
|
_setup_camera_limits()
|
|
|
|
func spawn_player():
|
|
# Instance and add player
|
|
var player = PLAYER_SCENE.instantiate()
|
|
player.global_position = player_spawn.global_position
|
|
add_child(player)
|
|
|
|
# Make camera follow player
|
|
camera.enabled = false # Disable level camera
|
|
# Player should have its own camera
|
|
|
|
func _setup_camera_limits():
|
|
# Get tilemap bounds
|
|
var used_rect = tilemap.get_used_rect()
|
|
var tile_size = tilemap.tile_set.tile_size if tilemap.tile_set else Vector2(16, 16)
|
|
|
|
# Set camera limits to level bounds
|
|
camera.limit_left = used_rect.position.x * tile_size.x
|
|
camera.limit_top = used_rect.position.y * tile_size.y
|
|
camera.limit_right = used_rect.end.x * tile_size.x
|
|
camera.limit_bottom = used_rect.end.y * tile_size.y
|
|
|
|
# Helper function to spawn enemies at runtime
|
|
func spawn_enemy(enemy_scene: PackedScene, position: Vector2):
|
|
var enemy = enemy_scene.instantiate()
|
|
enemy.global_position = position
|
|
$enemies.add_child(enemy)
|
|
|
|
# Helper function to spawn collectibles
|
|
func spawn_collectible(collectible_scene: PackedScene, position: Vector2):
|
|
var collectible = collectible_scene.instantiate()
|
|
collectible.global_position = position
|
|
$collectibles.add_child(collectible)
|
|
```
|
|
|
|
## 2D Projectile Template
|
|
|
|
Create scene with:
|
|
```
|
|
Area2D (root)
|
|
├── Sprite2D (sprite)
|
|
├── CollisionShape2D (collision)
|
|
└── Timer (lifetime_timer)
|
|
```
|
|
|
|
Node details:
|
|
- Sprite2D: centered = true, modulate = Color(1, 1, 0) (yellow)
|
|
- CollisionShape2D: shape = CircleShape2D (radius = 4)
|
|
- Timer: wait_time = 5.0, one_shot = true, autostart = true
|
|
|
|
Create accompanying script:
|
|
```gdscript
|
|
extends Area2D
|
|
|
|
@export var speed: float = 300.0
|
|
@export var damage: int = 10
|
|
@export var lifetime: float = 5.0
|
|
|
|
var direction: Vector2 = Vector2.RIGHT
|
|
|
|
@onready var sprite = $Sprite2D
|
|
@onready var lifetime_timer = $lifetime_timer
|
|
|
|
func _ready():
|
|
# Connect signals
|
|
body_entered.connect(_on_body_entered)
|
|
area_entered.connect(_on_area_entered)
|
|
lifetime_timer.timeout.connect(_on_lifetime_timeout)
|
|
|
|
# Set lifetime
|
|
lifetime_timer.wait_time = lifetime
|
|
|
|
func _physics_process(delta):
|
|
# Move in direction
|
|
position += direction.normalized() * speed * delta
|
|
|
|
func set_direction(new_direction: Vector2):
|
|
direction = new_direction.normalized()
|
|
|
|
# Rotate sprite to face direction
|
|
rotation = direction.angle()
|
|
|
|
func _on_body_entered(body):
|
|
# Hit something solid
|
|
if body.has_method("take_damage"):
|
|
body.take_damage(damage)
|
|
|
|
# Create hit effect
|
|
_create_hit_effect()
|
|
|
|
queue_free()
|
|
|
|
func _on_area_entered(area):
|
|
# Hit another area (enemy hitbox, etc.)
|
|
if area.get_parent().has_method("take_damage"):
|
|
area.get_parent().take_damage(damage)
|
|
|
|
# Create hit effect
|
|
_create_hit_effect()
|
|
|
|
queue_free()
|
|
|
|
func _on_lifetime_timeout():
|
|
# Despawn after lifetime expires
|
|
queue_free()
|
|
|
|
func _create_hit_effect():
|
|
# TODO: Spawn particle effect or animation
|
|
pass
|
|
```
|
|
|
|
## Collectible Template
|
|
|
|
Create scene with:
|
|
```
|
|
Area2D (root)
|
|
├── Sprite2D (sprite)
|
|
├── CollisionShape2D (collision)
|
|
├── AnimationPlayer (animation_player)
|
|
└── AudioStreamPlayer2D (pickup_sound)
|
|
```
|
|
|
|
Node details:
|
|
- Sprite2D: centered = true, modulate = Color(1, 0.8, 0) (gold color)
|
|
- CollisionShape2D: shape = CircleShape2D (radius = 8)
|
|
|
|
Create accompanying script:
|
|
```gdscript
|
|
extends Area2D
|
|
|
|
@export var collect_value: int = 1
|
|
@export var float_amplitude: float = 5.0
|
|
@export var float_speed: float = 2.0
|
|
|
|
var start_y: float
|
|
var time: float = 0.0
|
|
|
|
@onready var sprite = $Sprite2D
|
|
@onready var animation_player = $AnimationPlayer
|
|
@onready var pickup_sound = $pickup_sound
|
|
|
|
func _ready():
|
|
# Connect collection signal
|
|
body_entered.connect(_on_body_entered)
|
|
|
|
# Store starting position for floating animation
|
|
start_y = position.y
|
|
|
|
# Random offset for variety
|
|
time = randf() * TAU
|
|
|
|
func _process(delta):
|
|
# Floating animation
|
|
time += delta * float_speed
|
|
position.y = start_y + sin(time) * float_amplitude
|
|
|
|
# Rotate for visual interest
|
|
sprite.rotation += delta * 2.0
|
|
|
|
func _on_body_entered(body):
|
|
if body.is_in_group("player"):
|
|
# Notify player of collection
|
|
if body.has_method("collect_item"):
|
|
body.collect_item(collect_value)
|
|
|
|
# Play pickup sound
|
|
if pickup_sound and pickup_sound.stream:
|
|
# Play sound then destroy
|
|
pickup_sound.play()
|
|
|
|
# Hide visuals but keep node alive for sound
|
|
sprite.visible = false
|
|
$CollisionShape2D.set_deferred("disabled", true)
|
|
|
|
await pickup_sound.finished
|
|
|
|
queue_free()
|
|
```
|
|
|
|
## Interactable Object Template
|
|
|
|
Create scene with:
|
|
```
|
|
StaticBody2D (root)
|
|
├── Sprite2D (sprite)
|
|
├── CollisionShape2D (collision)
|
|
├── Area2D (interaction_area)
|
|
│ └── CollisionShape2D (interaction_collision)
|
|
└── Label (prompt_label)
|
|
```
|
|
|
|
Node details:
|
|
- Sprite2D: centered = true
|
|
- CollisionShape2D: shape = RectangleShape2D (32x32)
|
|
- Area2D/CollisionShape2D: shape = RectangleShape2D (48x48) for interaction detection
|
|
- Label: text = "Press E", horizontal_alignment = CENTER, visible = false
|
|
|
|
Create accompanying script:
|
|
```gdscript
|
|
extends StaticBody2D
|
|
|
|
signal interacted
|
|
|
|
@export var interaction_text: String = "Press E"
|
|
@export var can_interact_multiple_times: bool = false
|
|
|
|
var player_nearby: bool = false
|
|
var has_been_used: bool = false
|
|
|
|
@onready var sprite = $Sprite2D
|
|
@onready var interaction_area = $interaction_area
|
|
@onready var prompt_label = $prompt_label
|
|
|
|
func _ready():
|
|
# Connect area signals
|
|
interaction_area.body_entered.connect(_on_interaction_area_entered)
|
|
interaction_area.body_exited.connect(_on_interaction_area_exited)
|
|
|
|
# Set prompt text
|
|
prompt_label.text = interaction_text
|
|
|
|
# Position prompt above object
|
|
prompt_label.position.y = -40
|
|
|
|
func _process(_delta):
|
|
if player_nearby and Input.is_action_just_pressed("ui_accept"):
|
|
if not has_been_used or can_interact_multiple_times:
|
|
_interact()
|
|
|
|
func _on_interaction_area_entered(body):
|
|
if body.is_in_group("player"):
|
|
player_nearby = true
|
|
|
|
# Show prompt if not used or can use multiple times
|
|
if not has_been_used or can_interact_multiple_times:
|
|
prompt_label.visible = true
|
|
|
|
func _on_interaction_area_exited(body):
|
|
if body.is_in_group("player"):
|
|
player_nearby = false
|
|
prompt_label.visible = false
|
|
|
|
func _interact():
|
|
# Mark as used
|
|
has_been_used = true
|
|
|
|
# Hide prompt if can't be used again
|
|
if not can_interact_multiple_times:
|
|
prompt_label.visible = false
|
|
|
|
# Emit signal for other systems to respond
|
|
interacted.emit()
|
|
|
|
# TODO: Implement specific interaction logic
|
|
# Examples:
|
|
# - Open a door
|
|
# - Play an animation
|
|
# - Give an item
|
|
# - Show dialogue
|
|
# - Toggle a mechanism
|
|
|
|
print("Interacted with ", name)
|
|
```
|
|
|
|
# After Creating Scene
|
|
|
|
After creating the selected scene:
|
|
|
|
1. Inform the user of the files created:
|
|
- Scene file location
|
|
- Script file location
|
|
|
|
2. Provide customization tips:
|
|
- "The [scene name] has been created at [path]"
|
|
- "To customize this scene:"
|
|
- Replace the placeholder sprite with your own artwork
|
|
- Adjust the exported variables in the Inspector
|
|
- Add animations in the AnimationPlayer
|
|
- [Template-specific tips]
|
|
|
|
3. Provide usage instructions:
|
|
- For characters: "Add this to your level by dragging into the scene or instantiating in code"
|
|
- For levels: "Set this as your main scene in Project Settings or change to it with get_tree().change_scene_to_file()"
|
|
- For projectiles: "Instance this in your player/enemy script when firing"
|
|
- For collectibles/interactables: "Place these in your level scene as children"
|
|
|
|
4. Offer next steps:
|
|
- "Would you like me to:"
|
|
- Create related scenes (e.g., "create a projectile for this character?")
|
|
- Add more features (e.g., "add a dash ability?", "add more enemy states?")
|
|
- Set up animations (if AnimationPlayer exists)
|
|
- Create a test level to try it out
|