
Edit Aug 2023: I found package that can use grpc in unity, purer and easier setup than written in this article. https://github.com/Cysharp/yetanotherhttphandler
RPC or remote procedure call is common operation for developing partial or full online games. The operation could be as simple as saving player’s state or complex like crafting with various resources and currencies, if you build an API in the same programming language as the client you can reuse type definitions but if you don’t, you will end up writing two versions of same code also document for both. gRPC is one tool can help with this ( this is not only one but not cover in this post).
I use Windows in this post other OS may use different tool set in some step such as CMD/terminal or when setting PATH environment variable but the step still same
What is gRPC
gRPC is one of the rpc engine. you need to write proto file (protobuf) and transpile to programming language you want to implement, this operation also frees up frontend/backend to use the different language. proto file is simply an interface that is simple learn. I will be using Rust as the backend for this guide
gRPC has experimental support for Unity but it’s dropped support after the separate grpc-dotnet and no longer maintains the Grpc.Core package. The grpc protocol runs over HTTP/2 which unity does not support. Fortunately we have project called grpc-web that allows grpc work over HTTP/1. The disadvantage is that client streaming not supported, so we can only create partial online system this way (like update resources when player only do action)
The Project setup
we create root folder here the other folder will create in later step, but I put here for visualize
root
+ bin
+ unity-grpc
+ proto-grpc
+ server-grpc
create /bin
folder is require for toolchain basicly you need to complie .proto
in to .cs
file don’t forgot to put in PATH environment variables we will download Grpc.Tools from nuget the download button in the right side

extract .nupkg you downloaded, I use 7z for extract tool, but if you not want to install just change .nupkg to .zip and simply open with windows explorer then copy 2 files in tools/windows_x64
to /bin
folder (windows_x86 if you still use 32-bit version of windows)

we can check by run protoc --version
it’s will output like libprotoc 3.21.6
version may differ due update, if you see error like command not found, see PATH variable setup currectly and restart CMD for reload it
also rename grpc_csharp_plugin.exe
to protoc-gen-grpc_csharp.exe
for easier to run command
Proto
create proto-grpc
folder, this folder contains only .proto file it’s definition about our gRPC service, in case frontend/backend are separate team we are no need to pull large codebase that never used
create file helloworld.proto
in your favorite editor with content below
syntax = "proto3";
package helloworld;
// The greeting service definition.
service Greeter {
// Sends a greeting
rpc SayHello (HelloRequest) returns (HelloReply) {}
}
// The request message containing the user's name.
message HelloRequest {
string name = 1;
}
// The response message containing the greetings
message HelloReply {
string message = 1;
}
Server
the server I use Rust for create server just bacause I want to, but grpc web is avaliable in variety of languages, including C#, so you can use whatever language you love. For now let’s dive in /server-grpc
file cargo.toml
this is like project file, including infomation like name and dependencies
# cargo.toml
[package]
name = "server-grpc"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[[bin]] # Bin to run the HelloWorld gRPC server
name = "helloworld-server"
path = "src/server.rs"
[[bin]]
name = "helloworld-web-client"
path = "src/web-client.rs"
[dependencies]
tonic = "0.8"
tonic-web = "0.5"
prost = "0.11"
tokio = { version = "1.0", features = ["macros", "rt-multi-thread"] }
http = "0.2"
hyper = "0.14"
bytes = "1.4.0"
[build-dependencies]
tonic-build = "0.8"
build.rs
is a special file where they run before we build actual executable,which we use this time we use for generate rust code from proto files.
// build.rs
use std::{path::PathBuf, env};
fn main() -> Result<(), Box<dyn std::error::Error>> {
let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap());
tonic_build::configure()
.file_descriptor_set_path(out_dir.join("helloworld_descriptor.bin"))
.compile(&["../proto-grpc/helloworld.proto"], &["../proto-grpc/"])
.unwrap();
Ok(())
}
src/server.rs
the server code place here. This just use to proof our server this work then print to console and reply payload back to client
// src/server.rs
use tonic::{transport::Server, Request, Response, Status};
use hello_world::greeter_server::{Greeter, GreeterServer};
use hello_world::{HelloReply, HelloRequest};
use tonic_web::GrpcWebLayer;
pub mod hello_world {
tonic::include_proto!("helloworld");
}
#[derive(Default)]
pub struct MyGreeter {}
#[tonic::async_trait]
impl Greeter for MyGreeter {
async fn say_hello(
&self,
request: Request<HelloRequest>,
) -> Result<Response<HelloReply>, Status> {
println!("Got a request: {:?}", request);
let reply = hello_world::HelloReply {
message: format!("Hello {}!", request.into_inner().name).into(),
};
Ok(Response::new(reply))
}
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let addr = "127.0.0.1:8080".parse().unwrap();
let service = GreeterServer::new(MyGreeter::default());
Server::builder()
.accept_http1(true)
.layer(GrpcWebLayer::new())
.add_service(service)
.serve(addr)
.await?;
Ok(())
}
src/web-client.rs
grpc-web client, we just need for ensure our server is working before try it in unity, it’s easier to setup
// src/web-client.rs
use bytes::{Buf, BufMut, Bytes, BytesMut};
use hello_world::{HelloReply, HelloRequest};
use http::header::{ACCEPT, CONTENT_TYPE};
pub mod hello_world {
tonic::include_proto!("helloworld");
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let msg = HelloRequest {
name: "Bob".to_string(),
};
// a good old http/1.1 request
let request = http::Request::builder()
.version(http::Version::HTTP_11)
.method(http::Method::POST)
.uri("http://localhost:8080/helloworld.Greeter/SayHello")
.header(CONTENT_TYPE, "application/grpc-web")
.header(ACCEPT, "application/grpc-web")
.body(hyper::Body::from(encode_body(msg)))
.unwrap();
let client = hyper::Client::new();
let response = client.request(request).await.unwrap();
assert_eq!(
response.headers().get(CONTENT_TYPE).unwrap(),
"application/grpc-web+proto"
);
let body = response.into_body();
let reply = decode_body::<HelloReply>(body).await;
println!("REPLY={:?}", reply);
Ok(())
}
// one byte for the compression flag plus four bytes for the length
const GRPC_HEADER_SIZE: usize = 5;
fn encode_body<T>(msg: T) -> Bytes
where
T: prost::Message,
{
let msg_len = msg.encoded_len();
let mut buf = BytesMut::with_capacity(GRPC_HEADER_SIZE + msg_len);
// compression flag, 0 means "no compression"
buf.put_u8(0);
buf.put_u32(msg_len as u32);
msg.encode(&mut buf).unwrap();
buf.freeze()
}
async fn decode_body<T>(body: hyper::Body) -> T
where
T: Default + prost::Message,
{
let mut body = hyper::body::to_bytes(body).await.unwrap();
// ignore the compression flag
body.advance(1);
let len = body.get_u32();
#[allow(clippy::let_and_return)]
let msg = T::decode(&mut body.split_to(len as usize)).unwrap();
msg
}
now we can start the server
cargo run --bin helloworld-server
then open new CMD run our test client
cargo run --bin helloworld-web-client
you will see this
<cargo log ...>
REPLY=HelloReply { message: "Hello Bob!" }
also from server CMD
<cargo log ...>
Got a request: Request { metadata: MetadataMap { headers: {"content-type": "application/grpc", "accept": "application/grpc-web", "host": "localhost:8080", "te": "trailers", "accept-encoding": "identity,deflate,gzip"} }, message: HelloRequest { name: "Bob" }, extensions: Extensions }
now we know our server is working right now, let’s move to our client
Client
our client logic goes here just create empty unity project I use unity 2021.3.18f1
which latest LTS version at time of writing for this project.

