读一读

using RK_Runtime.RK_Space_UI;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;

public class VC_SubScrollViewCheck : RK_UIMonoViewComponent, IBeginDragHandler, IDragHandler, IEndDragHandler, IScrollHandler, IInitializePotentialDragHandler, ICanvasElement, ILayoutGroup
{
    public ScrollRect ParentScrollRect;
    private bool m_IsDrag = false;
    private Vector2 m_PrePos;

    public float minWidth;

    public void CalculateLayoutInputHorizontal()
    {
        ParentScrollRect.CalculateLayoutInputHorizontal();
    }

    public void CalculateLayoutInputVertical()
    {
        ParentScrollRect.CalculateLayoutInputVertical();
    }

    public void GraphicUpdateComplete()
    {
        ParentScrollRect.GraphicUpdateComplete();
    }

    public bool IsDestroyed()
    {
        return ParentScrollRect.IsDestroyed();
    }

    public void LayoutComplete()
    {
        ParentScrollRect.LayoutComplete();
    }

    public void OnBeginDrag(PointerEventData eventData)
    {
        m_IsDrag = true;
        m_PrePos = eventData.position;
        PassEvent(eventData, ExecuteEvents.beginDragHandler);
    }

    public void OnDrag(PointerEventData eventData)
    {
        bool parDir = ParentScrollRect.vertical;
        var offset = eventData.position - m_PrePos;
        var x = Mathf.Abs(offset.x);
        var y = Mathf.Abs(offset.y);

        if (parDir)
        {
            if (y > x)
            {
                PassEvent(eventData, ExecuteEvents.dragHandler);
            }
        }
        else
        {
            if (y < x)
            {
                PassEvent(eventData, ExecuteEvents.dragHandler);
            }
        }

        m_PrePos = eventData.position;
    }

    public void OnEndDrag(PointerEventData eventData)
    {
        m_IsDrag = false;
        PassEvent(eventData, ExecuteEvents.endDragHandler);
    }

    public void OnInitializePotentialDrag(PointerEventData eventData)
    {
        PassEvent(eventData, ExecuteEvents.initializePotentialDrag);
    }

    public void OnScroll(PointerEventData eventData)
    {
        PassEvent(eventData, ExecuteEvents.scrollHandler);
    }

    //把事件透下去
    public void PassEvent<T>(PointerEventData data, ExecuteEvents.EventFunction<T> function)
        where T : IEventSystemHandler
    {
        //List<RaycastResult> results = new List<RaycastResult>();
        //EventSystem.current.RaycastAll(data, results);
        //GameObject current = data.pointerCurrentRaycast.gameObject;
        //for (int i = 0; i < results.Count; i++)
        //{
        //    if (current != results[i].gameObject && GetComponent<VC_SubScrollViewCheck>() == null)
        //    {
        //        ExecuteEvents.Execute(ParentScrollRect.gameObject, data, function);
        //        //RaycastAll后ugui会自己排序,如果你只想响应透下去的最近的一个响应,这里ExecuteEvents.Execute后直接break就行。
        //        break;
        //    }
        //}
        ExecuteEvents.Execute(ParentScrollRect.gameObject, data, function);
    }

    public void Rebuild(CanvasUpdate executing)
    {
        ParentScrollRect.Rebuild(executing);
    }

    public void SetLayoutHorizontal()
    {
        ParentScrollRect.SetLayoutHorizontal();
    }

    public void SetLayoutVertical()
    {
        ParentScrollRect.SetLayoutVertical();
    }
}



build.py

import json
import os
import tail
import subprocess
import sys
import _thread
import time
import re
import send2trash
import xcode_pre_build

AppName = "Cascading_Stars"
xcode_build_type = "Release"

def tail_log(log_file):
    print("into tail file = %s" % log_file)
    while True:
        if os.path.exists(log_file):
            print("begin tail file = %s" % log_file)
            break
    t = tail.tail(log_file, print_log)

def print_log(txt):
    print(txt)

def build_android_app(ide_path, log_path, project_path, params):
    time1 = time.time()
    if os.path.exists(log_path):
        os.remove(log_path)

    _thread.start_new_thread(tail_log, (log_path, ))
    if params['aot'] == "1":
        cmd = ide_path + " -quit" + " -batchmode" + " -logfile " + log_path + " -projectPath " + project_path + " -executeMethod MenuItemTools.HybridClrAotBuildAndroid"
        process = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=project_path)
        process.wait()

    cmd = ide_path + " -quit" + " -batchmode" + " -logfile " + log_path + " -projectPath " + project_path + " -executeMethod MenuItemTools.PackageApk"
    process = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=project_path)
    process.wait()
    
    stdout,stderr = process.communicate()
    print(stdout.decode())
    if stderr:
        print(stderr.decode())
    
    time2 = time.time()
    print("build android time = " + str(time2-time1))

def export_xcode(ide_path, log_path, project_path):
    print("export unity to xcode")
    if os.path.exists(log_path):
        os.remove(log_path)
    cmd = ide_path + " -quit" + " -batchmode" + " -logfile " + log_path + " -projectPath " + project_path + " -executeMethod MenuItemTools.PackageIOS"
    _thread.start_new_thread(tail_log, (log_path, ))

    process = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=project_path)
    process.wait()
    
    stdout,stderr = process.communicate()
    print(stdout.decode())
    if stderr:
        print(stderr.decode())

