unity lifecycle

博客围绕Unity生命周期展开,虽暂无具体内容,但可知聚焦于Unity这一游戏开发引擎的生命周期相关信息技术知识。


#if !UNITY_WSA_10_0 using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using OpenCVForUnity.CoreModule; using OpenCVForUnity.ImgprocModule; using OpenCVForUnity.UnityIntegration; using OpenCVForUnity.UnityIntegration.Helper.Source2Mat; using OpenCVForUnity.UnityIntegration.Worker.DnnModule; using UnityEngine; using UnityEngine.SceneManagement; using UnityEngine.UI; namespace OpenCVForUnityExample { /// <summary> /// Object Detection DAMO-YOLO Example /// An example of using OpenCV dnn module with DAMO-YOLO Object Detection. /// Referring to : /// https://github.com/tinyvision/DAMO-YOLO /// https://github.com/tinyvision/DAMO-YOLO/blob/master/tools/demo.py /// /// [Tested Models] /// damoyolo_tinynasL18_Ns.onnx https://idstcv.oss-cn-zhangjiakou.aliyuncs.com/DAMO-YOLO/release_model/onnx/before_distill/damoyolo_tinynasL18_Ns.onnx /// damoyolo_tinynasL18_Nm.onnx https://idstcv.oss-cn-zhangjiakou.aliyuncs.com/DAMO-YOLO/release_model/onnx/before_distill/damoyolo_tinynasL18_Nm.onnx /// damoyolo_tinynasL20_T_436.onnx https://idstcv.oss-cn-zhangjiakou.aliyuncs.com/DAMO-YOLO/release_model/onnx/damoyolo_tinynasL20_T_436.onnx /// </summary> [RequireComponent(typeof(MultiSource2MatHelper))] public class ObjectDetectionDAMOYOLOExample : MonoBehaviour { // Public Fields [Header("Output")] [Tooltip("The RawImage for previewing the result.")] public RawImage ResultPreview; [Header("UI")] public Toggle UseAsyncInferenceToggle; public bool UseAsyncInference = false; [Header("Model Settings")] [Tooltip("Path to a binary file of model contains trained weights.")] public string Model = "OpenCVForUnityExamples/dnn/damoyolo_tinynasL18_Ns.onnx"; [Tooltip("Optional path to a text file with names of classes to label detected objects.")] public string Classes = "OpenCVForUnityExamples/dnn/coco.names"; [Tooltip("Confidence threshold.")] public float ConfThreshold = 0.35f; [Tooltip("Non-maximum suppression threshold.")] public float NmsThreshold = 0.6f; [Tooltip("Maximum detections per image.")] public int TopK = 300; [Tooltip("Preprocess input image by resizing to a specific width.")] public int InpWidth = 416; [Tooltip("Preprocess input image by resizing to a specific height.")] public int InpHeight = 416; // Private Fields private Texture2D _texture; private MultiSource2MatHelper _multiSource2MatHelper; private Mat _bgrMat; private DAMOYOLOObjectDetector _objectDetector; private string _classesFilepath; private string _modelFilepath; private FpsMonitor _fpsMonitor; private CancellationTokenSource _cts = new CancellationTokenSource(); private Mat _bgrMatForAsync; private Mat _latestDetectedObjects; private Task _inferenceTask; private readonly Queue<Action> _mainThreadQueue = new(); private readonly object _queueLock = new(); // Unity Lifecycle Methods private async void Start() { _fpsMonitor = GetComponent<FpsMonitor>(); _multiSource2MatHelper = gameObject.GetComponent<MultiSource2MatHelper>(); _multiSource2MatHelper.OutputColorFormat = Source2MatHelperColorFormat.RGBA; // Update GUI state #if !UNITY_WEBGL || UNITY_EDITOR UseAsyncInferenceToggle.isOn = UseAsyncInference; #else UseAsyncInferenceToggle.isOn = false; UseAsyncInferenceToggle.interactable = false; #endif // Asynchronously retrieves the readable file path from the StreamingAssets directory. if (_fpsMonitor != null) _fpsMonitor.ConsoleText = "Preparing file access..."; if (!string.IsNullOrEmpty(Classes)) { _classesFilepath = await OpenCVEnv.GetFilePathTaskAsync(Classes, cancellationToken: _cts.Token); if (string.IsNullOrEmpty(_classesFilepath)) Debug.Log("The file:" + Classes + " did not exist."); } if (!string.IsNullOrEmpty(Model)) { _modelFilepath = await OpenCVEnv.GetFilePathTaskAsync(Model, cancellationToken: _cts.Token); if (string.IsNullOrEmpty(_modelFilepath)) Debug.Log("The file:" + Model + " did not exist."); } if (_fpsMonitor != null) _fpsMonitor.ConsoleText = ""; Run(); } private void Run() { //if true, The error log of the Native side OpenCV will be displayed on the Unity Editor Console. OpenCVDebug.SetDebugMode(true); if (string.IsNullOrEmpty(_modelFilepath)) { Debug.LogError("model: " + Model + " or " + "classes: " + Classes + " is not loaded. Please use [Tools] > [OpenCV for Unity] > [Setup Tools] > [Example Assets Downloader]to download the asset files required for this example scene, and then move them to the \"Assets/StreamingAssets\" folder."); } else { _objectDetector = new DAMOYOLOObjectDetector(_modelFilepath, _classesFilepath, new Size(InpWidth, InpHeight), ConfThreshold, NmsThreshold, TopK); } _multiSource2MatHelper.Initialize(); } /// <summary> /// Raises the source to mat helper initialized event. /// </summary> public void OnSourceToMatHelperInitialized() { Debug.Log("OnSourceToMatHelperInitialized"); Mat rgbaMat = _multiSource2MatHelper.GetMat(); _texture = new Texture2D(rgbaMat.cols(), rgbaMat.rows(), TextureFormat.RGBA32, false); OpenCVMatUtils.MatToTexture2D(rgbaMat, _texture); ResultPreview.texture = _texture; ResultPreview.GetComponent<AspectRatioFitter>().aspectRatio = (float)_texture.width / _texture.height; if (_fpsMonitor != null) { _fpsMonitor.Add("width", rgbaMat.width().ToString()); _fpsMonitor.Add("height", rgbaMat.height().ToString()); _fpsMonitor.Add("orientation", Screen.orientation.ToString()); } _bgrMat = new Mat(rgbaMat.rows(), rgbaMat.cols(), CvType.CV_8UC3); _bgrMatForAsync = new Mat(); } /// <summary> /// Raises the source to mat helper disposed event. /// </summary> public void OnSourceToMatHelperDisposed() { Debug.Log("OnSourceToMatHelperDisposed"); if (_inferenceTask != null && !_inferenceTask.IsCompleted) _inferenceTask.Wait(500); _bgrMat?.Dispose(); _bgrMat = null; _bgrMatForAsync?.Dispose(); _latestDetectedObjects?.Dispose(); if (_texture != null) Texture2D.Destroy(_texture); _texture = null; } /// <summary> /// Raises the source to mat helper error occurred event. /// </summary> /// <param name="errorCode">Error code.</param> /// <param name="message">Message.</param> public void OnSourceToMatHelperErrorOccurred(Source2MatHelperErrorCode errorCode, string message) { Debug.Log("OnSourceToMatHelperErrorOccurred " + errorCode + ":" + message); if (_fpsMonitor != null) { _fpsMonitor.ConsoleText = "ErrorCode: " + errorCode + ":" + message; } } // Update is called once per frame private void Update() { ProcessMainThreadQueue(); if (_multiSource2MatHelper.IsPlaying() && _multiSource2MatHelper.DidUpdateThisFrame()) { Mat rgbaMat = _multiSource2MatHelper.GetMat(); if (_objectDetector == null) { Imgproc.putText(rgbaMat, "model file is not loaded.", new Point(5, rgbaMat.rows() - 30), Imgproc.FONT_HERSHEY_SIMPLEX, 0.7, new Scalar(255, 255, 255, 255), 2, Imgproc.LINE_AA, false); Imgproc.putText(rgbaMat, "Please read console message.", new Point(5, rgbaMat.rows() - 10), Imgproc.FONT_HERSHEY_SIMPLEX, 0.7, new Scalar(255, 255, 255, 255), 2, Imgproc.LINE_AA, false); } else { Imgproc.cvtColor(rgbaMat, _bgrMat, Imgproc.COLOR_RGBA2BGR); if (UseAsyncInference) { // asynchronous execution if (_inferenceTask == null || _inferenceTask.IsCompleted) { _bgrMat.copyTo(_bgrMatForAsync); // for asynchronous execution, deep copy _inferenceTask = Task.Run(async () => { try { // Object detector inference var newObjects = await _objectDetector.DetectAsync(_bgrMatForAsync); RunOnMainThread(() => { _latestDetectedObjects?.Dispose(); _latestDetectedObjects = newObjects; }); } catch (OperationCanceledException ex) { Debug.Log($"Inference canceled: {ex}"); } catch (Exception ex) { Debug.LogError($"Inference error: {ex}"); } }); } Imgproc.cvtColor(_bgrMat, rgbaMat, Imgproc.COLOR_BGR2RGBA); if (_latestDetectedObjects != null) { _objectDetector.Visualize(rgbaMat, _latestDetectedObjects, false, true); } } else { // synchronous execution // TickMeter tm = new TickMeter(); // tm.start(); // Object detector inference using (Mat objects = _objectDetector.Detect(_bgrMat)) { // tm.stop(); // Debug.Log("DAMOYOLOObjectDetector Inference time, ms: " + tm.getTimeMilli()); Imgproc.cvtColor(_bgrMat, rgbaMat, Imgproc.COLOR_BGR2RGBA); _objectDetector.Visualize(rgbaMat, objects, false, true); } } } OpenCVMatUtils.MatToTexture2D(rgbaMat, _texture); } } /// <summary> /// Raises the destroy event. /// </summary> private void OnDestroy() { _multiSource2MatHelper?.Dispose(); _objectDetector?.Dispose(); OpenCVDebug.SetDebugMode(false); _cts?.Dispose(); } /// <summary> /// Raises the back button click event. /// </summary> public void OnBackButtonClick() { SceneManager.LoadScene("OpenCVForUnityExample"); } /// <summary> /// Raises the play button click event. /// </summary> public void OnPlayButtonClick() { _multiSource2MatHelper.Play(); } /// <summary> /// Raises the pause button click event. /// </summary> public void OnPauseButtonClick() { _multiSource2MatHelper.Pause(); } /// <summary> /// Raises the stop button click event. /// </summary> public void OnStopButtonClick() { _multiSource2MatHelper.Stop(); } /// <summary> /// Raises the change camera button click event. /// </summary> public void OnChangeCameraButtonClick() { _multiSource2MatHelper.RequestedIsFrontFacing = !_multiSource2MatHelper.RequestedIsFrontFacing; } /// <summary> /// Raises the use async inference toggle value changed event. /// </summary> public void OnUseAsyncInferenceToggleValueChanged() { if (UseAsyncInferenceToggle.isOn != UseAsyncInference) { // Wait for inference to complete before changing the toggle if (_inferenceTask != null && !_inferenceTask.IsCompleted) _inferenceTask.Wait(500); UseAsyncInference = UseAsyncInferenceToggle.isOn; } } // Private Methods private void RunOnMainThread(Action action) { if (action == null) return; lock (_queueLock) { _mainThreadQueue.Enqueue(action); } } private void ProcessMainThreadQueue() { while (true) { Action action = null; lock (_queueLock) { if (_mainThreadQueue.Count == 0) break; action = _mainThreadQueue.Dequeue(); } try { action?.Invoke(); } catch (Exception ex) { Debug.LogException(ex); } } } } } #endif
09-19
using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; using System.Text.RegularExpressions; using UnityEditor; using UnityEngine; [InitializeOnLoad] public class AddVuforiaEnginePackage { static readonly string sPackagesPath = Path.Combine(Application.dataPath, "..", "Packages"); static readonly string sManifestJsonPath = Path.Combine(sPackagesPath, "manifest.json"); const string VUFORIA_VERSION = "11.4.4"; const string VUFORIA_TAR_FILE_DIR = "Assets/Editor/Migration/"; const string DEPENDENCIES_DIR = "Assets/Resources/VuforiaDependencies"; const string PACKAGES_RELATIVE_PATH = "Packages"; const string MRTK_PACKAGE = "org.mixedrealitytoolkit.core"; const string OPEN_XR_PACKAGE = "com.microsoft.mixedreality.openxr"; const string PACKAGE_NAME_REGEX = @"(([a-z]+)(\.[a-z0-9]+)*)(\-)?((\d+)\.(\d+)\.(\d+)(\-([a-z0-9\.])+)*)?(\.tgz)"; static readonly ScopedRegistry sVuforiaRegistry = new ScopedRegistry { name = "Vuforia", url = "https://registry.packages.developer.vuforia.com/", scopes = new[] { "com.ptc.vuforia" } }; static AddVuforiaEnginePackage() { if (Application.isBatchMode) return; var manifest = Manifest.JsonDeserialize(sManifestJsonPath); var packages = GetPackageDescriptions(); if (!packages.All(p => IsVuforiaUpToDate(manifest, p.BundleId))) DisplayAddPackageDialog(manifest, packages); ResolveDependencies(manifest); } public static void ResolveDependenciesSilent() { var manifest = Manifest.JsonDeserialize(sManifestJsonPath); var packages = GetDependencyDescriptions(); if (packages != null && packages.Count > 0) MoveDependencies(manifest, packages); CleanupDependenciesFolder(); } static void ResolveDependencies(Manifest manifest) { var packages = GetDependencyDescriptions(); if (packages != null && packages.Count > 0) DisplayDependenciesDialog(manifest, packages); } static bool IsVuforiaUpToDate(Manifest manifest, string bundleId) { var dependencies = manifest.Dependencies.Split(',').ToList(); var upToDate = false; if(dependencies.Any(d => d.Contains(bundleId) && d.Contains("file:"))) upToDate = IsUsingRightFileVersion(manifest, bundleId); return upToDate; } static bool IsUsingRightFileVersion(Manifest manifest, string bundleId) { var dependencies = manifest.Dependencies.Split(',').ToList(); return dependencies.Any(d => d.Contains(bundleId) && d.Contains("file:") && VersionNumberIsTheLatestTarball(d)); } static bool VersionNumberIsTheLatestTarball(string package) { var version = package.Split('-'); if (version.Length >= 2) { version[1] = version[1].TrimEnd(".tgz\"".ToCharArray()); return IsCurrentVersionHigher(version[1]); } return false; } static bool IsCurrentVersionHigher(string currentVersionString) { if (string.IsNullOrEmpty(currentVersionString) || string.IsNullOrEmpty(VUFORIA_VERSION)) return false; var currentVersion = TryConvertStringToVersion(currentVersionString); var updatingVersion = TryConvertStringToVersion(VUFORIA_VERSION); if (currentVersion >= updatingVersion) return true; return false; } static Version TryConvertStringToVersion(string versionString) { Version res; try { res = new Version(versionString); } catch (Exception) { return new Version(); } return new Version(res.Major, res.Minor, res.Build); } static void DisplayAddPackageDialog(Manifest manifest, IEnumerable<PackageDescription> packages) { if (EditorUtility.DisplayDialog("Add Vuforia Engine Package", $"Would you like to update your project to include the Vuforia Engine {VUFORIA_VERSION} package from the unitypackage?\n" + $"If an older Vuforia Engine package is already present in your project it will be upgraded to version {VUFORIA_VERSION}\n\n", "Update", "Cancel")) { foreach (var package in packages) { MovePackageFile(VUFORIA_TAR_FILE_DIR, package.FileName); UpdateManifest(manifest, package.BundleId, package.FileName); } } } static void DisplayDependenciesDialog(Manifest manifest, IEnumerable<PackageDescription> packages) { if (EditorUtility.DisplayDialog("Add Sample Dependencies", "Would you like to update your project to include all of its dependencies?\n" + "If a different version of the package is already present, it will be deleted.\n\n", "Update", "Cancel")) { MoveDependencies(manifest, packages); CleanupDependenciesFolder(); if (ShouldProjectRestart(packages)) DisplayRestartDialog(); } } static void DisplayRestartDialog() { if (EditorUtility.DisplayDialog("Restart Unity Editor", "Due to a Unity lifecycle issue, this project needs to be closed and re-opened " + "after importing this Vuforia Engine sample.\n\n", "Restart", "Cancel")) { RestartEditor(); } } static List<PackageDescription> GetPackageDescriptions() { var tarFilePaths = Directory.GetFiles(Path.Combine(Directory.GetCurrentDirectory(), VUFORIA_TAR_FILE_DIR)).Where(f => f.EndsWith(".tgz")); // Define a regular expression for repeated words. var rx = new Regex(PACKAGE_NAME_REGEX, RegexOptions.Compiled | RegexOptions.IgnoreCase); var packageDescriptions = new List<PackageDescription>(); foreach (var filePath in tarFilePaths) { var fileName = Path.GetFileName(filePath); // Find matches. var matches = rx.Matches(fileName); // Report on each match. foreach (Match match in matches) { var groups = match.Groups; var bundleId = groups[1].Value; var versionString = groups[5].Value; if (string.Equals(versionString, VUFORIA_VERSION)) { packageDescriptions.Add(new PackageDescription() { BundleId = bundleId, FileName = fileName }); } } } return packageDescriptions; } static List<PackageDescription> GetDependencyDescriptions() { var dependencyDirectory = Path.Combine(Directory.GetCurrentDirectory(), DEPENDENCIES_DIR); if (!Directory.Exists(dependencyDirectory)) return null; var tarFilePaths = Directory.GetFiles(dependencyDirectory).Where(f => f.EndsWith(".tgz")); // Define a regular expression for repeated words. var rx = new Regex(PACKAGE_NAME_REGEX, RegexOptions.Compiled | RegexOptions.IgnoreCase); var packageDescriptions = new List<PackageDescription>(); foreach (var filePath in tarFilePaths) { var fileName = Path.GetFileName(filePath); // Find matches. var matches = rx.Matches(fileName); // Report on each match. foreach (Match match in matches) { var groups = match.Groups; var bundleId = groups[1].Value; bundleId = bundleId.Replace(".tgz", ""); packageDescriptions.Add(new PackageDescription { BundleId = bundleId, FileName = fileName }); } } return packageDescriptions; } static void MoveDependencies(Manifest manifest, IEnumerable<PackageDescription> packages) { foreach (var package in packages) { RemoveDependency(manifest, package.BundleId, package.FileName); MovePackageFile(DEPENDENCIES_DIR, package.FileName); UpdateManifest(manifest, package.BundleId, package.FileName); } } static void MovePackageFile(string folder, string fileName) { var sourceFile = Path.Combine(Directory.GetCurrentDirectory(), folder, fileName); var destFile = Path.Combine(Directory.GetCurrentDirectory(), PACKAGES_RELATIVE_PATH, fileName); File.Copy(sourceFile, destFile, true); File.Delete(sourceFile); File.Delete(sourceFile + ".meta"); } static void UpdateManifest(Manifest manifest, string bundleId, string fileName) { //remove existing, outdated NPM scoped registry if present var registries = manifest.ScopedRegistries.ToList(); if (registries.Contains(sVuforiaRegistry)) { registries.Remove(sVuforiaRegistry); manifest.ScopedRegistries = registries.ToArray(); } //add specified vuforia version via Git URL SetVuforiaVersion(manifest, bundleId, fileName); manifest.JsonSerialize(sManifestJsonPath); AssetDatabase.Refresh(); } static void RemoveDependency(Manifest manifest, string bundleId, string fileName) { var destFile = Path.Combine(Directory.GetCurrentDirectory(), PACKAGES_RELATIVE_PATH, fileName); if (File.Exists(destFile)) File.Delete(destFile); // remove existing var dependencies = manifest.Dependencies.Split(',').ToList(); for (var i = 0; i < dependencies.Count; i++) { if (dependencies[i].Contains(bundleId)) { dependencies.RemoveAt(i); break; } } manifest.Dependencies = string.Join(",", dependencies); manifest.JsonSerialize(sManifestJsonPath); AssetDatabase.Refresh(); } static void CleanupDependenciesFolder() { if (!Directory.Exists(DEPENDENCIES_DIR)) return; Directory.Delete(DEPENDENCIES_DIR); File.Delete(DEPENDENCIES_DIR + ".meta"); AssetDatabase.Refresh(); } static bool ShouldProjectRestart(IEnumerable<PackageDescription> packages) { return packages.Any(p => p.BundleId == MRTK_PACKAGE || p.BundleId == OPEN_XR_PACKAGE); } static void RestartEditor() { EditorApplication.OpenProject(Directory.GetCurrentDirectory()); } static void SetVuforiaVersion(Manifest manifest, string bundleId, string fileName) { var dependencies = manifest.Dependencies.Split(',').ToList(); var versionEntry = $"\"file:{fileName}\""; var versionSet = false; for (var i = 0; i < dependencies.Count; i++) { if (!dependencies[i].Contains(bundleId)) continue; var kvp = dependencies[i].Split(':'); dependencies[i] = kvp[0] + ": " + versionEntry; versionSet = true; } if (!versionSet) dependencies.Insert(0, $"\n \"{bundleId}\": {versionEntry}"); manifest.Dependencies = string.Join(",", dependencies); } class Manifest { const int INDEX_NOT_FOUND = -1; const string DEPENDENCIES_KEY = "\"dependencies\""; public ScopedRegistry[] ScopedRegistries; public string Dependencies; public void JsonSerialize(string path) { var jsonString = GetJsonString(); var startIndex = GetDependenciesStart(jsonString); var endIndex = GetDependenciesEnd(jsonString, startIndex); var stringBuilder = new StringBuilder(); stringBuilder.Append(jsonString.Substring(0, startIndex)); stringBuilder.Append(Dependencies); stringBuilder.Append(jsonString.Substring(endIndex, jsonString.Length - endIndex)); File.WriteAllText(path, stringBuilder.ToString()); } string GetJsonString() { if (ScopedRegistries.Length > 0) return JsonUtility.ToJson( new UnitySerializableManifest { scopedRegistries = ScopedRegistries, dependencies = new DependencyPlaceholder() }, true); return JsonUtility.ToJson( new UnitySerializableManifestDependenciesOnly() { dependencies = new DependencyPlaceholder() }, true); } public static Manifest JsonDeserialize(string path) { var jsonString = File.ReadAllText(path); var registries = JsonUtility.FromJson<UnitySerializableManifest>(jsonString).scopedRegistries ?? new ScopedRegistry[0]; var dependencies = DeserializeDependencies(jsonString); return new Manifest { ScopedRegistries = registries, Dependencies = dependencies }; } static string DeserializeDependencies(string json) { var startIndex = GetDependenciesStart(json); var endIndex = GetDependenciesEnd(json, startIndex); if (startIndex == INDEX_NOT_FOUND || endIndex == INDEX_NOT_FOUND) return null; var dependencies = json.Substring(startIndex, endIndex - startIndex); return dependencies; } static int GetDependenciesStart(string json) { var dependenciesIndex = json.IndexOf(DEPENDENCIES_KEY, StringComparison.InvariantCulture); if (dependenciesIndex == INDEX_NOT_FOUND) return INDEX_NOT_FOUND; var dependenciesStartIndex = json.IndexOf('{', dependenciesIndex + DEPENDENCIES_KEY.Length); if (dependenciesStartIndex == INDEX_NOT_FOUND) return INDEX_NOT_FOUND; dependenciesStartIndex++; //add length of '{' to starting point return dependenciesStartIndex; } static int GetDependenciesEnd(string jsonString, int dependenciesStartIndex) { return jsonString.IndexOf('}', dependenciesStartIndex); } } class UnitySerializableManifestDependenciesOnly { public DependencyPlaceholder dependencies; } class UnitySerializableManifest { public ScopedRegistry[] scopedRegistries; public DependencyPlaceholder dependencies; } [Serializable] struct ScopedRegistry { public string name; public string url; public string[] scopes; public override bool Equals(object obj) { if (!(obj is ScopedRegistry)) return false; var other = (ScopedRegistry)obj; return name == other.name && url == other.url && scopes.SequenceEqual(other.scopes); } public static bool operator ==(ScopedRegistry a, ScopedRegistry b) { return a.Equals(b); } public static bool operator !=(ScopedRegistry a, ScopedRegistry b) { return !a.Equals(b); } public override int GetHashCode() { var hash = 17; foreach (var scope in scopes) hash = hash * 23 + (scope == null ? 0 : scope.GetHashCode()); hash = hash * 23 + (name == null ? 0 : name.GetHashCode()); hash = hash * 23 + (url == null ? 0 : url.GetHashCode()); return hash; } } [Serializable] struct DependencyPlaceholder { } struct PackageDescription { public string BundleId; public string FileName; } }能把代码加到这里面吗?
11-08
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值