create project structure like this, this is just my comfort structure you can see here

In assembly definition add reference to TextMeshPro (you can skip this step if not create assembly definition)

now you need to download nupkg from nuget.org you can extract .nupkg file like zip file. copy file under/lib/netstandard2.0/
package list below and it’s all dependencies in dependencies tab

you will get file like this (dependencies may change due update if unity can’t load some assembly look at nuget dependencies like above)

Create button and text, we will use for testing


run the script in /proto-grpc
folder, and run command below, it will create 2 files in runtime folder
protoc -I . --csharp_out=../unity-grpc/Assets/Scripts/Runtime --grpc_csharp_out=../unity-grpc/Assets/Scripts/Runtime helloworld.proto

Create scripts name Hello
under Runtime
folder content below
using Grpc.Core;
using Grpc.Net.Client.Web;
using Grpc.Net.Client;
using System.Net.Http;
using UnityEngine;
using UnityEngine.UI;
using TMPro;
using System;
using Helloworld;
public class Hello : MonoBehaviour
{
public Button Button;
public TMP_Text Text;
public string host = "http://localhost:8080";
private GrpcChannel channel;
private Greeter.GreeterClient client;
void Start()
{
Button.onClick.AddListener(OnHello);
var options = new GrpcChannelOptions();
var handler = new GrpcWebHandler(GrpcWebMode.GrpcWeb, new HttpClientHandler());
options.HttpHandler = handler;
options.Credentials = ChannelCredentials.Insecure;
channel = GrpcChannel.ForAddress(host, options);
client = new Greeter.GreeterClient(channel);
}
private void OnDestroy()
{
channel.Dispose();
}
public void OnHello()
{
var reply = client.SayHello(new HelloRequest { Name = "unity" });
Debug.Log(reply.ToString());
Text.text = $"[{DateTime.Now}] {reply}";
}
}
Add the component into scene somewhere, link button and text to component we added above

after press run and click the button we will see something like this


yeah it’s work right now, we all done here
Note
I put repository here contains all work, in real life this could be separate to 3 git repository.
also I don’t know much about how gRPC protocol really work underhood and how to implement in other language, but if you have question leave the comment I will try my best to find the answer.
Additional Resources
https://github.com/grpc/grpc-dotnet/issues/1309
https://grpc.io/blog/grpc-csharp-future/
tonic web example