Add creative-style building

- Add CreativeBuilding node / script,
  handles placing and breaking of blocks
- Move spawn block packets to static
  BlockPackets class, add destroy packet
- Move Cursor to its own canvas layer so
  its position can be used in other scripts
- Use viewport transform instead of
  camera position in Background script
main
copygirl 5 years ago
parent ef65a0e6ac
commit b06dd6f610
  1. 10
      project.godot
  2. 10
      scene/Block.tscn
  3. 32
      scene/GameScene.tscn
  4. 8
      scene/LocalPlayer.tscn
  5. 8
      scene/Player.tscn
  6. 10
      src/Background.cs
  7. 6
      src/Block.cs
  8. 61
      src/BlockPackets.cs
  9. 175
      src/CreativeBuilding.cs
  10. 2
      src/Cursor.cs
  11. 4
      src/EscapeMenuAppearance.cs
  12. 17
      src/Game.cs
  13. 17
      src/LocalPlayer.cs
  14. 48
      src/Network.cs
  15. 18
      src/Player.cs

@ -77,6 +77,16 @@ move_jump={
, Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":0,"button_index":1,"pressure":0.0,"pressed":false,"script":null)
]
}
interact_place={
"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":1,"pressed":false,"doubleclick":false,"script":null)
]
}
interact_break={
"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":2,"pressed":false,"doubleclick":false,"script":null)
]
}
[rendering]

@ -1,16 +1,16 @@
[gd_scene load_steps=3 format=2]
[gd_scene load_steps=4 format=2]
[ext_resource path="res://gfx/block.png" type="Texture" id=1]
[ext_resource path="res://src/Block.cs" type="Script" id=2]
[sub_resource type="RectangleShape2D" id=3]
[sub_resource type="RectangleShape2D" id=1]
extents = Vector2( 8, 8 )
[node name="Block" type="StaticBody2D"]
position = Vector2( 0, 96 )
script = ExtResource( 2 )
[node name="RectangleShape" type="CollisionShape2D" parent="."]
shape = SubResource( 3 )
shape = SubResource( 1 )
[node name="Sprite" type="Sprite" parent="."]
texture = ExtResource( 1 )

@ -12,10 +12,11 @@
[ext_resource path="res://gfx/background.png" type="Texture" id=10]
[ext_resource path="res://src/Background.cs" type="Script" id=11]
[node name="Game" type="Node"]
[node name="Game" type="Node2D"]
pause_mode = 2
script = ExtResource( 3 )
BlockContainerPath = NodePath("Blocks")
CursorPath = NodePath("CursorLayer/Cursor")
BlockContainerPath = NodePath("World/Blocks")
BlockScene = ExtResource( 6 )
[node name="Viewport" type="Node" parent="."]
@ -23,7 +24,7 @@ script = ExtResource( 7 )
[node name="Network" type="Node" parent="."]
script = ExtResource( 8 )
PlayerContainerPath = NodePath("../Players")
PlayerContainerPath = NodePath("../World/Players")
OtherPlayerScene = ExtResource( 9 )
[node name="Background" type="TextureRect" parent="."]
@ -39,22 +40,25 @@ __meta__ = {
"_edit_use_anchors_": false
}
[node name="Players" type="Node" parent="."]
[node name="World" type="Node" parent="."]
[node name="Players" type="Node" parent="World"]
pause_mode = 1
[node name="LocalPlayer" parent="Players" instance=ExtResource( 5 )]
position = Vector2( 0, -2 )
[node name="LocalPlayer" parent="World/Players" instance=ExtResource( 5 )]
[node name="Blocks" type="Node" parent="."]
[node name="Blocks" type="Node" parent="World"]
[node name="HUD" type="CanvasLayer" parent="."]
[node name="Cursor" type="Node2D" parent="HUD"]
z_index = 1
script = ExtResource( 2 )
[node name="Sprite" type="Sprite" parent="HUD/Cursor"]
texture = ExtResource( 4 )
[node name="EscapeMenu" parent="HUD" instance=ExtResource( 1 )]
visible = false
[node name="CursorLayer" type="CanvasLayer" parent="."]
layer = 2
follow_viewport_enable = true
[node name="Cursor" type="Sprite" parent="CursorLayer"]
z_index = 1000
texture = ExtResource( 4 )
script = ExtResource( 2 )