def xcode_to_app(xcode_path, params):
    name = None
    if str(params['debug']) == "0":
        name = "Release"
    else:
        name = "Debug"
    file_name = AppName + "-" + name + "-" + params['appver']
    export_path = os.path.join(xcode_path, "..", "..", "exportipa")
    archive_path = xcode_path + "/archive/export.xcarchive"
    process = subprocess.Popen('cd %s;xcodebuild clean -configuration %s' % (xcode_path, xcode_build_type), shell=True)
    process.wait()
    #process = subprocess.Popen('cd %s;pod update' % (xcode_path), shell=True)
    #process.wait()
    process = subprocess.Popen(
        'cd %s;xcodebuild archive -workspace Unity-iPhone.xcworkspace -scheme Unity-iPhone -configuration %s -archivePath %s -destination "generic/platform=iOS" || exit' % (
            xcode_path, xcode_build_type, archive_path), shell=True)
    process.wait()
    process = subprocess.Popen('mkdir %s' % export_path, shell=True)
    process.wait()
    plist_path = os.path.join(py_path, "exportOptions.plist")
    process = subprocess.Popen('xcodebuild -exportArchive -archivePath %s -exportOptionsPlist %s -exportPath %s/%s' % (
    archive_path, plist_path, export_path, file_name), shell=True)
    process.wait()

def build_ios_app(ide_path, log_path, project_path, xcode_path, params):
    time1 = time.time()
    if params['aot'] == "1":
        cmd = ide_path + " -quit" + " -batchmode" + " -logfile " + log_path + " -projectPath " + project_path + " -executeMethod MenuItemTools.HybridClrAotBuildIOS"
        process = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=project_path)
        process.wait()
    export_xcode(ide_path, log_path, project_path)
    time.sleep(2)
    
    #i2cppbuild_path = os.path.join(project_path, "HybridCLRData/iOSBuild")
    #process = subprocess.Popen('cd %s;bash ./build_libil2cpp.sh' %(i2cppbuild_path) , shell=True)
    #process.wait()

    xcode_pre_build.pre_build(xcode_path, project_path)
    xcode_to_app(xcode_path, params)
    time2 = time.time()
    print("build ios time = " + str(time2-time1))

def clear_server_data(project_path, params):
    data_path = None
    if params['ios'] == "1":
        data_path = os.path.join(project_path, "ServerData", "iOS")
    else:
        data_path = os.path.join(project_path, "ServerData", "Android")

    if os.path.exists(data_path):
        send2trash.send2trash(data_path)
    

def change_version_code(project_path, params):
    appver = '"AppVer" : "' + params["appver"] + '"'
    resver = '"ResVer" : "' + params["resver"] + '"'
    version_path = os.path.join(project_path, "Assets", "Resources", "Version", "Version.bytes")
    stream = open(version_path, 'r')
    content = stream.read()
    stream.close()

    pattern = re.compile(r'"AppVer" : "([\d\.]*)"')
    content = re.sub(pattern, appver, content)
    pattern = re.compile(r'"ResVer" : "([\d\.]*)"')
    content = re.sub(pattern, resver, content)

    stream = open(version_path, 'w')
    stream.write(content)
    stream.close()

def change_debug_code(project_path, params):
    debug = None
    channel = None
    if str(params['debug']) == "0":
        debug = '''Debug = false'''
        channel = '''{
"Head" : "Official",
"Version" : "http://update.whales-entertainment.com:9850/Update/Official/",
"Res" : "http://update.whales-entertainment.com:9850/Update/Official/",
"NeedAccount": false
}'''
    else:
        debug = '''Debug = true'''
        channel = '''{
"Head": "Test",
"Version": "http://88lsj.com:9850/Update/Test/",
"Res": "http://88lsj.com:9850/Update/Test/",
"NeedAccount": false
}'''

    config_path = os.path.join(project_path, "Assets", "Frame", "Runtime", "Config", "RK_FrameConfig.cs")
    stream = open(config_path, 'r')
    content = stream.read()
    stream.close()

    content = content.replace('''Debug = false''', debug)
    content = content.replace('''Debug = true''', debug)

    stream = open(config_path, 'w')
    stream.write(content)
    stream.close()
    
    config_path = os.path.join(project_path, "Assets", "StreamingAssets", "ChannelInfo.bytes")
    #stream = open(config_path, 'r')
    #content = stream.read()
    #stream.close()

    #content = content.replace('''Official''', name)
    #content = content.replace('''Test''', name)

    stream = open(config_path, 'w')
    stream.write(channel)
    stream.close()

def delete_striaotdlltemp(project_path):
    #这鬼东西 删不掉一直报错
    aotdll_temp_path = os.path.join(project_path, "HybridCLRData/StrippedAOTDllsTempProj")
    if os.path.exists(aotdll_temp_path):
        send2trash.send2trash(aotdll_temp_path)

