1656614160
This is a lightweight library that works as a connector to Binance public API
/api/*
/sapi/*
dotnet add package Binance.Spot
Usage example
using System;
using System.Threading.Tasks;
using Binance.Spot;
class Program
{
static async Task Main(string[] args)
{
Wallet wallet = new Wallet();
string status = await wallet.SystemStatus();
Console.WriteLine(status);
}
}
Please find more examples folder to check for more endpoints.
using System;
using System.Threading;
using System.Threading.Tasks;
using Binance.Spot;
class Program
{
static async Task Main(string[] args)
{
var websocket = new MarketDataWebSocket("btcusdt@aggTrade");
websocket.OnMessageReceived(
(data) =>
{
Console.WriteLine(data);
return Task.CompletedTask;
}, CancellationToken.None);
await websocket.ConnectAsync(CancellationToken.None);
}
}
More websocket examples are available in the Examples
folder
Once connected, the websocket server sends a ping frame every 3 minutes and requires a response pong frame back within a 10 minutes period. This package handles the pong responses automatically.
While /sapi/*
endpoints don't have testnet environment yet, /api/*
endpoints can be tested in Spot Testnet.
using Binance.Spot;
Wallet wallet = new Wallet(baseUrl: "https://testnet.binance.vision");
If baseUrl
is not provided, it defaults to https://api.binance.com
.
It's recommended to pass in the baseUrl
parameter, even in production as Binance provides alternative URLs in case of performance issues:
https://api1.binance.com
https://api2.binance.com
https://api3.binance.com
Additional parameter recvWindow
is available for endpoints requiring signature.
It defaults to 5000
(milliseconds) and can be any value lower than 60000
(milliseconds). Anything beyond the limit will result in an error response from Binance server.
using Binance.Spot;
Wallet wallet = new Wallet();
await wallet.AccountStatus(recvWindow=4000)
The default timeout is 100,000 milliseconds (100 seconds).
Usage Example
using System;
using System.Net.Http;
using Binance.Spot;
HttpClient httpClient = new HttpClient() {
Timeout = TimeSpan.FromSeconds(10)
}
Wallet wallet = new Wallet(httpClient: httpClient);
Usage Example
using System;
using System.Net;
using System.Net.Http;
using Binance.Spot;
WebProxy proxy = new WebProxy(new Uri("http://1.2.3.4:8080"));
HttpClientHandler proxyHandler = new HttpClientHandler { Proxy = proxy };
HttpClient httpClient = new HttpClient(handler: proxyHandler);
Wallet wallet = new Wallet(httpClient: httpClient);
There are 2 types of exceptions returned from the library:
Binance.Common.BinanceClientException
4XX
, it's an issue from client side.Code
- Server's error code, e.g. -1102
Message
- Server's error message, e.g. Unknown order sent.
Binance.Common.BinanceServerException
5XX
, it's an issue from server side.Both exceptions inherit Binance.Common.BinanceHttpException
along with the following properties:
StatusCode
- Response http status code, e.g. 401
Headers
- Dictionary with response headersThis library implements the .NET logging API that works with a variety of built-in and third-party logging providers.
For more information on how to configure logging in .NET, visit Microsoft's logging article
Usage Example
using System;
using System.Net;
using System.Net.Http;
using Binance.Spot;
public async Task LoggingExample(ILogger logger) {
BinanceLoggingHandler loggingHandler = new BinanceLoggingHandler(logger: logger);
HttpClient httpClient = new HttpClient(handler: loggingHandler);
Wallet wallet = new Wallet(httpClient: httpClient);
await wallet.SystemStatus();
}
Sample Output
Method: GET, RequestUri: 'https://www.binance.com/?timestamp=1631525776809&signature=f07558c98cb82bcb3556a6a21b8a8a2582bae93d0bb9604a0df72cae8c1c6642', Version: 1.1, Content: <null>, Headers: { }
StatusCode: 200, ReasonPhrase: 'OK', Version: 1.1, Content: <null>, Headers: {}
{"status": 0,"msg": "normal"}
dotnet test
Futures and Vanilla Options APIs are not supported:
Contributions are welcome.
If you've found a bug within this project, please open an issue to discuss what you would like to change.
If it's an issue with the API, please open a topic at Binance Developer Community
Download Details:
Author: binance
Source Code: https://github.com/binance/binance-connector-dotnet
License: MIT license
#Binance #blockchain #dotnet #csharp
1656584820
Binance API document has the details of how to hash the signature. However we are still seeing users spending hours to find out why server still complains about bad signatures. In this repo, we give the example script on how to do signature.
Code in this repository should NOT be used in production.
In each language, the script will try to hash following string and should return same signature
# hashing string
timestamp=1578963600000
# and return
d84e6641b1e328e7b418fff030caed655c266299c9355e36ce801ed14631eed4
# hashing string
symbol=LTCBTC&side=BUY&type=LIMIT&timeInForce=GTC&quantity=1&price=0.1&recvWindow=5000×tamp=1499827319559
# and return
c8db56825ae71d6d79447849e617115f4a920fa2acdcab2b053c4b2838bd6b71
The secret is the same as NhqPtmdSJYdKjVHjA7PZj4Mge3R5YNiP1e3UZjInClVN65XAbvqqM6A7H5fATj0j
Download Details:
Author: binance
Source Code: https://github.com/binance/binance-signature-examples
License:
#Binance #blockchain #python #csharp
1656543360
rich set of enterprise-class UI components based on Ant Design and Blazor.
WebAssembly static hosting examples:
![]() Edge / IE | ![]() Firefox | ![]() Chrome | ![]() Safari | ![]() Opera | ![]() Electron |
---|---|---|---|---|---|
Edge 16 / IE 11† | 522 | 57 | 11 | 44 | Chromium 57 |
Due to WebAssembly restriction, Blazor WebAssembly doesn't support IE browser, but Blazor Server supports IE 11† with additional polyfills. See official documentation.
From .NET 5, IE 11 is no longer officially supported. See Blazor: Updated browser support. Unofficial support is provided by Blazor.Polyfill community project.
Regularly synchronize with Official Ant Design specifications, you can check the sync logs online.
Therefore, you can use the custom theme styles of Ant Design directly.
We have provided the dotnet new
template to create a Boilerplate project out of the box:
Install the template
$ dotnet new --install AntDesign.Templates
Create the Boilerplate project with the template
$ dotnet new antdesign -o MyAntDesignApp
Options for the template:
Options | Description | Type | Default |
---|---|---|---|
-f | --full | If specified, generates all pages of Ant Design Pro | bool | false |
-ho | --host | Specify the hosting model | 'wasm' | 'server' | 'hosted' | 'wasm' |
--styles | Whether use NodeJS and Less to compile your custom themes. | css | less | css |
--no-restore | If specified, skips the automatic restore of the project on create | bool | false |
Go to the project folder of the application and install the Nuget package reference
$ dotnet add package AntDesign
Register the services in Program.cs
(client-side WebAssembly Blazor)
builder.Services.AddAntDesign();
or Startup.cs
(server-side Blazor)
services.AddAntDesign();
Link the static files in wwwroot/index.html
(client-side WebAssembly Blazor) or Pages/_Host.cshtml
(server-side Blazor)
<link href="_content/AntDesign/css/ant-design-blazor.css" rel="stylesheet" />
<script src="_content/AntDesign/js/ant-design-blazor.js"></script>
Add namespace in _Imports.razor
@using AntDesign
To display the pop-up component dynamically, you need to add the <AntContainer />
component in App.razor
.
<Router AppAssembly="@typeof(MainLayout).Assembly">
<Found Context="routeData">
<RouteView RouteData="routeData" DefaultLayout="@typeof(MainLayout)" />
</Found>
<NotFound>
<LayoutView Layout="@typeof(MainLayout)">
<Result Status="404" />
</LayoutView>
</NotFound>
</Router>
<AntContainer /> <-- add this component ✨
Finally, it can be referenced in the .razor
component!
<Button Type="@ButtonType.Primary">Hello World!</Button>
$ git clone git@github.com:ant-design-blazor/ant-design-blazor.git
$ cd ant-design-blazor
$ npm install
$ dotnet build ./site/AntDesign.Docs.Build/AntDesign.Docs.Build.csproj
$ npm start
Visual Studio 2022 is recommended for development.
Check out this issue to learn about our development plans for 2020.
Download Details:
Author: ant-design-blazor
Source Code: https://github.com/ant-design-blazor/ant-design-blazor
License: MIT, Unknown licenses found
#dotnet #aps.net #csharp #blazor
1656535920
A set of enterprise-class UI components based on Bootstrap and Blazor.
dotnet new -i Bootstrap.Blazor.Templates::*
dotnet new bbapp
Microsoft Market link
dotnet add package BootstrapBlazor
stylesheet
javascripts
file to your main index file - Pages/_Host.cshtml (Server)
or wwwroot/index.html (WebAssembly)
HTML
<!DOCTYPE html>
<html lang="en">
<head>
. . .
<link rel="stylesheet" href="_content/BootstrapBlazor/css/bootstrap.blazor.bundle.min.css">
</head>
<body>
. . .
<script src="_framework/blazor.server.js"></script>
<script src="_content/BootstrapBlazor/js/bootstrap.blazor.bundle.min.js"></script>
</body>
</html>
~/Startup.cs
file in the and register the Bootstrap Blazor
service:C#
namespace BootstrapBlazorAppName
{
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
//more code may be present here
services.AddBootstrapBlazor();
}
//more code may be present here
}
}
To create a new Bootstrap Blazor
UI for Blazor application, use the Create New Project Wizard. The wizard detects all installed versions of Bootstrap Blazor
for Blazor and lists them in the Version combobox—this enables you to start your project with the desired version. You can also get the latest version to make sure you are up to date.
To use the Create New Project Wizard, install the Bootstrap Blazor
UI for Blazor Visual Studio Extensions. You can get it from the:
To start the wizard, use either of the following approaches
Chrome | Firefox | Safari | Android Browser & WebView | Microsoft Edge | |
---|---|---|---|---|---|
iOS | Supported | Supported | Supported | N/A | Supported |
Android | Supported | Supported | N/A | Android v5.0+ supported | Supported |
Windows 10 Mobile | N/A | N/A | N/A | N/A | Supported |
Chrome | Firefox | Internet Explorer | Microsoft Edge | Opera | Safari | |
---|---|---|---|---|---|---|
Mac | Supported | Supported | N/A | N/A | Supported | Supported |
Linux | Supported | Supported | N/A | N/A | N/A | N/A |
Windows | Supported | Supported | Supported, IE11+ | Supported | Supported | Not supported |
Download Details:
Author: dotnetcore
Source Code: https://github.com/dotnetcore/BootstrapBlazor
License: Apache-2.0 license
#dotnet #aps.net #csharp #blazor #bootstrap
1656528420
A F# implementation of the Hawk authentication protocol. Few dependencies. No cruft. No thrown exceptions.
If this library throws an exception, report an issue - instead it uses return values that are structured instead.
paket add nuget Hawk
paket add nuget Hawk.Suave
Dependencies: { Aether, FSharp.Core, NodaTime }, nugets Hawk and Hawk.Suave.
For all API methods implemented, the full test suite for those methods has also been translated.
Sponsored by qvitoo – A.I. bookkeeping.
open Logibit.Hawk
open Logibit.Hawk.Types
open Logibit.Hawk.Server
open Suave
open Suave.Http // houses submodule 'Hawk'
open Suave.Http.Successful
open Suave.Http.RequestErrors
open Suave.Types
// your own user type
type User =
{ homepage : Uri
realName : string }
// this is the structure that is the 'context' for Logibit.Hawk
let settings =
// this is what the lib is looking for to verify the request
let sampleCreds =
{ id = "haf"
key = "werxhqb98rpaxn39848xrunpaw3489ruxnpa98w4rxn"
algorithm = SHA256 }
// the generic type param allows you to implement a generic user repository
// for your own user type (above)
{ Settings.empty<User>() with
// sign: UserId -> Choice<Credentials * 'a, CredsError>
credsRepo = fun id ->
(sampleCreds,
{ homepage = Uri("https://qvitoo.com"); realName = "Henrik" }
)
// no error:
|> Choice1Of2 }
// You can compose this into the rest of the app, as it's a web part. In this
// case you're doing a Authorization header authentication
let sampleApp settings : WebPart =
Hawk.authenticate
settings
Hawk.bindHeaderReq
// in here you can put your authenticated web parts
(fun (attr, creds, user) -> OK (sprintf "authenticated user '%s'" user.realName))
// on failure to authenticate the request
(fun err -> UNAUTHORIZED (err.ToString()))
// Similarly for bewits, where you want to authenticate a portion of the query
// string:
let sampleApp2 settings : WebPart =
Hawk.authenticateBewit
settings Hawk.bindQueryRequest
// in here you can put your authenticated web parts
(fun (attr, creds, user) -> OK (sprintf "authenticated user '%s'" user.realName))
// on failure to authenticate the request
(fun err -> UNAUTHORIZED (err.ToString()))
Currently the code is only fully documented - but not outside the code, so have a browse to the source code that you are interested in to see how the API composes.
Use the .js file from src/vendor/hawk.js/lib
, then you can wrap your ajax calls like this:
request.js: (using CommonJS module layout, which you can use to require it and get a function in return).
var Auth = require('./auth.js'),
Hawk = require('./lib/hawk.js'),
Logger = require('./logger.js'),
jQuery = require('jquery');
var qt = function(str) {
return "'" + str + "'";
}
var jqSetHawkHeader = function(opts, creds, jqXHR, settings) {
if (typeof opts.contentType == 'undefined') {
throw new Error('missing contentType from options');
}
var opts = jQuery.extend({ credentials: creds, payload: settings.data }, opts),
// header(uri, method, options): should have options values for
// - contentType
// - credentials
// - payload
header = Hawk.client.header(settings.url, settings.type, opts); // type = HTTP-method
if (typeof header.err !== 'undefined') {
Logger.error('(1/2) Hawk error:', qt(header.err), 'for', method, qt(settings.url));
Logger.error('(2/2) Using credentials', opts.credentials);
return;
}
Logger.debug('(1/3)', settings.type, settings.url);
Logger.debug('(2/3) opts:', opts);
Logger.debug('(3/3) header:', header.field);
jqXHR.setRequestHeader('Authorization', header.field);
};
module.exports = function (method, resource, data, opts) {
var origin = window.location.origin,
creds = Auth.getCredentials(),
url = origin + resource,
opts = jQuery.extend({
contentType: 'application/x-www-form-urlencoded; charset=UTF-8',
dataType: 'html'
}, (typeof opts !== 'undefined' ? opts : {})),
jqOpts = jQuery.extend({
type: method,
data: data,
url: url,
beforeSend: function(xhr, s) { jqSetHawkHeader(opts, creds, xhr, s) }
}, opts);
return jQuery.ajax(jqOpts);
};
Please have a look at Releases.
This is the public API of the library. It mimics the API of Hawk.js - the reference implementation.
Logibit.Hawk.Bewit
These functions are available to creating and verifying Bewits.
authenticate
detailsTBD - docs, in the meanwhilse, see Server authenticate (superset is validated).
Logibit.Hawk.Client
These functions are available, checked functions are implemented
Logibit.Hawk.Server
authenticate
to get credentials. Payload Validationauthenticate
detailsHow strictly does the server validate its input? Compared to reference implementation. This part is important since it will make or break the usability of your api/app. Just throwing SecurityException for any of these is not granular enough.
FaultyAuthorizationHeader
FaultyAuthorizationHeader
MissingAttribute
CredsError
BadMac
MissingAttribute
BadPayloadHash of hash_given * hash_calculated
NonceError AlreadySeen
, with in-memory cacheStaleTimestamp
Hints when not under attack (in dev)
If you see CredsError
, it's most likely a problem that you can't find the user with your repository function.
If you see BadMac
, it means probably means you haven't fed the right parameters to authenticate
. Log the input parameters, verify that host and port match (are you behind a reverse proxy?) and check that the length of the content is the same on the client as on the server.
The BadMac
error comes from hashing a normalised string of these parameters:
If you see PadPayloadHash, it means that the MAC check passed, so you're probably looking at an empty byte array, or your Content-Type isn't being passed to the server properly, or the server implementation doesn't feed the correct Content-Type header (e.g. it doesn't trim the stuff after the first MimeType declaration, before the semi-colon ;
).
Logibit.Hawk.Crypto
The crypto module contains functions for validating the pieces of the request.
Logibit.Hawk.Types
This module contains the shared types that you should use for interacting with the above modules.
This module also contains a module-per-type with lenses for that type. The lenses follow the same format as Aether recommends.
Logibit.Hawk.Logging
Types:
LogLevel
- the level of the LogLine.LogLine
- this is the data structure of the logging module, this is where you feed your data.Logger interface
- the main interface that we can log to/into.Logger module
- a module that contains functions equiv. to the instance methods of the logger interface.NoopLogger : Logger
- the default logger, you have to replace it yourselfIt's good to know that you have to construct your LogLine yourself. That LogLines with Verbose or Debug levels should be sent to the debug
or verbose
functions/methods of the module/interface Logger, which in turn takes functions, which are evaluated if it's the case that the logging infrastructure is indeed logging at that level.
This means that logging at that level, and computing the log lines, needs only be done if we can really do something with them.
There are some modules that are currently internal as to avoid conflicting with existing code. If these are made 'more coherent' or else moved to external libraries, they can be placed on their own and be made public. The modules like this are Random
, Prelude
, Parse
.
Download Details:
Author: logibit
Source Code: https://github.com/logibit/logibit.hawk/
License: View license
#dotnet #aps.net #csharp
1656521054
Very simple SAML 2.0 "consumer" implementation in C#.
It's a SAML client library, not a SAML server, allows adding SAML single-sign-on to your ASP.NET app, but not to provide auth services to other apps.
Consists of one short C# file you can throw into your project (or install via nuget) and start using it. It works with both ASP.NET Core and the "old" ASP.NET Framework
SAML workflow has 2 steps:
Here's how you do it (this example is for ASP.NET MVC):
//this example is an ASP.NET MVC action method
public ActionResult Login()
{
//TODO: specify the SAML provider url here, aka "Endpoint"
var samlEndpoint = "http://saml-provider-that-we-use.com/login/";
var request = new AuthRequest(
"http://www.myapp.com", //TODO: put your app's "entity ID" here
"http://www.myapp.com/SamlConsume" //TODO: put Assertion Consumer URL (where the provider should redirect users after authenticating)
);
//redirect the user to the SAML provider
return Redirect(request.GetRedirectUrl(samlEndpoint));
}
User is sent back to your app - you need to validate the SAML response ("assertion") that you recieved via POST.
Here's an example of how you do it in ASP.NET MVC
//ASP.NET MVC action method... But you can easily modify the code for Web-forms etc.
public ActionResult SamlConsume()
{
// 1. TODO: specify the certificate that your SAML provider gave you
string samlCertificate = @"-----BEGIN CERTIFICATE-----
BLAHBLAHBLAHBLAHBLAHBLAHBLAHBLAHBLAHBLAHBLAHBLAH123543==
-----END CERTIFICATE-----";
// 2. Let's read the data - SAML providers usually POST it into the "SAMLResponse" var
var samlResponse = new Response(samlCertificate, Request.Form["SAMLResponse"]);
// 3. We're done!
if (samlResponse.IsValid())
{
//WOOHOO!!! user is logged in
username = samlResponse.GetNameID();
}
}
Reading more attributes from the provider
SAML providers usually send more data with their response: username, first/last names etc. Here's how to get it:
if (samlResponse.IsValid())
{
//WOOHOO!!! user is logged in
//Some more optional stuff for you
//let's extract username/firstname etc
string username, email, firstname, lastname;
try
{
username = samlResponse.GetNameID();
email = samlResponse.GetEmail();
firstname = samlResponse.GetFirstName();
lastname = samlResponse.GetLastName();
}
catch(Exception ex)
{
//insert error handling code
//no, really, please do
return null;
}
//user has been authenticated, put your code here, like set a cookie or something...
//or call FormsAuthentication.SetAuthCookie()
//or call context.SignInAsync() in ASP.NET Core
//or do something else
}
Depending on your .NET version, your Project should reference System.Security
for .NET Framework and System.Security.Cryptography.Xml
for .NET Core.
I've published this to Nuget.
Install-Package AspNetSaml
This will simply add the cs-file to the root of your project.
A version of this library has been used for years in production in our helpdesk app.
Download Details:
Author: jitbit
Source Code: https://github.com/jitbit/AspNetSaml
License: Apache-2.0 license
#dotnet #aps.net #csharp
1656513678
Bundle Transformer - a modular extension for the System.Web.Optimization (also known as the Microsoft ASP.NET Web Optimization Framework). StyleTransformer
and ScriptTransformer
classes, included in the core of Bundle Transformer and implement the IBundleTransform
interface. They are intended to replace the standard classes: CssMinify
and JsMinify
.
The main differences of the StyleTransformer
and ScriptTransformer
classes from a standard implementations: ability to exclude unnecessary assets when adding assets from a directory, does not produce the re-minification of pre-minified assets, support automatic transformation of relative paths to absolute in CSS code (by using the UrlRewritingCssPostProcessor
), etc. These classes do not produce the minification of code in runtime, but this feature can be added by installing of minifier-modules (now available modules based on Microsoft Ajax Minifier, YUI Compressor for .NET, NUglify, Google Closure Compiler, Douglas Crockford's JSMin, Dean Edwards' Packer, Mihai Bazon's UglifyJS, Sergey Kryzhanovsky's CSSO (CSS Optimizer), WebGrease and Clean-css). In addition, you can also install translator-modules, that implement the translation of code on intermediate languages (LESS, Sass, SCSS, CoffeeScript, TypeScript, Mustache (by using Hogan) and Handlebars). Apart from this, in the Bundle Transformer there is a third type of modules - postprocessors. Postprocessors runs after translators and before minifiers. Now available following postprocessors: URL rewriting CSS postprocessor (included in core) and postprocessor-module based on the Andrey Sitnik's Autoprefixer.
This extension will help your web applications successfully pass a most part of the tests in Google PageSpeed.
Documentation is located on the wiki of this Repo.
See the changelog.
If you use Bundle Transformer in some project, please send me a message so I can include it in this list:
Bundle Transformer was created and is maintained by Andrey Taritsyn.
Download Details:
Author: Taritsyn
Source Code: https://github.com/Taritsyn/BundleTransformer
License: Apache-2.0 license
#dotnet #aps.net #csharp
1656507369
C# Wrapper for the official Binance exchange API
Compatible with .NET 4.5.1, .NET 4.5.2, .NET 4.6.1, .NETSTANDARD2.0
This repository provides a C# wrapper for the official Binance API, and provides rate limiting features (set to 10 by 10 out the box), a IAPICacheManager
interface to allow users to provide their own cache implementations, all REST
endpoints covered, and a best practice solution coupled with strongly typed responses and requests. It is built on the latest .NET Framework and in .NET Core
Feel free to raise issues and Pull Request to help improve the library. If you found this API useful, and you wanted to give back feel free to sign up to Binance via my referral link here.
The package is available in NuGet, or feel free to download: https://www.nuget.org/packages/BinanceDotNet/
Nuget PM
Install-Package BinanceDotNet
dotnet cli
dotnet add package BinanceDotNet
Upkeep of this API takes a lot of time from answering issues and PR's on the Repository, as well as tweets and direct emails. You can donate cryptocurrency of any amount to the following wallets:
ETH: 0xd5775e2dee4b9fa9a3be5222970c04ac574e8412
Want to send something else? Just tweet me! @Glitch100
Or you can help maintain the repository! Donations of time are welcome, just hit me up and we can work on it. From answering issues, to contributing code anyone can assist.
git clone git@github.com:glitch100/BinanceDotNet.git
ExampleProgram.cs
log4net
Interfaces supportedIAPICacheManager
abstraction for providing your own cache or using the build in concrete implementation. (Currently only one endpoint has caching)More examples are available to play around with within the repositorys Console application which can be found here. Otherwise there are some examples around utilising both WebSockets
and REST
API in the Usage
section below.
Work will continue on this API wrapper over the coming months adding and extending out the number of features that the BinanceDotNet
library has. Please raise issues for new features desired
Code examples below, or clone the repository and run the BinanceExchange.Console
project. This repository is built off dotnet core, and runs against C# 7.1
General usage just requires setting up the client with your credentials, and then calling the Client as necessary.
// Build out a client, provide a logger, and more configuration options, or even your own APIProcessor implementation
var client = new BinanceClient(new ClientConfiguration()
{
ApiKey = "YOUR_API_KEY",
SecretKey = "YOUR_SECRET_KEY",
});
//You an also specify symbols like this.
var desiredSymbol = TradingPairSymbols.BNBPairs.BNB_BTC,
IReponse response = await client.GetCompressedAggregateTrades(new GetCompressedAggregateTradesRequest(){
Symbol = "BNBBTC",
StartTime = DateTime.UtcNow().AddDays(-1),
EndTime = Date.UtcNow(),
});
For WebSocket endpoints, just instantiate the BinanceClient
, and provide it into the BinanceWebSocketClient
You can use a using
block or manual management.
var client = new BinanceClient(new ClientConfiguration()
{
ApiKey = "YOUR_API_KEY",
SecretKey = "YOUR_SECRET_KEY",
});
// Manual management
var manualWebSocketClient = new InstanceBinanceWebSocketClient(client);
var socketId = binanceWebSocketClient.ConnectToDepthWebSocket("ETHBTC", data =>
{
System.Console.WriteLine($"DepthCall: {JsonConvert.SerializeObject(data)}");
});
manualWebSocketClient.CloseWebSocketInstance(socketId);
// Disposable and managed
using (var binanceWebSocketClient = new DisposableBinanceWebSocketClient(client))
{
binanceWebSocketClient.ConnectToDepthWebSocket("ETHBTC", data =>
{
System.Console.WriteLine($"DepthCall: {JsonConvert.SerializeObject(data)}");
});
Thread.Sleep(180000);
}
The Binance API provides rich exceptions based on different error types. You can decorate calls like this if you would like to handle the various exceptions.
// Firing off a request and catching all the different exception types.
try
{
accountTrades = await client.GetAccountTrades(new AllTradesRequest()
{
FromId = 352262,
Symbol = "ETHBTC",
});
}
catch (BinanceBadRequestException badRequestException)
{
}
catch (BinanceServerException serverException)
{
}
catch (BinanceTimeoutException timeoutException)
{
}
catch (BinanceException unknownException)
{
}
The example is mainly 'all in one' so you can see a full runthrough of how it works. In your own implementations you may want to have a cache of only the most recent bids/asks, or perhaps will want the empty quanity/price trades.
You can also calculate volume and more from this cache. The following code is partial from the ExampleProgram.cs
.
private static async Task<Dictionary<string, DepthCacheObject>> BuildLocalDepthCache(IBinanceClient client)
{
// Code example of building out a Dictionary local cache for a symbol using deltas from the WebSocket
var localDepthCache = new Dictionary<string, DepthCacheObject> {{ "BNBBTC", new DepthCacheObject()
{
Asks = new Dictionary<decimal, decimal>(),
Bids = new Dictionary<decimal, decimal>(),
}}};
var bnbBtcDepthCache = localDepthCache["BNBBTC"];
// Get Order Book, and use Cache
var depthResults = await client.GetOrderBook("BNBBTC", true, 100);
//Populate our depth cache
depthResults.Asks.ForEach(a =>
{
if (a.Quantity != 0.00000000M)
{
bnbBtcDepthCache.Asks.Add(a.Price, a.Quantity);
}
});
depthResults.Bids.ForEach(a =>
{
if (a.Quantity != 0.00000000M)
{
bnbBtcDepthCache.Bids.Add(a.Price, a.Quantity);
}
});
// Store the last update from our result set;
long lastUpdateId = depthResults.LastUpdateId;
using (var binanceWebSocketClient = new DisposableBinanceWebSocketClient(client))
{
binanceWebSocketClient.ConnectToDepthWebSocket("BNBBTC", data =>
{
if (lastUpdateId < data.UpdateId)
{
data.BidDepthDeltas.ForEach((bd) =>
{
CorrectlyUpdateDepthCache(bd, bnbBtcDepthCache.Bids);
});
data.AskDepthDeltas.ForEach((ad) =>
{
CorrectlyUpdateDepthCache(ad, bnbBtcDepthCache.Asks);
});
}
lastUpdateId = data.UpdateId;
System.Console.Clear();
System.Console.WriteLine($"{JsonConvert.SerializeObject(bnbBtcDepthCache, Formatting.Indented)}");
System.Console.SetWindowPosition(0, 0);
});
Thread.Sleep(8000);
}
return localDepthCache;
}
You can use the data returned from above to utilise the ResultTransformations
static
class, to transform data returned from the API into more meaningful, known shapes, such as Volume etc.
// This builds a local depth cache from an initial call to the API and then continues to fill
// the cache with data from the WebSocket
var localDepthCache = await BuildLocalDepthCache(client);
// Build the Buy Sell volume from the results
var volume = ResultTransformations.CalculateTradeVolumeFromDepth("BNBBTC", localDepthCache);
Download Details:
Author: binance-exchange
Source Code: https://github.com/binance-exchange/BinanceDotNet
License: MIT license
#Binance #blockchain #dotnet #csharp
1656506068
DotNetty is a port of Netty, asynchronous event-driven network application framework for rapid development of maintainable high performance protocol servers & clients.
We gladly accept community contributions.
Download Details:
Author: Azure
Source Code: https://github.com/Azure/DotNetty
License: View license
#dotnet #aps.net #csharp
1656386666
Fritz has built a website to manage your collection, but how can we open up the website with an API to allow other integrations? In this session, Fritz adds an API to our website using ASP.NET Core
#aspnetcore #api #dotnet #csharp #webdev
1656378000
Les performances sont primordiales lorsque vous essayez de publier sur le Web, sur mobile, sur des consoles et même sur certains PC bas de gamme. Un jeu ou une application fonctionnant à moins de 30 FPS peut être source de frustration pour les utilisateurs. Jetons un coup d'œil à certaines des choses que nous pouvons utiliser pour augmenter les performances en réduisant la charge sur le processeur.
Dans cet article, nous expliquerons ce que sont async
, await
et Task
en C # et comment les utiliser dans Unity pour gagner en performance dans votre projet. Ensuite, nous examinerons certains des packages intégrés à Unity : les coroutines, le système de tâches C# et le compilateur en rafale. Nous verrons ce qu'ils sont, comment les utiliser et comment ils augmentent les performances de votre projet.
Pour démarrer ce projet, j'utiliserai Unity 2021.3.4f1. Je n'ai testé ce code sur aucune autre version de Unity ; tous les concepts ici devraient fonctionner sur n'importe quelle version d'Unity après Unity 2019.3. Vos résultats de performances peuvent différer si vous utilisez une version plus ancienne, car Unity a apporté des améliorations significatives avec le modèle de programmation async/wait en 2021. En savoir plus à ce sujet dans le blog Unity d'Unity et .NET, quelle est la prochaine étape , en particulier la section intitulée "Modernizing the Exécution de l'unité.
J'ai créé un nouveau projet Core 2D (URP), mais vous pouvez l'utiliser dans n'importe quel type de projet que vous aimez.
J'ai un sprite que j'ai obtenu de Space Shooter (Redux, plus polices et sons) de Kenney Vleugels .
J'ai créé un préfabriqué ennemi qui contient un Sprite Render et un composant ennemi. Le composant ennemi est un MonoBehaviour
qui a un Transform
et un float
pour garder une trace de la position et de la vitesse de déplacement sur l'axe y :
using UnityEngine;
public class Enemy
{
public Transform transform;
public float moveY;
}
async
, await
, et Task
sont en C#async
?En C#, les méthodes peuvent avoir un mot- async
clé devant elles, ce qui signifie que les méthodes sont des méthodes asynchrones. C'est juste une façon de dire au compilateur que nous voulons pouvoir exécuter du code à l'intérieur et permettre à l'appelant de cette méthode de continuer l'exécution en attendant que cette méthode se termine.
Un exemple de ceci serait la préparation d'un repas. Vous commencerez à cuire la viande, et pendant que la viande cuit et que vous attendez qu'elle se termine, vous commencerez à faire les côtés. Pendant que les aliments cuisent, vous commencez à mettre la table. Un exemple de ceci dans le code serait static async Task<Steak> MakeSteak(int number)
.
Unity possède également toutes sortes de méthodes intégrées que vous pouvez appeler de manière asynchrone ; voir les docs Unity pour une liste des méthodes. Avec la façon dont Unity gère la gestion de la mémoire, il utilise soit des coroutines , AsyncOperation
soit le système de tâches C# .
await
et comment l'utiliser ?En C#, vous pouvez attendre la fin d'une opération asynchrone en utilisant le mot- await
clé. Ceci est utilisé à l'intérieur de toute méthode qui a le mot- async
clé pour attendre qu'une opération continue :
Public async void Update()
{
// do stuff
await // some asynchronous method or task to finish
// do more stuff or do stuff with the data returned from the asynchronous task.
}
Consultez les documents Microsoft pour en savoir plus sur await
.
Task
et comment l'utiliser ?A Task
est une méthode asynchrone qui effectue une seule opération et ne renvoie pas de valeur. Pour a Task
qui renvoie une valeur, nous utiliserions Task<TResult>
.
Pour utiliser une tâche, nous la créons comme créer n'importe quel nouvel objet en C# : Task t1 = new Task(void Action)
. Ensuite, nous commençons la tâche t1.wait
. Enfin, nous attendons que la tâche se termine avec t1.wait
.
Il existe plusieurs façons de créer, démarrer et exécuter des tâches. Task t2 = Task.Run(void Action)
va créer et démarrer une tâche. await Task.Run(void Action)
créera, démarrera et attendra la fin de la tâche. Nous pouvons utiliser la méthode alternative la plus courante avec Task t3 = Task.Factory.Start(void Action)
.
Il existe plusieurs façons d'attendre que la tâche soit terminée. int index = Task.WaitAny(Task[])
attendra la fin de toute tâche et nous donnera l'index de la tâche terminée dans le tableau. await Task.WaitAll(Task[])
attendra que toutes les tâches soient terminées.
Pour plus d'informations sur les tâches, consultez les documents Microsoft .
task
private void Start()
{
Task t1 = new Task(() => Thread.Sleep(1000));
Task t2 = Task.Run(() => Thread.Sleep(2000000));
Task t3 = Task.Factory.StartNew(() => Thread.Sleep(1000));
t1.Start();
Task[] tasks = { t1, t2, t3 };
int index = Task.WaitAny(tasks);
Debug.Log($"Task {tasks[index].Id} at index {index} completed.");
Task t4 = new Task(() => Thread.Sleep(100));
Task t5 = Task.Run(() => Thread.Sleep(200));
Task t6 = Task.Factory.StartNew(() => Thread.Sleep(300));
t4.Start();
Task.WaitAll(t4, t5, t6);
Debug.Log($"All Task Completed!");
Debug.Log($"Task When any t1={t1.IsCompleted} t2={t2.IsCompleted} t3={t3.IsCompleted}");
Debug.Log($"All Task Completed! t4={t4.IsCompleted} t5={t5.IsCompleted} t6={t6.IsCompleted}");
}
public async void Update()
{
float startTime = Time.realtimeSinceStartup;
Debug.Log($"Update Started: {startTime}");
Task t1 = new Task(() => Thread.Sleep(10000));
Task t2 = Task.Run(() => Thread.Sleep(20000));
Task t3 = Task.Factory.StartNew(() => Thread.Sleep(30000));
await Task.WhenAll(t1, t2, t3);
Debug.Log($"Update Finished: {(Time.realtimeSinceStartup - startTime) * 1000f} ms");
}
Comparons maintenant les performances d'une tâche par rapport aux performances d'une méthode.
J'aurai besoin d'une classe statique que je peux utiliser dans toutes mes vérifications de performances. Il aura une méthode et une tâche qui simulent une opération gourmande en performances. La méthode et la tâche effectuent exactement la même opération :
using System.Threading.Tasks;
using Unity.Mathematics;
public static class Performance
{
public static void PerformanceIntensiveMethod(int timesToRepeat)
{
// Represents a Performance Intensive Method like some pathfinding or really complex calculation.
float value = 0f;
for (int i = 0; i < timesToRepeat; i++)
{
value = math.exp10(math.sqrt(value));
}
}
public static Task PerformanceIntensiveTask(int timesToRepeat)
{
return Task.Run(() =>
{
// Represents a Performance Intensive Method like some pathfinding or really complex calculation.
float value = 0f;
for (int i = 0; i < timesToRepeat; i++)
{
value = math.exp10(math.sqrt(value));
}
});
}
}
Maintenant, j'ai besoin d'un MonoBehaviour
que je peux utiliser pour tester l'impact des performances sur la tâche et la méthode. Juste pour que je puisse voir un meilleur impact sur les performances, je vais prétendre que je veux exécuter cela sur dix objets de jeu différents. Je garderai également une trace du temps Update
nécessaire à l'exécution de la méthode.
Dans Update
, j'obtiens l'heure de début. Si je teste la méthode, je parcours tous les objets de jeu simulés et j'appelle la méthode intensive en performances. Si je teste la tâche, je crée une nouvelle Task
boucle de tableau à travers tous les objets de jeu simulés et j'ajoute la tâche gourmande en performances au tableau de tâches. Je puis await
pour toutes les tâches à accomplir. En dehors de la vérification du type de méthode, je mets à jour l'heure de la méthode, en la convertissant en ms
. Je le connecte également.
public class PerformanceTaskCoroutineJob : MonoBehaviour
{
private enum MethodType
{
Normal,
Task
}
[SerializeField] private int numberGameObjectsToImitate
= 10;
[SerializeField] private MethodType method = MethodType.Normal;
[SerializeField] private float methodTime;
private async void Update()
{
float startTime = Time.realtimeSinceStartup;
switch (method)
{
case MethodType.Normal:
for (int i = 0; i < numberGameObjectsToImitate
; i++)
Performance.PerformanceIntensiveMethod(50000);
break;
case MethodType.Task:
Task[] tasks = new Task[numberGameObjectsToImitate
];
for (int i = 0; i < numberGameObjectsToImitate
; i++)
tasks[i] = Performance.PerformanceIntensiveTask(5000);
await Task.WhenAll(tasks);
break;
}
methodTime = (Time.realtimeSinceStartup - startTime) * 1000f;
Debug.Log($"{methodTime} ms");
}
}
La méthode intensive prend environ 65 ms pour se terminer avec le jeu fonctionnant à environ 12 FPS.
La tâche intensive prend environ 4 ms et le jeu tourne à environ 200 FPS.
Essayons ceci avec mille ennemis :
using System.Collections.Generic;
using System.Threading.Tasks;
using Unity.Collections;
using Unity.Jobs;
using Unity.Mathematics;
using UnityEngine;
using Random = UnityEngine.Random;
public class PerformanceTaskJob : MonoBehaviour
{
private enum MethodType
{
NormalMoveEnemy,
TaskMoveEnemy
}
[SerializeField] private int numberEnemiesToCreate = 1000;
[SerializeField] private Transform pfEnemy;
[SerializeField] private MethodType method = MethodType.NormalMoveEnemy;
[SerializeField] private float methodTime;
private readonly List<Enemy> m_enemies = new List<Enemy>();
private void Start()
{
for (int i = 0; i < numberEnemiesToCreate; i++)
{
Transform enemy = Instantiate(pfEnemy,
new Vector3(Random.Range(-8f, 8f), Random.Range(-8f, 8f)),
Quaternion.identity);
m_enemies.Add(new Enemy { transform = enemy, moveY = Random.Range(1f, 2f) });
}
}
private async void Update()
{
float startTime = Time.realtimeSinceStartup;
switch (method)
{
case MethodType.NormalMoveEnemy:
MoveEnemy();
break;
case MethodType.TaskMoveEnemy:
Task<Task[]> moveEnemyTasks = MoveEnemyTask();
await Task.WhenAll(moveEnemyTasks);
break;
default:
MoveEnemy();
break;
}
methodTime = (Time.realtimeSinceStartup - startTime) * 1000f;
Debug.Log($"{methodTime} ms");
}
private void MoveEnemy()
{
foreach (Enemy enemy in m_enemies)
{
enemy.transform.position += new Vector3(0, enemy.moveY * Time.deltaTime);
if (enemy.transform.position.y > 5f)
enemy.moveY = -math.abs(enemy.moveY);
if (enemy.transform.position.y < -5f)
enemy.moveY = +math.abs(enemy.moveY);
Performance.PerformanceIntensiveMethod(1000);
}
}
private async Task<Task[]> MoveEnemyTask()
{
Task[] tasks = new Task[m_enemies.Count];
for (int i = 0; i < m_enemies.Count; i++)
{
Enemy enemy = m_enemies[i];
enemy.transform.position += new Vector3(0, enemy.moveY * Time.deltaTime);
if (enemy.transform.position.y > 5f)
enemy.moveY = -math.abs(enemy.moveY);
if (enemy.transform.position.y < -5f)
enemy.moveY = +math.abs(enemy.moveY);
tasks[i] = Performance.PerformanceIntensiveTask(1000);
}
await Task.WhenAll(tasks);
return tasks;
}
L'affichage et le déplacement d'un millier d'ennemis avec la méthode ont pris environ 150 ms avec une fréquence d'images d'environ 7 FPS.
Afficher et déplacer un millier d'ennemis avec une tâche a pris environ 50 ms avec une fréquence d'images d'environ 30 FPS.
useTasks
?Les tâches sont extrêmement performantes et réduisent la pression sur les performances de votre système. Vous pouvez même les utiliser dans plusieurs threads à l'aide de la bibliothèque parallèle de tâches (TPL).
Il y a cependant quelques inconvénients à les utiliser dans Unity. Le principal inconvénient de l'utilisation Task
dans Unity est qu'ils s'exécutent tous sur le Main
thread. Oui, nous pouvons les faire fonctionner sur d'autres threads, mais Unity fait déjà sa propre gestion des threads et de la mémoire, et vous pouvez créer des erreurs en créant plus de threads que de cœurs de processeur, ce qui entraîne une concurrence pour les ressources.
Les tâches peuvent également être difficiles à exécuter correctement et à déboguer. Lors de l'écriture du code d'origine, je me suis retrouvé avec toutes les tâches en cours d'exécution, mais aucun des ennemis ne s'est déplacé à l'écran. Il a fini par être que j'avais besoin de retourner le Task[]
que j'ai créé dans le fichier Task
.
Les tâches créent beaucoup de déchets qui affectent les performances. Ils n'apparaissent pas non plus dans le profileur, donc si vous en avez un qui affecte les performances, il est difficile de le retrouver. De plus, j'ai remarqué que parfois mes tâches et mes fonctions de mise à jour continuent de s'exécuter à partir d'autres scènes.
Selon Unity , "Une coroutine est une fonction qui peut suspendre son exécution (rendement) jusqu'à ce que l' instruction YieldInstruction donnée se termine."
Cela signifie que nous pouvons exécuter du code et attendre la fin d'une tâche avant de continuer. Cela ressemble beaucoup à une méthode asynchrone. Il utilise un type de retour IEnumerator
et we yield return
au lieu de await
.
Unity a plusieurs types d' instructions de rendement que nous pouvons utiliser, c'est-à-dire , WaitForSeconds
, ou .WaitForEndOfFrameWaitUntilWaitWhile
Pour démarrer les coroutines, nous avons besoin de a MonoBehaviour
et utilisons le MonoBehaviour.StartCoroutine
.
Pour arrêter une coroutine avant qu'elle ne se termine, nous utilisons MonoBehaviour.StopCoroutine
. Lorsque vous arrêtez des coroutines, assurez-vous d'utiliser la même méthode que celle que vous avez utilisée pour la démarrer.
Les cas d'utilisation courants des coroutines dans Unity consistent à attendre que les actifs se chargent et à créer des temporisateurs de temps de recharge.
using System.Collections;
using UnityEngine;
using UnityEngine.SceneManagement;
public class SceneLoader : MonoBehaviour
{
public Coroutine loadSceneCoroutine;
public void Update()
{
if (Input.GetKeyDown(KeyCode.Space) && loadSceneCoroutine == null)
{
loadSceneCoroutine = StartCoroutine(LoadSceneAsync());
}
if (Input.GetKeyDown(KeyCode.Escape) && loadSceneCoroutine != null)
{
StopCoroutine(loadSceneCoroutine);
loadSceneCoroutine = null;
}
}
private IEnumerator LoadSceneAsync()
{
AsyncOperation asyncLoad = SceneManager.LoadSceneAsync("Scene2");
yield return new WaitWhile(() => !asyncLoad.isDone);
}
}
Voyons comment l'utilisation d'une coroutine impacte les performances de notre projet. Je ne vais le faire qu'avec la méthode intensive en performances.
J'ai ajouté le Coroutine
à l' MethodType
énumération et aux variables pour garder une trace de son état:
public class PerformanceTaskCoroutineJob : MonoBehaviour
{
private enum MethodType
{
Normal,
Task,
Coroutine
}
...
private Coroutine m_performanceCoroutine;
J'ai créé la coroutine. Ceci est similaire à la tâche et à la méthode gourmandes en performances que nous avons créées précédemment avec du code ajouté pour mettre à jour l'heure de la méthode :
private IEnumerator PerformanceCoroutine(int timesToRepeat, float startTime)
{
for (int count = 0; count < numberGameObjectsToImitate; count++)
{
// Represents a Performance Intensive Method like some pathfinding or really complex calculation.
float value = 0f;
for (int i = 0; i < timesToRepeat; i++)
{
value = math.exp10(math.sqrt(value));
}
}
methodTime = (Time.realtimeSinceStartup - startTime) * 1000f;
Debug.Log($"{methodTime} ms");
m_performanceCoroutine = null;
yield return null;
}
Dans la Update
méthode, j'ai ajouté la vérification de la coroutine. J'ai également modifié l'heure de la méthode, mis à jour le code et ajouté du code pour arrêter la coroutine si elle était en cours d'exécution et nous avons changé le type de méthode :
private async void Update()
{
float startTime = Time.realtimeSinceStartup;
switch (method)
{
...
case MethodType.Coroutine:
m_performanceCoroutine ??= StartCoroutine(PerformanceCoroutine(5000, startTime));
break;
default:
Performance.PerformanceIntensiveMethod(50000);
break;
}
if (method != MethodType.Coroutine)
{
methodTime = (Time.realtimeSinceStartup - startTime) * 1000f;
Debug.Log($"{methodTime} ms");
}
if (method != MethodType.Coroutine || m_performanceCoroutine == null) return;
StopCoroutine(m_performanceCoroutine);
m_performanceCoroutine = null;
}
La coroutine intensive prend environ 6 ms pour se terminer avec le jeu fonctionnant à environ 90 FPS.
Le système de tâches C# est l'implémentation par Unity de tâches faciles à écrire, qui ne génèrent pas les déchets générés par les tâches et utilisent les threads de travail que Unity a déjà créés. Cela corrige tous les inconvénients des tâches.
Unity compare les travaux en tant que threads, mais ils disent qu'un travail effectue une tâche spécifique. Les tâches peuvent également dépendre d'autres tâches à terminer avant de s'exécuter ; cela résout le problème avec la tâche que j'avais qui ne s'est pas correctement déplacée Units
parce qu'elle dépendait d'une autre tâche à terminer en premier.
Les dépendances de travail sont automatiquement prises en charge pour nous par Unity. Le système d'emploi dispose également d'un système de sécurité intégré principalement pour se protéger contre les conditions de course . Une mise en garde avec les travaux est qu'ils ne peuvent contenir que des variables membres qui sont soit des types blittables, soit des types NativeContainer ; c'est un inconvénient du système de sécurité.
Pour utiliser le système de travail, vous créez le travail, planifiez le travail, attendez que le travail se termine, puis utilisez les données renvoyées par le travail. Le système de tâches est nécessaire pour utiliser la pile technologique orientée données (DOTS) de Unity.
Pour plus de détails sur le système de tâches, consultez la documentation Unity .
Pour créer un travail, vous créez un stuct
qui implémente l'une des IJob
interfaces ( IJob , IJobFor , IJobParallelFor , Unity.Engine.Jobs.IJobParallelForTransform ). IJob
est un travail de base. IJobFor
et IJobForParallel
permettent d'effectuer la même opération sur chaque élément d'un conteneur natif ou pour plusieurs itérations. La différence entre eux est que le IJobFor s'exécute sur un seul thread où le IJobForParallel
sera divisé entre plusieurs threads.
Je vais l'utiliser IJob
pour créer un travail d'opération intensive IJobFor
et IJobForParallel
pour créer un travail qui déplacera plusieurs ennemis; c'est juste pour que nous puissions voir les différents impacts sur les performances. Ces tâches seront identiques aux tâches et méthodes que nous avons créées précédemment :
public struct PerformanceIntensiveJob : IJob { }
public struct MoveEnemyJob: IJobFor { }
public struct MoveEnemyParallelJob : IJobParallelFor { }
Ajoutez les variables membres. Dans mon cas, mon IJob
n'en a pas besoin. Le IJobFor
et IJobParallelFor
ont besoin d'un flottant pour le temps delta actuel car les travaux n'ont pas de concept de cadre ; ils opèrent en dehors de Unity MonoBehaviour
. Ils ont également besoin d'un tableau de float3
pour la position et d'un tableau pour la vitesse de déplacement sur l'axe y :
public struct MoveEnemyJob : IJobFor
{
public NativeArray<float3> positions;
public NativeArray<float> moveYs;
public float deltaTime;
}
public struct MoveEnemyParallelJob : IJobParallelFor
{
public NativeArray<float3> positions;
public NativeArray<float> moveYs;
public float deltaTime;
}
La dernière étape consiste à mettre en œuvre la Execute
méthode requise. Le IJobFor
et IJobForParallel
les deux nécessitent un int
pour l'index de l'itération actuelle que le travail est en train d'exécuter.
La différence est qu'au lieu d'accéder à l'ennemi transform
et de se déplacer, nous utilisons le tableau qui se trouve dans le travail :
public struct PerformanceIntensiveJob : IJob
{
#region Implementation of IJob
/// <inheritdoc />
public void Execute()
{
// Represents a Performance Intensive Method like some pathfinding or really complex calculation.
float value = 0f;
for (int i = 0; i < 50000; i++)
{
value = math.exp10(math.sqrt(value));
}
}
#endregion
}
// MoveEnemyJob and MoveEnemyParallelJob have the same exact Execute Method.
/// <inheritdoc />
public void Execute(int index)
{
positions[index] += new float3(0, moveYs[index] * deltaTime, 0);
if (positions[index].y > 5f)
moveYs[index] = -math.abs(moveYs[index]);
if (positions[index].y < -5f)
moveYs[index] = +math.abs(moveYs[index]);
// Represents a Performance Intensive Method like some pathfinding or really complex calculation.
float value = 0f;
for (int i = 0; i < 1000; i++)
{
value = math.exp10(math.sqrt(value));
}
} private JobHandle PerformanceIntensiveMethodJob()
{
PerformanceIntensiveJob job = new PerformanceIntensiveJob();
return job.Schedule();
}
Tout d'abord, nous devons établir le travail et remplir les données des travaux :
NativeArray<float> result = new NativeArray<float>(1, Allocator.TempJob);
MyJob jobData = new MyJob();
jobData.myFloat = result;
Ensuite, nous planifions le travail avec JobHandle jobHandle = jobData.Schedule();
. La Schedule
méthode retourne un JobHandle
qui peut être utilisé plus tard.
Nous ne pouvons pas planifier une tâche à partir d'une tâche. Cependant, nous pouvons créer de nouvelles tâches et remplir leurs données à partir d'une tâche. Une fois qu'une tâche a été planifiée, elle ne peut pas être interrompue.
Le travail à haute performance
J'ai créé une méthode qui crée un nouveau travail et le planifie. Il renvoie le descripteur de travail que je peux utiliser dans ma update
méthode :
public class PerformanceTaskCoroutineJob : MonoBehaviour
{
....
private JobHandle PerformanceIntensiveMethodJob()
{
PerformanceIntensiveJob job = new PerformanceIntensiveJob();
return job.Schedule();
}
}
J'ai ajouté le travail à mon énumération. Ensuite, dans la Update
méthode, j'ajoute le case
à la switch
section. J'ai créé un tableau de JobHandles
. Je parcours ensuite tous les objets de jeu simulés, en ajoutant une tâche planifiée pour chacun au tableau :
public class PerformanceTaskCoroutineJob : MonoBehaviour
{
private enum MethodType
{
Normal,
Task,
Coroutine,
Job
}
...
private async void Update()
{
float startTime = Time.realtimeSinceStartup;
switch (method)
{
...
case MethodType.Job:
NativeArray<JobHandle> jobHandles =
new NativeArray<JobHandle>(numberGameObjectsToImitate, Allocator.Temp);
for (int i = 0; i < numberGameObjectsToImitate; i++)
jobHandles[i] = PerformanceIntensiveMethodJob();
break;
default:
Performance.PerformanceIntensiveMethod(50000);
break;
}
...
}
}
Le MoveEnemy
etMoveEnemyParallelJob
Ensuite, j'ai ajouté les emplois à mon énumération. Puis dans la Update
méthode, j'appelle une nouvelle MoveEnemyJob
méthode, en passant le temps delta. Normalement, vous utiliseriez soit le JobFor
ou le JobParallelFor
:
public class PerformanceTaskJob : MonoBehaviour
{
private enum MethodType
{
NormalMoveEnemy,
TaskMoveEnemy,
MoveEnemyJob,
MoveEnemyParallelJob
}
...
private async void Update()
{
float startTime = Time.realtimeSinceStartup;
switch (method)
{
...
case MethodType.MoveEnemyJob:
case MethodType.MoveEnemyParallelJob:
MoveEnemyJob(Time.deltaTime);
break;
default:
MoveEnemy();
break;
}
...
}
...
La première chose que je fais est de définir un tableau pour les positions et un tableau pour les moveY
que je transmettrai aux tâches. Je remplis ensuite ces tableaux avec les données des ennemis. Ensuite, je crée le travail et définit les données du travail en fonction du travail que je souhaite utiliser. Après cela, je planifie le travail en fonction du travail que je veux utiliser et du type de planification que je veux faire :
private void MoveEnemyJob(float deltaTime)
{
NativeArray<float3> positions = new NativeArray<float3>(m_enemies.Count, Allocator.TempJob);
NativeArray<float> moveYs = new NativeArray<float>(m_enemies.Count, Allocator.TempJob);
for (int i = 0; i < m_enemies.Count; i++)
{
positions[i] = m_enemies[i].transform.position;
moveYs[i] = m_enemies[i].moveY;
}
// Use one or the other
if (method == MethodType.MoveEnemyJob)
{
MoveEnemyJob job = new MoveEnemyJob
{
deltaTime = deltaTime,
positions = positions,
moveYs = moveYs
};
// typically we would use one of these methods
switch (moveEnemyJobType)
{
case MoveEnemyJobType.ImmediateMainThread:
// Schedule job to run immediately on main thread.
// typically would not use.
job.Run(m_enemies.Count);
break;
case MoveEnemyJobType.ScheduleSingleWorkerThread:
case MoveEnemyJobType.ScheduleParallelWorkerThreads:
{
// Schedule job to run at a later point on a single worker thread.
// First parameter is how many for-each iterations to perform.
// The second parameter is a JobHandle to use for this job's dependencies.
// Dependencies are used to ensure that a job executes on worker threads after the dependency has completed execution.
// In this case we don't need our job to depend on anything so we can use a default one.
JobHandle scheduleJobDependency = new JobHandle();
JobHandle scheduleJobHandle = job.Schedule(m_enemies.Count, scheduleJobDependency);
switch (moveEnemyJobType)
{
case MoveEnemyJobType.ScheduleSingleWorkerThread:
break;
case MoveEnemyJobType.ScheduleParallelWorkerThreads:
{
// Schedule job to run on parallel worker threads.
// First parameter is how many for-each iterations to perform.
// The second parameter is the batch size,
// essentially the no-overhead inner-loop that just invokes Execute(i) in a loop.
// When there is a lot of work in each iteration then a value of 1 can be sensible.
// When there is very little work values of 32 or 64 can make sense.
// The third parameter is a JobHandle to use for this job's dependencies.
// Dependencies are used to ensure that a job executes on worker threads after the dependency has completed execution.
JobHandle scheduleParallelJobHandle =
job.ScheduleParallel(m_enemies.Count, m_enemies.Count / 10, scheduleJobHandle);
break;
}
}
break;
}
}
}
else if (method == MethodType.MoveEnemyParallelJob)
{
MoveEnemyParallelJob job = new MoveEnemyParallelJob
{
deltaTime = deltaTime,
positions = positions,
moveYs = moveYs
};
// Schedule a parallel-for job. First parameter is how many for-each iterations to perform.
// The second parameter is the batch size,
// essentially the no-overhead inner-loop that just invokes Execute(i) in a loop.
// When there is a lot of work in each iteration then a value of 1 can be sensible.
// When there is very little work values of 32 or 64 can make sense.
JobHandle jobHandle = job.Schedule(m_enemies.Count, m_enemies.Count / 10);
}
}
Nous devons attendre que le travail soit terminé. Nous pouvons obtenir le statut à partir du JobHandle
que nous avons utilisé lorsque nous avons programmé le travail pour le terminer. Cela attendra que le travail soit terminé avant de poursuivre l'exécution : > handle.Complete();
ou JobHandle.CompleteAll(jobHandles)
. Une fois le travail terminé, le NativeContainer
que nous avons utilisé pour configurer le travail aura toutes les données que nous devons utiliser. Une fois que nous en avons récupéré les données, nous devons en disposer correctement.
Le travail à haute performance
C'est assez simple puisque je ne suis pas en train de lire ou d'écrire des données dans le travail. J'attends que tous les travaux programmés soient terminés, puis je supprime le Native
tableau :
public class PerformanceTaskCoroutineJob : MonoBehaviour
{
...
private async void Update()
{
float startTime = Time.realtimeSinceStartup;
switch (method)
{
...
case MethodType.Job:
....
JobHandle.CompleteAll(jobHandles);
jobHandles.Dispose();
break;
default:
Performance.PerformanceIntensiveMethod(50000);
break;
}
...
}
}
Le travail intensif prend environ 6 ms et le jeu tourne à environ 90 FPS.
Le MoveEnemy
travail
J'ajoute les vérifications complètes appropriées :
private void MoveEnemyJob(float deltaTime)
{
....
if (method == MethodType.MoveEnemyJob)
{
....
switch (moveEnemyJobType)
{
case MoveEnemyJobType.ScheduleSingleWorkerThread:
case MoveEnemyJobType.ScheduleParallelWorkerThreads:
{
....
// typically one or the other
switch (moveEnemyJobType)
{
case MoveEnemyJobType.ScheduleSingleWorkerThread:
scheduleJobHandle.Complete();
break;
case MoveEnemyJobType.ScheduleParallelWorkerThreads:
{
scheduleParallelJobHandle.Complete();
break;
}
}
break;
}
}
}
else if (method == MethodType.MoveEnemyParallelJob)
{
....
jobHandle.Complete();
}
}
Après les vérifications du type de méthode, je parcoure tous les ennemis, en définissant leurs transform
positions et moveY
les données qui ont été définies dans le travail. Ensuite, je dispose correctement des tableaux natifs :
private void MoveEnemyJob(float deltaTime)
{
....
if (method == MethodType.MoveEnemyJob)
{
....
}
else if (method == MethodType.MoveEnemyParallelJob)
{
....
}
for (int i = 0; i < m_enemies.Count; i++)
{
m_enemies[i].transform.position = positions[i];
m_enemies[i].moveY = moveYs[i];
}
// Native arrays must be disposed manually.
positions.Dispose();
moveYs.Dispose();
}
L'affichage et le déplacement d'un millier d'ennemis avec le travail ont pris environ 160 ms avec une fréquence d'images d'environ 7 FPS sans gain de performances.
L'affichage et le déplacement d'un millier d'ennemis avec un travail parallèle ont pris environ 30 ms avec une fréquence d'images d'environ 30 FPS.
Le compilateur en rafale est un compilateur qui traduit du bytecode en code natif. L'utiliser avec le système de tâches C # améliore la qualité du code généré, vous donnant une augmentation significative des performances ainsi qu'une réduction de la consommation de la batterie sur les appareils mobiles.
Pour l'utiliser, il vous suffit d'indiquer à Unity que vous souhaitez utiliser la compilation en rafale sur le travail avec l' [BurstCompile]
attribut :
using Unity.Burst;
using Unity.Jobs;
using Unity.Mathematics;
[BurstCompile]
public struct PerformanceIntensiveJob : IJob
{
...
}
using Unity.Burst;
using Unity.Collections;
using Unity.Jobs;
using Unity.Mathematics;
[BurstCompile]
public struct MoveEnemyJob : IJobFor
{
...
}
[BurstCompile]
public struct MoveEnemyParallelJob : IJobParallelFor
{
...
}
Ensuite, dans Unity, sélectionnez Jobs > Burst > Enable Completion
Burst est juste à temps (JIT) dans l'éditeur, ce qui signifie qu'il peut être désactivé en mode lecture. Lorsque vous construisez votre projet, c'est Ahead-Of-Time (AOT), ce qui signifie que cela doit être activé avant de construire votre projet. Vous pouvez le faire en modifiant la section Burst AOT Settings dans la fenêtre Project Settings .
Pour plus de détails sur le compilateur de rafale, consultez la documentation Unity .
Un travail intensif avec rafale prend environ 3 ms pour se terminer avec le jeu fonctionnant à environ 150 FPS.
Affichage et déplacement d'un millier d'ennemis, le travail avec rafale a pris environ 30 ms avec une fréquence d'images d'environ 30 FPS.
Affichage et déplacement d'un millier d'ennemis, le travail parallèle à la rafale a pris environ 6 ms avec une fréquence d'images d'environ 80 à 90 FPS.
Nous pouvons utiliser Task
pour augmenter les performances de nos applications Unity, mais leur utilisation présente plusieurs inconvénients. Il est préférable d'utiliser les éléments fournis dans Unity en fonction de ce que nous voulons faire. Utilisez des coroutines si nous voulons attendre que quelque chose finisse de se charger de manière asynchrone ; nous pouvons démarrer la coroutine et ne pas arrêter l'exécution du processus de notre programme.
Nous pouvons utiliser le système de tâches C # avec le compilateur en rafale pour obtenir un gain de performances considérable sans avoir à nous soucier de toutes les tâches de gestion des threads lors de l'exécution de tâches gourmandes en processus. En utilisant les systèmes intégrés, nous sommes sûrs que cela est fait d'une manière sûre qui ne cause pas d'erreurs ou de bugs indésirables.
Les tâches s'exécutaient un peu mieux que les travaux sans utiliser le compilateur en rafale, mais cela est dû au peu de surcharge supplémentaire dans les coulisses pour tout configurer en toute sécurité pour nous. Lors de l'utilisation du compilateur de rafale, nos travaux ont effectué nos tâches. Lorsque vous avez besoin de toutes les performances supplémentaires que vous pouvez obtenir, utilisez le système de tâches C# avec burst.
Les fichiers de projet pour cela peuvent être trouvés sur mon GitHub .
1656375600
当您尝试发布到 Web、移动设备、控制台甚至一些低端 PC 时,性能就是一切。以低于 30 FPS 的速度运行的游戏或应用程序可能会让用户感到沮丧。让我们看一下可以通过减少 CPU 负载来提高性能的一些方法。
在这篇文章中,我们将介绍 C# 中的 、 和 是什么async
以及await
如何Task
在 Unity 中使用它们来提高项目的性能。接下来,我们将看一下 Unity 的一些内置包:协程、C# 作业系统和突发编译器。我们将了解它们是什么、如何使用它们以及它们如何提高项目的性能。
为了启动这个项目,我将使用 Unity 2021.3.4f1。我没有在任何其他版本的 Unity 上测试过这段代码;这里的所有概念都应该适用于 Unity 2019.3 之后的任何 Unity 版本。如果使用旧版本,您的性能结果可能会有所不同,因为 Unity 在 2021 年确实对 async/await 编程模型进行了一些重大改进。在 Unity 的博客Unity and .NET, what's next中阅读有关它的更多信息,特别是标有“现代化Unity 运行时。”
我创建了一个新的 2D (URP) Core 项目,但您可以在任何您喜欢的项目中使用它。
我有一个来自Kenney Vleugels的 Space Shooter(Redux,加上字体和声音)的精灵。
我创建了一个包含 Sprite Render 和 Enemy 组件的敌人预制件。Enemy 组件是MonoBehaviour
具有 aTransform
和 a 的 afloat
来跟踪在 y 轴上移动的位置和速度:
using UnityEngine;
public class Enemy
{
public Transform transform;
public float moveY;
}
async
,await
和是什么Task
async
?在 C# 中,方法async
前面可以有一个关键字,表示方法是异步方法。这只是告诉编译器我们希望能够在其中执行代码并允许该方法的调用者在等待该方法完成时继续执行的一种方式。
这方面的一个例子是做饭。您将开始烹饪肉,当肉在烹饪并且您正在等待它完成时,您将开始制作侧面。当食物在烹饪时,你会开始摆桌子。代码中的一个示例是static async Task<Steak> MakeSteak(int number)
.
Unity 还有各种可以异步调用的内置方法;有关方法列表,请参阅Unity 文档。通过 Unity 处理内存管理的方式,它使用协程、AsyncOperation
或C# Job System。
await
以及如何使用它?await
在 C# 中,您可以使用关键字等待异步操作完成。这在任何具有async
关键字等待操作继续的方法中使用:
Public async void Update()
{
// do stuff
await // some asynchronous method or task to finish
// do more stuff or do stuff with the data returned from the asynchronous task.
}
有关更多信息,请参阅Microsoft 文档await
。
Task
以及如何使用它?ATask
是一种异步方法,它执行单个操作并且不返回值。对于Task
返回值的 a,我们将使用Task<TResult>
.
要使用任务,我们创建它就像在 C# 中创建任何新对象一样:Task t1 = new Task(void Action)
。接下来,我们开始任务t1.wait
。最后,我们等待任务完成t1.wait
。
有多种方法可以创建、启动和运行任务。Task t2 = Task.Run(void Action)
将创建并启动一个任务。await Task.Run(void Action)
将创建、启动并等待任务完成。我们可以使用最常见的替代方式Task t3 = Task.Factory.Start(void Action)
。
我们可以通过多种方式等待 Task 完成。int index = Task.WaitAny(Task[])
将等待任何任务完成并为我们提供数组中已完成任务的索引。await Task.WaitAll(Task[])
将等待所有任务完成。
有关任务的更多信息,请参阅Microsoft 文档。
task
例子private void Start()
{
Task t1 = new Task(() => Thread.Sleep(1000));
Task t2 = Task.Run(() => Thread.Sleep(2000000));
Task t3 = Task.Factory.StartNew(() => Thread.Sleep(1000));
t1.Start();
Task[] tasks = { t1, t2, t3 };
int index = Task.WaitAny(tasks);
Debug.Log($"Task {tasks[index].Id} at index {index} completed.");
Task t4 = new Task(() => Thread.Sleep(100));
Task t5 = Task.Run(() => Thread.Sleep(200));
Task t6 = Task.Factory.StartNew(() => Thread.Sleep(300));
t4.Start();
Task.WaitAll(t4, t5, t6);
Debug.Log($"All Task Completed!");
Debug.Log($"Task When any t1={t1.IsCompleted} t2={t2.IsCompleted} t3={t3.IsCompleted}");
Debug.Log($"All Task Completed! t4={t4.IsCompleted} t5={t5.IsCompleted} t6={t6.IsCompleted}");
}
public async void Update()
{
float startTime = Time.realtimeSinceStartup;
Debug.Log($"Update Started: {startTime}");
Task t1 = new Task(() => Thread.Sleep(10000));
Task t2 = Task.Run(() => Thread.Sleep(20000));
Task t3 = Task.Factory.StartNew(() => Thread.Sleep(30000));
await Task.WhenAll(t1, t2, t3);
Debug.Log($"Update Finished: {(Time.realtimeSinceStartup - startTime) * 1000f} ms");
}
现在让我们比较一个任务的性能和一个方法的性能。
我需要一个可以在所有性能检查中使用的静态类。它将具有模拟性能密集型操作的方法和任务。方法和任务都执行相同的操作:
using System.Threading.Tasks;
using Unity.Mathematics;
public static class Performance
{
public static void PerformanceIntensiveMethod(int timesToRepeat)
{
// Represents a Performance Intensive Method like some pathfinding or really complex calculation.
float value = 0f;
for (int i = 0; i < timesToRepeat; i++)
{
value = math.exp10(math.sqrt(value));
}
}
public static Task PerformanceIntensiveTask(int timesToRepeat)
{
return Task.Run(() =>
{
// Represents a Performance Intensive Method like some pathfinding or really complex calculation.
float value = 0f;
for (int i = 0; i < timesToRepeat; i++)
{
value = math.exp10(math.sqrt(value));
}
});
}
}
现在我需要一个MonoBehaviour
可以用来测试对任务和方法的性能影响。为了更好地看到对性能的影响,我将假装我想在十个不同的游戏对象上运行它。我还将跟踪Update
方法运行所需的时间。
在Update
中,我得到了开始时间。如果我正在测试该方法,我会遍历所有模拟的游戏对象并调用性能密集型方法。如果我正在测试任务,我会创建一个Task
遍历所有模拟游戏对象的新数组循环,并将性能密集型任务添加到任务数组中。然后我await
为所有的任务完成。在方法类型检查之外,我更新方法时间,将其转换为ms
. 我也记录下来。
public class PerformanceTaskCoroutineJob : MonoBehaviour
{
private enum MethodType
{
Normal,
Task
}
[SerializeField] private int numberGameObjectsToImitate
= 10;
[SerializeField] private MethodType method = MethodType.Normal;
[SerializeField] private float methodTime;
private async void Update()
{
float startTime = Time.realtimeSinceStartup;
switch (method)
{
case MethodType.Normal:
for (int i = 0; i < numberGameObjectsToImitate
; i++)
Performance.PerformanceIntensiveMethod(50000);
break;
case MethodType.Task:
Task[] tasks = new Task[numberGameObjectsToImitate
];
for (int i = 0; i < numberGameObjectsToImitate
; i++)
tasks[i] = Performance.PerformanceIntensiveTask(5000);
await Task.WhenAll(tasks);
break;
}
methodTime = (Time.realtimeSinceStartup - startTime) * 1000f;
Debug.Log($"{methodTime} ms");
}
}
强化方法大约需要 65 毫秒才能完成,游戏以大约 12 FPS 的速度运行。
密集型任务大约需要 4 毫秒才能完成,游戏以大约 200 FPS 的速度运行。
让我们用一千个敌人试试这个:
using System.Collections.Generic;
using System.Threading.Tasks;
using Unity.Collections;
using Unity.Jobs;
using Unity.Mathematics;
using UnityEngine;
using Random = UnityEngine.Random;
public class PerformanceTaskJob : MonoBehaviour
{
private enum MethodType
{
NormalMoveEnemy,
TaskMoveEnemy
}
[SerializeField] private int numberEnemiesToCreate = 1000;
[SerializeField] private Transform pfEnemy;
[SerializeField] private MethodType method = MethodType.NormalMoveEnemy;
[SerializeField] private float methodTime;
private readonly List<Enemy> m_enemies = new List<Enemy>();
private void Start()
{
for (int i = 0; i < numberEnemiesToCreate; i++)
{
Transform enemy = Instantiate(pfEnemy,
new Vector3(Random.Range(-8f, 8f), Random.Range(-8f, 8f)),
Quaternion.identity);
m_enemies.Add(new Enemy { transform = enemy, moveY = Random.Range(1f, 2f) });
}
}
private async void Update()
{
float startTime = Time.realtimeSinceStartup;
switch (method)
{
case MethodType.NormalMoveEnemy:
MoveEnemy();
break;
case MethodType.TaskMoveEnemy:
Task<Task[]> moveEnemyTasks = MoveEnemyTask();
await Task.WhenAll(moveEnemyTasks);
break;
default:
MoveEnemy();
break;
}
methodTime = (Time.realtimeSinceStartup - startTime) * 1000f;
Debug.Log($"{methodTime} ms");
}
private void MoveEnemy()
{
foreach (Enemy enemy in m_enemies)
{
enemy.transform.position += new Vector3(0, enemy.moveY * Time.deltaTime);
if (enemy.transform.position.y > 5f)
enemy.moveY = -math.abs(enemy.moveY);
if (enemy.transform.position.y < -5f)
enemy.moveY = +math.abs(enemy.moveY);
Performance.PerformanceIntensiveMethod(1000);
}
}
private async Task<Task[]> MoveEnemyTask()
{
Task[] tasks = new Task[m_enemies.Count];
for (int i = 0; i < m_enemies.Count; i++)
{
Enemy enemy = m_enemies[i];
enemy.transform.position += new Vector3(0, enemy.moveY * Time.deltaTime);
if (enemy.transform.position.y > 5f)
enemy.moveY = -math.abs(enemy.moveY);
if (enemy.transform.position.y < -5f)
enemy.moveY = +math.abs(enemy.moveY);
tasks[i] = Performance.PerformanceIntensiveTask(1000);
}
await Task.WhenAll(tasks);
return tasks;
}
使用该方法显示和移动一千个敌人大约需要 150 毫秒,帧速率约为 7 FPS。
显示和移动一千个敌人的任务大约需要 50 毫秒,帧速率约为 30 FPS。
useTasks
呢?任务非常高效,可以减少系统性能的压力。您甚至可以使用任务并行库 (TPL) 在多个线程中使用它们。
然而,在 Unity 中使用它们有一些缺点。在 Unity 中使用的主要缺点Task
是它们都在Main
线程上运行。是的,我们可以让它们在其他线程上运行,但是 Unity 已经做了自己的线程和内存管理,你可以通过创建比 CPU Cores 更多的线程来创建错误,这会导致资源竞争。
任务也很难正确执行和调试。在编写原始代码时,我结束了所有任务都在运行,但没有一个敌人在屏幕上移动。结果是我需要返回Task[]
我在Task
.
任务会产生大量影响性能的垃圾。它们也不会出现在分析器中,所以如果你有一个影响性能的,很难追踪。另外,我注意到有时我的任务和更新功能会继续从其他场景运行。
根据Unity的说法,“协程是一个可以暂停执行(yield)直到给定的YieldInstruction完成的函数。”
这意味着我们可以运行代码并等待任务完成后再继续。这很像一个异步方法。它使用返回类型IEnumerator
和 weyield return
而不是await
.
Unity 有几种不同类型的yield 指令可供我们使用,即WaitForSeconds
、WaitForEndOfFrame
、WaitUntil
或WaitWhile
。
要启动协程,我们需要 aMonoBehaviour
并使用MonoBehaviour.StartCoroutine
.
要在协程完成之前停止协程,我们使用MonoBehaviour.StopCoroutine
. 停止协程时,请确保使用与启动协程相同的方法。
Unity 中协程的常见用例是等待资产加载并创建冷却计时器。
using System.Collections;
using UnityEngine;
using UnityEngine.SceneManagement;
public class SceneLoader : MonoBehaviour
{
public Coroutine loadSceneCoroutine;
public void Update()
{
if (Input.GetKeyDown(KeyCode.Space) && loadSceneCoroutine == null)
{
loadSceneCoroutine = StartCoroutine(LoadSceneAsync());
}
if (Input.GetKeyDown(KeyCode.Escape) && loadSceneCoroutine != null)
{
StopCoroutine(loadSceneCoroutine);
loadSceneCoroutine = null;
}
}
private IEnumerator LoadSceneAsync()
{
AsyncOperation asyncLoad = SceneManager.LoadSceneAsync("Scene2");
yield return new WaitWhile(() => !asyncLoad.isDone);
}
}
让我们看看使用协程如何影响我们项目的性能。我只会使用性能密集型方法来做到这一点。
我添加Coroutine
到MethodType
枚举和变量中以跟踪其状态:
public class PerformanceTaskCoroutineJob : MonoBehaviour
{
private enum MethodType
{
Normal,
Task,
Coroutine
}
...
private Coroutine m_performanceCoroutine;
我创建了协程。这类似于我们之前创建的性能密集型任务和方法,添加了代码来更新方法时间:
private IEnumerator PerformanceCoroutine(int timesToRepeat, float startTime)
{
for (int count = 0; count < numberGameObjectsToImitate; count++)
{
// Represents a Performance Intensive Method like some pathfinding or really complex calculation.
float value = 0f;
for (int i = 0; i < timesToRepeat; i++)
{
value = math.exp10(math.sqrt(value));
}
}
methodTime = (Time.realtimeSinceStartup - startTime) * 1000f;
Debug.Log($"{methodTime} ms");
m_performanceCoroutine = null;
yield return null;
}
在Update
方法中,我添加了对协程的检查。我还修改了方法时间,更新了代码,并添加了代码来停止协程(如果它正在运行)并且我们更改了方法类型:
private async void Update()
{
float startTime = Time.realtimeSinceStartup;
switch (method)
{
...
case MethodType.Coroutine:
m_performanceCoroutine ??= StartCoroutine(PerformanceCoroutine(5000, startTime));
break;
default:
Performance.PerformanceIntensiveMethod(50000);
break;
}
if (method != MethodType.Coroutine)
{
methodTime = (Time.realtimeSinceStartup - startTime) * 1000f;
Debug.Log($"{methodTime} ms");
}
if (method != MethodType.Coroutine || m_performanceCoroutine == null) return;
StopCoroutine(m_performanceCoroutine);
m_performanceCoroutine = null;
}
密集的协程大约需要 6 毫秒才能完成,游戏以大约 90 FPS 的速度运行。
C# Job System 是 Unity 对易于编写的任务的实现,不会像任务那样产生垃圾,并利用Unity 已经创建的工作线程。这解决了任务的所有缺点。
Unity 将作业比作线程,但他们确实说作业执行一项特定任务。作业也可以在运行前依赖其他作业完成;这解决了我没有正确移动我的任务的问题,Units
因为它依赖于首先完成的另一个任务。
Unity 会自动为我们处理作业依赖项。工作系统还内置了一个安全系统,主要用于防止竞争条件。对作业的一个警告是,它们只能包含blittable 类型或NativeContainer类型的成员变量。这是安全系统的一个缺点。
要使用作业系统,您需要创建作业、安排作业、等待作业完成,然后使用作业返回的数据。需要作业系统才能使用 Unity 的面向数据的技术堆栈 (DOTS)。
有关作业系统的更多详细信息,请参阅Unity 文档。
要创建作业,您需要创建一个stuct
实现其中一个IJob
接口(IJob、IJobFor、IJobParallelFor、Unity.Engine.Jobs.IJobParallelForTransform)的作业。IJob
是一项基本工作。IJobFor
并IJobForParallel
用于对本机容器的每个元素执行相同的操作或进行多次迭代。它们之间的区别在于 IJobFor 在单个线程上运行,其中IJobForParallel
将在多个线程之间拆分。
我将用于IJob
创建一个密集的操作工作,IJobFor
并IJobForParallel
创建一个可以移动多个敌人的工作;这只是为了让我们可以看到对性能的不同影响。这些作业将与我们之前创建的任务和方法相同:
public struct PerformanceIntensiveJob : IJob { }
public struct MoveEnemyJob: IJobFor { }
public struct MoveEnemyParallelJob : IJobParallelFor { }
添加成员变量。就我而言,我IJob
不需要任何东西。由于作业没有框架的概念,IJobFor
并且需要当前增量时间的浮点数;IJobParallelFor
他们在 Unity 之外运行MonoBehaviour
。他们还需要一个float3
用于位置的数组和一个用于 y 轴移动速度的数组:
public struct MoveEnemyJob : IJobFor
{
public NativeArray<float3> positions;
public NativeArray<float> moveYs;
public float deltaTime;
}
public struct MoveEnemyParallelJob : IJobParallelFor
{
public NativeArray<float3> positions;
public NativeArray<float> moveYs;
public float deltaTime;
}
最后一步是实现所需的Execute
方法。和IJobFor
都IJobForParallel
需要int
作业正在执行的当前迭代的索引。
transform
不同之处在于,我们使用工作中的数组,而不是访问敌人的和移动:
public struct PerformanceIntensiveJob : IJob
{
#region Implementation of IJob
/// <inheritdoc />
public void Execute()
{
// Represents a Performance Intensive Method like some pathfinding or really complex calculation.
float value = 0f;
for (int i = 0; i < 50000; i++)
{
value = math.exp10(math.sqrt(value));
}
}
#endregion
}
// MoveEnemyJob and MoveEnemyParallelJob have the same exact Execute Method.
/// <inheritdoc />
public void Execute(int index)
{
positions[index] += new float3(0, moveYs[index] * deltaTime, 0);
if (positions[index].y > 5f)
moveYs[index] = -math.abs(moveYs[index]);
if (positions[index].y < -5f)
moveYs[index] = +math.abs(moveYs[index]);
// Represents a Performance Intensive Method like some pathfinding or really complex calculation.
float value = 0f;
for (int i = 0; i < 1000; i++)
{
value = math.exp10(math.sqrt(value));
}
} private JobHandle PerformanceIntensiveMethodJob()
{
PerformanceIntensiveJob job = new PerformanceIntensiveJob();
return job.Schedule();
}
首先,我们需要设置作业并填充作业数据:
NativeArray<float> result = new NativeArray<float>(1, Allocator.TempJob);
MyJob jobData = new MyJob();
jobData.myFloat = result;
然后我们用JobHandle jobHandle = jobData.Schedule();
. 该Schedule
方法返回一个JobHandle
可以在以后使用的。
我们无法从作业中安排作业。但是,我们可以创建新的工作并从工作中填充他们的数据。一旦安排了作业,就不能中断。
性能密集型工作
我创建了一个创建新作业并安排它的方法。它返回我可以在我的update
方法中使用的作业句柄:
public class PerformanceTaskCoroutineJob : MonoBehaviour
{
....
private JobHandle PerformanceIntensiveMethodJob()
{
PerformanceIntensiveJob job = new PerformanceIntensiveJob();
return job.Schedule();
}
}
我将这份工作添加到我的枚举中。然后,在Update
方法中,我将 添加case
到该switch
部分。我创建了一个数组JobHandles
。然后,我循环遍历所有模拟的游戏对象,为每个对象添加一个预定作业到数组中:
public class PerformanceTaskCoroutineJob : MonoBehaviour
{
private enum MethodType
{
Normal,
Task,
Coroutine,
Job
}
...
private async void Update()
{
float startTime = Time.realtimeSinceStartup;
switch (method)
{
...
case MethodType.Job:
NativeArray<JobHandle> jobHandles =
new NativeArray<JobHandle>(numberGameObjectsToImitate, Allocator.Temp);
for (int i = 0; i < numberGameObjectsToImitate; i++)
jobHandles[i] = PerformanceIntensiveMethodJob();
break;
default:
Performance.PerformanceIntensiveMethod(50000);
break;
}
...
}
}
和_MoveEnemyMoveEnemyParallelJob
接下来,我将作业添加到我的枚举中。然后在Update
方法中,我调用一个新MoveEnemyJob
方法,传递增量时间。通常你会使用JobFor
或JobParallelFor
:
public class PerformanceTaskJob : MonoBehaviour
{
private enum MethodType
{
NormalMoveEnemy,
TaskMoveEnemy,
MoveEnemyJob,
MoveEnemyParallelJob
}
...
private async void Update()
{
float startTime = Time.realtimeSinceStartup;
switch (method)
{
...
case MethodType.MoveEnemyJob:
case MethodType.MoveEnemyParallelJob:
MoveEnemyJob(Time.deltaTime);
break;
default:
MoveEnemy();
break;
}
...
}
...
我要做的第一件事是为职位设置一个数组,并为moveY
我将传递给工作的数组设置一个数组。然后我用来自敌人的数据填充这些数组。接下来,我创建作业并根据我要使用的作业设置作业的数据。之后,我根据要使用的作业和要执行的调度类型来安排作业:
private void MoveEnemyJob(float deltaTime)
{
NativeArray<float3> positions = new NativeArray<float3>(m_enemies.Count, Allocator.TempJob);
NativeArray<float> moveYs = new NativeArray<float>(m_enemies.Count, Allocator.TempJob);
for (int i = 0; i < m_enemies.Count; i++)
{
positions[i] = m_enemies[i].transform.position;
moveYs[i] = m_enemies[i].moveY;
}
// Use one or the other
if (method == MethodType.MoveEnemyJob)
{
MoveEnemyJob job = new MoveEnemyJob
{
deltaTime = deltaTime,
positions = positions,
moveYs = moveYs
};
// typically we would use one of these methods
switch (moveEnemyJobType)
{
case MoveEnemyJobType.ImmediateMainThread:
// Schedule job to run immediately on main thread.
// typically would not use.
job.Run(m_enemies.Count);
break;
case MoveEnemyJobType.ScheduleSingleWorkerThread:
case MoveEnemyJobType.ScheduleParallelWorkerThreads:
{
// Schedule job to run at a later point on a single worker thread.
// First parameter is how many for-each iterations to perform.
// The second parameter is a JobHandle to use for this job's dependencies.
// Dependencies are used to ensure that a job executes on worker threads after the dependency has completed execution.
// In this case we don't need our job to depend on anything so we can use a default one.
JobHandle scheduleJobDependency = new JobHandle();
JobHandle scheduleJobHandle = job.Schedule(m_enemies.Count, scheduleJobDependency);
switch (moveEnemyJobType)
{
case MoveEnemyJobType.ScheduleSingleWorkerThread:
break;
case MoveEnemyJobType.ScheduleParallelWorkerThreads:
{
// Schedule job to run on parallel worker threads.
// First parameter is how many for-each iterations to perform.
// The second parameter is the batch size,
// essentially the no-overhead inner-loop that just invokes Execute(i) in a loop.
// When there is a lot of work in each iteration then a value of 1 can be sensible.
// When there is very little work values of 32 or 64 can make sense.
// The third parameter is a JobHandle to use for this job's dependencies.
// Dependencies are used to ensure that a job executes on worker threads after the dependency has completed execution.
JobHandle scheduleParallelJobHandle =
job.ScheduleParallel(m_enemies.Count, m_enemies.Count / 10, scheduleJobHandle);
break;
}
}
break;
}
}
}
else if (method == MethodType.MoveEnemyParallelJob)
{
MoveEnemyParallelJob job = new MoveEnemyParallelJob
{
deltaTime = deltaTime,
positions = positions,
moveYs = moveYs
};
// Schedule a parallel-for job. First parameter is how many for-each iterations to perform.
// The second parameter is the batch size,
// essentially the no-overhead inner-loop that just invokes Execute(i) in a loop.
// When there is a lot of work in each iteration then a value of 1 can be sensible.
// When there is very little work values of 32 or 64 can make sense.
JobHandle jobHandle = job.Schedule(m_enemies.Count, m_enemies.Count / 10);
}
}
我们必须等待工作完成。我们可以从JobHandle
我们安排作业完成时使用的状态获取状态。这将在继续执行之前等待作业完成: >handle.Complete();
或JobHandle.CompleteAll(jobHandles)
. 作业完成后,NativeContainer
我们用来设置作业的那个将拥有我们需要使用的所有数据。一旦我们从它们那里检索到数据,我们就必须妥善处理它们。
性能密集型工作
这非常简单,因为我没有在作业中读取或写入任何数据。我等待所有计划完成的作业,然后处理Native
数组:
public class PerformanceTaskCoroutineJob : MonoBehaviour
{
...
private async void Update()
{
float startTime = Time.realtimeSinceStartup;
switch (method)
{
...
case MethodType.Job:
....
JobHandle.CompleteAll(jobHandles);
jobHandles.Dispose();
break;
default:
Performance.PerformanceIntensiveMethod(50000);
break;
}
...
}
}
密集的工作大约需要 6 毫秒才能完成,游戏以大约 90 FPS 的速度运行。
工作_MoveEnemy
我添加了适当的完整检查:
private void MoveEnemyJob(float deltaTime)
{
....
if (method == MethodType.MoveEnemyJob)
{
....
switch (moveEnemyJobType)
{
case MoveEnemyJobType.ScheduleSingleWorkerThread:
case MoveEnemyJobType.ScheduleParallelWorkerThreads:
{
....
// typically one or the other
switch (moveEnemyJobType)
{
case MoveEnemyJobType.ScheduleSingleWorkerThread:
scheduleJobHandle.Complete();
break;
case MoveEnemyJobType.ScheduleParallelWorkerThreads:
{
scheduleParallelJobHandle.Complete();
break;
}
}
break;
}
}
}
else if (method == MethodType.MoveEnemyParallelJob)
{
....
jobHandle.Complete();
}
}
在方法类型检查之后,我遍历所有敌人,设置他们的transform
位置和moveY
作业中设置的数据。接下来,我正确处理原生数组:
private void MoveEnemyJob(float deltaTime)
{
....
if (method == MethodType.MoveEnemyJob)
{
....
}
else if (method == MethodType.MoveEnemyParallelJob)
{
....
}
for (int i = 0; i < m_enemies.Count; i++)
{
m_enemies[i].transform.position = positions[i];
m_enemies[i].moveY = moveYs[i];
}
// Native arrays must be disposed manually.
positions.Dispose();
moveYs.Dispose();
}
显示和移动一千个有工作的敌人大约需要 160 毫秒,帧速率约为 7 FPS,而没有性能提升。
以约 30 FPS 的帧速率显示和移动 1000 个并行作业的敌人大约需要 30 毫秒。
突发编译器是一种将字节码转换为本机代码的编译器。将此与 C# 作业系统一起使用可提高生成代码的质量,从而显着提高性能并减少移动设备上的电池消耗。
要使用它,您只需告诉 Unity 您想在具有以下[BurstCompile]
属性的作业上使用突发编译:
using Unity.Burst;
using Unity.Jobs;
using Unity.Mathematics;
[BurstCompile]
public struct PerformanceIntensiveJob : IJob
{
...
}
using Unity.Burst;
using Unity.Collections;
using Unity.Jobs;
using Unity.Mathematics;
[BurstCompile]
public struct MoveEnemyJob : IJobFor
{
...
}
[BurstCompile]
public struct MoveEnemyParallelJob : IJobParallelFor
{
...
}
然后在 Unity 中,选择Jobs > Burst > Enable Completion
Burst在编辑器中是即时 (JIT),这意味着在播放模式下可以关闭。当您构建项目时,它是 Ahead-Of-Time (AOT),这意味着需要在构建项目之前启用它。您可以通过编辑Project Settings Window中的Burst AOT Settings部分来实现。
有关突发编译器的更多详细信息,请参阅Unity 文档。
在游戏以大约 150 FPS 的速度运行时,一个带有突发的密集工作大约需要 3 毫秒才能完成。
显示和移动一千个敌人,爆发的工作大约需要 30 毫秒,帧速率约为 30 FPS。
显示和移动一千个敌人,与爆发并行的工作大约需要 6 毫秒,帧速率约为 80 到 90 FPS。
我们可以使用它Task
来提高 Unity 应用程序的性能,但使用它们有几个缺点。根据我们想要做的事情,最好使用 Unity 中打包的东西。如果我们想等待某些东西完成异步加载,请使用协程;我们可以启动协程,而不是停止程序进程的运行。
我们可以使用 C# 作业系统和突发编译器来获得巨大的性能提升,同时在执行进程密集型任务时不必担心所有线程管理问题。使用内置系统,我们确信它以安全的方式完成,不会导致任何不必要的错误或错误。
在不使用突发编译器的情况下,任务确实比作业运行得更好,但这是由于在幕后为我们安全地设置一切而产生的额外开销。使用突发编译器时,我们的工作执行了我们的任务。当您需要可以获得的所有额外性能时,请使用 C# Job System with burst。
这个项目文件可以在我的 GitHub 上找到。
1656374400
Hiệu suất là tất cả mọi thứ khi bạn đang cố gắng xuất bản lên web, thiết bị di động, bảng điều khiển và thậm chí một số PC cấp thấp hơn. Một trò chơi hoặc ứng dụng chạy ở tốc độ dưới 30 FPS có thể gây ra sự thất vọng cho người dùng. Chúng ta hãy xem xét một số thứ chúng ta có thể sử dụng để tăng hiệu suất bằng cách giảm tải cho CPU.
Trong bài đăng này, chúng tôi sẽ đề cập đến những gì async
, await
và Task
trong C # là gì và cách sử dụng chúng trong Unity để đạt được hiệu suất trong dự án của bạn. Tiếp theo, chúng ta sẽ xem xét một số gói có sẵn của Unity: coroutines, C # Job System và trình biên dịch bùng nổ. Chúng tôi sẽ xem xét chúng là gì, cách sử dụng chúng và cách chúng tăng hiệu suất trong dự án của bạn.
Để bắt đầu dự án này, tôi sẽ sử dụng Unity 2021.3.4f1. Tôi chưa thử nghiệm mã này trên bất kỳ phiên bản Unity nào khác; tất cả các khái niệm ở đây sẽ hoạt động trên bất kỳ phiên bản Unity nào sau Unity 2019.3. Kết quả hiệu suất của bạn có thể khác nếu sử dụng phiên bản cũ hơn vì Unity đã thực hiện một số cải tiến đáng kể với mô hình lập trình async / await vào năm 2021. Đọc thêm về nó trong blog Unity và .NET của Unity, phần tiếp theo là phần có nhãn “Hiện đại hóa Thời gian chạy thống nhất. ”
Tôi đã tạo một dự án 2D (URP) Core mới, nhưng bạn có thể sử dụng dự án này trong bất kỳ loại dự án nào bạn thích.
Tôi có một sprite mà tôi nhận được từ Space Shooter (Redux, cùng với phông chữ và âm thanh) của Kenney Vleugels .
Tôi đã tạo một nhà lắp ghép của kẻ thù có chứa Sprite Render và một Thành phần của kẻ thù. Thành phần địch là một thành phần MonoBehaviour
có a Transform
và a float
để theo dõi vị trí và tốc độ di chuyển trên trục y:
using UnityEngine;
public class Enemy
{
public Transform transform;
public float moveY;
}
async
, await
và Task
có trong C #async
?Trong C #, các phương thức có thể có một async
từ khóa phía trước, nghĩa là các phương thức đó là phương thức không đồng bộ. Đây chỉ là một cách để nói với trình biên dịch rằng chúng ta muốn có thể thực thi mã bên trong và cho phép người gọi phương thức đó tiếp tục thực thi trong khi chờ phương thức này kết thúc.
Một ví dụ về điều này sẽ là nấu một bữa ăn. Bạn sẽ bắt đầu nấu thịt, và trong khi thịt đang nấu và bạn chờ nó hoàn thành, bạn sẽ bắt đầu làm các mặt. Trong khi thức ăn đang nấu, bạn sẽ bắt đầu dọn bàn ăn. Một ví dụ về điều này trong mã sẽ là static async Task<Steak> MakeSteak(int number)
.
Unity cũng có tất cả các loại phương thức có sẵn mà bạn có thể gọi không đồng bộ; xem tài liệu Unity để biết danh sách các phương pháp. Với cách Unity xử lý việc quản lý bộ nhớ, nó sử dụng coroutines hoặcAsyncOperation
Hệ thống công việc C # .
await
sử dụng nó như thế nào và nó là gì?Trong C #, bạn có thể đợi hoạt động không đồng bộ hoàn tất bằng cách sử dụng await
từ khóa. Điều này được sử dụng bên trong bất kỳ phương thức nào có async
từ khóa để chờ một thao tác tiếp tục:
Public async void Update()
{
// do stuff
await // some asynchronous method or task to finish
// do more stuff or do stuff with the data returned from the asynchronous task.
}
Xem các tài liệu của Microsoft để biết thêm về await
.
Task
và bạn sử dụng nó như thế nào?A Task
là một phương thức không đồng bộ thực hiện một thao tác đơn lẻ và không trả về giá trị. Đối với một Task
trả về một giá trị, chúng tôi sẽ sử dụng Task<TResult>
.
Để sử dụng một tác vụ, chúng ta tạo nó giống như tạo bất kỳ đối tượng mới nào trong C # Task t1 = new Task(void Action)
:. Tiếp theo, chúng ta bắt đầu nhiệm vụ t1.wait
. Cuối cùng, chúng tôi đợi nhiệm vụ hoàn thành với t1.wait
.
Có một số cách để tạo, bắt đầu và chạy tác vụ. Task t2 = Task.Run(void Action)
sẽ tạo và bắt đầu một nhiệm vụ. await Task.Run(void Action)
sẽ tạo, bắt đầu và đợi tác vụ hoàn thành. Chúng tôi có thể sử dụng cách thay thế phổ biến nhất với Task t3 = Task.Factory.Start(void Action)
.
Có một số cách mà chúng ta có thể đợi Tác vụ hoàn thành. int index = Task.WaitAny(Task[])
sẽ đợi bất kỳ nhiệm vụ nào hoàn thành và cung cấp cho chúng tôi chỉ số của nhiệm vụ đã hoàn thành trong mảng. await Task.WaitAll(Task[])
sẽ đợi tất cả các nhiệm vụ hoàn thành.
Để biết thêm về các tác vụ, hãy xem Tài liệu Microsoft .
task
ví dụ đơn giảnprivate void Start()
{
Task t1 = new Task(() => Thread.Sleep(1000));
Task t2 = Task.Run(() => Thread.Sleep(2000000));
Task t3 = Task.Factory.StartNew(() => Thread.Sleep(1000));
t1.Start();
Task[] tasks = { t1, t2, t3 };
int index = Task.WaitAny(tasks);
Debug.Log($"Task {tasks[index].Id} at index {index} completed.");
Task t4 = new Task(() => Thread.Sleep(100));
Task t5 = Task.Run(() => Thread.Sleep(200));
Task t6 = Task.Factory.StartNew(() => Thread.Sleep(300));
t4.Start();
Task.WaitAll(t4, t5, t6);
Debug.Log($"All Task Completed!");
Debug.Log($"Task When any t1={t1.IsCompleted} t2={t2.IsCompleted} t3={t3.IsCompleted}");
Debug.Log($"All Task Completed! t4={t4.IsCompleted} t5={t5.IsCompleted} t6={t6.IsCompleted}");
}
public async void Update()
{
float startTime = Time.realtimeSinceStartup;
Debug.Log($"Update Started: {startTime}");
Task t1 = new Task(() => Thread.Sleep(10000));
Task t2 = Task.Run(() => Thread.Sleep(20000));
Task t3 = Task.Factory.StartNew(() => Thread.Sleep(30000));
await Task.WhenAll(t1, t2, t3);
Debug.Log($"Update Finished: {(Time.realtimeSinceStartup - startTime) * 1000f} ms");
}
Bây giờ chúng ta hãy so sánh hiệu suất của một tác vụ so với hiệu suất của một phương pháp.
Tôi sẽ cần một lớp tĩnh mà tôi có thể sử dụng trong tất cả các lần kiểm tra hiệu suất của mình. Nó sẽ có một phương thức và một nhiệm vụ mô phỏng một hoạt động đòi hỏi nhiều hiệu suất. Cả phương pháp và tác vụ đều thực hiện cùng một hoạt động chính xác:
using System.Threading.Tasks;
using Unity.Mathematics;
public static class Performance
{
public static void PerformanceIntensiveMethod(int timesToRepeat)
{
// Represents a Performance Intensive Method like some pathfinding or really complex calculation.
float value = 0f;
for (int i = 0; i < timesToRepeat; i++)
{
value = math.exp10(math.sqrt(value));
}
}
public static Task PerformanceIntensiveTask(int timesToRepeat)
{
return Task.Run(() =>
{
// Represents a Performance Intensive Method like some pathfinding or really complex calculation.
float value = 0f;
for (int i = 0; i < timesToRepeat; i++)
{
value = math.exp10(math.sqrt(value));
}
});
}
}
Bây giờ tôi cần một MonoBehaviour
cái mà tôi có thể sử dụng để kiểm tra tác động của hiệu suất đối với tác vụ và phương pháp. Để tôi có thể thấy tác động tốt hơn đến hiệu suất, tôi sẽ giả vờ rằng tôi muốn chạy điều này trên mười đối tượng trò chơi khác nhau. Tôi cũng sẽ theo dõi khoảng thời gian mà Update
phương thức cần để chạy.
Trong Update
, tôi nhận được thời gian bắt đầu. Nếu tôi đang thử nghiệm phương pháp này, tôi lặp qua tất cả các đối tượng trò chơi được mô phỏng và gọi phương pháp tăng cường hiệu suất. Nếu tôi đang thử nghiệm tác vụ, tôi sẽ tạo một Task
vòng lặp mảng mới thông qua tất cả các đối tượng trò chơi được mô phỏng và thêm nhiệm vụ chuyên sâu về hiệu suất vào mảng tác vụ. Sau đó tôi await
cho tất cả các nhiệm vụ để hoàn thành. Bên ngoài kiểm tra loại phương thức, tôi cập nhật thời gian phương thức, chuyển đổi nó thành ms
. Tôi cũng ghi lại nó.
public class PerformanceTaskCoroutineJob : MonoBehaviour
{
private enum MethodType
{
Normal,
Task
}
[SerializeField] private int numberGameObjectsToImitate
= 10;
[SerializeField] private MethodType method = MethodType.Normal;
[SerializeField] private float methodTime;
private async void Update()
{
float startTime = Time.realtimeSinceStartup;
switch (method)
{
case MethodType.Normal:
for (int i = 0; i < numberGameObjectsToImitate
; i++)
Performance.PerformanceIntensiveMethod(50000);
break;
case MethodType.Task:
Task[] tasks = new Task[numberGameObjectsToImitate
];
for (int i = 0; i < numberGameObjectsToImitate
; i++)
tasks[i] = Performance.PerformanceIntensiveTask(5000);
await Task.WhenAll(tasks);
break;
}
methodTime = (Time.realtimeSinceStartup - startTime) * 1000f;
Debug.Log($"{methodTime} ms");
}
}
Phương pháp chuyên sâu mất khoảng 65 mili giây để hoàn thành trò chơi chạy ở khoảng 12 FPS.
Nhiệm vụ chuyên sâu mất khoảng 4ms để hoàn thành với trò chơi chạy ở khoảng 200 FPS.
Hãy thử điều này với hàng nghìn kẻ thù:
using System.Collections.Generic;
using System.Threading.Tasks;
using Unity.Collections;
using Unity.Jobs;
using Unity.Mathematics;
using UnityEngine;
using Random = UnityEngine.Random;
public class PerformanceTaskJob : MonoBehaviour
{
private enum MethodType
{
NormalMoveEnemy,
TaskMoveEnemy
}
[SerializeField] private int numberEnemiesToCreate = 1000;
[SerializeField] private Transform pfEnemy;
[SerializeField] private MethodType method = MethodType.NormalMoveEnemy;
[SerializeField] private float methodTime;
private readonly List<Enemy> m_enemies = new List<Enemy>();
private void Start()
{
for (int i = 0; i < numberEnemiesToCreate; i++)
{
Transform enemy = Instantiate(pfEnemy,
new Vector3(Random.Range(-8f, 8f), Random.Range(-8f, 8f)),
Quaternion.identity);
m_enemies.Add(new Enemy { transform = enemy, moveY = Random.Range(1f, 2f) });
}
}
private async void Update()
{
float startTime = Time.realtimeSinceStartup;
switch (method)
{
case MethodType.NormalMoveEnemy:
MoveEnemy();
break;
case MethodType.TaskMoveEnemy:
Task<Task[]> moveEnemyTasks = MoveEnemyTask();
await Task.WhenAll(moveEnemyTasks);
break;
default:
MoveEnemy();
break;
}
methodTime = (Time.realtimeSinceStartup - startTime) * 1000f;
Debug.Log($"{methodTime} ms");
}
private void MoveEnemy()
{
foreach (Enemy enemy in m_enemies)
{
enemy.transform.position += new Vector3(0, enemy.moveY * Time.deltaTime);
if (enemy.transform.position.y > 5f)
enemy.moveY = -math.abs(enemy.moveY);
if (enemy.transform.position.y < -5f)
enemy.moveY = +math.abs(enemy.moveY);
Performance.PerformanceIntensiveMethod(1000);
}
}
private async Task<Task[]> MoveEnemyTask()
{
Task[] tasks = new Task[m_enemies.Count];
for (int i = 0; i < m_enemies.Count; i++)
{
Enemy enemy = m_enemies[i];
enemy.transform.position += new Vector3(0, enemy.moveY * Time.deltaTime);
if (enemy.transform.position.y > 5f)
enemy.moveY = -math.abs(enemy.moveY);
if (enemy.transform.position.y < -5f)
enemy.moveY = +math.abs(enemy.moveY);
tasks[i] = Performance.PerformanceIntensiveTask(1000);
}
await Task.WhenAll(tasks);
return tasks;
}
Hiển thị và di chuyển một nghìn kẻ thù với phương pháp này mất khoảng 150ms với tốc độ khung hình khoảng 7 FPS.
Hiển thị và di chuyển hàng nghìn kẻ thù trong một nhiệm vụ mất khoảng 50ms với tốc độ khung hình khoảng 30 FPS.
useTasks
?Các tác vụ cực kỳ thành thạo và giảm bớt căng thẳng về hiệu suất trên hệ thống của bạn. Bạn thậm chí có thể sử dụng chúng trong nhiều chủ đề bằng cách sử dụng Thư viện song song tác vụ (TPL).
Tuy nhiên, có một số hạn chế khi sử dụng chúng trong Unity. Hạn chế lớn khi sử dụng Task
trong Unity là tất cả chúng đều chạy trên Main
luồng. Có, chúng tôi có thể làm cho chúng chạy trên các luồng khác, nhưng Unity đã thực hiện quản lý luồng và bộ nhớ của riêng mình, và bạn có thể tạo lỗi bằng cách tạo nhiều luồng hơn Lõi CPU, điều này gây ra sự cạnh tranh về tài nguyên.
Các tác vụ cũng có thể khó thực hiện chính xác và gỡ lỗi. Khi viết mã ban đầu, tôi kết thúc với tất cả các nhiệm vụ đang chạy, nhưng không có kẻ thù nào di chuyển trên màn hình. Kết quả là tôi cần trả lại cái Task[]
mà tôi đã tạo trong Task
.
Tác vụ tạo ra nhiều rác ảnh hưởng đến hiệu suất. Chúng cũng không hiển thị trong hồ sơ, vì vậy nếu bạn có một cái đang ảnh hưởng đến hiệu suất, thật khó để theo dõi. Ngoài ra, tôi nhận thấy rằng đôi khi các tác vụ và chức năng cập nhật của tôi tiếp tục chạy từ các cảnh khác.
Theo Unity , “Một quy trình đăng ký là một hàm có thể tạm dừng việc thực thi của nó (lợi nhuận) cho đến khi kết thúc YieldInstruction đã cho .”
Điều này có nghĩa là chúng ta có thể chạy mã và đợi một tác vụ hoàn thành trước khi tiếp tục. Điều này giống như một phương pháp không đồng bộ. Nó sử dụng kiểu trả về IEnumerator
và chúng tôi yield return
thay vì await
.
Unity có một số loại hướng dẫn lợi nhuận khác nhau mà chúng tôi có thể sử dụng, chẳng hạn như WaitForSeconds
, hoặc .WaitForEndOfFrameWaitUntilWaitWhile
Để bắt đầu coroutines, chúng ta cần một MonoBehaviour
và sử dụng MonoBehaviour.StartCoroutine
.
Để dừng một quy trình đăng ký trước khi hoàn tất, chúng tôi sử dụng MonoBehaviour.StopCoroutine
. Khi dừng coroutines, hãy đảm bảo rằng bạn sử dụng cùng một phương pháp như bạn đã sử dụng để bắt đầu nó.
Các trường hợp sử dụng phổ biến cho coroutines trong Unity là đợi tải nội dung và tạo bộ định thời gian hồi chiêu.
using System.Collections;
using UnityEngine;
using UnityEngine.SceneManagement;
public class SceneLoader : MonoBehaviour
{
public Coroutine loadSceneCoroutine;
public void Update()
{
if (Input.GetKeyDown(KeyCode.Space) && loadSceneCoroutine == null)
{
loadSceneCoroutine = StartCoroutine(LoadSceneAsync());
}
if (Input.GetKeyDown(KeyCode.Escape) && loadSceneCoroutine != null)
{
StopCoroutine(loadSceneCoroutine);
loadSceneCoroutine = null;
}
}
private IEnumerator LoadSceneAsync()
{
AsyncOperation asyncLoad = SceneManager.LoadSceneAsync("Scene2");
yield return new WaitWhile(() => !asyncLoad.isDone);
}
}
Hãy xem việc sử dụng một quy trình điều tra tác động như thế nào đến hiệu suất của dự án của chúng tôi. Tôi sẽ chỉ làm điều này với phương pháp chuyên sâu về hiệu suất.
Tôi đã thêm vào Coroutine
enum MethodType
và các biến để theo dõi trạng thái của nó:
public class PerformanceTaskCoroutineJob : MonoBehaviour
{
private enum MethodType
{
Normal,
Task,
Coroutine
}
...
private Coroutine m_performanceCoroutine;
Tôi đã tạo quy trình đăng quang. Điều này tương tự với tác vụ và phương thức chuyên sâu về hiệu suất mà chúng tôi đã tạo trước đó với mã được bổ sung để cập nhật thời gian của phương thức:
private IEnumerator PerformanceCoroutine(int timesToRepeat, float startTime)
{
for (int count = 0; count < numberGameObjectsToImitate; count++)
{
// Represents a Performance Intensive Method like some pathfinding or really complex calculation.
float value = 0f;
for (int i = 0; i < timesToRepeat; i++)
{
value = math.exp10(math.sqrt(value));
}
}
methodTime = (Time.realtimeSinceStartup - startTime) * 1000f;
Debug.Log($"{methodTime} ms");
m_performanceCoroutine = null;
yield return null;
}
Trong Update
phương thức này, tôi đã thêm dấu kiểm cho quy trình đăng quang. Tôi cũng đã sửa đổi thời gian phương thức, cập nhật mã và thêm mã để dừng chương trình đăng quang nếu nó đang chạy và chúng tôi đã thay đổi loại phương thức:
private async void Update()
{
float startTime = Time.realtimeSinceStartup;
switch (method)
{
...
case MethodType.Coroutine:
m_performanceCoroutine ??= StartCoroutine(PerformanceCoroutine(5000, startTime));
break;
default:
Performance.PerformanceIntensiveMethod(50000);
break;
}
if (method != MethodType.Coroutine)
{
methodTime = (Time.realtimeSinceStartup - startTime) * 1000f;
Debug.Log($"{methodTime} ms");
}
if (method != MethodType.Coroutine || m_performanceCoroutine == null) return;
StopCoroutine(m_performanceCoroutine);
m_performanceCoroutine = null;
}
Quá trình đăng quang chuyên sâu mất khoảng 6 mili giây để hoàn thành khi trò chơi chạy ở khoảng 90 FPS.
Hệ thống công việc C # là việc Unity thực hiện các tác vụ dễ viết, không tạo ra rác mà các tác vụ thực hiện và sử dụng các chuỗi công nhân mà Unity đã tạo. Điều này khắc phục tất cả các nhược điểm của nhiệm vụ.
Unity so sánh các công việc như các chủ đề, nhưng họ nói rằng một công việc thực hiện một nhiệm vụ cụ thể. Các công việc cũng có thể phụ thuộc vào các công việc khác để hoàn thành trước khi chạy; điều này khắc phục sự cố với nhiệm vụ mà tôi đã không di chuyển đúng cách của tôi Units
vì nó phụ thuộc vào một nhiệm vụ khác để hoàn thành trước.
Các phụ thuộc công việc sẽ được Unity tự động giải quyết cho chúng tôi. Hệ thống công việc cũng có một hệ thống an toàn được tích hợp chủ yếu để bảo vệ khỏi các điều kiện của cuộc đua . Một lưu ý với các công việc là chúng chỉ có thể chứa các biến thành viên là kiểu blittable hoặc kiểu NativeContainer ; đây là một nhược điểm của hệ thống an toàn.
Để sử dụng hệ thống công việc, bạn tạo công việc, lên lịch công việc, đợi công việc hoàn thành, sau đó sử dụng dữ liệu trả về của công việc. Hệ thống công việc là cần thiết để sử dụng Ngăn xếp Công nghệ Định hướng Dữ liệu (DOTS) của Unity.
Để biết thêm chi tiết về hệ thống công việc, hãy xem tài liệu Unity .
Để tạo một công việc, bạn tạo một công stuct
việc triển khai một trong các IJob
giao diện ( IJob , IJobFor , IJobParallelFor , Unity.Engine.Jobs.IJobParallelForTransform ). IJob
là một công việc cơ bản. IJobFor
và IJobForParallel
được sử dụng để thực hiện cùng một thao tác trên mỗi phần tử của vùng chứa gốc hoặc cho một số lần lặp. Sự khác biệt giữa chúng là IJobFor chạy trên một luồng duy nhất, nơi IJobForParallel
sẽ được chia thành nhiều luồng.
Tôi sẽ sử dụng IJob
để tạo ra một công việc hoạt động chuyên sâu IJobFor
và IJobForParallel
để tạo ra một công việc có thể di chuyển nhiều kẻ thù xung quanh; điều này chỉ để chúng ta có thể thấy các tác động khác nhau đến hiệu suất. Các công việc này sẽ giống với các tác vụ và phương pháp mà chúng tôi đã tạo trước đó:
public struct PerformanceIntensiveJob : IJob { }
public struct MoveEnemyJob: IJobFor { }
public struct MoveEnemyParallelJob : IJobParallelFor { }
Thêm các biến thành viên. Trong trường hợp của tôi, tôi IJob
không cần bất kỳ. Và cần một IJobFor
phao IJobParallelFor
cho thời gian đồng bằng hiện tại vì các công việc không có khái niệm về khung; họ hoạt động bên ngoài Unity's MonoBehaviour
. Họ cũng cần một mảng float3
cho vị trí và một mảng cho tốc độ di chuyển trên trục y:
public struct MoveEnemyJob : IJobFor
{
public NativeArray<float3> positions;
public NativeArray<float> moveYs;
public float deltaTime;
}
public struct MoveEnemyParallelJob : IJobParallelFor
{
public NativeArray<float3> positions;
public NativeArray<float> moveYs;
public float deltaTime;
}
Bước cuối cùng là thực hiện Execute
phương thức được yêu cầu. Cả IJobFor
hai IJobForParallel
đều yêu cầu một int
chỉ mục của lần lặp hiện tại mà công việc đang thực thi.
Sự khác biệt là thay vì truy cập kẻ thù transform
và di chuyển, chúng tôi sử dụng mảng có trong công việc:
public struct PerformanceIntensiveJob : IJob
{
#region Implementation of IJob
/// <inheritdoc />
public void Execute()
{
// Represents a Performance Intensive Method like some pathfinding or really complex calculation.
float value = 0f;
for (int i = 0; i < 50000; i++)
{
value = math.exp10(math.sqrt(value));
}
}
#endregion
}
// MoveEnemyJob and MoveEnemyParallelJob have the same exact Execute Method.
/// <inheritdoc />
public void Execute(int index)
{
positions[index] += new float3(0, moveYs[index] * deltaTime, 0);
if (positions[index].y > 5f)
moveYs[index] = -math.abs(moveYs[index]);
if (positions[index].y < -5f)
moveYs[index] = +math.abs(moveYs[index]);
// Represents a Performance Intensive Method like some pathfinding or really complex calculation.
float value = 0f;
for (int i = 0; i < 1000; i++)
{
value = math.exp10(math.sqrt(value));
}
} private JobHandle PerformanceIntensiveMethodJob()
{
PerformanceIntensiveJob job = new PerformanceIntensiveJob();
return job.Schedule();
}
Đầu tiên, chúng ta cần cài đặt công việc và điền dữ liệu công việc:
NativeArray<float> result = new NativeArray<float>(1, Allocator.TempJob);
MyJob jobData = new MyJob();
jobData.myFloat = result;
Sau đó, chúng tôi lên lịch công việc với JobHandle jobHandle = jobData.Schedule();
. Phương Schedule
thức trả về một JobHandle
có thể được sử dụng sau này.
Chúng ta không thể sắp xếp một công việc từ bên trong một công việc. Tuy nhiên, chúng tôi có thể tạo công việc mới và điền dữ liệu của họ từ bên trong công việc. Một khi công việc đã được lên lịch, nó không thể bị gián đoạn.
Công việc đòi hỏi hiệu suất cao
Tôi đã tạo ra một phương pháp tạo một công việc mới và lên lịch cho nó. Nó trả về công việc xử lý mà tôi có thể sử dụng trong update
phương thức của mình:
public class PerformanceTaskCoroutineJob : MonoBehaviour
{
....
private JobHandle PerformanceIntensiveMethodJob()
{
PerformanceIntensiveJob job = new PerformanceIntensiveJob();
return job.Schedule();
}
}
Tôi đã thêm công việc vào enum của mình. Sau đó, trong Update
phương thức, tôi thêm phần case
vào switch
phần. Tôi đã tạo một mảng JobHandles
. Sau đó, tôi lặp qua tất cả các đối tượng trò chơi được mô phỏng, thêm một công việc đã lên lịch cho từng đối tượng vào mảng:
public class PerformanceTaskCoroutineJob : MonoBehaviour
{
private enum MethodType
{
Normal,
Task,
Coroutine,
Job
}
...
private async void Update()
{
float startTime = Time.realtimeSinceStartup;
switch (method)
{
...
case MethodType.Job:
NativeArray<JobHandle> jobHandles =
new NativeArray<JobHandle>(numberGameObjectsToImitate, Allocator.Temp);
for (int i = 0; i < numberGameObjectsToImitate; i++)
jobHandles[i] = PerformanceIntensiveMethodJob();
break;
default:
Performance.PerformanceIntensiveMethod(50000);
break;
}
...
}
}
Và MoveEnemy
_MoveEnemyParallelJob
Tiếp theo, tôi đã thêm các công việc vào enum của mình. Sau đó, trong Update
phương thức, tôi gọi một MoveEnemyJob
phương thức mới, vượt qua thời gian delta. Thông thường, bạn sẽ sử JobFor
dụng JobParallelFor
:
public class PerformanceTaskJob : MonoBehaviour
{
private enum MethodType
{
NormalMoveEnemy,
TaskMoveEnemy,
MoveEnemyJob,
MoveEnemyParallelJob
}
...
private async void Update()
{
float startTime = Time.realtimeSinceStartup;
switch (method)
{
...
case MethodType.MoveEnemyJob:
case MethodType.MoveEnemyParallelJob:
MoveEnemyJob(Time.deltaTime);
break;
default:
MoveEnemy();
break;
}
...
}
...
Điều đầu tiên tôi làm là thiết lập một mảng cho các vị trí và một mảng cho vị trí moveY
mà tôi sẽ chuyển cho các công việc. Sau đó, tôi điền vào các mảng này với dữ liệu từ kẻ thù. Tiếp theo, tôi tạo công việc và thiết lập dữ liệu của công việc tùy thuộc vào công việc mà tôi muốn sử dụng. Sau đó, tôi lên lịch công việc tùy thuộc vào công việc mà tôi muốn sử dụng và loại lập lịch mà tôi muốn thực hiện:
private void MoveEnemyJob(float deltaTime)
{
NativeArray<float3> positions = new NativeArray<float3>(m_enemies.Count, Allocator.TempJob);
NativeArray<float> moveYs = new NativeArray<float>(m_enemies.Count, Allocator.TempJob);
for (int i = 0; i < m_enemies.Count; i++)
{
positions[i] = m_enemies[i].transform.position;
moveYs[i] = m_enemies[i].moveY;
}
// Use one or the other
if (method == MethodType.MoveEnemyJob)
{
MoveEnemyJob job = new MoveEnemyJob
{
deltaTime = deltaTime,
positions = positions,
moveYs = moveYs
};
// typically we would use one of these methods
switch (moveEnemyJobType)
{
case MoveEnemyJobType.ImmediateMainThread:
// Schedule job to run immediately on main thread.
// typically would not use.
job.Run(m_enemies.Count);
break;
case MoveEnemyJobType.ScheduleSingleWorkerThread:
case MoveEnemyJobType.ScheduleParallelWorkerThreads:
{
// Schedule job to run at a later point on a single worker thread.
// First parameter is how many for-each iterations to perform.
// The second parameter is a JobHandle to use for this job's dependencies.
// Dependencies are used to ensure that a job executes on worker threads after the dependency has completed execution.
// In this case we don't need our job to depend on anything so we can use a default one.
JobHandle scheduleJobDependency = new JobHandle();
JobHandle scheduleJobHandle = job.Schedule(m_enemies.Count, scheduleJobDependency);
switch (moveEnemyJobType)
{
case MoveEnemyJobType.ScheduleSingleWorkerThread:
break;
case MoveEnemyJobType.ScheduleParallelWorkerThreads:
{
// Schedule job to run on parallel worker threads.
// First parameter is how many for-each iterations to perform.
// The second parameter is the batch size,
// essentially the no-overhead inner-loop that just invokes Execute(i) in a loop.
// When there is a lot of work in each iteration then a value of 1 can be sensible.
// When there is very little work values of 32 or 64 can make sense.
// The third parameter is a JobHandle to use for this job's dependencies.
// Dependencies are used to ensure that a job executes on worker threads after the dependency has completed execution.
JobHandle scheduleParallelJobHandle =
job.ScheduleParallel(m_enemies.Count, m_enemies.Count / 10, scheduleJobHandle);
break;
}
}
break;
}
}
}
else if (method == MethodType.MoveEnemyParallelJob)
{
MoveEnemyParallelJob job = new MoveEnemyParallelJob
{
deltaTime = deltaTime,
positions = positions,
moveYs = moveYs
};
// Schedule a parallel-for job. First parameter is how many for-each iterations to perform.
// The second parameter is the batch size,
// essentially the no-overhead inner-loop that just invokes Execute(i) in a loop.
// When there is a lot of work in each iteration then a value of 1 can be sensible.
// When there is very little work values of 32 or 64 can make sense.
JobHandle jobHandle = job.Schedule(m_enemies.Count, m_enemies.Count / 10);
}
}
Chúng ta phải đợi cho công việc được hoàn thành. Chúng ta có thể lấy trạng thái từ trạng thái JobHandle
mà chúng ta đã sử dụng khi lên lịch để hoàn thành công việc. Thao tác này sẽ đợi công việc hoàn tất trước khi tiếp tục thực hiện:> handle.Complete();
hoặc JobHandle.CompleteAll(jobHandles)
. Sau khi công việc hoàn tất, công việc NativeContainer
mà chúng tôi đã sử dụng để thiết lập công việc sẽ có tất cả dữ liệu mà chúng tôi cần sử dụng. Khi chúng tôi lấy dữ liệu từ chúng, chúng tôi phải xử lý chúng đúng cách.
Công việc đòi hỏi hiệu suất cao
Điều này khá đơn giản vì tôi không đọc hoặc ghi bất kỳ dữ liệu nào cho công việc. Tôi đợi cho tất cả các công việc đã được lên lịch hoàn thành sau đó xử lý Native
mảng:
public class PerformanceTaskCoroutineJob : MonoBehaviour
{
...
private async void Update()
{
float startTime = Time.realtimeSinceStartup;
switch (method)
{
...
case MethodType.Job:
....
JobHandle.CompleteAll(jobHandles);
jobHandles.Dispose();
break;
default:
Performance.PerformanceIntensiveMethod(50000);
break;
}
...
}
}
Công việc chuyên sâu mất khoảng 6ms để hoàn thành với trò chơi chạy ở khoảng 90 FPS.
Công MoveEnemy
việc
Tôi thêm các séc hoàn chỉnh thích hợp:
private void MoveEnemyJob(float deltaTime)
{
....
if (method == MethodType.MoveEnemyJob)
{
....
switch (moveEnemyJobType)
{
case MoveEnemyJobType.ScheduleSingleWorkerThread:
case MoveEnemyJobType.ScheduleParallelWorkerThreads:
{
....
// typically one or the other
switch (moveEnemyJobType)
{
case MoveEnemyJobType.ScheduleSingleWorkerThread:
scheduleJobHandle.Complete();
break;
case MoveEnemyJobType.ScheduleParallelWorkerThreads:
{
scheduleParallelJobHandle.Complete();
break;
}
}
break;
}
}
}
else if (method == MethodType.MoveEnemyParallelJob)
{
....
jobHandle.Complete();
}
}
Sau khi kiểm tra loại phương pháp, tôi lặp qua tất cả kẻ thù, thiết lập transform
vị trí của chúng và moveY
đến dữ liệu đã được thiết lập trong công việc. Tiếp theo, tôi xử lý đúng cách các mảng gốc:
private void MoveEnemyJob(float deltaTime)
{
....
if (method == MethodType.MoveEnemyJob)
{
....
}
else if (method == MethodType.MoveEnemyParallelJob)
{
....
}
for (int i = 0; i < m_enemies.Count; i++)
{
m_enemies[i].transform.position = positions[i];
m_enemies[i].moveY = moveYs[i];
}
// Native arrays must be disposed manually.
positions.Dispose();
moveYs.Dispose();
}
Việc hiển thị và di chuyển hàng nghìn kẻ thù với công việc mất khoảng 160ms với tốc độ khung hình khoảng 7 FPS mà không tăng hiệu suất.
Hiển thị và di chuyển hàng nghìn kẻ thù song song với công việc mất khoảng 30ms với tốc độ khung hình khoảng 30 FPS.
Trình biên dịch liên tục là trình biên dịch chuyển từ mã bytecode sang mã gốc. Việc sử dụng tính năng này với Hệ thống công việc C # sẽ cải thiện chất lượng của mã được tạo, giúp bạn tăng hiệu suất đáng kể cũng như giảm mức tiêu thụ pin trên thiết bị di động.
Để sử dụng điều này, bạn chỉ cần nói với Unity rằng bạn muốn sử dụng trình biên dịch liên tục trong công việc với [BurstCompile]
thuộc tính:
using Unity.Burst;
using Unity.Jobs;
using Unity.Mathematics;
[BurstCompile]
public struct PerformanceIntensiveJob : IJob
{
...
}
using Unity.Burst;
using Unity.Collections;
using Unity.Jobs;
using Unity.Mathematics;
[BurstCompile]
public struct MoveEnemyJob : IJobFor
{
...
}
[BurstCompile]
public struct MoveEnemyParallelJob : IJobParallelFor
{
...
}
Sau đó, trong Unity, chọn Jobs > Burst> Enable Completion
Burst là Just-In-Time (JIT) khi ở trong Trình chỉnh sửa, có nghĩa là điều này có thể ngừng hoạt động khi ở Chế độ phát. Khi bạn xây dựng dự án của mình, nó là Ahead-Of-Time (AOT), có nghĩa là điều này cần được kích hoạt trước khi xây dựng dự án của bạn. Bạn có thể làm như vậy bằng cách chỉnh sửa phần Cài đặt Burst AOT trong Cửa sổ Cài đặt Dự án .
Để biết thêm chi tiết về trình biên dịch cụm, hãy xem tài liệu Unity .
Một công việc chuyên sâu với liên tục mất khoảng 3ms để hoàn thành với trò chơi chạy ở khoảng 150 FPS.
Hiển thị và di chuyển hàng nghìn kẻ thù, công việc với liên tục mất khoảng 30ms với tốc độ khung hình khoảng 30 FPS.
Hiển thị và di chuyển hàng nghìn kẻ thù, công việc song song với liên tục mất khoảng 6ms với tốc độ khung hình khoảng 80 đến 90 FPS.
Chúng tôi có thể sử dụng Task
để tăng hiệu suất của các ứng dụng Unity của mình, nhưng có một số hạn chế khi sử dụng chúng. Tốt hơn là sử dụng những thứ được đóng gói trong Unity tùy thuộc vào những gì chúng ta muốn làm. Sử dụng coroutines nếu chúng ta muốn đợi một thứ gì đó kết thúc tải một cách không đồng bộ; chúng tôi có thể bắt đầu quy trình đăng ký và không dừng quá trình chạy chương trình của chúng tôi.
Chúng ta có thể sử dụng Hệ thống công việc C # với trình biên dịch liên tục để đạt được hiệu suất lớn trong khi không phải lo lắng về tất cả nội dung quản lý luồng khi thực hiện các tác vụ đòi hỏi nhiều quy trình. Sử dụng các hệ thống có sẵn, chúng tôi chắc chắn rằng nó được thực hiện một cách an toàn và không gây ra bất kỳ lỗi hoặc lỗi không mong muốn nào.
Các công việc đã chạy tốt hơn một chút so với các công việc không sử dụng trình biên dịch liên tục, nhưng đó là do chi phí bổ sung đằng sau hậu trường ít hơn để thiết lập mọi thứ một cách an toàn cho chúng tôi. Khi sử dụng trình biên dịch bùng nổ, các công việc của chúng tôi đã thực hiện các nhiệm vụ của chúng tôi. Khi bạn cần tất cả hiệu suất bổ sung mà bạn có thể nhận được, hãy sử dụng Hệ thống công việc C # với liên tục.
Các tệp dự án cho việc này có thể được tìm thấy trên GitHub của tôi .
1656373200
Desempenho é tudo quando você está tentando publicar na web, dispositivos móveis, consoles e até mesmo alguns dos PCs de baixo custo. Um jogo ou aplicativo rodando a menos de 30 FPS pode causar frustração para os usuários. Vamos dar uma olhada em algumas das coisas que podemos usar para aumentar o desempenho reduzindo a carga na CPU.
Neste post, abordaremos o que são async
, await
, e Task
em C# e como usá-los no Unity para obter desempenho em seu projeto. Em seguida, vamos dar uma olhada em alguns dos pacotes embutidos do Unity: corrotinas, o C# Job System e o compilador de intermitência. Veremos o que são, como usá-los e como eles aumentam o desempenho em seu projeto.
Para iniciar este projeto, usarei o Unity 2021.3.4f1. Não testei este código em nenhuma outra versão do Unity; todos os conceitos aqui devem funcionar em qualquer versão do Unity após o Unity 2019.3. Seus resultados de desempenho podem diferir se você usar uma versão mais antiga, pois o Unity fez algumas melhorias significativas com o modelo de programação async/await em 2021. Leia mais sobre isso no blog do Unity Unity and .NET, o que vem a seguir , em particular a seção intitulada “Modernizing the Tempo de execução da unidade.”
Criei um novo projeto 2D (URP) Core, mas você pode usá-lo em qualquer tipo de projeto que desejar.
Eu tenho um sprite que peguei do Space Shooter (Redux, além de fontes e sons) de Kenney Vleugels .
Eu criei um prefab inimigo que contém um Sprite Render e um Enemy Component. O Componente Inimigo é um MonoBehaviour
que tem a Transform
e a float
para acompanhar a posição e a velocidade para se mover no eixo y:
using UnityEngine;
public class Enemy
{
public Transform transform;
public float moveY;
}
async
, await
e Task
estão em C#async
?Em C#, os métodos podem ter uma palavra- async
chave na frente deles, o que significa que os métodos são métodos assíncronos. Esta é apenas uma maneira de dizer ao compilador que queremos poder executar o código e permitir que o chamador desse método continue a execução enquanto aguarda a conclusão desse método.
Um exemplo disso seria cozinhar uma refeição. Você começará a cozinhar a carne e, enquanto a carne estiver cozinhando e você estiver esperando que ela termine, você começará a fazer os lados. Enquanto a comida está cozinhando, você deve começar a colocar a mesa. Um exemplo disso no código seria static async Task<Steak> MakeSteak(int number)
.
O Unity também tem todos os tipos de métodos embutidos que você pode chamar de forma assíncrona; consulte os documentos do Unity para obter uma lista de métodos. Com a maneira como o Unity lida com o gerenciamento de memória, ele usa coroutines , AsyncOperation
, ou o C# Job System .
await
e como você usa?Em C#, você pode aguardar a conclusão de uma operação assíncrona usando a palavra- await
chave. Isso é usado dentro de qualquer método que tenha a async
palavra-chave para aguardar a continuação de uma operação:
Public async void Update()
{
// do stuff
await // some asynchronous method or task to finish
// do more stuff or do stuff with the data returned from the asynchronous task.
}
Consulte os documentos da Microsoft para saber mais sobre await
.
Task
e como você o usa?A Task
é um método assíncrono que executa uma única operação e não retorna um valor. Para um Task
que retorna um valor, usaríamos Task<TResult>
.
Para usar uma tarefa, nós a criamos como criar qualquer novo objeto em C#: Task t1 = new Task(void Action)
. Em seguida, iniciamos a tarefa t1.wait
. Por fim, esperamos que a tarefa seja concluída com t1.wait
.
Há várias maneiras de criar, iniciar e executar tarefas. Task t2 = Task.Run(void Action)
irá criar e iniciar uma tarefa. await Task.Run(void Action)
irá criar, iniciar e aguardar a conclusão da tarefa. Podemos usar a maneira alternativa mais comum com Task t3 = Task.Factory.Start(void Action)
.
Existem várias maneiras pelas quais podemos esperar que a tarefa seja concluída. int index = Task.WaitAny(Task[])
aguardará a conclusão de qualquer tarefa e nos fornecerá o índice da tarefa concluída na matriz. await Task.WaitAll(Task[])
aguardará a conclusão de todas as tarefas.
Para obter mais informações sobre tarefas, consulte os Documentos da Microsoft .
task
private void Start()
{
Task t1 = new Task(() => Thread.Sleep(1000));
Task t2 = Task.Run(() => Thread.Sleep(2000000));
Task t3 = Task.Factory.StartNew(() => Thread.Sleep(1000));
t1.Start();
Task[] tasks = { t1, t2, t3 };
int index = Task.WaitAny(tasks);
Debug.Log($"Task {tasks[index].Id} at index {index} completed.");
Task t4 = new Task(() => Thread.Sleep(100));
Task t5 = Task.Run(() => Thread.Sleep(200));
Task t6 = Task.Factory.StartNew(() => Thread.Sleep(300));
t4.Start();
Task.WaitAll(t4, t5, t6);
Debug.Log($"All Task Completed!");
Debug.Log($"Task When any t1={t1.IsCompleted} t2={t2.IsCompleted} t3={t3.IsCompleted}");
Debug.Log($"All Task Completed! t4={t4.IsCompleted} t5={t5.IsCompleted} t6={t6.IsCompleted}");
}
public async void Update()
{
float startTime = Time.realtimeSinceStartup;
Debug.Log($"Update Started: {startTime}");
Task t1 = new Task(() => Thread.Sleep(10000));
Task t2 = Task.Run(() => Thread.Sleep(20000));
Task t3 = Task.Factory.StartNew(() => Thread.Sleep(30000));
await Task.WhenAll(t1, t2, t3);
Debug.Log($"Update Finished: {(Time.realtimeSinceStartup - startTime) * 1000f} ms");
}
Agora vamos comparar o desempenho de uma tarefa versus o desempenho de um método.
Vou precisar de uma classe estática que possa usar em todas as minhas verificações de desempenho. Ele terá um método e uma tarefa que simula uma operação de alto desempenho. Tanto o método quanto a tarefa executam a mesma operação exata:
using System.Threading.Tasks;
using Unity.Mathematics;
public static class Performance
{
public static void PerformanceIntensiveMethod(int timesToRepeat)
{
// Represents a Performance Intensive Method like some pathfinding or really complex calculation.
float value = 0f;
for (int i = 0; i < timesToRepeat; i++)
{
value = math.exp10(math.sqrt(value));
}
}
public static Task PerformanceIntensiveTask(int timesToRepeat)
{
return Task.Run(() =>
{
// Represents a Performance Intensive Method like some pathfinding or really complex calculation.
float value = 0f;
for (int i = 0; i < timesToRepeat; i++)
{
value = math.exp10(math.sqrt(value));
}
});
}
}
Agora eu preciso de um MonoBehaviour
que eu possa usar para testar o impacto do desempenho na tarefa e no método. Só para que eu possa ver um melhor impacto no desempenho, vou fingir que quero rodar isso em dez objetos de jogo diferentes. Também acompanharei a quantidade de tempo que o Update
método leva para ser executado.
Em Update
, recebo a hora de início. Se estou testando o método, percorro todos os objetos de jogo simulados e chamo o método de alto desempenho. Se estou testando a tarefa, crio um novo Task
loop de matriz por meio de todos os objetos de jogo simulados e adiciono a tarefa de alto desempenho à matriz de tarefas. Eu, então, await
para que todas as tarefas sejam concluídas. Fora da verificação do tipo de método, atualizo o tempo do método, convertendo-o para ms
. Eu também registro.
public class PerformanceTaskCoroutineJob : MonoBehaviour
{
private enum MethodType
{
Normal,
Task
}
[SerializeField] private int numberGameObjectsToImitate
= 10;
[SerializeField] private MethodType method = MethodType.Normal;
[SerializeField] private float methodTime;
private async void Update()
{
float startTime = Time.realtimeSinceStartup;
switch (method)
{
case MethodType.Normal:
for (int i = 0; i < numberGameObjectsToImitate
; i++)
Performance.PerformanceIntensiveMethod(50000);
break;
case MethodType.Task:
Task[] tasks = new Task[numberGameObjectsToImitate
];
for (int i = 0; i < numberGameObjectsToImitate
; i++)
tasks[i] = Performance.PerformanceIntensiveTask(5000);
await Task.WhenAll(tasks);
break;
}
methodTime = (Time.realtimeSinceStartup - startTime) * 1000f;
Debug.Log($"{methodTime} ms");
}
}
O método intensivo leva cerca de 65ms para ser concluído com o jogo rodando a cerca de 12 FPS.
A tarefa intensiva leva cerca de 4ms para ser concluída com o jogo rodando a cerca de 200 FPS.
Vamos tentar isso com mil inimigos:
using System.Collections.Generic;
using System.Threading.Tasks;
using Unity.Collections;
using Unity.Jobs;
using Unity.Mathematics;
using UnityEngine;
using Random = UnityEngine.Random;
public class PerformanceTaskJob : MonoBehaviour
{
private enum MethodType
{
NormalMoveEnemy,
TaskMoveEnemy
}
[SerializeField] private int numberEnemiesToCreate = 1000;
[SerializeField] private Transform pfEnemy;
[SerializeField] private MethodType method = MethodType.NormalMoveEnemy;
[SerializeField] private float methodTime;
private readonly List<Enemy> m_enemies = new List<Enemy>();
private void Start()
{
for (int i = 0; i < numberEnemiesToCreate; i++)
{
Transform enemy = Instantiate(pfEnemy,
new Vector3(Random.Range(-8f, 8f), Random.Range(-8f, 8f)),
Quaternion.identity);
m_enemies.Add(new Enemy { transform = enemy, moveY = Random.Range(1f, 2f) });
}
}
private async void Update()
{
float startTime = Time.realtimeSinceStartup;
switch (method)
{
case MethodType.NormalMoveEnemy:
MoveEnemy();
break;
case MethodType.TaskMoveEnemy:
Task<Task[]> moveEnemyTasks = MoveEnemyTask();
await Task.WhenAll(moveEnemyTasks);
break;
default:
MoveEnemy();
break;
}
methodTime = (Time.realtimeSinceStartup - startTime) * 1000f;
Debug.Log($"{methodTime} ms");
}
private void MoveEnemy()
{
foreach (Enemy enemy in m_enemies)
{
enemy.transform.position += new Vector3(0, enemy.moveY * Time.deltaTime);
if (enemy.transform.position.y > 5f)
enemy.moveY = -math.abs(enemy.moveY);
if (enemy.transform.position.y < -5f)
enemy.moveY = +math.abs(enemy.moveY);
Performance.PerformanceIntensiveMethod(1000);
}
}
private async Task<Task[]> MoveEnemyTask()
{
Task[] tasks = new Task[m_enemies.Count];
for (int i = 0; i < m_enemies.Count; i++)
{
Enemy enemy = m_enemies[i];
enemy.transform.position += new Vector3(0, enemy.moveY * Time.deltaTime);
if (enemy.transform.position.y > 5f)
enemy.moveY = -math.abs(enemy.moveY);
if (enemy.transform.position.y < -5f)
enemy.moveY = +math.abs(enemy.moveY);
tasks[i] = Performance.PerformanceIntensiveTask(1000);
}
await Task.WhenAll(tasks);
return tasks;
}
Exibir e mover mil inimigos com o método levou cerca de 150ms com uma taxa de quadros de cerca de 7 FPS.
Exibir e mover mil inimigos com uma tarefa levou cerca de 50ms com uma taxa de quadros de cerca de 30 FPS.
useTasks
?As tarefas são extremamente eficientes e reduzem a pressão sobre o desempenho do seu sistema. Você pode até usá-los em vários threads usando a Biblioteca Paralela de Tarefas (TPL).
No entanto, existem algumas desvantagens em usá-los no Unity. A principal desvantagem de usar Task
no Unity é que todos eles são executados no Main
encadeamento. Sim, podemos fazê-los rodar em outros threads, mas o Unity já faz seu próprio gerenciamento de thread e memória, e você pode criar erros criando mais threads do que CPU Cores, o que causa competição por recursos.
As tarefas também podem ser difíceis de executar corretamente e depurar. Ao escrever o código original, acabei com as tarefas todas em execução, mas nenhum dos inimigos se moveu na tela. Acabou sendo que precisei retornar o Task[]
que criei no Task
.
As tarefas criam muito lixo que afeta o desempenho. Eles também não aparecem no criador de perfil, portanto, se você tiver um que esteja afetando o desempenho, é difícil rastrear. Além disso, notei que às vezes minhas tarefas e funções de atualização continuam sendo executadas em outras cenas.
De acordo com Unity , “Uma corrotina é uma função que pode suspender sua execução (yield) até que o YieldInstruction termine”.
O que isso significa é que podemos executar o código e esperar que uma tarefa seja concluída antes de continuar. Isso é muito parecido com um método assíncrono. Ele usa um tipo de retorno IEnumerator
e nós yield return
em vez de await
.
O Unity tem vários tipos diferentes de instruções de rendimento que podemos usar, ou seja, WaitForSeconds
, WaitForEndOfFrame
, WaitUntil
ou WaitWhile
.
Para iniciar as corrotinas, precisamos de um MonoBehaviour
e usamos o MonoBehaviour.StartCoroutine
.
Para parar uma corrotina antes que ela seja concluída, usamos MonoBehaviour.StopCoroutine
. Ao parar as corrotinas, certifique-se de usar o mesmo método usado para iniciá-las.
Casos de uso comuns para corrotinas no Unity são aguardar o carregamento dos ativos e criar temporizadores de resfriamento.
using System.Collections;
using UnityEngine;
using UnityEngine.SceneManagement;
public class SceneLoader : MonoBehaviour
{
public Coroutine loadSceneCoroutine;
public void Update()
{
if (Input.GetKeyDown(KeyCode.Space) && loadSceneCoroutine == null)
{
loadSceneCoroutine = StartCoroutine(LoadSceneAsync());
}
if (Input.GetKeyDown(KeyCode.Escape) && loadSceneCoroutine != null)
{
StopCoroutine(loadSceneCoroutine);
loadSceneCoroutine = null;
}
}
private IEnumerator LoadSceneAsync()
{
AsyncOperation asyncLoad = SceneManager.LoadSceneAsync("Scene2");
yield return new WaitWhile(() => !asyncLoad.isDone);
}
}
Vamos ver como o uso de uma corrotina afeta o desempenho do nosso projeto. Eu só vou fazer isso com o método de desempenho intensivo.
Eu adicionei Coroutine
ao MethodType
enum e variáveis para acompanhar seu estado:
public class PerformanceTaskCoroutineJob : MonoBehaviour
{
private enum MethodType
{
Normal,
Task,
Coroutine
}
...
private Coroutine m_performanceCoroutine;
Eu criei a corrotina. Isso é semelhante à tarefa e ao método de alto desempenho que criamos anteriormente com código adicionado para atualizar o tempo do método:
private IEnumerator PerformanceCoroutine(int timesToRepeat, float startTime)
{
for (int count = 0; count < numberGameObjectsToImitate; count++)
{
// Represents a Performance Intensive Method like some pathfinding or really complex calculation.
float value = 0f;
for (int i = 0; i < timesToRepeat; i++)
{
value = math.exp10(math.sqrt(value));
}
}
methodTime = (Time.realtimeSinceStartup - startTime) * 1000f;
Debug.Log($"{methodTime} ms");
m_performanceCoroutine = null;
yield return null;
}
No Update
método, adicionei a verificação da corrotina. Também modifiquei o tempo do método, atualizei o código e adicionei código para parar a corrotina se estivesse em execução e alteramos o tipo do método:
private async void Update()
{
float startTime = Time.realtimeSinceStartup;
switch (method)
{
...
case MethodType.Coroutine:
m_performanceCoroutine ??= StartCoroutine(PerformanceCoroutine(5000, startTime));
break;
default:
Performance.PerformanceIntensiveMethod(50000);
break;
}
if (method != MethodType.Coroutine)
{
methodTime = (Time.realtimeSinceStartup - startTime) * 1000f;
Debug.Log($"{methodTime} ms");
}
if (method != MethodType.Coroutine || m_performanceCoroutine == null) return;
StopCoroutine(m_performanceCoroutine);
m_performanceCoroutine = null;
}
A corrotina intensiva leva cerca de 6ms para ser concluída com o jogo rodando a cerca de 90 FPS.
O C# Job System é a implementação do Unity de tarefas que são fáceis de escrever, não geram o lixo que as tarefas fazem e utilizam os threads de trabalho que o Unity já criou. Isso corrige todas as desvantagens das tarefas.
O Unity compara jobs como threads, mas eles dizem que um job faz uma tarefa específica. Os trabalhos também podem depender de outros trabalhos para serem concluídos antes de serem executados; isso corrige o problema com a tarefa que eu tinha que não moveu minha corretamente Units
porque dependia de outra tarefa para ser concluída primeiro.
As dependências de trabalho são automaticamente cuidadas para nós pelo Unity. O sistema de trabalho também possui um sistema de segurança integrado principalmente para proteção contra condições de corrida . Uma ressalva com os jobs é que eles só podem conter variáveis de membro que sejam tipos blittable ou tipos NativeContainer ; esta é uma desvantagem do sistema de segurança.
Para usar o sistema de trabalho, você cria o trabalho, agende o trabalho, aguarde a conclusão do trabalho e use os dados retornados pelo trabalho. O sistema de trabalho é necessário para usar o Data-Oriented Technology Stack (DOTS) da Unity.
Para obter mais detalhes sobre o sistema de tarefas, consulte a documentação do Unity .
Para criar um trabalho, você cria um stuct
que implementa uma das IJob
interfaces ( IJob , IJobFor , IJobParallelFor , Unity.Engine.Jobs.IJobParallelForTransform ). IJob
é um trabalho básico. IJobFor
e IJobForParallel
são usados para executar a mesma operação em cada elemento de um contêiner nativo ou em várias iterações. A diferença entre eles é que o IJobFor é executado em um único thread, onde IJobForParallel
será dividido entre vários threads.
Eu usarei IJob
para criar um trabalho de operação intensiva IJobFor
e IJobForParallel
para criar um trabalho que moverá vários inimigos ao redor; isso é apenas para que possamos ver os diferentes impactos no desempenho. Esses trabalhos serão idênticos às tarefas e métodos que criamos anteriormente:
public struct PerformanceIntensiveJob : IJob { }
public struct MoveEnemyJob: IJobFor { }
public struct MoveEnemyParallelJob : IJobParallelFor { }
Adicione as variáveis de membro. No meu caso, o meu IJob
não precisa de nenhum. O IJobFor
e IJobParallelFor
precisa de um float para o tempo delta atual, pois os trabalhos não têm um conceito de quadro; eles operam fora do Unity MonoBehaviour
. Eles também precisam de uma matriz de float3
para a posição e uma matriz para a velocidade de movimento no eixo y:
public struct MoveEnemyJob : IJobFor
{
public NativeArray<float3> positions;
public NativeArray<float> moveYs;
public float deltaTime;
}
public struct MoveEnemyParallelJob : IJobParallelFor
{
public NativeArray<float3> positions;
public NativeArray<float> moveYs;
public float deltaTime;
}
A última etapa é implementar o Execute
método necessário. O IJobFor
e IJobForParallel
ambos exigem um int
para o índice da iteração atual que o trabalho está executando.
A diferença é que ao invés de acessar o inimigo transform
e se mover, usamos o array que está no trabalho:
public struct PerformanceIntensiveJob : IJob
{
#region Implementation of IJob
/// <inheritdoc />
public void Execute()
{
// Represents a Performance Intensive Method like some pathfinding or really complex calculation.
float value = 0f;
for (int i = 0; i < 50000; i++)
{
value = math.exp10(math.sqrt(value));
}
}
#endregion
}
// MoveEnemyJob and MoveEnemyParallelJob have the same exact Execute Method.
/// <inheritdoc />
public void Execute(int index)
{
positions[index] += new float3(0, moveYs[index] * deltaTime, 0);
if (positions[index].y > 5f)
moveYs[index] = -math.abs(moveYs[index]);
if (positions[index].y < -5f)
moveYs[index] = +math.abs(moveYs[index]);
// Represents a Performance Intensive Method like some pathfinding or really complex calculation.
float value = 0f;
for (int i = 0; i < 1000; i++)
{
value = math.exp10(math.sqrt(value));
}
} private JobHandle PerformanceIntensiveMethodJob()
{
PerformanceIntensiveJob job = new PerformanceIntensiveJob();
return job.Schedule();
}
Primeiro, precisamos instalar o trabalho e preencher os dados dos trabalhos:
NativeArray<float> result = new NativeArray<float>(1, Allocator.TempJob);
MyJob jobData = new MyJob();
jobData.myFloat = result;
Em seguida, agendamos o trabalho com JobHandle jobHandle = jobData.Schedule();
. O Schedule
método retorna um JobHandle
que pode ser usado posteriormente.
Não podemos agendar um trabalho de dentro de um trabalho. Podemos, no entanto, criar novos trabalhos e preencher seus dados de dentro de um trabalho. Depois que um trabalho é agendado, ele não pode ser interrompido.
O trabalho de alto desempenho
Eu criei um método que cria um novo trabalho e o agenda. Ele retorna o identificador de trabalho que posso usar no meu update
método:
public class PerformanceTaskCoroutineJob : MonoBehaviour
{
....
private JobHandle PerformanceIntensiveMethodJob()
{
PerformanceIntensiveJob job = new PerformanceIntensiveJob();
return job.Schedule();
}
}
Eu adicionei o trabalho ao meu enum. Então, no Update
método, eu adiciono o case
à switch
seção. Eu criei uma matriz de JobHandles
. Em seguida, faço um loop por todos os objetos de jogo simulados, adicionando um trabalho agendado para cada um ao array:
public class PerformanceTaskCoroutineJob : MonoBehaviour
{
private enum MethodType
{
Normal,
Task,
Coroutine,
Job
}
...
private async void Update()
{
float startTime = Time.realtimeSinceStartup;
switch (method)
{
...
case MethodType.Job:
NativeArray<JobHandle> jobHandles =
new NativeArray<JobHandle>(numberGameObjectsToImitate, Allocator.Temp);
for (int i = 0; i < numberGameObjectsToImitate; i++)
jobHandles[i] = PerformanceIntensiveMethodJob();
break;
default:
Performance.PerformanceIntensiveMethod(50000);
break;
}
...
}
}
O MoveEnemy
eMoveEnemyParallelJob
Em seguida, adicionei os trabalhos ao meu enum. Então, no Update
método, chamo um novo MoveEnemyJob
método, passando o tempo delta. Normalmente você usaria o JobFor
ou o JobParallelFor
:
public class PerformanceTaskJob : MonoBehaviour
{
private enum MethodType
{
NormalMoveEnemy,
TaskMoveEnemy,
MoveEnemyJob,
MoveEnemyParallelJob
}
...
private async void Update()
{
float startTime = Time.realtimeSinceStartup;
switch (method)
{
...
case MethodType.MoveEnemyJob:
case MethodType.MoveEnemyParallelJob:
MoveEnemyJob(Time.deltaTime);
break;
default:
MoveEnemy();
break;
}
...
}
...
A primeira coisa que faço é definir um array para as posições e um array para o moveY
que vou passar para os jobs. Eu então preencho essas matrizes com os dados dos inimigos. Em seguida, crio o trabalho e defino os dados do trabalho dependendo de qual trabalho quero usar. Depois disso, agendo o trabalho dependendo do trabalho que quero usar e do tipo de agendamento que quero fazer:
private void MoveEnemyJob(float deltaTime)
{
NativeArray<float3> positions = new NativeArray<float3>(m_enemies.Count, Allocator.TempJob);
NativeArray<float> moveYs = new NativeArray<float>(m_enemies.Count, Allocator.TempJob);
for (int i = 0; i < m_enemies.Count; i++)
{
positions[i] = m_enemies[i].transform.position;
moveYs[i] = m_enemies[i].moveY;
}
// Use one or the other
if (method == MethodType.MoveEnemyJob)
{
MoveEnemyJob job = new MoveEnemyJob
{
deltaTime = deltaTime,
positions = positions,
moveYs = moveYs
};
// typically we would use one of these methods
switch (moveEnemyJobType)
{
case MoveEnemyJobType.ImmediateMainThread:
// Schedule job to run immediately on main thread.
// typically would not use.
job.Run(m_enemies.Count);
break;
case MoveEnemyJobType.ScheduleSingleWorkerThread:
case MoveEnemyJobType.ScheduleParallelWorkerThreads:
{
// Schedule job to run at a later point on a single worker thread.
// First parameter is how many for-each iterations to perform.
// The second parameter is a JobHandle to use for this job's dependencies.
// Dependencies are used to ensure that a job executes on worker threads after the dependency has completed execution.
// In this case we don't need our job to depend on anything so we can use a default one.
JobHandle scheduleJobDependency = new JobHandle();
JobHandle scheduleJobHandle = job.Schedule(m_enemies.Count, scheduleJobDependency);
switch (moveEnemyJobType)
{
case MoveEnemyJobType.ScheduleSingleWorkerThread:
break;
case MoveEnemyJobType.ScheduleParallelWorkerThreads:
{
// Schedule job to run on parallel worker threads.
// First parameter is how many for-each iterations to perform.
// The second parameter is the batch size,
// essentially the no-overhead inner-loop that just invokes Execute(i) in a loop.
// When there is a lot of work in each iteration then a value of 1 can be sensible.
// When there is very little work values of 32 or 64 can make sense.
// The third parameter is a JobHandle to use for this job's dependencies.
// Dependencies are used to ensure that a job executes on worker threads after the dependency has completed execution.
JobHandle scheduleParallelJobHandle =
job.ScheduleParallel(m_enemies.Count, m_enemies.Count / 10, scheduleJobHandle);
break;
}
}
break;
}
}
}
else if (method == MethodType.MoveEnemyParallelJob)
{
MoveEnemyParallelJob job = new MoveEnemyParallelJob
{
deltaTime = deltaTime,
positions = positions,
moveYs = moveYs
};
// Schedule a parallel-for job. First parameter is how many for-each iterations to perform.
// The second parameter is the batch size,
// essentially the no-overhead inner-loop that just invokes Execute(i) in a loop.
// When there is a lot of work in each iteration then a value of 1 can be sensible.
// When there is very little work values of 32 or 64 can make sense.
JobHandle jobHandle = job.Schedule(m_enemies.Count, m_enemies.Count / 10);
}
}
Temos que esperar que o trabalho seja concluído. Podemos obter o status do JobHandle
que usamos quando agendamos o trabalho para concluí-lo. Isso aguardará a conclusão do trabalho antes de continuar a execução: > handle.Complete();
ou JobHandle.CompleteAll(jobHandles)
. Quando o trabalho estiver concluído, o NativeContainer
que usamos para configurar o trabalho terá todos os dados que precisamos usar. Uma vez que recuperamos os dados deles, temos que descartá-los adequadamente.
O trabalho de alto desempenho
Isso é bem simples, pois não estou lendo ou gravando nenhum dado no trabalho. Aguardo todos os trabalhos que foram agendados para serem concluídos e descarto a Native
matriz:
public class PerformanceTaskCoroutineJob : MonoBehaviour
{
...
private async void Update()
{
float startTime = Time.realtimeSinceStartup;
switch (method)
{
...
case MethodType.Job:
....
JobHandle.CompleteAll(jobHandles);
jobHandles.Dispose();
break;
default:
Performance.PerformanceIntensiveMethod(50000);
break;
}
...
}
}
O trabalho intensivo leva cerca de 6ms para ser concluído com o jogo rodando a cerca de 90 FPS.
O MoveEnemy
trabalho
Eu adiciono as verificações completas apropriadas:
private void MoveEnemyJob(float deltaTime)
{
....
if (method == MethodType.MoveEnemyJob)
{
....
switch (moveEnemyJobType)
{
case MoveEnemyJobType.ScheduleSingleWorkerThread:
case MoveEnemyJobType.ScheduleParallelWorkerThreads:
{
....
// typically one or the other
switch (moveEnemyJobType)
{
case MoveEnemyJobType.ScheduleSingleWorkerThread:
scheduleJobHandle.Complete();
break;
case MoveEnemyJobType.ScheduleParallelWorkerThreads:
{
scheduleParallelJobHandle.Complete();
break;
}
}
break;
}
}
}
else if (method == MethodType.MoveEnemyParallelJob)
{
....
jobHandle.Complete();
}
}
Após a verificação do tipo de método, eu percorro todos os inimigos, definindo suas transform
posições e moveY
os dados que foram definidos no trabalho. Em seguida, descarto adequadamente os arrays nativos:
private void MoveEnemyJob(float deltaTime)
{
....
if (method == MethodType.MoveEnemyJob)
{
....
}
else if (method == MethodType.MoveEnemyParallelJob)
{
....
}
for (int i = 0; i < m_enemies.Count; i++)
{
m_enemies[i].transform.position = positions[i];
m_enemies[i].moveY = moveYs[i];
}
// Native arrays must be disposed manually.
positions.Dispose();
moveYs.Dispose();
}
Exibir e mover mil inimigos com trabalho levou cerca de 160ms com uma taxa de quadros de cerca de 7 FPS sem ganhos de desempenho.
Exibir e mover mil inimigos com trabalho paralelo levou cerca de 30ms com uma taxa de quadros de cerca de 30 FPS.
O compilador de intermitência é um compilador que traduz de bytecode para código nativo. Usar isso com o C# Job System melhora a qualidade do código gerado, proporcionando um aumento significativo no desempenho, além de reduzir o consumo da bateria em dispositivos móveis.
Para usar isso, basta informar ao Unity que deseja usar a compilação de rajada no trabalho com o [BurstCompile]
atributo:
using Unity.Burst;
using Unity.Jobs;
using Unity.Mathematics;
[BurstCompile]
public struct PerformanceIntensiveJob : IJob
{
...
}
using Unity.Burst;
using Unity.Collections;
using Unity.Jobs;
using Unity.Mathematics;
[BurstCompile]
public struct MoveEnemyJob : IJobFor
{
...
}
[BurstCompile]
public struct MoveEnemyParallelJob : IJobParallelFor
{
...
}
Em seguida, no Unity, selecione Jobs > Burst > Enable Completion
Burst é Just-In-Time (JIT) enquanto estiver no Editor, o que significa que isso pode ser desativado enquanto estiver no modo Play. Quando você compila seu projeto, ele é Ahead-Of-Time (AOT), o que significa que isso precisa ser ativado antes de compilar seu projeto. Você pode fazer isso editando a seção Burst AOT Settings na janela Project Settings .
Para obter mais detalhes sobre o compilador de intermitência, consulte a documentação do Unity .
Um trabalho intensivo com rajada leva cerca de 3ms para ser concluído com o jogo rodando a cerca de 150 FPS.
Exibindo e movendo mil inimigos, o trabalho com burst levou cerca de 30ms com uma taxa de quadros de cerca de 30 FPS.
Exibindo e movendo mil inimigos, o trabalho paralelo com burst levou cerca de 6ms com uma taxa de quadros de cerca de 80 a 90 FPS.
Podemos usar Task
para aumentar o desempenho de nossos aplicativos Unity, mas há várias desvantagens em usá-los. É melhor usar as coisas que vêm empacotadas no Unity dependendo do que queremos fazer. Use corrotinas se quisermos esperar que algo termine de carregar de forma assíncrona; podemos iniciar a corrotina e não interromper a execução do processo do nosso programa.
Podemos usar o C# Job System com o compilador de intermitência para obter um enorme ganho de desempenho sem precisar se preocupar com todas as coisas de gerenciamento de threads ao executar tarefas de processo intenso. Usando os sistemas embutidos, temos certeza de que é feito de maneira segura que não causa erros ou bugs indesejados.
As tarefas foram executadas um pouco melhor do que os trabalhos sem usar o compilador de rajadas, mas isso se deve à pequena sobrecarga extra nos bastidores para configurar tudo com segurança para nós. Ao usar o compilador de intermitência, nossos trabalhos executavam nossas tarefas. Quando você precisar de todo o desempenho extra que pode obter, use o C# Job System com burst.
Os arquivos de projeto para isso podem ser encontrados no meu GitHub .
1656372720
Performance is everything when you are trying to publish to the web, mobile, consoles, and even some of the lower-end PCs. A game or application running at less than 30 FPS can cause frustration for the users. Let’s take a look at some of the things we can use to increase the performance by reducing the load on the CPU.
In this post, we will be covering what async
, await
, and Task
in C# are and how to use them in Unity to gain performance in your project. Next, we will take a look at some of Unity’s inbuilt packages: coroutines, the C# Job System, and the burst compiler. We will look at what they are, how to use them, and how they increase the performance in your project.
To start this project off, I will be using Unity 2021.3.4f1. I have not tested this code on any other version of Unity; all concepts here should work on any Unity version after Unity 2019.3. Your performance results may differ if using an older version as Unity did make some significant improvements with the async/await programming model in 2021. Read more about it in Unity’s blog Unity and .NET, what’s next, in particular the section labeled “Modernizing the Unity runtime.”
I created a new 2D (URP) Core project, but you can use this in any type of project that you like.
I have a sprite that I got from Space Shooter (Redux, plus fonts and sounds) by Kenney Vleugels.
I created an enemy prefab that contains a Sprite Render and an Enemy Component. The Enemy Component is a MonoBehaviour
that has a Transform
and a float
to keep track of the position and the speed to move on the y axis:
using UnityEngine;
public class Enemy
{
public Transform transform;
public float moveY;
}
async
, await
, and Task
are in C#async
?In C#, methods can have an async
keyword in front of them, meaning that the methods are asynchronous methods. This is just a way of telling the compiler that we want to be able to execute code within and allow the caller of that method to continue execution while waiting for this method to finish.
An example of this would be cooking a meal. You will start cooking the meat, and while the meat is cooking and you are waiting for it to finish, you would start making the sides. While the food is cooking, you would start setting the table. An example of this in code would be static async Task<Steak> MakeSteak(int number)
.
Unity also has all kinds of inbuilt methods that you can call asynchronously; see the Unity docs for a list of methods. With the way Unity handles memory management, it uses either coroutines, AsyncOperation
, or the C# Job System.
await
and how do you use it?In C#, you can wait for an asynchronous operation to complete by using the await
keyword. This is used inside any method that has the async
keyword to wait for an operation to continue:
Public async void Update()
{
// do stuff
await // some asynchronous method or task to finish
// do more stuff or do stuff with the data returned from the asynchronous task.
}
See the Microsoft documents for more on await
.
Task
and how do you use it?A Task
is an asynchronous method that performs a single operation and does not return a value. For a Task
that returns a value, we would use Task<TResult>
.
To use a task, we create it like creating any new object in C#: Task t1 = new Task(void Action)
. Next, we start the task t1.wait
. Lastly, we wait for the task to complete with t1.wait
.
There are several ways to create, start, and run tasks. Task t2 = Task.Run(void Action)
will create and start a task. await Task.Run(void Action)
will create, start, and wait for the task to complete. We can use the most common alternative way with Task t3 = Task.Factory.Start(void Action)
.
There are several ways that we can wait for Task to complete. int index = Task.WaitAny(Task[])
will wait for any task to complete and give us the index of the completed task in the array. await Task.WaitAll(Task[])
will wait for all of the tasks to complete.
For more on tasks, see the Microsoft Documents.
task
exampleprivate void Start()
{
Task t1 = new Task(() => Thread.Sleep(1000));
Task t2 = Task.Run(() => Thread.Sleep(2000000));
Task t3 = Task.Factory.StartNew(() => Thread.Sleep(1000));
t1.Start();
Task[] tasks = { t1, t2, t3 };
int index = Task.WaitAny(tasks);
Debug.Log($"Task {tasks[index].Id} at index {index} completed.");
Task t4 = new Task(() => Thread.Sleep(100));
Task t5 = Task.Run(() => Thread.Sleep(200));
Task t6 = Task.Factory.StartNew(() => Thread.Sleep(300));
t4.Start();
Task.WaitAll(t4, t5, t6);
Debug.Log($"All Task Completed!");
Debug.Log($"Task When any t1={t1.IsCompleted} t2={t2.IsCompleted} t3={t3.IsCompleted}");
Debug.Log($"All Task Completed! t4={t4.IsCompleted} t5={t5.IsCompleted} t6={t6.IsCompleted}");
}
public async void Update()
{
float startTime = Time.realtimeSinceStartup;
Debug.Log($"Update Started: {startTime}");
Task t1 = new Task(() => Thread.Sleep(10000));
Task t2 = Task.Run(() => Thread.Sleep(20000));
Task t3 = Task.Factory.StartNew(() => Thread.Sleep(30000));
await Task.WhenAll(t1, t2, t3);
Debug.Log($"Update Finished: {(Time.realtimeSinceStartup - startTime) * 1000f} ms");
}
Now let’s compare the performance of a task versus the performance of a method.
I will need a static class that I can use in all of my performance checks. It will have a method and a task that simulates a performance-intensive operation. Both the method and the task perform the same exact operation:
using System.Threading.Tasks;
using Unity.Mathematics;
public static class Performance
{
public static void PerformanceIntensiveMethod(int timesToRepeat)
{
// Represents a Performance Intensive Method like some pathfinding or really complex calculation.
float value = 0f;
for (int i = 0; i < timesToRepeat; i++)
{
value = math.exp10(math.sqrt(value));
}
}
public static Task PerformanceIntensiveTask(int timesToRepeat)
{
return Task.Run(() =>
{
// Represents a Performance Intensive Method like some pathfinding or really complex calculation.
float value = 0f;
for (int i = 0; i < timesToRepeat; i++)
{
value = math.exp10(math.sqrt(value));
}
});
}
}
Now I need a MonoBehaviour
that I can use to test the performance impact on the task and the method. Just so I can see a better impact on the performance, I will pretend that I want to run this on ten different game objects. I will also keep track of the amount of time the Update
method takes to run.
In Update
, I get the start time. If I am testing the method, I loop through all of the simulated game objects and call the performance-intensive method. If I am testing the task, I create a new Task
array loop through all of the simulated game objects and add the performance-intensive task to the task array. I then await
for all of the tasks to complete. Outside of the method type check, I update the method time, converting it to ms
. I also log it.
public class PerformanceTaskCoroutineJob : MonoBehaviour
{
private enum MethodType
{
Normal,
Task
}
[SerializeField] private int numberGameObjectsToImitate
= 10;
[SerializeField] private MethodType method = MethodType.Normal;
[SerializeField] private float methodTime;
private async void Update()
{
float startTime = Time.realtimeSinceStartup;
switch (method)
{
case MethodType.Normal:
for (int i = 0; i < numberGameObjectsToImitate
; i++)
Performance.PerformanceIntensiveMethod(50000);
break;
case MethodType.Task:
Task[] tasks = new Task[numberGameObjectsToImitate
];
for (int i = 0; i < numberGameObjectsToImitate
; i++)
tasks[i] = Performance.PerformanceIntensiveTask(5000);
await Task.WhenAll(tasks);
break;
}
methodTime = (Time.realtimeSinceStartup - startTime) * 1000f;
Debug.Log($"{methodTime} ms");
}
}
The intensive method takes around 65ms to complete with the game running at about 12 FPS.
The intensive task takes around 4ms to complete with the game running at about 200 FPS.
Let’s try this with a thousand enemies:
using System.Collections.Generic;
using System.Threading.Tasks;
using Unity.Collections;
using Unity.Jobs;
using Unity.Mathematics;
using UnityEngine;
using Random = UnityEngine.Random;
public class PerformanceTaskJob : MonoBehaviour
{
private enum MethodType
{
NormalMoveEnemy,
TaskMoveEnemy
}
[SerializeField] private int numberEnemiesToCreate = 1000;
[SerializeField] private Transform pfEnemy;
[SerializeField] private MethodType method = MethodType.NormalMoveEnemy;
[SerializeField] private float methodTime;
private readonly List<Enemy> m_enemies = new List<Enemy>();
private void Start()
{
for (int i = 0; i < numberEnemiesToCreate; i++)
{
Transform enemy = Instantiate(pfEnemy,
new Vector3(Random.Range(-8f, 8f), Random.Range(-8f, 8f)),
Quaternion.identity);
m_enemies.Add(new Enemy { transform = enemy, moveY = Random.Range(1f, 2f) });
}
}
private async void Update()
{
float startTime = Time.realtimeSinceStartup;
switch (method)
{
case MethodType.NormalMoveEnemy:
MoveEnemy();
break;
case MethodType.TaskMoveEnemy:
Task<Task[]> moveEnemyTasks = MoveEnemyTask();
await Task.WhenAll(moveEnemyTasks);
break;
default:
MoveEnemy();
break;
}
methodTime = (Time.realtimeSinceStartup - startTime) * 1000f;
Debug.Log($"{methodTime} ms");
}
private void MoveEnemy()
{
foreach (Enemy enemy in m_enemies)
{
enemy.transform.position += new Vector3(0, enemy.moveY * Time.deltaTime);
if (enemy.transform.position.y > 5f)
enemy.moveY = -math.abs(enemy.moveY);
if (enemy.transform.position.y < -5f)
enemy.moveY = +math.abs(enemy.moveY);
Performance.PerformanceIntensiveMethod(1000);
}
}
private async Task<Task[]> MoveEnemyTask()
{
Task[] tasks = new Task[m_enemies.Count];
for (int i = 0; i < m_enemies.Count; i++)
{
Enemy enemy = m_enemies[i];
enemy.transform.position += new Vector3(0, enemy.moveY * Time.deltaTime);
if (enemy.transform.position.y > 5f)
enemy.moveY = -math.abs(enemy.moveY);
if (enemy.transform.position.y < -5f)
enemy.moveY = +math.abs(enemy.moveY);
tasks[i] = Performance.PerformanceIntensiveTask(1000);
}
await Task.WhenAll(tasks);
return tasks;
}
Displaying and moving a thousand enemies with the method took around 150ms with a frame rate of about 7 FPS.
Displaying and moving a thousand enemies with a task took around 50ms with a frame rate of about 30 FPS.
useTasks
?Tasks are extremely perficient and reduce the strain on performance on your system. You can even use them in multiple threads using the Task Parallel Library (TPL).
There are some drawbacks to using them in Unity, however. The major drawback with using Task
in Unity is that they all run on the Main
thread. Yes, we can make them run on other threads, but Unity already does its own thread and memory management, and you can create errors by creating more threads than CPU Cores, which causes competition for resources.
Tasks can also be difficult to get to perform correctly and debug. When writing the original code, I ended up with the tasks all running, but none of the enemies moved on screen. It ended up being that I needed to return the Task[]
that I created in the Task
.
Tasks create a lot of garbage that affect the performance. They also do not show up in the profiler, so if you have one that is affecting the performance, it is hard to track down. Also, I have noticed that sometimes my tasks and update functions continue to run from other scenes.
According to Unity, “A coroutine is a function that can suspend its execution (yield) until the given YieldInstruction finishes.”
What this means is that we can run code and wait for a task to complete before continuing. This is much like an async method. It uses a return type IEnumerator
and we yield return
instead of await
.
Unity has several different types of yield instructions that we can use, i.e., WaitForSeconds
, WaitForEndOfFrame
, WaitUntil
, or WaitWhile
.
To start coroutines, we need a MonoBehaviour
and use the MonoBehaviour.StartCoroutine
.
To stop a coroutine before it completes, we use MonoBehaviour.StopCoroutine
. When stopping coroutines, make sure that you use the same method as you used to start it.
Common use cases for coroutines in Unity are to wait for assets to load and to create cooldown timers.
using System.Collections;
using UnityEngine;
using UnityEngine.SceneManagement;
public class SceneLoader : MonoBehaviour
{
public Coroutine loadSceneCoroutine;
public void Update()
{
if (Input.GetKeyDown(KeyCode.Space) && loadSceneCoroutine == null)
{
loadSceneCoroutine = StartCoroutine(LoadSceneAsync());
}
if (Input.GetKeyDown(KeyCode.Escape) && loadSceneCoroutine != null)
{
StopCoroutine(loadSceneCoroutine);
loadSceneCoroutine = null;
}
}
private IEnumerator LoadSceneAsync()
{
AsyncOperation asyncLoad = SceneManager.LoadSceneAsync("Scene2");
yield return new WaitWhile(() => !asyncLoad.isDone);
}
}
Let’s see how using a coroutine impacts the performance of our project. I am only going to do this with the performance-intensive method.
I added the Coroutine
to the MethodType
enum and variables to keep track of its state:
public class PerformanceTaskCoroutineJob : MonoBehaviour
{
private enum MethodType
{
Normal,
Task,
Coroutine
}
...
private Coroutine m_performanceCoroutine;
I created the coroutine. This is similar to the performance-intensive task and method that we created earlier with added code to update the method time:
private IEnumerator PerformanceCoroutine(int timesToRepeat, float startTime)
{
for (int count = 0; count < numberGameObjectsToImitate; count++)
{
// Represents a Performance Intensive Method like some pathfinding or really complex calculation.
float value = 0f;
for (int i = 0; i < timesToRepeat; i++)
{
value = math.exp10(math.sqrt(value));
}
}
methodTime = (Time.realtimeSinceStartup - startTime) * 1000f;
Debug.Log($"{methodTime} ms");
m_performanceCoroutine = null;
yield return null;
}
In the Update
method, I added the check for the coroutine. I also modified the method time, updated code, and added code to stop the coroutine if it was running and we changed the method type:
private async void Update()
{
float startTime = Time.realtimeSinceStartup;
switch (method)
{
...
case MethodType.Coroutine:
m_performanceCoroutine ??= StartCoroutine(PerformanceCoroutine(5000, startTime));
break;
default:
Performance.PerformanceIntensiveMethod(50000);
break;
}
if (method != MethodType.Coroutine)
{
methodTime = (Time.realtimeSinceStartup - startTime) * 1000f;
Debug.Log($"{methodTime} ms");
}
if (method != MethodType.Coroutine || m_performanceCoroutine == null) return;
StopCoroutine(m_performanceCoroutine);
m_performanceCoroutine = null;
}
The intensive coroutine takes around 6ms to complete with the game running at about 90 FPS.
The C# Job System is Unity’s implementation of tasks that are easy to write, do not generate the garbage that tasks do, and utilize the worker threads that Unity has already created. This fixes all of the downsides of tasks.
Unity compares jobs as threads, but they do say that a job does one specific task. Jobs can also depend on other jobs to complete before running; this fixes the issue with the task that I had that did not properly move my Units
because it was depending on another task to complete first.
The job dependencies are automatically taken care of for us by Unity. The job system also has a safety system built in mainly to protect against race conditions. One caveat with jobs is that they can only contain member variables that are either blittable types or NativeContainer types; this is a drawback of the safety system.
To use the job system, you create the job, schedule the job, wait for the job to complete, then use the data returned by the job. The job system is needed in order to use Unity’s Data-Oriented Technology Stack (DOTS).
For more details on the job system, see the Unity documentation.
To create a job, you create a stuct
that implements one of the IJob
interfaces (IJob, IJobFor, IJobParallelFor, Unity.Engine.Jobs.IJobParallelForTransform). IJob
is a basic job. IJobFor
and IJobForParallel
are used to perform the same operation on each element of a native container or for a number of iterations. The difference between them is that the IJobFor runs on a single thread where the IJobForParallel
will be split up between multiple threads.
I will use IJob
to create an intensive operation job, IJobFor
and IJobForParallel
to create a job that will move multiple enemies around; this is just so we can see the different impacts on performance. These jobs will be identical to the tasks and methods that we created earlier:
public struct PerformanceIntensiveJob : IJob { }
public struct MoveEnemyJob: IJobFor { }
public struct MoveEnemyParallelJob : IJobParallelFor { }
Add the member variables. In my case, my IJob
does not need any. The IJobFor
and IJobParallelFor
need a float for the current delta time as jobs do not have a concept of a frame; they operate outside of Unity’s MonoBehaviour
. They also need an array of float3
for the position and an array for the move speed on the y axis:
public struct MoveEnemyJob : IJobFor
{
public NativeArray<float3> positions;
public NativeArray<float> moveYs;
public float deltaTime;
}
public struct MoveEnemyParallelJob : IJobParallelFor
{
public NativeArray<float3> positions;
public NativeArray<float> moveYs;
public float deltaTime;
}
The last step is to implement the required Execute
method. The IJobFor
and IJobForParallel
both require an int
for the index of the current iteration that the job is executing.
The difference is instead of accessing the enemy’s transform
and move, we use the array that are in the job:
public struct PerformanceIntensiveJob : IJob
{
#region Implementation of IJob
/// <inheritdoc />
public void Execute()
{
// Represents a Performance Intensive Method like some pathfinding or really complex calculation.
float value = 0f;
for (int i = 0; i < 50000; i++)
{
value = math.exp10(math.sqrt(value));
}
}
#endregion
}
// MoveEnemyJob and MoveEnemyParallelJob have the same exact Execute Method.
/// <inheritdoc />
public void Execute(int index)
{
positions[index] += new float3(0, moveYs[index] * deltaTime, 0);
if (positions[index].y > 5f)
moveYs[index] = -math.abs(moveYs[index]);
if (positions[index].y < -5f)
moveYs[index] = +math.abs(moveYs[index]);
// Represents a Performance Intensive Method like some pathfinding or really complex calculation.
float value = 0f;
for (int i = 0; i < 1000; i++)
{
value = math.exp10(math.sqrt(value));
}
} private JobHandle PerformanceIntensiveMethodJob()
{
PerformanceIntensiveJob job = new PerformanceIntensiveJob();
return job.Schedule();
}
First, we need to instate the job and populate the jobs data:
NativeArray<float> result = new NativeArray<float>(1, Allocator.TempJob);
MyJob jobData = new MyJob();
jobData.myFloat = result;
Then we schedule the job with JobHandle jobHandle = jobData.Schedule();
. The Schedule
method returns a JobHandle
that can be used later on.
We can not schedule a job from within a job. We can, however, create new jobs and populate their data from within a job. Once a job has been scheduled, it cannot be interrupted.
The performance-intensive job
I created a method that creates a new job and schedules it. It returns the job handle that I can use in my update
method:
public class PerformanceTaskCoroutineJob : MonoBehaviour
{
....
private JobHandle PerformanceIntensiveMethodJob()
{
PerformanceIntensiveJob job = new PerformanceIntensiveJob();
return job.Schedule();
}
}
I added the job to my enum. Then, in the Update
method, I add the case
to the switch
section. I created an array of JobHandles
. I then loop through all of the simulated game objects, adding a scheduled job for each to the array:
public class PerformanceTaskCoroutineJob : MonoBehaviour
{
private enum MethodType
{
Normal,
Task,
Coroutine,
Job
}
...
private async void Update()
{
float startTime = Time.realtimeSinceStartup;
switch (method)
{
...
case MethodType.Job:
NativeArray<JobHandle> jobHandles =
new NativeArray<JobHandle>(numberGameObjectsToImitate, Allocator.Temp);
for (int i = 0; i < numberGameObjectsToImitate; i++)
jobHandles[i] = PerformanceIntensiveMethodJob();
break;
default:
Performance.PerformanceIntensiveMethod(50000);
break;
}
...
}
}
The MoveEnemy
and MoveEnemyParallelJob
Next, I added the jobs to my enum. Then in the Update
method, I call a new MoveEnemyJob
method, passing the delta time. Normally you would use either the JobFor
or the JobParallelFor
:
public class PerformanceTaskJob : MonoBehaviour
{
private enum MethodType
{
NormalMoveEnemy,
TaskMoveEnemy,
MoveEnemyJob,
MoveEnemyParallelJob
}
...
private async void Update()
{
float startTime = Time.realtimeSinceStartup;
switch (method)
{
...
case MethodType.MoveEnemyJob:
case MethodType.MoveEnemyParallelJob:
MoveEnemyJob(Time.deltaTime);
break;
default:
MoveEnemy();
break;
}
...
}
...
The first thing I do is set an array for the positions and an array for the moveY
that I will pass on to the jobs. I then fill these arrays with the data from the enemies. Next, I create the job and set the job’s data depending on which job I want to use. After that, I schedule the job depending on the job that I want to use and the type of scheduling I want to do:
private void MoveEnemyJob(float deltaTime)
{
NativeArray<float3> positions = new NativeArray<float3>(m_enemies.Count, Allocator.TempJob);
NativeArray<float> moveYs = new NativeArray<float>(m_enemies.Count, Allocator.TempJob);
for (int i = 0; i < m_enemies.Count; i++)
{
positions[i] = m_enemies[i].transform.position;
moveYs[i] = m_enemies[i].moveY;
}
// Use one or the other
if (method == MethodType.MoveEnemyJob)
{
MoveEnemyJob job = new MoveEnemyJob
{
deltaTime = deltaTime,
positions = positions,
moveYs = moveYs
};
// typically we would use one of these methods
switch (moveEnemyJobType)
{
case MoveEnemyJobType.ImmediateMainThread:
// Schedule job to run immediately on main thread.
// typically would not use.
job.Run(m_enemies.Count);
break;
case MoveEnemyJobType.ScheduleSingleWorkerThread:
case MoveEnemyJobType.ScheduleParallelWorkerThreads:
{
// Schedule job to run at a later point on a single worker thread.
// First parameter is how many for-each iterations to perform.
// The second parameter is a JobHandle to use for this job's dependencies.
// Dependencies are used to ensure that a job executes on worker threads after the dependency has completed execution.
// In this case we don't need our job to depend on anything so we can use a default one.
JobHandle scheduleJobDependency = new JobHandle();
JobHandle scheduleJobHandle = job.Schedule(m_enemies.Count, scheduleJobDependency);
switch (moveEnemyJobType)
{
case MoveEnemyJobType.ScheduleSingleWorkerThread:
break;
case MoveEnemyJobType.ScheduleParallelWorkerThreads:
{
// Schedule job to run on parallel worker threads.
// First parameter is how many for-each iterations to perform.
// The second parameter is the batch size,
// essentially the no-overhead inner-loop that just invokes Execute(i) in a loop.
// When there is a lot of work in each iteration then a value of 1 can be sensible.
// When there is very little work values of 32 or 64 can make sense.
// The third parameter is a JobHandle to use for this job's dependencies.
// Dependencies are used to ensure that a job executes on worker threads after the dependency has completed execution.
JobHandle scheduleParallelJobHandle =
job.ScheduleParallel(m_enemies.Count, m_enemies.Count / 10, scheduleJobHandle);
break;
}
}
break;
}
}
}
else if (method == MethodType.MoveEnemyParallelJob)
{
MoveEnemyParallelJob job = new MoveEnemyParallelJob
{
deltaTime = deltaTime,
positions = positions,
moveYs = moveYs
};
// Schedule a parallel-for job. First parameter is how many for-each iterations to perform.
// The second parameter is the batch size,
// essentially the no-overhead inner-loop that just invokes Execute(i) in a loop.
// When there is a lot of work in each iteration then a value of 1 can be sensible.
// When there is very little work values of 32 or 64 can make sense.
JobHandle jobHandle = job.Schedule(m_enemies.Count, m_enemies.Count / 10);
}
}
We have to wait for the job to be completed. We can get the status from the JobHandle
that we used when we scheduled the job to complete it. This will wait for the job to be complete before continuing execution: >handle.Complete();
or JobHandle.CompleteAll(jobHandles)
. Once the job is complete, the NativeContainer
that we used to set up the job will have all the data that we need to use. Once we retrieve the data from them, we have to properly dispose of them.
The performance-intensive job
This is pretty simple since I am not reading or writing any data to the job. I wait for all of the jobs that were scheduled to be completed then dispose of the Native
array:
public class PerformanceTaskCoroutineJob : MonoBehaviour
{
...
private async void Update()
{
float startTime = Time.realtimeSinceStartup;
switch (method)
{
...
case MethodType.Job:
....
JobHandle.CompleteAll(jobHandles);
jobHandles.Dispose();
break;
default:
Performance.PerformanceIntensiveMethod(50000);
break;
}
...
}
}
The intensive job takes around 6ms to complete with the game running at about 90 FPS.
The MoveEnemy
job
I add the appropriate complete checks:
private void MoveEnemyJob(float deltaTime)
{
....
if (method == MethodType.MoveEnemyJob)
{
....
switch (moveEnemyJobType)
{
case MoveEnemyJobType.ScheduleSingleWorkerThread:
case MoveEnemyJobType.ScheduleParallelWorkerThreads:
{
....
// typically one or the other
switch (moveEnemyJobType)
{
case MoveEnemyJobType.ScheduleSingleWorkerThread:
scheduleJobHandle.Complete();
break;
case MoveEnemyJobType.ScheduleParallelWorkerThreads:
{
scheduleParallelJobHandle.Complete();
break;
}
}
break;
}
}
}
else if (method == MethodType.MoveEnemyParallelJob)
{
....
jobHandle.Complete();
}
}
After the method type checks, I loop through all of the enemies, setting their transform
positions and moveY
to the data that was set in the job. Next, I properly dispose of the native arrays:
private void MoveEnemyJob(float deltaTime)
{
....
if (method == MethodType.MoveEnemyJob)
{
....
}
else if (method == MethodType.MoveEnemyParallelJob)
{
....
}
for (int i = 0; i < m_enemies.Count; i++)
{
m_enemies[i].transform.position = positions[i];
m_enemies[i].moveY = moveYs[i];
}
// Native arrays must be disposed manually.
positions.Dispose();
moveYs.Dispose();
}
Displaying and moving a thousand enemies with job took around 160ms with a frame rate of about 7 FPS with no performance gains.
Displaying and moving a thousand enemies with job parallel took around 30ms with a frame rate of about 30 FPS.
The burst compiler is a compiler that translates from bytecode to native code. Using this with the C# Job System improves the quality of the code generated, giving you a significant boost in performance as well as reducing the consumption of the battery on mobile devices.
To use this, you just tell Unity that you want to use burst compile on the job with the [BurstCompile]
attribute:
using Unity.Burst;
using Unity.Jobs;
using Unity.Mathematics;
[BurstCompile]
public struct PerformanceIntensiveJob : IJob
{
...
}
using Unity.Burst;
using Unity.Collections;
using Unity.Jobs;
using Unity.Mathematics;
[BurstCompile]
public struct MoveEnemyJob : IJobFor
{
...
}
[BurstCompile]
public struct MoveEnemyParallelJob : IJobParallelFor
{
...
}
Then in Unity, select Jobs > Burst > Enable Completion
Burst is Just-In-Time (JIT) while in the Editor, meaning that this can be down while in Play Mode. When you build your project it is Ahead-Of-Time (AOT), meaning that this needs to be enabled before building your project. You can do so by editing the Burst AOT Settings section in the Project Settings Window.
For more details on the burst compiler, see the Unity documentation.
An intensive job with burst takes around 3ms to complete with the game running at about 150 FPS.
Displaying and moving a thousand enemies, the job with burst took around 30ms with a frame rate of about 30 FPS.
Displaying and moving a thousand enemies, the job parallel with burst took around 6ms with a frame rate of about 80 to 90 FPS.
We can use Task
to increase the performance of our Unity applications, but there are several drawbacks to using them. It is better to use the things that come packaged in Unity depending on what we want to do. Use coroutines if we want to wait for something to finish loading asynchronously; we can start the coroutine and not stop the process of our program from running.
We can use the C# Job System with the burst compiler to get a massive gain in performance while not having to worry about all of the thread management stuff when performing process-intensive tasks. Using the inbuilt systems, we are sure that it is done in a safe manner that does not cause any unwanted errors or bugs.
Tasks did run a little better than the jobs without using the burst compiler, but that is due to the little extra overhead behind the scenes to set everything up safely for us. When using the burst compiler, our jobs performed our tasks. When you need all of the extra performance that you can get, use the C# Job System with burst.
The project files for this can be found on my GitHub.