@ -1,11 +1,15 @@
[gd_scene load_steps=3 format=2]
[gd_scene load_steps=4 format=2]
[ext_resource path="res://scene/Player.tscn" type="PackedScene" id=1]
[ext_resource path="res://src/CreativeBuilding.cs" type="Script" id=2]
[ext_resource path="res://src/LocalPlayer.cs" type="Script" id=3]
[node name="LocalPlayer" instance=ExtResource( 1 )]
script = ExtResource( 3 )
[node name="Camera" type="Camera2D" parent="." index="0"]
[node name="Camera" type="Camera2D" parent="." index="3"]
pause_mode = 2
current = true
[node name="CreativeBuilding" type="Node2D" parent="." index="4"]
script = ExtResource( 2 )

@ -17,10 +17,6 @@ SpritePath = NodePath("Sprite")
[node name="CircleShape" type="CollisionShape2D" parent="."]
shape = SubResource( 1 )
[node name="Sprite" type="Sprite" parent="."]
z_index = -5
texture = ExtResource( 2 )
[node name="DisplayName" type="Label" parent="."]
modulate = Color( 1, 1, 1, 0.501961 )
anchor_left = 0.5
@ -36,3 +32,7 @@ valign = 1
__meta__ = {
"_edit_use_anchors_": false
}
[node name="Sprite" type="Sprite" parent="."]
z_index = -5
texture = ExtResource( 2 )

@ -4,11 +4,9 @@ public class Background : TextureRect
{
public override void _Process(float delta)
{
var offset = new Vector2(8, 8);
var tileSize = Texture.GetSize();
var viewportSize = GetViewport().Size;
var cameraPos = LocalPlayer.Instance.GetNode<Camera2D>("Camera").GetCameraPosition();
RectPosition = ((cameraPos - viewportSize / 2) / tileSize).Floor() * tileSize - offset;
RectSize = ((viewportSize + offset) / tileSize + Vector2.One).Ceil() * tileSize;
var offset = new Vector2(8, 8);
var tileSize = Texture.GetSize();
RectPosition = (-GetViewportTransform().origin / tileSize).Floor() * tileSize - offset;
RectSize = ((GetViewport().Size + offset) / tileSize + Vector2.One).Ceil() * tileSize;
}
}

@ -0,0 +1,6 @@
using Godot;
public class Block : StaticBody2D
{
// Empty, but useful to find out whether an object is a "block".
}

@ -0,0 +1,61 @@
using System.Collections.Generic;
using System.Linq;
using Godot;
public static class BlockPackets
{
public static void Register()
{
Network.API.RegisterS2CPacket<SpawnBlockPacket>(OnSpawnBlockPacket);
Network.API.RegisterS2CPacket<SpawnBlocksPacket>(OnSpawnBlocksPacket);
Network.API.RegisterS2CPacket<DestroyBlockPacket>(OnDestroyBlockPacket);
}
private static void OnSpawnBlockPacket(SpawnBlockPacket packet)
{
// Delete any block previously at this position.
Game.Instance.GetBlockAt(packet.Position)?.QueueFree();
var block = Game.Instance.BlockScene.Init<Block>();
block.Position = packet.Position;
block.Modulate = packet.Color;
Game.Instance.BlockContainer.AddChild(block);
}
private static void OnSpawnBlocksPacket(SpawnBlocksPacket packet)
{
Game.Instance.ClearBlocks();
foreach (var blockInfo in packet.Blocks) {
var block = Game.Instance.BlockScene.Init<Block>();
block.Position = blockInfo.Position;
block.Modulate = blockInfo.Color;
Game.Instance.BlockContainer.AddChild(block);
}
}
private static void OnDestroyBlockPacket(DestroyBlockPacket packet)
=> Game.Instance.GetBlockAt(packet.Position)?.QueueFree();
}
public class SpawnBlockPacket
{
public Vector2 Position { get; }
public Color Color { get; }
public SpawnBlockPacket(Block block)
{ Position = block.Position; Color = block.Modulate; }
}
public class SpawnBlocksPacket
{
public List<SpawnBlockPacket> Blocks { get; }
public SpawnBlocksPacket()
=> Blocks = Game.Instance.BlockContainer.GetChildren().OfType<Block>()
.Select(block => new SpawnBlockPacket(block)).ToList();
}
public class DestroyBlockPacket
{
public Vector2 Position { get; }
public DestroyBlockPacket(Block block)
{ Position = block.Position; }
}