def set_proxy():
    process = subprocess.Popen('export.UTF-8', shell=True)
    process.wait()
    process = subprocess.Popen('export LANGUAGE=en_US.UTF-8', shell=True)
    process.wait()
    process = subprocess.Popen('export LC_ALL=en_US.UTF-8', shell=True)
    process.wait()
    process = subprocess.Popen('export http_proxy=http://127.0.0.1:7890', shell=True)
    process.wait()
    process = subprocess.Popen('export https_proxy=http://127.0.0.1:7890', shell=True)
    process.wait()
    process = subprocess.Popen('git config --global http.proxy http://127.0.0.1:7890', shell=True)
    process.wait()
    process = subprocess.Popen('git config --global http.proxy https://127.0.0.1:7890', shell=True)
    process.wait()
    
def unset_proxy():
    process = subprocess.Popen('git config --global --unset http.proxy', shell=True)
    process.wait()
    process = subprocess.Popen('git config --global --unset https.proxy', shell=True)
    process.wait()

if __name__ == "__main__":
    default_params = '''{"ios":"1", "aot":"0" ,"appver":"1.0.0", "resver":"1000", "debug":"1"}'''
    params = json.loads(default_params)
    for arg in sys.argv:
        par = arg.split("=")
        if(len(par)>1):
            params[par[0]] = par[1]
            
    ide_path = "/Applications/Unity/Hub/Editor/2021.3.44f1c1/Unity.app/Contents/MacOS/Unity"
    py_path = os.path.split(os.path.realpath(__file__))[0]
    project_path = os.path.join(py_path, "../")
    log_path = os.path.join(py_path, "package.log")
    
    
    delete_striaotdlltemp(project_path)
    clear_server_data(project_path, params)
    change_version_code(project_path, params)
    change_debug_code(project_path, params)
    
    name = None
    if str(params['debug']) == "0":
        name = "Release"
    else:
        name = "Debug"
    
    #set_proxy()
    if str(params['ios']) == "0":
        build_android_app(ide_path, log_path, project_path,params)
    else:
        xcode_path = os.path.join(project_path, 'Build', 'iPhone', AppName + "-" + params['appver'] + "-" + name)
        build_ios_app(ide_path, log_path, project_path, xcode_path, params)
    
    #unset_proxy()



xcode_pre_build.py

import os
import shutil
import subprocess

def copy_il2cpplib(xcode_path, project_path):
    origin_file = os.path.join(project_path, "HybridCLRData", "iOSBuild", "build", "libil2cpp.a")
    target_file = os.path.join(xcode_path, "Libraries", "libil2cpp.a")
    shutil.copy(origin_file, target_file)
    print("copy src:" + origin_file + " to:" + target_file)

def change_plist(xcode_path):
    info_plist_path = os.path.join(xcode_path, 'Info.plist')
    stream = open(info_plist_path, 'r')
    plist = stream.read()
    stream.close()
    plist = plist.replace('<string>fb-messenger-api</string>', '<string>fb</string><string>fb-messenger-share-api</string><string>fb-messenger-api</string>', 1)
    stream = open(info_plist_path, 'w')
    stream.write(plist)
    stream.close()
    
    deleteInfoPlist(info_plist_path,'NSAppTransportSecurity:NSAllowsArbitraryLoadsInWebContent')

def pre_build(xcode_path, project_path):
    #copy_il2cpplib(xcode_path, project_path)
    change_plist(xcode_path)
    
@staticmethod
def deleteInfoPlist(infoPlistPath,key):
    (stauts, output) = subprocess.getstatusoutput(
        '/usr/libexec/PlistBuddy -c \"Delete :{}\" {} '.format(key, infoPlistPath))
    if stauts != 0:
        if output.find('Does Not Exist') == -1:
            print('删除Plist失败 key=%s,[%s]' % (key, infoPlistPath))



-executeMethod MenuItemTools.PackageApk   -executeMethod MenuItemTools.PackageIOS Unity中提供这两个静态方法打包

using UnityEditor;
using UnityEngine;

public class MenuItemTools
{
    [MenuItem("Package/Build/Android", false,1)]
    public static void PackageApk()
    {
        if (EditorUserBuildSettings.activeBuildTarget == BuildTarget.Android)
        {
            Package.PackageApk();
        }
        else
        {
            EditorUserBuildSettings.activeBuildTargetChanged = delegate ()
            {
                if (EditorUserBuildSettings.activeBuildTarget == BuildTarget.Android)
                {
                    Package.PackageApk();
                }
            };
            EditorUserBuildSettings.SwitchActiveBuildTarget(BuildTargetGroup.Android, BuildTarget.Android);
        }
    }

    [MenuItem("Package/Build/Android-AAB", false, 1)]
    public static void PackageAAB()
    {
        if (EditorUserBuildSettings.activeBuildTarget == BuildTarget.Android)
        {
            Package.PackageAAB();
        }
        else
        {
            EditorUserBuildSettings.activeBuildTargetChanged = delegate ()
            {
                if (EditorUserBuildSettings.activeBuildTarget == BuildTarget.Android)
                {
                    Package.PackageAAB();
                }
            };
            EditorUserBuildSettings.SwitchActiveBuildTarget(BuildTargetGroup.Android, BuildTarget.Android);
        }
    }
    
