This is the csharp source code for the Flappy Bird example on how to integrate Unity3d and Ethereum. It is assumed some familiarity with Nethereum as the Unity3d support extends the current implementation.
If you are not familiar with Unity3d, this sample is based on the Unity3d tutorial on how to build a flappy bird style game, full tutorial here: https://unity3d.com/learn/tutorials/topics/2d-game-creation/project-goals
And full game here: https://assetstore.unity.com/packages/templates/flappy-bird-style-example-game-80330
Note: For up to date how to get started templates using Nethereum, use the following:
Unity3dSimpleSampleNet461: A solution for all platforms using the latest version of Nethereum and integrating Unity and Nethereum check also this tutorial. https://github.com/Nethereum/Unity3dSimpleSampleNet461
Nethereum Unity Webgl: A solution example on how to integrate with Nethereum/Metamask and NFTs https://github.com/Nethereum/Nethereum.Unity.Webgl
To enable cross platform compatibility and the threading mechanism using coroutines for Unity3d, Nethereum uses a new type of RPC Client, the UnityRpcClient.
All the Unity RPCRequests inherit now from this client, for example to retrieve the current blocknumber you will need to use: EthBlockNumberUnityRequest
The UnityRpcClient similarly to other RPC providers accepts an RPCRequest, but internally uses UnityWebRequest which is compatible with Webgl and Il2Cpp.
Coroutines do not support any return values, to overcome this, the client inherits from UnityRequest, which provide the following properties:
publicclassUnityRequest<TResult>{publicTResultResult{get;set;}publicExceptionException{get;set;}}All this UnityRPC requests, internally wrap the core RPC requests, decoupling the RPC clients but maintaining the integrity with the core requests.
So how does this work? Let’s start with simple RPC calls, like the one to retrieve the current block number on the Ethereum Flappy Unicorn game.
Note: in this sample, a special INFURA API key is used: 7238211010344719ad14a89db874158c. If you wish to use this sample in your own project you’ll need to sign up on INFURA and use your own key.
varblockNumberRequest=newEthBlockNumberUnityRequest("https://rinkeby.infura.io/v3/7238211010344719ad14a89db874158c");Normally we would use web3 to manage all the RPC requests, but for Unity3d we use a specific request per each type of RPC call, including information about the RPC client.
In this scenario the request is using as the RPC provider the Rinkbey public node provided by Infura.
When requesting a block number we don’t need any parameters, so we can simply just send the request and “yield” until it is complete.
yieldreturnblockNumberRequest.SendRequest();Once is completed, we can check for any errors and parse the result
if(blockNumberRequest.Exception == null) {
var blockNumber = blockNumberRequest.Result.Value;
blockNumberText.text = "Block: " + blockNumber.ToString();
}
Full source code:
usingSystem;usingSystem.Collections;usingNethereum.Hex.HexTypes;usingNethereum.JsonRpc.UnityClient;usingNethereum.RPC.Eth.Blocks;usingUnityEngine;usingUnityEngine.Networking;usingUnityEngine.UI;publicclassBlockNumber:MonoBehaviour{publicTextblockNumberText;privatefloatblockCheckRate=3f;privatefloatlastBlockCheckTime;voidStart(){lastBlockCheckTime=0f;StartCoroutine(CheckBlockNumber());}publicIEnumeratorCheckBlockNumber(){varwait=1;while(true){yieldreturnnewWaitForSeconds(wait);wait=10;varblockNumberRequest=newEthBlockNumberUnityRequest("https://rinkeby.infura.io/v3/7238211010344719ad14a89db874158c");yieldreturnblockNumberRequest.SendRequest();if(blockNumberRequest.Exception==null){varblockNumber=blockNumberRequest.Result.Value;blockNumberText.text="Block: "+blockNumber.ToString();}}}}Contract calls are made in a similar way to any other Unity RPC call, but they need the request to be encoded.
For example to retrieve the user top score we will create first a EthCallUnityRequest, responsible to make an eth_call rpc request.
varuserTopScoreRequest=newEthCallUnityRequest("https://rinkeby.infura.io/v3/7238211010344719ad14a89db874158c");Using the contract ABI and contract address we can then create a new contract and retrieve the function.
varcontract=newContract(null,ABI,contractAddress);varfunction=newcontract.GetFunction("userTopScores");Note The contract does not have now the generic RPC client as a constructor parameter.
The contract function can build the call input to retrieve the user top score.
varcallInput=function.CreateCallInput(userAddress);varblockParameter=Nethereum.RPC.Eth.DTOs.BlockParameter.CreateLatest();yieldreturnuserTopScoreRequest.SendRequest(callInput,blockParameter);Once we have retrieved the result successfully we use the function to decode the output.
vartopScore=function.DecodeSimpleTypeOutput<int>(userTopScoreRequest.Result);Full source code:
//Check if the user top score has changed on the contract chain every 2 secondspublicIEnumeratorCheckTopScore(){varwait=0;while(true){yieldreturnnewWaitForSeconds(wait);wait=2;//Create a unity call request (we have a request for each type of rpc operation)varuserTopScoreRequest=newEthCallUnityRequest(_url);if(_userAddress!=null){//Use the service to create a call input which includes the encoded varuserTopScoreCallInput=_scoreContractService.CreateUserTopScoreCallInput(_userAddress);//Call request sends and yield for response yieldreturnuserTopScoreRequest.SendRequest(userTopScoreCallInput,Nethereum.RPC.Eth.DTOs.BlockParameter.CreateLatest());//Each request has a exception and a result. The exception is set when an error occurs.//Follows a similar patter to the www and unitywebrequestif(userTopScoreRequest.Exception==null){//decode the top score using the servicevartopScoreUser=_scoreContractService.DecodeUserTopScoreOutput(userTopScoreRequest.Result);//and set it to the text boxtopScoreText.text="Your top: "+topScoreUser.ToString();//set the value on the global worlGameControl.instance.TopScoreRecorded=topScoreUser;wait=3;}else{Debug.Log(userTopScoreRequest.Exception.ToString());}}}}Before we submit the transaction in a similar way when we make a call we need to build the transaction input. For example to submit the user top score.
function.CreateTransactionInput(addressFrom,gas,valueAmount,score,v,r,s);In this scenario the input parameters are the score, and the signature values for v, r, s.
There are many way that you can sign the transactions, as usual this depends on where the user stores their private keys and / or what is the best user experience.
An option, could be, that for Desktop and Mobile applications the user can open the web3 secret storage definition file (account file in geth / parity) and sign the transaction with their private key.
vartransactionSignedRequest=newTransactionSignedUnityRequest(_url,key,_userAddress);yieldreturntransactionSignedRequest.SignAndSendTransaction(transactionInput);Decrypting using WebGL / JavaScript the account file is extremely slow, so when deploying a game to the browser, as per the Unicorn Flappy sample, it makes more sense to delegate the signing to Metamask.
To achieve this you can create your own external library to interact with the injected web3 library in the browser. For more information on External libraries check the Unity documentation https://docs.unity3d.com/Manual/webgl-interactingwithbrowserscripting.html
NOTECheck for an updated version on metamask integration https://github.com/Nethereum/Nethereum.Unity.Webgl
[DllImport("__Internal")]privatestaticexternstringSendTransaction(stringto,stringdata);//Game control sets a signalif(GameControl.instance.SubmitTopScore&&!submitting){if(_userAddress!=null){submitting=true;Debug.Log("Submitting tx");//Create the transaction input with encoded values for the functionvartransactionInput=_scoreContractService.CreateSetTopScoreTransactionInput(_userAddress,_addressOwner,_privateKey,GameControl.instance.TopScore,newHexBigInteger(4712388));if(ExternalProvider){Debug.Log("Submitting tx to score using external: "+transactionInput.Data);SendTransaction(transactionInput.To,transactionInput.Data);}else{//Create Unity Request with the private key, url and user address //(the address could be recovered from private key as in normal Nethereum, could put this an overload)// premature optimisationvartransactionSignedRequest=newTransactionSignedUnityRequest(_url,GameControl.instance.Key,_userAddress);//send and waityieldreturntransactionSignedRequest.SignAndSendTransaction(transactionInput);if(transactionSignedRequest.Exception==null){//get transaction receiptDebug.Log("Top score submitted tx: "+transactionSignedRequest.Result);}else{Debug.Log("Error submitted tx: "+transactionSignedRequest.Exception.Message);}}}GameControl.instance.SubmitTopScore=false;submitting=false;}}The Javascript interop it is achieved using the JsLib (as per Unity3d docs https://docs.unity3d.com/Manual/webgl-interactingwithbrowserscripting.html). This is an example of usage.
NOTECheck for an updated version on usage and integrated components https://github.com/Nethereum/Nethereum.Unity.Webgl
//This file allows you to interop with Web3js / Metamask include it in your assets foldermergeInto(LibraryManager.library,{GetAccount: function(){varaccount='';if(typeofweb3!=='undefined'){account=web3.eth.accounts[0];if(typeofaccount==='undefined'){account='';}}varbuffer=_malloc(lengthBytesUTF8(account)+1);stringToUTF8(account,buffer,account.length+1);returnbuffer;},SendTransaction: function(to,data){vartostr=Pointer_stringify(to);varfrom=web3.eth.accounts[0];vardatastr=Pointer_stringify(data);varparams=[{"from": from,"to": tostr,"data": datastr}];varmessage={method: 'eth_sendTransaction',params: params,from: from};newPromise(function(resolve,reject){ethereum.send(message,function(error,result){console.log(result);resolve(JSON.stringify(result));});}).then(function(response){console.log(response);});},});And the source code of the html file which uses the usual web3js and metamask checks
<!DOCTYPE html><script>vargameInstance=UnityLoader.instantiate("gameContainer","Build/webglmeta4.json",{onProgress: UnityProgress});window.addEventListener('load',function(){// Checking metamaskcheckMetamask();});functioncheckMetamask(){if(typeofwindow.ethereum!=='undefined'&&window.ethereum.isMetaMask&&window.ethereum.isConnected()){web3.setProvider(window.ethereum);web3.version.getNetwork((err,netId)=>{if(netId!=4){document.getElementById("metamaskWarning").innerText='Please connect to Rinkeby to view and submit your top scores';document.getElementById("btnConnectToMetamask").style.visibility="visible";web3=undefined;}else{window.web3=newWeb3(web3.currentProvider);if(typeof(window.web3.eth.accounts[0])=='undefined'){document.getElementById("metamaskWarning").innerText='Please unlock Metamask to view and submit your top scores';document.getElementById("btnConnectToMetamask").style.visibility="visible";}else{document.getElementById("metamaskWarning").innerText='';document.getElementById("btnConnectToMetamask").style.visibility="hidden";}}});}else{document.getElementById("metamaskWarning").innerText='Please install Metamask and connect to Rinkeby to view and submit your top scores';}}asyncfunctionconnectToMetamask(){try{awaitwindow.ethereum.enable();checkMetamask();}catch(error){// Handle error. Likely the user rejected the loginconsole.error(error)}}</script></head><body><divclass="container"><divclass="row"><divclass="webgl-content"><divid="gameContainer" style="width: 960px; height: 600px"></div><sectionclass="jumbotron text-center"><divclass="container"><h1class="jumbotron-heading">Ethereum Flappy Unicorn PoC using Nethereum, Metamask and Infura</h1><pclass="lead text-muted">Can you beat your top score and the top 5? Your top score will be stored in an ethereum blockchain smart contract once the game is finished</p><pid="metamaskWarning" class="lead" style="color:red">Please install Metamask and connect to Rinkeby to submit your top score</p><p><ahref="#" id="btnConnectToMetamask" onClick="connectToMetamask()" class="btn btn-secondary btn-lg btn-block">Connect to Metamask</a></p></div></section>When compiling to Webgl or IOS, you need to ensure that dlls are not stripped when running IL2CPP. An example of the link.xml file can be found in the flappy source code. You may find this issue if you encounter the error “No parameterless constructor defined for Nethereum.Unity.RpcModel.RpcParametersJsonConverter”.
NOTE The library uses the custom https://github.com/SaladLab/Json.Net.Unity3D Json.Net library, this is included.