Skip to content

Commit

Permalink
Overhaul bomber tutorial
Browse files Browse the repository at this point in the history
  • Loading branch information
AngelOnFira committed Jan 8, 2024
1 parent 4f235b5 commit 8da97f2
Show file tree
Hide file tree
Showing 100 changed files with 2,890 additions and 449 deletions.
7 changes: 0 additions & 7 deletions .gitattributes
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,6 @@
*.webm filter=lfs diff=lfs merge=lfs -text
*.zip filter=lfs diff=lfs merge=lfs -text

# Godot
*.tscn filter=lfs diff=lfs merge=lfs -text
*.tres filter=lfs diff=lfs merge=lfs -text
*.scn filter=lfs diff=lfs merge=lfs -text
*.res filter=lfs diff=lfs merge=lfs -text
*.import filter=lfs diff=lfs merge=lfs -text

# Unity
*.anim filter=lfs diff=lfs merge=lfs -text
*.asset filter=lfs diff=lfs merge=lfs -text
Expand Down
25 changes: 4 additions & 21 deletions godot/bomber/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,33 +1,16 @@
FROM ubuntu:22.04 AS builder

# Install Godot & templates
ENV GODOT_VERSION="4.0.2"
RUN apt update -y \
&& apt install -y wget unzip \
&& wget https://downloads.tuxfamily.org/godotengine/${GODOT_VERSION}/Godot_v${GODOT_VERSION}-stable_linux.x86_64.zip \
&& wget https://downloads.tuxfamily.org/godotengine/${GODOT_VERSION}/Godot_v${GODOT_VERSION}-stable_export_templates.tpz

