Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] Add Multiplayer Example #905

Draft
wants to merge 30 commits into
base: master
Choose a base branch
from
Draft

Conversation

ValorZard
Copy link

@ValorZard ValorZard commented Sep 23, 2024

Add multiplayer example!
heavily inspired by -> https://youtu.be/e0JLO_5UgQo?si=cauEVPrbe6ThtDTh
Fixes #903
pretty simple, but i do want to add some quality of life stuff

Also, we need to replace the old assets with better ones, since the ones I used have a weird license

@Bromeon Bromeon marked this pull request as draft September 24, 2024 07:21
@GodotRust
Copy link

API docs are being generated and will be shortly available at: https://godot-rust.github.io/docs/gdext/pr-905

@ValorZard
Copy link
Author

ValorZard commented Sep 24, 2024

Okay, so, spent all of today working on this, and I would love some fresh eyes.
I got the connection to work (ish) with a dedicated server mode.
However, for some reason, if you connect too fast, the game bugs out. Not sure why, probably something to do with the connection or game scene not initializing yet.
Tried to keep it as simple as possible, but I did attempt to add an actual game into this, by giving players health and the ability to shoot each other. When they die, they should respawn in a random location.
Really hope the code makes sense. I largely followed the code in FinePoinCGI's git repository, but cleaned it up and tried to make it sane.

To test this:

  1. Open up two or more clients.
  2. Choose one of them to be the host and click "Host"
  3. On all the other clients, click "Join"
  4. Wait a second or so, for some reason if you click start game too fast it doesn't work
  5. Once everyone is linked up, click "Start Game"
  6. Have fun!

In order to test dedicated server mode, run one of the clients with the arguments "--headless --server" and all of the other clients click "Join"
image

I would REALLY appreciate the help on this PR, this was a lot more complicated than I thought. Multiplayer is hard!

@ValorZard
Copy link
Author

I just realized that instead of randomly choosing a spawn location, I could have just saved the spawn point the player originally came from, and then just make the player respawn there on death. Though, that wouldn’t be as fair…

@ValorZard ValorZard marked this pull request as ready for review September 24, 2024 08:36
@Bromeon
Copy link
Member

Bromeon commented Sep 24, 2024

Thanks a lot for all your work on this, a little multiplayer demo sounds like an amazing addition 🙂

I'll give it more detailed review when out of draft, but some initial feedback already:

  • Instead of "WIP" in the title, you can create the PR as a draft (I converted it now). Later, you can mark it as ready. For this, please make sure the following are fulfilled:
    • CI is green. There is check.sh for local checks.
    • Contribution Guidelines are followed, especially number of commits.
  • Please don't change unrelated code like the dodge-the-creeps example.
  • The GDScript code is just needed during porting phase, right? Shouldn't be part of the final example.
  • Examples should focus on mostly one area of the engine or feature set and not try to pack as many features as possible. The reason is that the latter increases complexity and the time it takes for users to understand the code. Concretely:
    • There are some java-esque patterns like SceneManager and GameManager. Can we combine the two classes? How would a design look that doesn't require singletons? I don't want to promote this pattern in an example, especially since it's not yet thread-safe.
    • Do we need a massive animation setup and texture atlas? A minimal animation can be nice for visuals, but having dozens of frames adds nothing to the understanding of multiplayer.
  • As mentioned on Discord, all assets must have a clear license, preferrably Creative Commons and preferrably no restrictions on commercial use.

I haven't run it yet, is the game currently already playable?

@Bromeon Bromeon added feature Adds functionality to the library c: examples Code specific to examples changed (not just follow-up from API updates) labels Sep 24, 2024
@ValorZard
Copy link
Author

In order

  1. Cool! Didn’t know that
  2. That was totally by accident haha
  3. I forgot I still had GDScript in there, I’ll need to remove those and some other stuff in there like the non-creative common assets and some test scenes I can delete
  4. Yeah we can remove the animation stuff, and honestly the player can just be a static sprite. I was thinking we could maybe reuse some of the assets from dodge the creeps actually, but idk.
    Also, I’m not really sure how to get this working without the two managers to be honest. I’d love to hear feedback on better ways to design this!

Also, as mentioned earlier, this is playable right now! It’s just a little buggy for reasons I can’t quite grasp yet. I wrote up instructions in an earlier reply.

@Bromeon
Copy link
Member

Bromeon commented Sep 24, 2024

Do you think you could merge the 2 managers to 1? Maybe I can then see if there's a way to architect it differently.

Cool to hear it's playable! I don't know when I get around to test it out, but probably not today 🙂

@ValorZard
Copy link
Author

Actually, I just realized that it’s totally doable. GameManager is essentially just a wrapper class around a hash map, and I could move that hashmap into SceneManager itself!

@ValorZard
Copy link
Author

Alright, did some cleanup, and GameManager is now removed! Horray! we just pass around the hashmap containing all the player data now. Only problem is, the connection is still really inconsistent. I think its because the test scene spawns at different times for all the different clients, which leads to desyncs. Not really sure how to solve this, will need to look into this more.

@ValorZard
Copy link
Author

Some more assorted thoughts:
Setting spawn points and respawning players should all be done on the server side. Player will probably have a function called set_global_position_from_server() that is an rpc that can ONLY be called from server side, and sets both the sync and global position to whatever the server wants it to be.
There should also be a function in SceneManager called spawn_players() that is an RPC called to the server during ready(), where the server spawns all the players in the spawn points.