    [MenuItem("Package/Build/Export to Xcode", false, 3)]
    public static void PackageIOS()
    {
        if (EditorUserBuildSettings.activeBuildTarget == BuildTarget.iOS)
        {
            Package.PackageIos();
        }
        else
        {
            EditorUserBuildSettings.activeBuildTargetChanged = delegate ()
            {
                if (EditorUserBuildSettings.activeBuildTarget == BuildTarget.iOS)
                {
                    Package.PackageIos();
                }
            };
            EditorUserBuildSettings.SwitchActiveBuildTarget(BuildTargetGroup.iOS, BuildTarget.iOS);
        }
    }
}



Package.cs

using System.Collections.Generic;
using System.IO;
using UnityEditor;
using HybridCLR.Editor.Commands;
using UnityEngine;
using UnityEditor.Build.Content;


#if RK_GoogleAds
using GoogleMobileAds.Editor;
#endif

public class Package
{
    public static void BuildSceneStream()
    {
        UnityEngine.Object[] scenes = Selection.GetFiltered(typeof(SceneAsset), SelectionMode.Assets);

        foreach (UnityEngine.Object o in scenes)
        {
            string path = AssetDatabase.GetAssetPath(o);
            if (!path.Contains(".unity")) continue;

            string sceneName = o.name + ".scene";

            BuildPipeline.BuildPlayer(new string[] { path }, "Res/Scenes/" + sceneName, BuildTarget.Android, BuildOptions.BuildAdditionalStreamedScenes);
        }
    }

    public static void PackageApk()
    {
        PreparePackage();
        EditorUserBuildSettings.buildAppBundle = false;
        BuildPipeline.BuildPlayer(GetBuildScenes(), $"Build/Android/{GetAppFileName()}.apk", BuildTarget.Android, BuildOptions.CompressWithLz4HC);
    }

    public static void PackageAAB()
    {
        PreparePackage();
        EditorUserBuildSettings.buildAppBundle = true;
        BuildPipeline.BuildPlayer(GetBuildScenes(), $"Build/Android/{GetAppFileName()}.aab", BuildTarget.Android, BuildOptions.CompressWithLz4HC);
    }

    public static void PackageStandaloneWindows64()
    {
        PreparePackage();
        BuildPipeline.BuildPlayer(GetBuildScenes(), $"Build/StandaloneWindows64/{AotConfig.ProductName}-{GetAppVer()}/{AotConfig.ProductName}.exe", BuildTarget.StandaloneWindows64, BuildOptions.CompressWithLz4HC);
    }

    public static void PackageIos()
    {
        PreparePackage();
        string module = null;
        if (RK_FrameConfig.IsDebug)
        {
            module = "Debug";
        }
        else
        {
            module = "Release";

        }
        BuildPipeline.BuildPlayer(GetBuildScenes(), $"Build/iPhone/{AotConfig.ProductName}-{GetAppVer()}-{module}", BuildTarget.iOS, BuildOptions.CompressWithLz4HC);
    }

    public static void PackageOSX()
    {
        PreparePackage();
        BuildPipeline.BuildPlayer(GetBuildScenes(), $"Build/Osx/{AotConfig.ProductName}-{GetAppVer()}/{AotConfig.ProductName}", BuildTarget.StandaloneOSX, BuildOptions.CompressWithLz4HC);
    }

    private static string GetAppFileName()
    {

        string name = AotConfig.ProductName;
#if RK_GameDebug
        name += "-Debug";
#else
        name += "-Release";
#endif

#if RK_CSJ
        name += "-CSJ";
#endif
#if RK_TapTap
        name += "-TapTap";
#endif
#if RK_GoogleAds
        name += "-Admod";
#endif
#if RK_TopOnAds
        name += "-Topon";
#endif
        if (AotConfig.IsSkipAd)
        {
            name += "-SkipAd";
        }
        else
        {
            if (AotConfig.IsTestAd)
            {
                name += "-TestAd";
            }
        }

        if (AotConfig.NoPay)
        {
            name += "-NoPay";
        }

        name += "-" + AotConfig.DefaultLanguage;
        name += "-" + GetAppVer();
        return name;
    }

    public static void PreparePackage()
    {
        Debug.Log("PreparePackage");
        //ChangeDebugSymbol();
        //ChangeReviewSymbol();

        //PlayerSettings.bundleVersion = GetAppVer();
        //PlayerSettings.Android.bundleVersionCode = GetIntAppVer();
        if (AotConfig.DefaultLanguage == "Chinese")
        {
            PlayerSettings.productName = AotConfig.SettingProductName_CN;
        }
        else
        {
            PlayerSettings.productName = AotConfig.SettingProductName_EN;
        }
        PlayerSettings.iOS.buildNumber = "0";
        if(!AotConfig.DisableHybridCLR)
            PrebuildCommand.GenerateAll();
        PackageRes();
        AssetDatabase.Refresh();
    }

    public static void ChangeDebugSymbol()
    {
        SymbolMenuItemTools.ChangeDebugSymbol(RK_FrameConfig.IsDebug);
    }

