From d640ad707fa4a7954ab82452424f7fc4441036a7 Mon Sep 17 00:00:00 2001 From: David Lenfesty Date: Tue, 19 Sep 2023 21:56:03 -0600 Subject: [PATCH] Start the game! Just adds a basic player character with rough movement --- .gitattributes | 2 + .gitignore | 2 + Sprites/hockey_player.png | Bin 0 -> 1404 bytes Sprites/hockey_player.png.import | 34 +++++++++++++ Sprites/hockey_player.xcf | Bin 0 -> 7054 bytes game.gd | 6 +++ game.tscn | 12 +++++ icon.svg | 1 + icon.svg.import | 37 ++++++++++++++ player.gd | 78 ++++++++++++++++++++++++++++ player.tscn | 30 +++++++++++ player_controller.gd | 85 +++++++++++++++++++++++++++++++ player_controller.tscn | 6 +++ project.godot | 39 ++++++++++++++ 14 files changed, 332 insertions(+) create mode 100644 .gitattributes create mode 100644 .gitignore create mode 100644 Sprites/hockey_player.png create mode 100644 Sprites/hockey_player.png.import create mode 100644 Sprites/hockey_player.xcf create mode 100644 game.gd create mode 100644 game.tscn create mode 100644 icon.svg create mode 100644 icon.svg.import create mode 100644 player.gd create mode 100644 player.tscn create mode 100644 player_controller.gd create mode 100644 player_controller.tscn create mode 100644 project.godot diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..8ad74f7 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +# Normalize EOL for all files that Git considers text files. +* text=auto eol=lf diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4709183 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +# Godot 4+ specific ignores +.godot/ diff --git a/Sprites/hockey_player.png b/Sprites/hockey_player.png new file mode 100644 index 0000000000000000000000000000000000000000..37ce2c94216fffc36aa67710cdef6517bc7c7733 GIT binary patch literal 1404 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 diff --git a/icon.svg.import b/icon.svg.import new file mode 100644 index 0000000..5c23b70 --- /dev/null +++ b/icon.svg.import @@ -0,0 +1,37 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://bg5wwos0fgs03" +path="res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://icon.svg" +dest_files=["res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 +svg/scale=1.0 +editor/scale_with_editor_scale=false +editor/convert_colors_with_editor_theme=false diff --git a/player.gd b/player.gd new file mode 100644 index 0000000..17c5c03 --- /dev/null +++ b/player.gd @@ -0,0 +1,78 @@ +extends Area2D + + + +# Maximum radial turning speed of the player +@export var turning_speed = PI/4 # (rad/sec) +# Maximum speed added to player's velocity per push +@export var skate_speed = 100 # (pixels/sec) +# Maximum speed deceleration while stopping +@export var braking_speed = 200 # (pixels/sec^2) +# "Natural" deceleration +@export var decel_ratio = 0.15 # (decel applied per second) + +var velocity: Vector2 + +func _ready(): + velocity = Vector2.ZERO + + +func _physics_process(delta: float): + # Flip sprite + if velocity.length() > 0: + $AnimatedSprite2D.flip_h = velocity.x > 0 + + # Update position + position = clamp_position(position + (velocity * delta)) + + # Decelerate naturally + # TODO fix vector math, need to apply to hypotenuse, not the sides + # TODO fix decel math, think compounding interest, it doesn't work this way + # TODO brake harder when almost stopped + velocity = velocity * (1 - decel_ratio * delta) + + +func push(effort: float, angle: float): + var delta_v = effort * skate_speed + velocity += delta_v * Vector2.from_angle(angle) + + +# TODO how to do doc comments? +# TODO should applying delta come from the controller or the player? It doesn't +# matter too much because these objects are pretty tightly coupled anyways. +func turn(effort: float, delta: float): + # Continuosly call with the effort while turning + + # effort is +ve right, -ve left? maybe, some orientation + velocity = velocity.rotated(effort * turning_speed * delta) + + +func brake(effort: float, delta: float): + var delta_v = effort * braking_speed * delta + delta_v = min(delta_v, velocity.length()) + velocity -= delta_v * velocity.normalized() + + +# TODO setup collision from world, and edges of arena, not the window +func clamp_position(new_pos: Vector2) -> Vector2: + var size = Vector2(get_window().size) + if new_pos.x > size.x: + if velocity.x > 0: + velocity.x = 0 + new_pos.x = size.x + if new_pos.x < 0: + if velocity.x < 0: + velocity.x = 0 + + new_pos.x = 0 + + if new_pos.y > size.y: + if velocity.y > 0: + velocity.y = 0 + new_pos.y = size.y + if new_pos.y < 0: + if velocity.y < 0: + velocity.y = 0 + new_pos.y = 0 + + return new_pos diff --git a/player.tscn b/player.tscn new file mode 100644 index 0000000..d24e85e --- /dev/null +++ b/player.tscn @@ -0,0 +1,30 @@ +[gd_scene load_steps=5 format=3 uid="uid://b3llhcsie7y4u"] + +[ext_resource type="Texture2D" uid="uid://bknjh6k2ocrri" path="res://Sprites/hockey_player.png" id="1_dtrap"] +[ext_resource type="Script" path="res://player.gd" id="1_gh3p0"] + +[sub_resource type="SpriteFrames" id="SpriteFrames_oslmh"] +animations = [{ +"frames": [{ +"duration": 1.0, +"texture": ExtResource("1_dtrap") +}], +"loop": true, +"name": &"default", +"speed": 5.0 +}] + +[sub_resource type="RectangleShape2D" id="RectangleShape2D_exun0"] +size = Vector2(42, 108) + +[node name="Player" type="Area2D"] +process_physics_priority = 1 +script = ExtResource("1_gh3p0") +metadata/_edit_group_ = true + +[node name="AnimatedSprite2D" type="AnimatedSprite2D" parent="."] +sprite_frames = SubResource("SpriteFrames_oslmh") + +[node name="CollisionShape2D" type="CollisionShape2D" parent="."] +position = Vector2(-7, -6) +shape = SubResource("RectangleShape2D_exun0") diff --git a/player_controller.gd b/player_controller.gd new file mode 100644 index 0000000..2dbd303 --- /dev/null +++ b/player_controller.gd @@ -0,0 +1,85 @@ +extends Node + +# Variables used by controller +@export var skate_push_start_len = 0.2 +@export var skate_push_end_len = 0.9 +@export var skate_min_time = 0.02 +@export var skate_max_time = 0.1 +@export var skate_max_turn_radius = PI / 8 +@export var brake_deadzone = 0.3 + +var skate_occurred: bool +var time_for_push: float + +var player_node: Node = null + +# Called when the node enters the scene tree for the first time. +func _ready(): + skate_occurred = false + time_for_push = 0 + + +# Called every physics processing step +func _physics_process(delta): + if player_node == null: + # Don't call the processing functions if the player node isn't set + return + + var joy = Input.get_vector("move_left", "move_right", "move_up", "move_down", 0) + handle_skate_push(joy, delta) + handle_turning(joy, delta) + handle_braking(joy, delta) + + +func set_player_node(node: Node): + player_node = node + + +func handle_skate_push(joy: Vector2, delta: float): + var pos = joy.length() + if pos >= skate_push_start_len and !skate_occurred: + time_for_push += delta + + if pos >= skate_push_end_len and !skate_occurred: + if time_for_push >= skate_min_time and time_for_push <= skate_max_time: + var effort = 1 - time_for_push / skate_max_time + player_node.push(effort, joy.angle()) + + skate_occurred = true + + elif pos < skate_push_start_len: + skate_occurred = false + time_for_push = 0 + + +func handle_turning(joy: Vector2, delta: float): + if joy.length() < 0.95: + # Only turn if the stick is at the edge + return + + # TODO this doesn't work well when you hit a wall + var radial_diff = player_node.velocity.angle_to(joy) + if (radial_diff >= PI/3): + # Only turn for roughlyone "half" of the stick. The other half is used for braking + return + + var is_negative: bool = radial_diff < 0 + var turn_effort = min(abs(radial_diff), skate_max_turn_radius) / skate_max_turn_radius + if is_negative: + turn_effort = -turn_effort + + player_node.turn(turn_effort, delta) + + +func handle_braking(joy: Vector2, delta: float): + if joy.length() < brake_deadzone: + # Only brake past a certain amount + return + + var radial_diff = player_node.velocity.angle_to(joy) + if (radial_diff < 3 * PI / 4): + # Only brake if controller is mostly facing away from velocity + return + + player_node.brake(joy.length(), delta) + diff --git a/player_controller.tscn b/player_controller.tscn new file mode 100644 index 0000000..89c64b4 --- /dev/null +++ b/player_controller.tscn @@ -0,0 +1,6 @@ +[gd_scene load_steps=2 format=3 uid="uid://clntxmhd5uhlt"] + +[ext_resource type="Script" path="res://player_controller.gd" id="1_fj1gd"] + +[node name="PlayerController" type="Node"] +script = ExtResource("1_fj1gd") diff --git a/project.godot b/project.godot new file mode 100644 index 0000000..1169ea8 --- /dev/null +++ b/project.godot @@ -0,0 +1,39 @@ +; Engine configuration file. +; It's best edited using the editor UI and not directly, +; since the parameters that go here are not all obvious. +; +; Format: +; [section] ; section goes between [] +; param=value ; assign values to parameters + +config_version=5 + +[application] + +config/name="Offside" +run/main_scene="res://game.tscn" +config/features=PackedStringArray("4.1", "Forward Plus") +config/icon="res://icon.svg" + +[input] + +move_left={ +"deadzone": 0.5, +"events": [Object(InputEventJoypadMotion,"resource_local_to_scene":false,"resource_name":"","device":-1,"axis":0,"axis_value":-1.0,"script":null) +] +} +move_right={ +"deadzone": 0.5, +"events": [Object(InputEventJoypadMotion,"resource_local_to_scene":false,"resource_name":"","device":-1,"axis":0,"axis_value":1.0,"script":null) +] +} +move_up={ +"deadzone": 0.5, +"events": [Object(InputEventJoypadMotion,"resource_local_to_scene":false,"resource_name":"","device":-1,"axis":1,"axis_value":-1.0,"script":null) +] +} +move_down={ +"deadzone": 0.5, +"events": [Object(InputEventJoypadMotion,"resource_local_to_scene":false,"resource_name":"","device":-1,"axis":1,"axis_value":1.0,"script":null) +] +}