@ValorZard
Copy link
Author

ValorZard commented Sep 24, 2024

Yeah, im officially going to need some help with this.
From what I understand, SOMETHING is going horribly wrong, and I'm at the point where I think I would really appreciate someone else testing this out and looking at the code. All of the other issues (singletons, asset replacement) has been address.
Here's what the current problem is:
Sometimes, if you start the game too fast right after all clients connect, things horribly desync and I have no idea why.
I've tried debugging and refactoring, and while it looks like it may have helped with making the code more sane (or at least I hope so), the bug is still there and I literally have no idea why. This was never an issue with the GDScript version, and I'm starting to suspect some bizarre thread stuff is going on here with the Rust bindings.
This is where I stop for now, feel free to test it out and tell me your thoughts!

To test this out:
image
Click Customize Run Instances
image
set up the instances like so
image
click run
image
click join on both clients
image
once both clients are connected, click start!
image
it will sometimes look like this
image
and other times it will look like this
image
this error keeps popping up and I can't tell where

(Feel free to convert this back into a draft, I thought I was done but HO BOY)

@@ -13,7 +13,7 @@ members = [
"itest/rust",
"itest/repo-tweak",
"examples/dodge-the-creeps/rust",
"examples/hot-reload/rust",
"examples/hot-reload/rust", "examples/multiplayer-lan/rust",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you forget newline

Copy link
Contributor

@Yarwin Yarwin left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I left some comments. I'm not too experienced when it comes to networking with godot so I focused purely on godot-rust side of things. My main issue – why don't we just use one of godot's examples instead of frozen-in-time youtube video? Who is going to maintain this example and make sure it is up-to-date with latest changes and best practices?


const LOCALHOST: &str = "127.0.0.1";
const PORT: i32 = 8910;
#[derive(GodotClass, Clone)]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why does it derive a clone? Shouldn't it be passed around using Gd smart pointer?

Self {
address: LOCALHOST.into(),
port: PORT,
game_scene: PackedScene::new_gd(),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

… do we really want to create PackedScene::new_gd() instead of using an option? Not filling this property might lead to weird behaviours

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed this, you're right

}

#[derive(GodotClass)]
#[class(base=Control)]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

does it need custom init really? Can't we use #[init(default = …]?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ill be honest, i dont know what you mean by this

address: GString,
port: i32,
#[export]
game_scene: Gd<PackedScene>,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not option? What will happen if this property won't be filled? Why isn't it commented to inform user about your design decision?


#[godot_api]
impl MultiplayerController {
// called when a new "peer" gets connected with the server. Both client and server get notified about this
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

/// for docs.


/*
#[func]
fn respawn_player(&self, &mut player: Gd<Player>)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

leftover/dead code




Largely adapted from FinePointCGI's Godot multiplayer tutorial:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why are we adapting youtube tutorial instead of one of godot's demo projects? https://github.com/godotengine/godot-demo-projects/tree/master/networking

ProjectSettings::singleton()
.get_setting("physics/2d/default_gravity".into())
.try_to::<f64>(),
"default setting in Godot",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

…what does it error message do? What does it inform user about? "default setting in godot" informing about… not being able to cast given setting to float?

fn physics_process(&mut self, delta: f64) {
// delete bullet once LIFETIME seconds have passed
self.time_left -= delta;
if self.time_left <= 0.0 {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would rather use scene tree timers or at least SystemTime

}
// have bullet fall down while flying
if !self.base().is_on_floor() {
self.base_mut().get_velocity().x += (self.gravity * 1. * delta) as f32;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

get velocity returns Vector2 not &mut Vector2… https://godot-rust.github.io/docs/gdext/master/godot/classes/struct.CharacterBody2D.html#method.get_velocity. Have you ran this example?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

removed gravity since it seems unnecessary anyways

use crate::NetworkId;

const LOCALHOST: &str = "127.0.0.1";
const PORT: i32 = 8910;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

stupid question, but shouldn't it be unsigned int 32b?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Port numbers are usually a u16, e.g. std::net::SocketAddr::new. In Godot it's passed as a string, though. Maybe it'd be nice if we changed that string for a SocketAddr at one point.

@Yarwin
Copy link
Contributor

Yarwin commented Sep 27, 2024

CI/CD to run this example (same as we do with dodge the creeps) would be cool too

I'll build against this example and check it later

@@ -0,0 +1,57 @@
[gd_scene load_steps=2 format=3 uid="uid://btw6ethmr2246"]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this tmp file necessary to check in?

@Bromeon Bromeon marked this pull request as draft September 27, 2024 18:33
@ValorZard
Copy link
Author

Stopping here for now.

I took some advice from the server, and decided to start from scratch and port the multiplayer bomber example instead. However, I ran into the same problem I had when I was working on v1 of this example: namely, the signal/callback api.

So, my current plan is to wait until a later version of gdext when that is more user-friendly to continue with this PR. If anyone still wants to see how using multiplayer with gdext would work, I would advice to look at this commit: 043db66
^ this version of the code ALMOST works, which might be good enough for most people to then go one and figure it out on their own.

Good luck to anyone who tries to follow in my footsteps, here be dragons!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
c: examples Code specific to examples changed (not just follow-up from API updates) feature Adds functionality to the library
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Add example on how to use Godot Rust with godot's multiplayer API
6 participants