    public static void PackageRes()
    {
        ClearServerData();
        //#if UNITY_EDITOR_WIN
        //        ProtobufTool.AllProto2CS();
        //#endif
        AotHotUpdateDll.BuildAndCopyABAOTHotUpdateDlls();
        AASUtility.CleanPlayerContent();
        PackageAtlas.PackAtlas();
        AssetDatabase.Refresh();
        AddressableTool.MarkAssets();
        AddressableTool.BuildPlayerContent();
    }

    public static void HybridClrAotBuild()
    {
        string path = Path.Combine(Application.dataPath, "../Build/AotTemp");
        if (Directory.Exists(path))
        { 
            Directory.Delete(path, true);
        }
        BuildTarget target = EditorUserBuildSettings.activeBuildTarget;
        CompileDllCommand.CompileDll(target);
        Il2CppDefGeneratorCommand.GenerateIl2CppDef();
        LinkGeneratorCommand.GenerateLinkXml(target);
        BuildPipeline.BuildPlayer(GetBuildScenes(), $"Build/AotTemp/{AotConfig.ProductName}-{GetAppVer()}", target, BuildOptions.CompressWithLz4HC);
    }

    public static void ClearServerData()
    {
        string path = Path.Combine(Application.dataPath, "../","ServerData");
        if (EditorUserBuildSettings.activeBuildTarget == BuildTarget.StandaloneWindows)
        {
            path = Path.Combine(path, "StandaloneWindows");
        }
        else if (EditorUserBuildSettings.activeBuildTarget == BuildTarget.StandaloneWindows64)
        {
            path = Path.Combine(path, "StandaloneWindows64");
        }
        else if (EditorUserBuildSettings.activeBuildTarget == BuildTarget.Android)
        {
            path = Path.Combine(path, "Android");
        }
        else if (EditorUserBuildSettings.activeBuildTarget == BuildTarget.iOS)
        {
            path = Path.Combine(path, "iOS");
        }

        Debug.Log(path);

        if (Directory.Exists(path))
        {
            Directory.Delete(path, true);
        }
    }
    

    private static string[] GetBuildScenes()
    {
        List<EditorBuildSettingsScene> editorBuildSettingsScenes = new List<EditorBuildSettingsScene>();
        foreach (EditorBuildSettingsScene e in EditorBuildSettings.scenes)
        {
            string scenePath = e.path;
            if (!string.IsNullOrEmpty(scenePath))
                editorBuildSettingsScenes.Add(new EditorBuildSettingsScene(scenePath, true));
        }
        EditorBuildSettings.scenes = editorBuildSettingsScenes.ToArray();

        List<string> names = new List<string>();
        foreach (EditorBuildSettingsScene e in EditorBuildSettings.scenes)
        {
            if (e != null && e.enabled)
            {
                names.Add(e.path);
            }
        }
        return names.ToArray();
    }

    private static string GetAppVer()
    {
        var json = Resources.Load<TextAsset>(BootMainConfig.LocalVersionPath).text;
        var local_version = JsonUtility.FromJson<VersionInfo>(json);
        return local_version.AppVer;
    }
}



初始化UnityServer

using Firebase.Extensions;
using RK_Runtime;
using System;
using System.Collections;
using System.Collections.Generic;
using Unity.Services.Core;
using UnityEngine;

public class UnityServiceTool : RK_Singleton<UnityServiceTool>
{

    public bool IsInit = false;
    const string k_Environment = "production";

    public override void StartUp()
    {
        base.StartUp();
        Initialize(OnSuccess, OnError);
    }

    void Initialize(Action onSuccess, Action<string> onError)
    {
        try
        {
            var options = new InitializationOptions();
            options.SetOption("Environment", k_Environment);

            UnityServices.InitializeAsync(options).ContinueWithOnMainThread(task => onSuccess());
        }
        catch (Exception exception)
        {
            onError(exception.Message);
        }
    }

    void OnSuccess()
    {
        IsInit = true;
        var text = "Congratulations!\nUnity Gaming Services has been successfully initialized.";
        RK.Log.Log(text);

        MG_IAPurchaseManager.Instance.Initialize(0, false);
    }

    void OnError(string message)
    {
        var text = $"Unity Gaming Services failed to initialize with error: {message}.";
        RK.Log.LogError(text);
    }
}



支付管理类

using cfg.Tpl;
using RK_Runtime;
using RK_Runtime.RK_Space_Http;
using System.Collections.Generic;
using System.Text;
using UnityEngine;
using UnityEngine.Purchasing;
using UnityEngine.Purchasing.Extension;
using SimpleJSON;
using System;
using System.Threading.Tasks;
using Newtonsoft.Json;
using RK_Runtime.RK_Space_Event;
using RK_Runtime.Utility;
//using UnityEngine.Purchasing.Security;

//using DataTower.Core;

public class MG_IAPurchaseManager : RK_MonoSingleton<MG_IAPurchaseManager>,IDetailedStoreListener
{
    private IStoreController m_StoreController;         // Unity 采购系统
    private IExtensionProvider m_StoreExtensionProvider; // 商店特定的采购子系统.

    private Product m_PurchasedProduct;
    private int m_InitReTryTime = 0;
    private bool m_IsIniting = false;
    private string m_PurchasedProductID = string.Empty;