@ -0,0 +1,175 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Godot;
public class CreativeBuilding : Node2D
{
private enum BuildMode
{
Placing,
Breaking,
}
private static readonly Vector2[] _neighborPositions = new Vector2[]{
Vector2.Left*16, Vector2.Right*16, Vector2.Up*16, Vector2.Down*16 };
[Export] public int MaxLength { get; set; } = 6;
private Texture _blockTex;
private Vector2 _startPos;
private Vector2 _direction;
private int _length;
private bool _canBuild;
private BuildMode? _currentMode = null;
private IEnumerable<Vector2> BlockPositions =>
Enumerable.Range(0, _length + 1).Select(i => _startPos + _direction * (i * 16));
public override void _Ready()
{
_blockTex = GD.Load<Texture>("res://gfx/block.png");
}
public override void _Process(float delta)
{
Update();
if (EscapeMenu.Instance.Visible || !Game.Cursor.Visible)
{ _currentMode = null; return; }
switch (_currentMode) {
case null:
if (Input.IsActionJustPressed("interact_place"))
if (_canBuild) _currentMode = BuildMode.Placing;
if (Input.IsActionJustPressed("interact_break"))
_currentMode = BuildMode.Breaking;
break;
case BuildMode.Placing:
if (Input.IsActionJustPressed("interact_break")) _currentMode = null;
else if (!Input.IsActionPressed("interact_place")) {
if (_canBuild)
foreach (var pos in BlockPositions)
PlaceBlock(pos);
_currentMode = null;
}
break;
case BuildMode.Breaking:
if (Input.IsActionJustPressed("interact_place")) _currentMode = null;
else if (!Input.IsActionPressed("interact_break")) {
foreach (var pos in BlockPositions) {
var block = Game.Instance.GetBlockAt(pos);
if (block != null) BreakBlock(block);
}
_currentMode = null;
}
break;
}
if (_currentMode != null) {
var rad90 = Mathf.Deg2Rad(90.0F);
var angle = Mathf.Round(_startPos.AngleToPoint(Game.Cursor.Position) / rad90) * rad90;
_direction = new Vector2(-Mathf.Cos(angle), -Mathf.Sin(angle));
_length = Math.Min(MaxLength, Mathf.RoundToInt(_startPos.DistanceTo(Game.Cursor.Position) / 16));
} else {
_startPos = (Game.Cursor.Position / 16).Round() * 16;
_length = 0;
}
bool IsBlockAt(Vector2 pos) => Game.Instance.GetBlockAt(pos) != null;
_canBuild = !IsBlockAt(_startPos) && _neighborPositions.Any(pos => IsBlockAt(_startPos + pos));
}
private Block PlaceBlock(Vector2 position)
{
if (Game.Instance.GetBlockAt(position) != null) return null;
// FIXME: Test if there is a player in the way.
var block = Game.Instance.BlockScene.Init<Block>();
block.Position = position;
block.Modulate = Game.LocalPlayer.Color.Blend(Color.FromHsv(0.0F, 0.0F, GD.Randf(), 0.2F));
Game.Instance.BlockContainer.AddChild(block);
if (Network.IsMultiplayerReady) {
if (Network.IsServer) Network.API.SendToEveryone(new SpawnBlockPacket(block));
else Network.API.SendToServer(new PlaceBlockPacket(position));
}
return block;
}
private void BreakBlock(Block block)
{
// FIXME: Use a different (safer) way to check if a block is one of the default ones.
if (block.Modulate.s < 0.5F) return;
if (Network.IsMultiplayerReady) {
if (Network.IsServer) Network.API.SendToEveryone(new DestroyBlockPacket(block));
else Network.API.SendToServer(new BreakBlockPacket(block));
}
block.QueueFree();
}
public override void _Draw()
{
if (!Game.Cursor.Visible) return;
var green = Color.FromHsv(1.0F / 3, 1.0F, 1.0F, 0.4F);
var red = Color.FromHsv(0.0F, 1.0F, 1.0F, 0.4F);
var black = new Color(0.0F, 0.0F, 0.0F, 0.65F);
foreach (var pos in BlockPositions) {
var hasBlock = Game.Instance.GetBlockAt(pos) != null;
var color = (_currentMode != BuildMode.Breaking)
? ((_canBuild && !hasBlock) ? green : red)
: (hasBlock ? black : red);
DrawTexture(_blockTex, ToLocal(pos - _blockTex.GetSize() / 2), color);
}
}
public static void RegisterPackets()
{
Network.API.RegisterC2SPacket<PlaceBlockPacket>(OnPlaceBlockPacket);
Network.API.RegisterC2SPacket<BreakBlockPacket>(OnBreakBlockPacket);
}
private class PlaceBlockPacket
{
public Vector2 Position { get; }
public PlaceBlockPacket(Vector2 position) => Position = position;
}
private static void OnPlaceBlockPacket(Player player, PlaceBlockPacket packet)
{
if (Game.Instance.GetBlockAt(packet.Position) != null) return;
var block = Game.Instance.BlockScene.Init<Block>();
block.Position = packet.Position;
block.Modulate = player.Color.Blend(Color.FromHsv(0.0F, 0.0F, GD.Randf(), 0.2F));
Game.Instance.BlockContainer.AddChild(block);
Network.API.SendToEveryone(new SpawnBlockPacket(block));
}
private class BreakBlockPacket
{
public Vector2 Position { get; }
public BreakBlockPacket(Block block) => Position = block.Position;
}
private static void OnBreakBlockPacket(Player player, BreakBlockPacket packet)
{
var block = Game.Instance.GetBlockAt(packet.Position);
if (block == null) return;
if (block.Modulate.s < 0.5F) {
// TODO: Respawn the block the client thought it destroyed?
return;
}
// TODO: Further verify whether player can break a block at this position.
Network.API.SendToEveryoneExcept(player, new DestroyBlockPacket(block));
block.QueueFree();
}
}

