Add weapon fire rate, capacity, reloading

- Can't exceed weapon's fire rate
- Automatic weapons fire automatically
- Require reloading after running out of rounds
- Reload on demand with key
- Add WeaponInfo HUD
main
copygirl 4 years ago
parent 1dc140d31d
commit 26523ea511
  1. 5
      project.godot
  2. 58
      scene/ClientScene.tscn
  3. 29
      scene/Player.tscn
  4. 195
      src/Items/Weapon.cs
  5. 8
      src/Objects/Bullet.cs
  6. 6
      src/RadialMenu.cs
  7. 52
      src/WeaponInfo.cs

@ -86,6 +86,11 @@ interact_secondary={
"events": [ Object(InputEventMouseButton,"resource_local_to_scene":false,"resource_name":"","device":0,"alt":false,"shift":false,"control":false,"meta":false,"command":false,"button_mask":0,"position":Vector2( 0, 0 ),"global_position":Vector2( 0, 0 ),"factor":1.0,"button_index":2,"pressed":false,"doubleclick":false,"script":null)
]
}
interact_reload={
"deadzone": 0.5,
"events": [ Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"alt":false,"shift":false,"control":false,"meta":false,"command":false,"pressed":false,"scancode":82,"unicode":0,"echo":false,"script":null)
]
}
interact_select={
"deadzone": 0.5,
"events": [ Object(InputEventMouseButton,"resource_local_to_scene":false,"resource_name":"","device":0,"alt":false,"shift":false,"control":false,"meta":false,"command":false,"button_mask":0,"position":Vector2( 0, 0 ),"global_position":Vector2( 0, 0 ),"factor":1.0,"button_index":3,"pressed":false,"doubleclick":false,"script":null)

@ -1,4 +1,4 @@
[gd_scene load_steps=12 format=2]
[gd_scene load_steps=16 format=2]
[ext_resource path="res://scene/GameScene.tscn" type="PackedScene" id=1]
[ext_resource path="res://src/Cursor.cs" type="Script" id=2]
@ -11,6 +11,31 @@
[ext_resource path="res://src/Network/IntegratedServer.cs" type="Script" id=9]
[ext_resource path="res://ui_theme.tres" type="Theme" id=10]
[ext_resource path="res://src/RadialMenu.cs" type="Script" id=11]
[ext_resource path="res://src/WeaponInfo.cs" type="Script" id=12]
[sub_resource type="StyleBoxFlat" id=1]
bg_color = Color( 0, 0, 0, 0.752941 )
border_width_left = 1
border_width_top = 1
border_width_right = 1
border_width_bottom = 1
border_color = Color( 0, 0, 0, 0.501961 )
corner_detail = 1
expand_margin_left = 1.0
expand_margin_right = 1.0
expand_margin_top = 1.0
expand_margin_bottom = 1.0
[sub_resource type="StyleBoxFlat" id=2]
bg_color = Color( 0, 0.752941, 0, 0.752941 )
corner_detail = 1
[sub_resource type="Theme" id=3]
ProgressBar/colors/font_color = Color( 0.94, 0.94, 0.94, 1 )
ProgressBar/colors/font_color_shadow = Color( 0, 0, 0, 1 )
ProgressBar/fonts/font = null
ProgressBar/styles/bg = SubResource( 1 )
ProgressBar/styles/fg = SubResource( 2 )
[node name="Client" instance=ExtResource( 1 )]
script = ExtResource( 8 )
@ -70,3 +95,34 @@ follow_viewport_enable = true
z_index = 1000
texture = ExtResource( 3 )
script = ExtResource( 2 )
[node name="WeaponInfo" type="Node2D" parent="CursorLayer/Cursor" index="0"]
script = ExtResource( 12 )
[node name="Rounds" type="Label" parent="CursorLayer/Cursor/WeaponInfo" index="0"]
anchor_left = 0.5
anchor_right = 0.5
margin_left = -40.0
margin_top = 7.0
margin_right = 40.0
margin_bottom = 16.0
theme = ExtResource( 10 )
text = "12/12"
align = 1
valign = 1
__meta__ = {
"_edit_use_anchors_": false
}
[node name="Reloading" type="ProgressBar" parent="CursorLayer/Cursor/WeaponInfo" index="1"]
margin_left = -16.0
margin_top = 10.0
margin_right = 16.0
margin_bottom = 13.0
theme = SubResource( 3 )
max_value = 1.0
value = 0.5
percent_visible = false
__meta__ = {
"_edit_use_anchors_": false
}

@ -67,12 +67,15 @@ visible = false
texture = ExtResource( 7 )
offset = Vector2( 8, 0 )
script = ExtResource( 8 )
RateOfFire = 400
Capacity = 6
ReloadTime = 1.4
Knockback = 50.0
Spread = 1.5
SpreadIncrease = 1.0
RecoilMin = 3.0
RecoilMax = 5.0
BulletSpeed = 1200
BulletVelocity = 1200
[node name="Tip" type="Node2D" parent="Items/Revolver"]
position = Vector2( 15, -2.5 )
@ -85,15 +88,17 @@ visible = false
texture = ExtResource( 9 )
offset = Vector2( 8, 0 )
script = ExtResource( 8 )
EffectiveRange = 240
MaximumRange = 360
Capacity = 2
ReloadTime = 2.0
Knockback = 135.0
Spread = 8.0
SpreadIncrease = 10.0
RecoilMin = 6.0
RecoilMax = 12.0
BulletSpeed = 1000
BulletsPetShot = 8
EffectiveRange = 240
MaximumRange = 360
BulletVelocity = 1000
BulletsPerShot = 6
BulletOpacity = 0.1
[node name="Tip" type="Node2D" parent="Items/Shotgun"]
@ -107,13 +112,15 @@ visible = false
texture = ExtResource( 11 )
offset = Vector2( 8, 0 )
script = ExtResource( 8 )
EffectiveRange = 480
MaximumRange = 920
Capacity = 1
ReloadTime = 1.6
Knockback = 100.0
SpreadIncrease = 2.0
RecoilMin = 8.0
RecoilMax = 8.0
BulletSpeed = 4000
EffectiveRange = 480
MaximumRange = 920
BulletVelocity = 4000
BulletOpacity = 0.4
[node name="Tip" type="Node2D" parent="Items/Rifle"]
@ -127,11 +134,16 @@ visible = false
texture = ExtResource( 12 )
offset = Vector2( 8, 0 )
script = ExtResource( 8 )
Automatic = true
RateOfFire = 600
Capacity = 30
ReloadTime = 2.4
Knockback = 30.0
Spread = 0.6
SpreadIncrease = 0.8
RecoilMin = 1.0
RecoilMax = 2.5
BulletOpacity = 0.15
[node name="Tip" type="Node2D" parent="Items/Assault Rifle"]
position = Vector2( 22, -1.5 )
@ -144,6 +156,7 @@ visible = false
texture = ExtResource( 10 )
offset = Vector2( 8, 0 )
script = ExtResource( 8 )
Automatic = true
[node name="Tip" type="Node2D" parent="Items/Super Soaker"]
position = Vector2( 17, 0.5 )

@ -1,83 +1,65 @@
using System;
using Godot;
// TODO: "Click" sound when attempting to fire when not ready, or empty.
// TODO: "Reload" sound when reloading.
// TODO: "Single reload" for revolver & shotgun.
// TODO: Add outline around sprites.
public class Weapon : Sprite
{
[Export] public int EffectiveRange { get; set; } = 320;
[Export] public int MaximumRange { get; set; } = 640;
[Export] public float Knockback { get; set; } = 0.0F;
[Export] public bool Automatic { get; set; } = false;
[Export] public int RateOfFire { get; set; } = 100; // rounds/minute
[Export] public int Capacity { get; set; } = 12;
[Export] public float ReloadTime { get; set; } = 1.0F;
[Export] public float Knockback { get; set; } = 0.0F;
[Export] public float Spread { get; set; } = 0.0F;
[Export] public float SpreadIncrease { get; set; } = 0.0F;
[Export] public float RecoilMin { get; set; } = 0.0F;
[Export] public float RecoilMax { get; set; } = 0.0F;
[Export] public int BulletSpeed { get; set; } = 2000;
[Export] public int BulletsPetShot { get; set; } = 1;
[Export] public int EffectiveRange { get; set; } = 320;
[Export] public int MaximumRange { get; set; } = 640;
[Export] public int BulletVelocity { get; set; } = 2000;
[Export] public int BulletsPerShot { get; set; } = 1;
[Export] public float BulletOpacity { get; set; } = 0.2F;
public Cursor Cursor { get; private set; }
public Player Player { get; private set; }
public float _fireDelay;
public float? _reloading;
private float _currentSpreadInc = 0.0F;
private float _currentRecoil = 0.0F;
public int Rounds { get; private set; }
public float AimDirection { get; private set; }
public TimeSpan? HoldingTrigger { get; private set; }
// TODO: Tell the server when we're pressing/releasing the trigger.
public float? ReloadProgress => 1 - _reloading / ReloadTime;
public Cursor Cursor { get; private set; }
public Player Player { get; private set; }
public override void _Ready()
{
Rounds = Capacity;
Cursor = this.GetClient()?.Cursor;
Player = GetParent().GetParent<Player>();
}
public override void _UnhandledInput(InputEvent ev)
{
if (!(Player is LocalPlayer localPlayer)) return;
if (ev.IsActionPressed("interact_primary")) {
var seed = unchecked((int)GD.Randi());
ShootInternal(AimDirection, Scale.y > 0, seed);
RpcId(1, nameof(Shoot), AimDirection, Scale.y > 0, seed);
localPlayer.Velocity -= Mathf.Polar2Cartesian(Knockback, Rotation);
}
}
[Remote]
private void Shoot(float aimDirection, bool toRight, int seed)
{
if (this.GetGame() is Server) {
if (Player.NetworkID != GetTree().GetRpcSenderId()) return;
// TODO: Verify input.
Rpc(nameof(Shoot), aimDirection, toRight, seed);
} else if (Player is LocalPlayer) return;
ShootInternal(aimDirection, toRight, seed);
}
private void ShootInternal(float aimDirection, bool toRight, int seed)
{
if (this.GetGame() is Client)
GetNodeOrNull<AudioStreamPlayer2D>("Fire")?.Play();
var random = new Random(seed);
var angle = aimDirection - _currentRecoil * (toRight ? 1 : -1);
var tip = GetNode<Node2D>("Tip").Position;
if (!toRight) tip.y *= -1;
tip = tip.Rotated(angle);
for (var i = 0; i < BulletsPetShot; i++) {
var spread = (Mathf.Deg2Rad(Spread) + _currentSpreadInc) * Mathf.Clamp(random.NextGaussian(0.4F), -1, 1);
var dir = Mathf.Polar2Cartesian(1, angle + spread);
var color = new Color(Player.Color, BulletOpacity);
var bullet = new Bullet(Player.Position + tip, dir, EffectiveRange, MaximumRange, BulletSpeed, color);
this.GetWorld().AddChild(bullet);
if (!(Player is LocalPlayer)) return;
if (ev.IsActionPressed("interact_primary"))
{ HoldingTrigger = TimeSpan.Zero; OnTriggerPressed(); }
if (ev.IsActionPressed("interact_reload") && (Rounds < Capacity) && (_reloading == null))
{ _reloading = ReloadTime; }
}
_currentSpreadInc += Mathf.Deg2Rad(SpreadIncrease);
_currentRecoil += Mathf.Deg2Rad(random.NextFloat(RecoilMin, RecoilMax));
}
protected virtual void OnTriggerPressed() => Fire();
protected virtual void OnTriggerReleased() { }
public override void _Process(float delta)
{
@ -86,11 +68,45 @@ public class Weapon : Sprite
_currentSpreadInc = Mathf.Max(0, _currentSpreadInc - spreadDecrease * delta);
_currentRecoil = Mathf.Max(0, _currentRecoil - recoilDecrease * delta);
if (Visible && (Player is LocalPlayer)) {
if (Visible) {
if (HoldingTrigger is TimeSpan holding)
HoldingTrigger = holding + TimeSpan.FromSeconds(delta);
if (_reloading is float reloading) {
_reloading = reloading - delta;
if (_reloading <= 0) {
Rounds = Capacity;
_reloading = null;
}
} else if (Rounds <= 0)
// Automatically reload when out of rounds.
_reloading = ReloadTime;
if (_fireDelay > 0) {
_fireDelay -= delta;
// We allow _fireDelay to go into negatives to allow
// for more accurate rate of fire for automatic weapons.
// Though, if the trigger isn't held down, reset it to 0.
if ((_fireDelay < 0) && (!Automatic || (HoldingTrigger == null)))
_fireDelay = 0;
}
if (Player is LocalPlayer) {
if (HoldingTrigger != null) {
if (!Input.IsActionPressed("interact_primary")) {
HoldingTrigger = null;
OnTriggerReleased();
} else if (Automatic)
Fire();
}
AimDirection = Cursor.Position.AngleToPoint(Player.Position);
RpcId(1, nameof(SendAimAngle), AimDirection);
Update();
}
} else {
_reloading = null;
}
var angle = Mathf.PosMod(AimDirection + Mathf.Pi, Mathf.Tau) - Mathf.Pi;
angle = Mathf.Abs(Mathf.Rad2Deg(angle));
@ -99,21 +115,10 @@ public class Weapon : Sprite
Rotation = AimDirection - _currentRecoil * ((Scale.y > 0) ? 1 : -1);
}
[Remote]
private void SendAimAngle(float value)
{
if (this.GetGame() is Server) {
if (Player.NetworkID != GetTree().GetRpcSenderId()) return;
// TODO: Verify input.
// if ((value < 0) || (value > Mathf.Tau)) return;
Rpc(nameof(SendAimAngle), value);
} else if (!(Player is LocalPlayer))
AimDirection = value;
}
public override void _Draw()
{
if (!(Player is LocalPlayer)) return;
// Draws an "aiming cone" to show where bullets might travel.
var tip = GetNode<Node2D>("Tip").Position + new Vector2(4, 0);
var angle = Mathf.Sin((Mathf.Deg2Rad(Spread) + _currentSpreadInc) / 2);
@ -153,4 +158,70 @@ public class Weapon : Sprite
}
private static Vector3 To3(Vector2 vec)
=> new Vector3(vec.x, vec.y, 0);
[Remote]
private void SendAimAngle(float value)
{
if (this.GetGame() is Server) {
if (Player.NetworkID != GetTree().GetRpcSenderId()) return;
// TODO: Verify input.
// if ((value < 0) || (value > Mathf.Tau)) return;
Rpc(nameof(SendAimAngle), value);
} else if (!(Player is LocalPlayer))
AimDirection = value;
}
private void Fire()
{
var seed = unchecked((int)GD.Randi());
if (!FireInternal(AimDirection, Scale.y > 0, seed)) return;
RpcId(1, nameof(Fire), AimDirection, Scale.y > 0, seed);
((LocalPlayer)Player).Velocity -= Mathf.Polar2Cartesian(Knockback, Rotation);
}
[Remote]
private void Fire(float aimDirection, bool toRight, int seed)
{
if (this.GetGame() is Server) {
if (Player.NetworkID != GetTree().GetRpcSenderId()) return;
// TODO: Verify input.
if (FireInternal(aimDirection, toRight, seed))
Rpc(nameof(Fire), aimDirection, toRight, seed);
} else if (!(Player is LocalPlayer))
FireInternal(aimDirection, toRight, seed);
}
protected virtual bool FireInternal(float aimDirection, bool toRight, int seed)
{
if ((_reloading != null) || (Rounds <= 0) || (_fireDelay > 0)) return false;
if (this.GetGame() is Client)
GetNodeOrNull<AudioStreamPlayer2D>("Fire")?.Play();
var random = new Random(seed);
var angle = aimDirection - _currentRecoil * (toRight ? 1 : -1);
var tip = GetNode<Node2D>("Tip").Position;
if (!toRight) tip.y *= -1;
tip = tip.Rotated(angle);
for (var i = 0; i < BulletsPerShot; i++) {
var spread = (Mathf.Deg2Rad(Spread) + _currentSpreadInc) * Mathf.Clamp(random.NextGaussian(0.4F), -1, 1);
var dir = Mathf.Polar2Cartesian(1, angle + spread);
var color = new Color(Player.Color, BulletOpacity);
var bullet = new Bullet(Player.Position + tip, dir, EffectiveRange, MaximumRange, BulletVelocity, color);
this.GetWorld().AddChild(bullet);
}
_currentSpreadInc += Mathf.Deg2Rad(SpreadIncrease);
_currentRecoil += Mathf.Deg2Rad(random.NextFloat(RecoilMin, RecoilMax));
if ((this.GetGame() is Server) || (Player is LocalPlayer)) {
// Do not keep track of fire rate or ammo for other players.
_fireDelay += 60.0F / RateOfFire;
Rounds -= 1;
}
return true;
}
}

@ -8,7 +8,7 @@ public class Bullet : Node2D
public Vector2 Direction { get; }
public int EffectiveRange { get; }
public int MaximumRange { get; }
public int Speed { get; }
public int Velocity { get; }
public Color Color { get; }
private readonly Vector2 _startPosition;
@ -16,13 +16,13 @@ public class Bullet : Node2D
private float _distance;
public Bullet(Vector2 position, Vector2 direction,
int effectiveRange, int maximumRange, int speed, Color color)
int effectiveRange, int maximumRange, int velocity, Color color)
{
_startPosition = Position = position;
Direction = direction;
EffectiveRange = effectiveRange;
MaximumRange = maximumRange;
Speed = speed;
Velocity = velocity;
Color = color;
}
@ -42,7 +42,7 @@ public class Bullet : Node2D
public override void _PhysicsProcess(float delta)
{
var previousPosition = Position;
_distance = Mathf.Min(MaximumRange, Speed * (float)_age.TotalSeconds);
_distance = Mathf.Min(MaximumRange, Velocity * (float)_age.TotalSeconds);
Position = _startPosition + Direction * _distance;
var collision = GetWorld2d().DirectSpaceState.IntersectRay(

@ -1,6 +1,7 @@
using System;
using Godot;
// TODO: Display number of rounds for weapons? Add an even smaller font for this?
public class RadialMenu : Node2D
{
[Export] public int InnerRadius { get; set; } = 32;
@ -28,6 +29,8 @@ public class RadialMenu : Node2D
public override void _UnhandledInput(InputEvent ev)
{
if (GetItems() == null) return;
if (ev.IsActionPressed("interact_select")) {
Position = this.GetClient().Cursor.ScreenPosition.Round();
Visible = true;
@ -41,6 +44,7 @@ public class RadialMenu : Node2D
ActiveName.Text = _selected?.Name ?? "";
Update();
}
if (ev.IsActionPressed("interact_select_dec") || ev.IsActionPressed("interact_select_inc")) {
var diff = ev.IsActionPressed("interact_select_inc") ? 1 : -1;
var items = GetItems();
@ -88,6 +92,8 @@ public class RadialMenu : Node2D
}
var items = GetItems();
if (items == null) return;
var cursor = ToLocal(this.GetClient().Cursor.ScreenPosition);
var angle = cursor.Angle() - _startAngle;
var index = (int)((angle / Mathf.Tau + 1) % 1 * MinElements);

@ -0,0 +1,52 @@
using System;
using Godot;
// TODO: Render rounds as sprites?
public class WeaponInfo : Node2D
{
private static readonly TimeSpan VISIBLE_DURATION = TimeSpan.FromSeconds(0.8);
public Label Rounds { get; private set; }
public ProgressBar Reloading { get; private set; }
private float _visibleFor;
private Weapon _previousWeapon;
private int _previousRounds;
public override void _Ready()
{
Rounds = GetNode<Label>("Rounds");
Reloading = GetNode<ProgressBar>("Reloading");
}
public override void _Process(float delta)
{
if (!(this.GetClient().LocalPlayer?.GetNode<IItems>("Items").Current is Weapon weapon))
{ Visible = false; _previousWeapon = null; return; }
Visible = true;
if ((_visibleFor += delta) > VISIBLE_DURATION.TotalSeconds) {
Modulate = new Color(Modulate, Modulate.a - delta);
if (Modulate.a <= 0) Visible = false;
}
if ((weapon != _previousWeapon) ||
(weapon.Rounds != _previousRounds) ||
(weapon.ReloadProgress != null)) {
_visibleFor = 0.0F;
Modulate = Colors.White;
Rounds.Visible = weapon.Capacity > 1;
Rounds.Text = $"{weapon.Rounds}/{weapon.Capacity}";
if (weapon.ReloadProgress is float reloading) {
Reloading.Visible = true;
Reloading.Value = reloading;
} else Reloading.Visible = false;
_previousWeapon = weapon;
_previousRounds = weapon.Rounds;
}
}
}
Loading…
Cancel
Save