    private Dictionary<string,bool> m_OrderMap = new Dictionary<string,bool>();

    class PayCheckReceiveJsonData
    {
        public int Code = 0;
        public string Data;
    }

    private void Start()
    {
        RK.Event.Add(OnEventApplicationPause);
    }

    [RK_EventHandle(EventDefine.EventApplicationPause)]
    private static void OnEventApplicationPause(object data)
    {
        bool pause = RK_ValueRef<bool>.GetValue(data);
        if (pause)
        {
            //PL_CommonLoadingPanel.HideLoading("PurchaseProduct");
        }
    }

    public async Task Initialize(int trytime = 0, bool isLoading = true)
    {
        if (IsInitialized())
        {
            return;
        }
        RK.Log.LogWarning("IAPurchaseManager initializing.");

        var orderStr = PlayerPrefs.GetString(PlayerPrefsKey.OrderInfoKey, "");
        if (!string.IsNullOrEmpty(orderStr))
        { 
            var dic = JsonConvert.DeserializeObject<Dictionary<string,bool>>(orderStr);
            if (dic != null)
            {
                m_OrderMap = dic;
            }
        }

        m_IsIniting = true;
        m_InitReTryTime = trytime;
        if(isLoading)
            //PL_CommonLoadingPanel.ShowLoading("PayInitialize", delayTime: 0, when: () => { return !m_IsIniting; });

        while (!UnityServiceTool.Instance.IsInit)//等待服务初始化
        { 
            await Task.Yield();
        }

        var module = StandardPurchasingModule.Instance();
        module.useFakeStoreUIMode = FakeStoreUIMode.StandardUser;
        module.useFakeStoreAlways = false;
        var builder = ConfigurationBuilder.Instance(module);
        for (int i = 0; i < SH_TableData.Instance.Datas.Goods.DataList.Count; i++)
        {
            if (!string.IsNullOrEmpty(SH_TableData.Instance.Datas.Goods.DataList[i].ProductID))
            {
                builder.AddProduct(SH_TableData.Instance.Datas.Goods.DataList[i].ProductID, ProductType.Consumable);
            }
        }

        //StringBuilder stringBuilder = new StringBuilder();
        //foreach (var item in builder.products)
        //{
        //    stringBuilder.AppendLine(item.id);
        //}
        //RK.Log.LogWarning(stringBuilder.ToString());

        if (Application.platform == RuntimePlatform.Android)
        {
            var config = builder.Configure<IGooglePlayConfiguration>();
            config.SetObfuscatedAccountId("UID:" + SH_UserData.Instance.LoginID);
        }

        UnityPurchasing.Initialize(this, builder);
    }

    public bool IsInitialized()
    {
        return m_StoreController != null && m_StoreExtensionProvider != null;
    }

    public void PurchaseProduct(string productId)
    {
        RK.Log.Log("Purchase:" + productId);

        //if (AotConfig.NoPay)
        //{
        //    SH_ShopData.Instance.BuySuccess(productId);
        //    return;
        //}

        if (IsInitialized())
        {
            if (m_PurchasedProduct != null)
            {
                //有订单正在购买中
                //UT_CommonUtil.ShowErrorToast("TitleBuyConfirm");
                CheckOrder(m_PurchasedProduct);//再去验证下
                return;
            }

            Product product = m_StoreController.products.WithID(productId);
            if (product != null && product.availableToPurchase)
            {
                //PL_CommonLoadingPanel.ShowLoading("PurchaseProduct", delayTime: 0);
                BaseAdManager.Instance.SkipOpenAdTime++;
                m_PurchasedProduct = product;
                m_StoreController.InitiatePurchase(productId);
            }
            else
            {
                RK.Log.LogError(string.Format("Purchasing product asychronously: '{0}'", product.definition.id));
            }
        }
        else
        {
            m_PurchasedProductID = productId;
            if (m_IsIniting)
            {
                m_InitReTryTime = 2;
                //PL_CommonLoadingPanel.ShowLoading("PayInitialize", delayTime: 0, when: () => { return !m_IsIniting; });
                return;
            }
            Initialize(2);
        }

    }

    public void RestorePurchases()
    {
        if (!IsInitialized())
        {
            Initialize();
            return;
        }

        // If we are running on an Apple device ... 
        if (Application.platform == RuntimePlatform.IPhonePlayer ||
            Application.platform == RuntimePlatform.OSXPlayer)
        {
            // ... begin restoring purchases
            Debug.Log("RestorePurchases started ...");

            // Fetch the Apple store-specific subsystem.
            var apple = m_StoreExtensionProvider.GetExtension<IAppleExtensions>();
            // Begin the asynchronous process of restoring purchases. Expect a confirmation response in 
            // the Action<bool> below, and ProcessPurchase if there are previously purchased products to restore.
            apple.RestoreTransactions((result, error) =>
            {
                // The first phase of restoration. If no more responses are received on ProcessPurchase then 
                // no purchases are available to be restored.
                if (result)
                {
                    Debug.Log("RestorePurchases continuing: " + result +
                              ". If no further messages, no purchases available to restore.");
                }
                else
                {
                    RK.Log.LogWarning(error);
                }
            });
        }
        // Otherwise ...
        else
        {
            // We are not running on an Apple device. No work is necessary to restore purchases.
            Debug.Log("RestorePurchases FAIL. Not supported on this platform. Current = " + Application.platform);
        }
    }