@ -17,6 +17,6 @@ public class Cursor : Node2D
public override void _Process(float delta)
{
Position = GetGlobalMousePosition();
Position = GetGlobalMousePosition() - GetViewport().CanvasTransform.origin;
}
}

@ -19,7 +19,7 @@ public class EscapeMenuAppearance : CenterContainer
ColorSlider.Value = GD.Randf();
var color = Color.FromHsv((float)ColorSlider.Value, 1.0F, 1.0F);
LocalPlayer.Instance.Color = ColorPreview.Modulate = color;
Game.LocalPlayer.Color = ColorPreview.Modulate = color;
}
@ -46,7 +46,7 @@ public class EscapeMenuAppearance : CenterContainer
private void _on_Appearance_visibility_changed()
{
if (!IsVisibleInTree())
Player.ChangeAppearance(LocalPlayer.Instance,
Player.ChangeAppearance(Game.LocalPlayer,
DisplayName.Text, ColorPreview.Modulate,
Network.IsClient);
}

@ -1,9 +1,16 @@
using System.Linq;
using Godot;
using Godot.Collections;
public class Game : Node
public class Game : Node2D
{
public static Game Instance { get; private set; }
public static LocalPlayer LocalPlayer { get; internal set; }
public static Cursor Cursor { get; private set; }
[Export] public NodePath CursorPath { get; set; }
[Export] public NodePath BlockContainerPath { get; set; }
[Export] public PackedScene BlockScene { get; set; }
@ -17,6 +24,7 @@ public class Game : Node
public override void _Ready()
{
Cursor = GetNode<Cursor>(CursorPath);
BlockContainer = GetNode(BlockContainerPath);
SpawnDefaultBlocks();
}
@ -30,10 +38,15 @@ public class Game : Node
public void SpawnDefaultBlocks()
{
for (var x = -6; x <= 6; x++) {
var block = BlockScene.Init<Node2D>();
var block = BlockScene.Init<Block>();
block.Position = new Vector2(x * 16, 48);
block.Modulate = Color.FromHsv(GD.Randf(), 0.1F, 1.0F);
BlockContainer.AddChild(block);
}
}
// FIXME: Can only be called during _physics_process?!
public Block GetBlockAt(Vector2 position)
=> GetWorld2d().DirectSpaceState.IntersectPoint(position).Cast<Dictionary>()
.Select(c => c["collider"]).OfType<Block>().FirstOrDefault();
}

@ -1,16 +1,15 @@
using System;
using Godot;
// TODO: Implement "low jumps" activated by releasing the jump button early.
public class LocalPlayer : Player
{
public static LocalPlayer Instance { get; private set; }
public TimeSpan JumpEarlyTime { get; } = TimeSpan.FromSeconds(0.2F);
public TimeSpan JumpCoyoteTime { get; } = TimeSpan.FromSeconds(0.2F);
[Export] public float Speed { get; set; } = 120;
[Export] public float JumpSpeed { get; set; } = 180;
[Export] public float Gravity { get; set; } = 400;
[Export] public float MovementSpeed { get; set; } = 160;
[Export] public float JumpVelocity { get; set; } = 240;
[Export] public float Gravity { get; set; } = 480;
[Export(PropertyHint.Range, "0,1")]
public float Friction { get; set; } = 0.1F;
@ -21,8 +20,8 @@ public class LocalPlayer : Player
private DateTime? _jumpPressed = null;
private DateTime? _lastOnFloor = null;
public override void _EnterTree() => Instance = this;
public override void _ExitTree() => Instance = null;
public override void _EnterTree() => Game.LocalPlayer = this;
public override void _ExitTree() => Game.LocalPlayer = null;
public override void _PhysicsProcess(float delta)
{
@ -33,7 +32,7 @@ public class LocalPlayer : Player
jumpPressed = Input.IsActionJustPressed("move_jump");
}
Velocity.x = (moveDir != 0) ? Mathf.Lerp(Velocity.x, moveDir * Speed, Acceleration)
Velocity.x = (moveDir != 0) ? Mathf.Lerp(Velocity.x, moveDir * MovementSpeed, Acceleration)
: Mathf.Lerp(Velocity.x, 0, Friction);
Velocity.y += Gravity * delta;
Velocity = MoveAndSlide(Velocity, Vector2.Up);
@ -43,7 +42,7 @@ public class LocalPlayer : Player
if (((DateTime.Now - _jumpPressed) <= JumpEarlyTime) &&
((DateTime.Now - _lastOnFloor) <= JumpCoyoteTime)) {
Velocity.y = -JumpSpeed;
Velocity.y = -JumpVelocity;
_jumpPressed = null;
_lastOnFloor = null;
}

@ -1,6 +1,5 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Godot;
public enum NetworkStatus
@ -63,9 +62,9 @@ public class Network : Node
API.RegisterC2SPacket<ClientAuthPacket>(OnClientAuthPacket);
API.RegisterS2CPacket<SpawnPlayerPacket>(OnSpawnPlayerPacket);
API.RegisterS2CPacket<SpawnBlockPacket>(OnSpawnBlockPacket);
API.RegisterS2CPacket<SpawnBlocksPacket>(OnSpawnBlocksPacket);
Player.RegisterPackets();
CreativeBuilding.RegisterPackets();
BlockPackets.Register();
}
// Let NetworkAPI handle receiving of custom packages.
@ -75,7 +74,7 @@ public class Network : Node
public void ResetGame()
{
LocalPlayer.Instance.NetworkID = -1;
Game.LocalPlayer.NetworkID = -1;
// Clear other players.
foreach (var player in _playersById.Values)
@ -106,8 +105,8 @@ public class Network : Node
if (error != Error.Ok) return error;
GetTree().NetworkPeer = peer;
LocalPlayer.Instance.NetworkID = 1;
_playersById.Add(1, LocalPlayer.Instance);
Game.LocalPlayer.NetworkID = 1;
_playersById.Add(1, Game.LocalPlayer);
ChangeStatus(NetworkStatus.ServerRunning);
return Error.Ok;
@ -143,10 +142,10 @@ public class Network : Node
ChangeStatus(NetworkStatus.Authenticating);
var id = GetTree().GetNetworkUniqueId();
LocalPlayer.Instance.NetworkID = id;
_playersById.Add(id, LocalPlayer.Instance);
Game.LocalPlayer.NetworkID = id;
_playersById.Add(id, Game.LocalPlayer);
API.SendToServer(new ClientAuthPacket(LocalPlayer.Instance));
API.SendToServer(new ClientAuthPacket(Game.LocalPlayer));
}
public void DisconnectFromServer()
@ -214,42 +213,13 @@ public class Network : Node
private void OnSpawnPlayerPacket(SpawnPlayerPacket packet)
{
if (packet.NetworkID == LocalNetworkID) {
var player = LocalPlayer.Instance;
var player = Game.LocalPlayer;
player.Position = packet.Position;
player.Velocity = Vector2.Zero;
ChangeStatus(NetworkStatus.ConnectedToServer);
} else SpawnOtherPlayer(packet.NetworkID, packet.Position, packet.DisplayName, packet.Color);
}
private struct SpawnBlockPacket
{
public Vector2 Position { get; }
public Color Color { get; }
public SpawnBlockPacket(Node2D block)
{ Position = block.Position; Color = block.Modulate; }
}
private void OnSpawnBlockPacket(SpawnBlockPacket packet)
{
var block = Game.Instance.BlockScene.Init<Node2D>();
block.Position = packet.Position;
block.Modulate = packet.Color;
Game.Instance.BlockContainer.AddChild(block);
}
private class SpawnBlocksPacket
{
public List<SpawnBlockPacket> Blocks { get; }
public SpawnBlocksPacket()
=> Blocks = Game.Instance.BlockContainer.GetChildren().OfType<Node2D>()
.Select(block => new SpawnBlockPacket(block)).ToList();
}
private void OnSpawnBlocksPacket(SpawnBlocksPacket packet)
{
Game.Instance.ClearBlocks();
foreach (var block in packet.Blocks)
OnSpawnBlockPacket(block);
}
private void OnPeerConnected(int id)
{

@ -91,35 +91,35 @@ public class Player : KinematicBody2D, IInitializer
private class PositionChangedPacket
{
public int ID { get; set; }
public Vector2 Position { get; set; }
public int ID { get; }
public Vector2 Position { get; }
public PositionChangedPacket(Player player)
{ ID = player.NetworkID; Position = player.Position; }
}
private class DisplayNameChangedPacket
{
public int ID { get; set; }
public string DisplayName { get; set; }
public int ID { get; }
public string DisplayName { get; }
public DisplayNameChangedPacket(Player player)
{ ID = player.NetworkID; DisplayName = player.DisplayName; }
}
private class ColorChangedPacket
{
public int ID { get; set; }
public Color Color { get; set; }
public int ID { get; }
public Color Color { get; }
public ColorChangedPacket(Player player)
{ ID = player.NetworkID; Color = player.Color; }
}
private class MovePacket
{
public Vector2 Position { get; set; }
public Vector2 Position { get; }
public MovePacket(Vector2 position) => Position = position;
}
private class ChangeAppearancePacket
{
public string DisplayName { get; set; }
public Color Color { get; set; }
public string DisplayName { get; }
public Color Color { get; }
public ChangeAppearancePacket(string displayName, Color color)
{ DisplayName = displayName; Color = color; }
}

Loading…
Cancel
Save