RUN mkdir -p ~/.cache ~/.config/godot ~/.local/share/godot/export_templates/${GODOT_VERSION}.stable \
&& unzip Godot_v${GODOT_VERSION}-stable_linux.x86_64.zip \
&& mv Godot_v${GODOT_VERSION}-stable_linux.x86_64 /usr/local/bin/godot \
&& unzip Godot_v${GODOT_VERSION}-stable_export_templates.tpz \
&& mv templates/* ~/.local/share/godot/export_templates/${GODOT_VERSION}.stable \
&& rm Godot_v${GODOT_VERSION}-stable_export_templates.tpz Godot_v${GODOT_VERSION}-stable_linux.x86_64.zip

# Build application
# MARK: Builder
FROM ghcr.io/angelonfira/rivet-godot-docker/godot:4.2 AS builder
WORKDIR /app
COPY . .
RUN mkdir -p build/linux \
&& godot -v --export-release "Linux/X11" --headless ./build/linux/game.x86_64

# ===

# MARK: Runner
FROM ubuntu:22.04
RUN apt update -y \
&& apt install -y expect-dev \
&& rm -rf /var/lib/apt/lists/*
COPY --from=builder /app/build/linux/ /app

# Unbuffer output so the logs get flushed
CMD ["sh", "-c", "unbuffer /app/game.x86_64 --verbose --headless -- --server | cat"]

CMD ["sh", "-c", "echo 'test' && env && unbuffer /app/game.x86_64 --verbose --headless -- --server | cat"]
2 changes: 1 addition & 1 deletion godot/bomber/LICENSE
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
MIT License

Copyright (c) 2024 Rivet
Copyright (c) 2023 Rivet

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
Expand Down
34 changes: 9 additions & 25 deletions godot/bomber/README.md
Original file line number Diff line number Diff line change
@@ -1,31 +1,15 @@
# Bomber
# Multiplayer Bomber

<p align="center">
<img src="./_media/preview_512.png" />
</p>
A multiplayer implementation of the classical bomberman game.
One of the players should press "host", while the other
should type in his address and press "play".

Language: GDScript

[Visit Tutorial](https://rivet.gg/learn/godot/tutorials/crash-course)
Renderer: GLES 2

Check out this demo on the asset library: https://godotengine.org/asset-library/asset/139

| Engine Version | Language | Networking |
| --- | --- | --- |
| 4.0.0 | [GDScript](https://docs.godotengine.org/en/stable/getting_started/scripting/gdscript/gdscript_basics.html) | [High-Level Multiplayer](https://docs.godotengine.org/en/stable/tutorials/networking/high_level_multiplayer.html) |

**Rivet Features**

- [♟️ Matchmaker](https://rivet.gg/docs/matchmaker)
- [🌐 Dynamic Servers](https://rivet.gg/docs/dynamic-servers)


## Running locally

1. [Clone the GitHub repo](https://docs.github.com/en/repositories/creating-and-managing-repositories/cloning-a-repository)
2. Open this folder
3. Run: `rivet init`
4. Run `yarn start`

## Deploying to Rivet

[Documentation](https://rivet.gg/learn/godot/tutorials/crash-course#step-4-deploy-to-rivet)
## Screenshots

![Screenshot](screenshots/bomber.png)
132 changes: 132 additions & 0 deletions godot/bomber/addons/rivet/api/rivet_api.gd
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
class_name RivetApi
const RivetRequest = preload("rivet_request.gd")

static var CONFIGURATION_CACHE

static func _get_configuration():
if CONFIGURATION_CACHE:
return CONFIGURATION_CACHE

if FileAccess.file_exists(RivetPluginBridge.RIVET_CONFIGURATION_FILE_PATH):
var config_file = ResourceLoader.load(RivetPluginBridge.RIVET_CONFIGURATION_FILE_PATH)
if config_file and 'new' in config_file:
CONFIGURATION_CACHE = config_file.new()
return CONFIGURATION_CACHE

if FileAccess.file_exists(RivetPluginBridge.RIVET_DEPLOYED_CONFIGURATION_FILE_PATH):
var deployed_config_file = ResourceLoader.load(RivetPluginBridge.RIVET_DEPLOYED_CONFIGURATION_FILE_PATH)
if deployed_config_file and 'new' in deployed_config_file:
CONFIGURATION_CACHE = deployed_config_file.new()
return CONFIGURATION_CACHE

push_warning("Rivet configuration file not found")
CONFIGURATION_CACHE = null
return CONFIGURATION_CACHE

static func _get_api_url():
# Use plugin config if available
var plugin = RivetPluginBridge.get_plugin()
if plugin:
return plugin.api_endpoint

# Override shipped configuration endpoint
var url_env = OS.get_environment("RIVET_API_ENDPOINT")
if url_env:
return url_env

# Use configuration shipped with game
var config = _get_configuration()
if config:
return config.api_endpoint

# Fallback
return "https://api.rivet.gg"

## Get authorization token used from within only the plugin for cloud-specific
## actions.
static func _get_cloud_token():
# Use plugin config if available
var plugin = RivetPluginBridge.get_plugin()
if plugin:
return plugin.cloud_token

OS.crash("Rivet cloud token not found, this should only be called within the plugin")

## Get authorization token used for making requests from within the game.
##
## The priority of tokens is:
##
## - If in editor, use the plugin token
## - If provided by environment, then use that (allows for testing)
## - Assume config is provided by the game client
static func _get_runtime_token():
# Use plugin config if available
var plugin = RivetPluginBridge.get_plugin()
if plugin:
return plugin.namespace_token

# Use configuration shipped with game
var token_env = OS.get_environment("RIVET_TOKEN")
if token_env:
return token_env

# Use configuration shipped with game
var config = _get_configuration()
if config:
return config.namespace_token

OS.crash("Rivet token not found, validate a config is shipped with the game in the .rivet folder")

## Builds the headers for a request, including the authorization token
static func _build_headers(service: String) -> PackedStringArray:
var token = _get_cloud_token() if service == "cloud" else _get_runtime_token()
return [
"Authorization: Bearer " + token,
]

## Builds a URL to Rivet cloud services
static func _build_url(path: String, service: String) -> String:
var path_segments := path.split("/", false)
path_segments.remove_at(0)
return _get_api_url() + "/%s/%s" % [service, "/".join(path_segments)]

## Gets service name from a path (e.g. /users/123 -> users)
static func _get_service_from_path(path: String) -> String:
var path_segments := path.split("/", false)
return path_segments[0]

## Creates a POST request to Rivet cloud services
## @experimental
static func POST(owner: Node, path: String, body: Dictionary) -> RivetRequest:
var service := _get_service_from_path(path)
var url := _build_url(path, service)
var body_json := JSON.stringify(body)

return RivetRequest.new(owner, HTTPClient.METHOD_POST, url, {
"headers": _build_headers(service),
"body": body_json
})

## Creates a GET request to Rivet cloud services
## @experimental
static func GET(owner: Node, path: String, body: Dictionary) -> RivetRequest:
var service := _get_service_from_path(path)
var url := _build_url(path, service)
var body_json := JSON.stringify(body)

return RivetRequest.new(owner, HTTPClient.METHOD_GET, url, {
"headers": _build_headers(service),
"body": body_json
})

## Creates a PUT request to Rivet cloud services
## @experimental
static func PUT(owner: Node, path: String, body: Dictionary) -> RivetRequest:
var service := _get_service_from_path(path)
var url := _build_url(path, service)
var body_json := JSON.stringify(body)

return RivetRequest.new(owner, HTTPClient.METHOD_PUT, url, {
"headers": _build_headers(service),
"body": body_json
})
97 changes: 97 additions & 0 deletions godot/bomber/addons/rivet/api/rivet_packages.gd
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
const _RivetResponse = preload("rivet_response.gd")

## Lobby
## @experimental
class Lobby:
## Finds a lobby based on the given criteria. If a lobby is not found and
## prevent_auto_create_lobby is true, a new lobby will be created.
##
## [url]{https://rivet.gg/docs/matchmaker/api/lobbies/find}[/url]
func find(body: Dictionary = {}):
return await Rivet.POST("matchmaker/lobbies/find", body).wait_completed()

## Joins a specific lobby. This request will use the direct player count
## configured for the lobby group.
##
## [url]{https://rivet.gg/docs/matchmaker/api/lobbies/join}[/url]
func join(body: Dictionary = {}) -> _RivetResponse:
return await Rivet.POST("matchmaker/lobbies/join", body).wait_completed()

## Marks the current lobby as ready to accept connections. Players will not
## be able to connect to this lobby until the lobby is flagged as ready.
##
## [url]{https://rivet.gg/docs/matchmaker/api/lobbies/ready}[/url]
func ready(body: Dictionary = {}) -> _RivetResponse:
return await Rivet.POST("matchmaker/lobbies/ready", body).wait_completed()

## If is_closed is true, the matchmaker will no longer route players to the
## lobby. Players can still join using the /join endpoint (this can be disabled
## by the developer by rejecting all new connections after setting the lobby
## to closed). Does not shutdown the lobby.
##
## [url]{https://rivet.gg/docs/matchmaker/api/lobbies/set-closed}[/url]
func setClosed(body: Dictionary = {}) -> _RivetResponse:
return await Rivet.PUT("matchmaker/lobbies/set_closed", body).wait_completed()

## Creates a custom lobby.
##
## [url]{https://rivet.gg/docs/matchmaker/api/lobbies/create}[/url]
func create(body: Dictionary = {}) -> _RivetResponse:
return await Rivet.POST("matchmaker/lobbies/create", body).wait_completed()

## Lists all open lobbies.
##
## [url]{https://rivet.gg/docs/matchmaker/api/lobbies/list}[/url]
func list(body: Dictionary = {}) -> _RivetResponse:
return await Rivet.GET("matchmaker/lobbies/list", body).wait_completed()

##
##
## [url]{https://rivet.gg/docs/matchmaker/api/lobbies/set-state}[/url]
func setState(body: Dictionary = {}) -> _RivetResponse:
return await Rivet.PUT("matchmaker/lobbies/state", body).wait_completed()

##
##
## [url]{https://rivet.gg/docs/matchmaker/api/lobbies/get-state}[/url]
func getState(body: Dictionary = {}) -> _RivetResponse:
return await Rivet.GET("matchmaker/lobbies/state", body).wait_completed()

## Player
## @experimental
class Player:
## Validates the player token is valid and has not already been consumed then
## marks the player as connected.
##
## [url]{https://rivet.gg/docs/matchmaker/api/players/connected}[/url]
func connected(body: Dictionary = {}) -> _RivetResponse:
return await Rivet.POST("matchmaker/players/connected", body).wait_completed()

## Marks a player as disconnected. # Ghost Players.
##
## [url]{https://rivet.gg/docs/matchmaker/api/players/disconnected}[/url]
func disconnected(body: Dictionary = {}) -> _RivetResponse:
return await Rivet.POST("matchmaker/players/disconnected", body).wait_completed()

## Gives matchmaker statistics about the players in game.
##
## [url]{https://rivet.gg/docs/matchmaker/api/players/statistics}[/url]
func getStatistics(body: Dictionary = {}) -> _RivetResponse:
return await Rivet.GET("matchmaker/players/statistics", body).wait_completed()

class Region:
## Returns a list of regions available to this namespace.
## Regions are sorted by most optimal to least optimal.
## The player's IP address is used to calculate the regions' optimality.
##
## [url]{https://rivet.gg/docs/matchmaker/api/regions/list}[/url]
func list(body: Dictionary = {}) -> _RivetResponse:
return await Rivet.GET("matchmaker/regions", body).wait_completed()

## Matchmaker
## @experimental
## @tutorial: https://rivet.gg/docs/matchmaker
class Matchmaker:
static var lobby: Lobby = Lobby.new()
static var player: Player = Player.new()
static var region: Region = Region.new()
62 changes: 62 additions & 0 deletions godot/bomber/addons/rivet/api/rivet_request.gd
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
extends RefCounted
## A wrapper around HTTPRequest that emits a signal when the request is completed.
## This is a workaround for the fact that `HTTPRequest.request()` is blocking.
## To run a request, create a new RivetRequest, connect to the completed signal,
## and call `request().wait_completed()` to wait for the request to complete.


const _RivetResponse := preload("rivet_response.gd")
const _RivetRequest := preload("rivet_request.gd")

var response: _RivetResponse = null
var _opts: Dictionary
var _http_request: HTTPRequest

var _success_callback: Callable
var _failure_callback: Callable

signal completed(response: _RivetResponse)
signal succeeded(response: _RivetResponse)
signal failed(response: _RivetResponse)

func _init(owner: Node, method: HTTPClient.Method, url: String, opts: Variant = null):
self._http_request = HTTPRequest.new()
self._http_request.request_completed.connect(_on_request_completed)
self._opts = {
"method": method,
"url": url,
"body": opts.body,
"headers": opts.headers,
}
owner.add_child(self._http_request)
self._http_request.request(_opts.url, _opts.headers, _opts.method, _opts.body)

func set_success_callback(callback: Callable) -> _RivetRequest:
self._success_callback = callback
return self

func set_failure_callback(callback: Callable) -> _RivetRequest:
self._failure_callback = callback
return self

func _on_request_completed(result, response_code, headers, body):
self.response = _RivetResponse.new(result, response_code, headers, body)
if result == OK:
succeeded.emit(response)
if self._success_callback:
self._success_callback.call(response)
else:
failed.emit(response)
if self._failure_callback:
self._failure_callback.call(response)
completed.emit(response)

## Waits for the request to complete and returns the response in non-blocking way
func wait_completed() -> _RivetResponse:
await completed
return response

## Waits for the request to complete and returns the response in non-blocking way
func wait_completed2():
return completed

Loading

0 comments on commit 8da97f2

Please sign in to comment.