    /*
     * 验证
     */
    public void CheckOrder(Product product)
    {
        if (!product.hasReceipt)
        {
            m_PurchasedProduct = null;
            return;
        }


        RK.Log.Log("Success : " + product.ToString());
        RK.Log.Log("Success receipt : " + product.receipt);

        List<string> produces = new List<string>();
        List<string> orders = new List<string>();

        bool validPurchase = true; // 假设对没有收据验证的平台有效。
        bool hasProduct = false; // 假设对没有收据验证的平台有效。
//#if !UNITY_EDITOR
//        var validator = new CrossPlatformValidator(GooglePlayTangle.Data(),
//            AppleTangle.Data(), Application.identifier);
        
//        try {
//            //在 Google Play 上,结果中仅有一个商品 ID。
//            //在 Apple 商店中,收据包含多个商品。
//            var validateResult = validator.Validate(product.receipt);
//            //为便于参考,我们将收据列出
//            Debug.Log("Receipt is valid. Contents:");
//            //validateResult.
//            foreach (IPurchaseReceipt productReceipt in validateResult) {
//                produces.Add(productReceipt.productID);
//                orders.Add(productReceipt.transactionID);
//            }
//        } catch (IAPSecurityException) {
//            Debug.Log("Invalid receipt, not unlocking content");
//            validPurchase = false;
//        }
//#endif
        if (validPurchase)
        {
            List<string> lists = new List<string>();
            for (int i = 0; i < produces.Count; i++)
            {
                if (!m_OrderMap.TryGetValue(orders[i], out var flat))
                {
                    SH_ShopData.Instance.BuySuccess(produces[i]);
                    lists.Add(orders[i]);
                }
            }

            RK.Event.Call(EventDefine.EventUpdateUserInfo);

            m_StoreController.ConfirmPendingPurchase(product);
            m_PurchasedProduct = null;

            for (int i = 0; i < lists.Count; i++)
            {
                m_OrderMap.Add(lists[i], true);
            }

            PlayerPrefs.SetString(PlayerPrefsKey.OrderInfoKey, JsonConvert.SerializeObject(m_OrderMap));
            PlayerPrefs.Save();

            SH_UserData.Instance.ReqSaveUserInfo(()=>{
               
            });
            return;
        }
        else
        {
            m_PurchasedProduct = null;
            SH_ShopData.Instance.BuyFail(product.definition.id);
        }
    }

    public float GetPrice(string id)
    {

        return 0;
    }

    public void Release()
    {
        m_PurchasedProduct = null;
        m_StoreController = null;
        m_StoreExtensionProvider = null;
        m_PurchasedProductID = null;
        m_InitReTryTime = 0;
        m_IsIniting = false;
    }


    //----------------------------------IDetailedStoreListener----------------------------

    public void OnInitialized(IStoreController controller, IExtensionProvider extensions)
    {
        m_IsIniting = false;
        m_InitReTryTime = 0;
        RK.Log.Log($"IAPurchaseManager Init Success");
        m_StoreController = controller;
        m_StoreExtensionProvider = extensions;

        if (Application.platform == RuntimePlatform.IPhonePlayer)
        {
            extensions.GetExtension<IAppleExtensions>().SetApplicationUsername("特定的格式-UUID 映射uid");
        }



        if (!string.IsNullOrEmpty(m_PurchasedProductID))
        {
            PurchaseProduct(m_PurchasedProductID);
            m_PurchasedProductID = null;
        }
    }

    public void OnInitializeFailed(InitializationFailureReason error, string message)
    {
        if (m_InitReTryTime > 0)
        {
            RK.Log.Log($"IAPurchaseManager Init : {error}  {message}  retry ");
            m_InitReTryTime--;
            Initialize(m_InitReTryTime);
        }
        else
        {
            m_IsIniting = false;
            RK.Log.LogError($"IAPurchaseManager Init : {error}  {message}  ");
            if (!string.IsNullOrEmpty(m_PurchasedProductID))
            {
                //弹出提示
                //UT_CommonUtil.ShowErrorToast(RK_I18NLanguageUtil.GetI18nString("PaymentFailed"));
            }
        }
    }

    public PurchaseProcessingResult ProcessPurchase(PurchaseEventArgs purchaseEvent)
    {

        //让服务端去确认
        m_PurchasedProduct = purchaseEvent.purchasedProduct;
        CheckOrder(m_PurchasedProduct);

        return PurchaseProcessingResult.Pending;
    }

    public void OnPurchaseFailed(Product product, PurchaseFailureDescription failureDescription)
    {
        m_PurchasedProduct = null;
        RK.Log.Log(failureDescription.reason.ToString() + ":" + failureDescription.message);
        //CommonUtil.ShowToast(failureDescription.message);
        switch (failureDescription.reason)
        {
            case PurchaseFailureReason.UserCancelled:
                //UT_CommonUtil.ShowToast(RK_I18NLanguageUtil.GetI18nString("ButtonCancle"));
                break;
            default:
                UT_CommonUtil.ShowErrorToast(failureDescription.reason.ToString());
                break;
        }


        // Dictionary<string, string> param = new Dictionary<string, string>();
        // param.Add("platform", Application.platform == RuntimePlatform.IPhonePlayer ? "ios" : "android");
        // param.Add("productId", failureDescription.productId);
        // param.Add("transactionID", product.transactionID);
        // param.Add("appleOriginalTransactionID", product.appleOriginalTransactionID);
        // param.Add("reason", failureDescription.reason.ToString());
        // param.Add("message", failureDescription.message);
        //
        // string json = JsonConvert.SerializeObject(param);
        SH_ShopData.Instance.BuyFail(product.definition.id);
    }
    


    public void OnInitializeFailed(InitializationFailureReason error)
    {
        throw new System.NotImplementedException();
    }

    public void OnPurchaseFailed(Product product, PurchaseFailureReason failureReason)
    {
        m_PurchasedProduct = null;
        RK.Log.Log(failureReason.ToString());
        //CommonUtil.ShowToast(failureDescription.message);
        switch (failureReason)
        {
            case PurchaseFailureReason.UserCancelled:
                //UT_CommonUtil.ShowToast(RK_I18NLanguageUtil.GetI18nString("ButtonCancle"));
                break;
            default:
                UT_CommonUtil.ShowErrorToast(failureReason.ToString());
                break;
        }


        // Dictionary<string, string> param = new Dictionary<string, string>();
        // param.Add("platform", Application.platform == RuntimePlatform.IPhonePlayer ? "ios" : "android");
        // param.Add("productId", failureDescription.productId);
        // param.Add("transactionID", product.transactionID);
        // param.Add("appleOriginalTransactionID", product.appleOriginalTransactionID);
        // param.Add("reason", failureDescription.reason.ToString());
        // param.Add("message", failureDescription.message);
        //
        // string json = JsonConvert.SerializeObject(param);
        SH_ShopData.Instance.BuyFail(product.definition.id);
    }

    
}



薯塔、bugly、Firebase、In App Purchasing

关闭 HybridClr  关闭热更

webgl的addressable的bundle是url来的 都用下载的方式


m2repository的声明放在Assets/目录/m2repository,这样下载的库文件路径和自动生成的AndroidResolverDependencies.xml里配置的引用路径才对

image.png


可以在unity中设置图形API如下:

image.png


就可以复现android手机上出现的情况,经排查顶点uv等数据输出,法线uv会随着旋转视角改变时而变动,经尝试是模型太大了,缩放后正常了

image.png


unity-jar-resolver android

cocoapods ios


打包失败一般是依赖库下载失败 脚本打包失败,可以打开Unity,尝试resolusion(android),ios直接xcode export,然后命令行声明代理,再pod update or pod intsall


带CanvasUI是不在剪裁的目标之内的,所以需要一个中介自身是剪裁目标的,然后获取到这些带Canvas的UI通知它们也做相应处理。

image.png


Proxy的脚本为:

using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

[DisallowMultipleComponent]
public class RectMask2DClipProxy : MaskableGraphic
{
    private List<MaskableGraphic> _maskableList;

    protected RectMask2DClipProxy()
    {
        useLegacyMeshGeneration = false;
    }
     protected override void OnPopulateMesh(VertexHelper toFill)
     {
         toFill.Clear();
     }

    protected override void Awake()
    {
        base.Awake();
        _maskableList = new List<MaskableGraphic>();
    }

    public override void Cull(Rect clipRect, bool validRect)
    {
        base.Cull(clipRect, validRect);
        GetComponentsInChildren(false, _maskableList);
        if (null != _maskableList)
        {
            for (int i = 0; i < _maskableList.Count; i++)
            { 
                var maskable = _maskableList[i];
                if (maskable != null && maskable != this)
                { 
                    maskable.Cull(clipRect, validRect);
                }
            }
        }
    }

    public override void SetClipRect(Rect clipRect, bool validRect)
    {
        base.SetClipRect(clipRect, validRect);
        GetComponentsInChildren(false, _maskableList);
        if (null != _maskableList)
        {
            for (int i = 0; i < _maskableList.Count; i++)
            {
                var maskable = _maskableList[i];
                if (maskable != null && maskable != this)
                {
                    maskable.SetClipRect(clipRect, validRect);
                }
            }
        }
    }
}


如果元素不是动态变化的,就缓存起来。


inline float SoftUnityGet2DClipping (in float2 position, in float4 clipRect)
{
    float2 xy = (position.xy-clipRect.xy)/float2(_ClipSoftX,_ClipSoftY)*step(clipRect.xy, position.xy);
    float2 zw = (clipRect.zw-position.xy)/float2(_ClipSoftX,_ClipSoftY)*step(position.xy,clipRect.zw);
    float2 factor = clamp(0, zw, xy);
    return saturate(min(factor.x,factor.y));
}

c.a *= SoftUnityGet2DClipping(i.wpos.xy, float4(_MinX, _MinY, _MaxX, _MaxY));