Unity接入ios SDK(小7手游)沒有你想的那么難

前言

大約一個月前收到領(lǐng)導新布置的任務,要用Unity直接接入發(fā)行方ios sdk。當時我一下子就懵了,ios的Object-c沒接觸過啊,Unity和ios該怎么交互呀,完全什么都不懂。接到消息的那一刻整個人狀態(tài)都不好了,查閱了很多資料完全沒有頭緒也看不進去任何有關(guān)OC的基礎(chǔ)知識。還好有我們部門大楊哥耐心的講了一遍怎么弄。經(jīng)過大楊鍋的講解還有Google理解出來的一些知識,現(xiàn)已完整的對接完好幾個IOS 的SDK的接入工作。其實ios sdk接入并沒有你想的那么難,接下來我會舉例說明,跟大家分享一下我學到的東西,讓新手同學不要跟我一樣上來就懵。

妹子.jpg


文章目錄

  • Unity與IOS交互層C#代碼編寫

  • Unity接 iOS SDK你需要了解的Objective-C基礎(chǔ)知識

  • Unity 與IOS交互工作原理

  • ios 小7手游sdk接入演示


編譯器版本介紹

Unity :Unity 19.4.2f1 Personal

Xcode : Xcode 12.2


Unity與IOS交互層C#代碼編寫

Unity 廣泛的支持原生插件,即用 C、C++、Objective-C 等編寫的原生代碼庫。插件允許游戲代碼(用 C# 編寫)調(diào)用這些庫中的函數(shù)。

為了使用原生插件,首先需要使用基于 C 的語言編寫函數(shù)來訪問所需的功能并將它們編譯到庫中。在 Unity 中,還需要創(chuàng)建一個 C# 腳本來調(diào)用本機庫中的函數(shù)。

原生插件應提供一個簡單的 C 接口供 C# 腳本隨后向其他用戶腳本公開。當某些低級渲染事件發(fā)生時(例如,創(chuàng)建圖形設(shè)備時),Unity 也可以調(diào)用原生插件導出的函數(shù)?,F(xiàn)在編寫一下C#的腳本,此代碼來源于實際項目部分截取。

using System.Runtime.InteropServices;

namespace GameChannel
{
   public class ChannelManager : Singleton<ChannelManager>
    {
        #if UNITY_IOS          //c#中宏的概念 ,意思是當前平臺是iOS
        [DllImport("__Internal")]
        private static extern void SDk_Login();  //登錄
        [DllImport("__Internal")]
        private static extern void SDk_Logout();//注銷
        [DllImport("__Internal")]
        private static extern void SDk_SwitchAccount();//切換賬號(可選參數(shù))
        [DllImport("__Internal")]
        private static extern void SDk_Pay(string payData);//支付
        [DllImport("__Internal")]
        private static extern void SDk_Data(string thisdata); //向渠道發(fā)送游戲數(shù)據(jù)

       //當前平臺是安卓,交互層插件對外調(diào)用名稱是通用的,但是方式上是略有不同用宏的概念做區(qū)分。
       #elif UNITY_ANDROID 
    }
}

對上述代碼做下說明:在 iOS 上,插件以靜態(tài)方式鏈接到可執(zhí)行文件中,因此我們必須使用“ __Internal” 作為庫名。其他的平臺會通過動態(tài)的方式加載插件 [DllImport ("PluginName")]名稱。C#調(diào)用其他模塊的接口都是通過DllImport的方式來實現(xiàn)的。例如c#定義了void SDk_Login()方法,在ios的object-c中 也一定有void SDk_Login()方法。


Unity接 iOS SDK你需要了解的Objective-C基礎(chǔ)知識

Unity項目開發(fā),iOS平臺要接SDK的話,就需要寫Objective-C原生代碼的,對于沒使用過Objective-C的小伙伴不要慌。我一說你就懂了。

  • .h : 頭文件作為一種包含功能函數(shù)、數(shù)據(jù)接口聲明的載體文件,主要用于保存程序的聲明,而定義文件用于保存程序的實現(xiàn)

  • .m : 它是對.h頭文件中方法的實現(xiàn),外部不能訪問

  • .mm : 源代碼文件。和.m文件類似,唯一的不同點就是,除了可以包含Objective-C和C代碼以外,還可以包含C++代碼。

include與#import

當你需要在源代碼中包含頭文件的時候,你可以使用#include編譯選項也可以使用#import ,但是OC官方更推薦的方法是:#import。這個跟java的import 導包思想上非常相似。

""和<>的區(qū)別

例如 #import "UnityIos.h" 和 #import <Foundation/Foundation.h>兩種。使用""引入的是本地工程的文件,而使用<>引入的是系統(tǒng)庫的文件。

@interface與@implementation

@interface是類為對象提供特性描述(接口),@implementation是對@interface定義接口具體的實現(xiàn)。這兩個跟java中的接口的定義與實現(xiàn)上思想上是一致的。

方法前的+ 和 -

加號(+)的方法為類方法,這類方法是可以直接用類名來調(diào)用的。

減號(-)的方法為實例方法,必須使用這個類的實例才可以調(diào)用它。

打印日志

NSLog打印日志。如 NSlog(@"")

基本數(shù)據(jù)類型

NSString : 字符串

CGfloat : 浮點值的基本類型

NSInteger : 整型

BOOL : 布爾型

json使用

Unity和OC要傳遞數(shù)據(jù),常用的就是json格式。但是OC還和java的不一樣。

//json字符串轉(zhuǎn)化成字典

-(NSDictionary*)getJsonDic:(NSString*)jsonString{
NSData* jsonData = [jsonString dataUsingEncoding:NSUTF8StringEncoding];
return [NSJSONSerialization JSONObjectWithData:jsonData options:NSJSONReadingMutableLeaves error:nil];
}
//字典轉(zhuǎn)化成json字符串

-(NSString*)arrayToJson:(NSMutableDictionary *)dic{
NSError *parseError = nil;
NSData *jsonData = [NSJSONSerialization dataWithJSONObject:dic options:NSJSONWritingPrettyPrinted error:&parseError];
return [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding];
}

這些知識僅做作為基礎(chǔ)了解,其他詳細的用法大家可以找下Google和度娘


Unity 與IOS交互工作原理

熟悉工作原理前大家不妨看一下之前寫的博客:Unity 導出Xcode 項目的結(jié)構(gòu) 作為一個了解,每個Unity ios Xcode 項目都會有如下結(jié)構(gòu):

  • UnityFramework 庫文件部分,其中包含源、插件和相關(guān)框架。它還生成 UnityFramework.framework 文件。

  • Unity-iPhone 主啟動器部分,其中包含應用程序表示數(shù)據(jù)并會運行該庫。Unity-iPhone 目標對 UnityFramework 目標具有單一依賴關(guān)系。

要將 Unity 集成到另一個 Xcode 項目中,必須將兩個 Xcode 項目(原生項目和 Unity 生成的項目)合并到一個 Xcode 工作空間中,并將 UnityFramework.framework 文件添加到原生 Xcode 項目的應用程序 (Application) 目標的嵌入式二進制文件 (Embedded Binaries) 中。完成此操作后,可以使用 UnityFramework 類來控制 Unity 運行時。Unity直接導出Xcode工程目錄結(jié)構(gòu)如下圖:

Xcode_Project.png

要想了解原理就要找到程序的入口,熟悉OC或者C的朋友一定知道m(xù)ain方法,這是整個程序的入口。我們先看下MainApp/main.mm,這個文件做了什么呢。

main.mm..png
UnityFramework.png

從代碼中大概讀懂的意思將/Frameworks/UnityFramework.framework庫文件加載到應用程序中。然后看下UnityFramework/UnityFramework.h,通過UnityFramework Objective-C 類(該類是 UnityFramework.framework 的主體類)的實例來控制 Unity 運行時:其中的屬性方法我羅列一下。

UnityFramework類

  • +(UnityFramework*)getInstance :單例類方法,可將實例返回到 UnityFramework。

  • -(UnityAppController*)appController :返回 UIApplicationDelegate 的 UnityAppController 子類。這是原生端的根 Unity 類,可以訪問應用程序的視圖相關(guān)對象,例如 UIView、UIViewControllers、CADisplayLink 或 DisplayConnection。

  • -(void)setDataBundleId:(const char*)bundleId:設(shè)置捆綁包,Unity 運行時應在其中查找 Data 文件夾。應在調(diào)用 runUIApplicationMainWithArgc 或 runEmbeddedWithArgc 之前調(diào)用此方法。

  • -(void)runUIApplicationMainWithArgc:(int)argc argv:(char*[])argv:從沒有其他視圖的主要方法中運行 Unity 的默認方式。

  • -(void)runEmbeddedWithArgc:(int)argc argv:(char[])argv appLaunchOpts:(NSDictionary)appLaunchOpts:存在其他視圖時,如果需要運行 Unity,需要調(diào)用此方法。

  • -(void)unloadApplication :調(diào)用此方法可卸載 Unity,并在卸載完成后接收對 UnityFrameworkListener 的回調(diào)。Unity 將釋放占用的大部分內(nèi)存,但不會全部釋放。

  • -(void)registerFrameworkListener:(id<UnityFrameworkListener>)obj :注冊監(jiān)聽器對象,用于接收 UnityFramework 生命周期相關(guān)事件的回調(diào)。

  • -(void)unregisterFrameworkListener:(id<UnityFrameworkListener>)obj:取消注冊監(jiān)聽器對象。

  • -(void)showUnityWindow:在顯示非 Unity 視圖時調(diào)用此方法,也會顯示已經(jīng)在運行的 Unity 視圖。

  • -(void)pause:(bool)pause:暫停 Unity
  • -(void)setExecuteHeader:(const MachHeader*)header:必須在運行 Unity 之前調(diào)用此命令,CrashReporter 才能正常工作。
  • -(void)sendMessageToGOWithName:(const char)goName functionName:(const char)name message:(const char*)msg:此方法是 UnitySendMessage 的代理。它通過名稱查找游戲?qū)ο?,并使用單字符串消息參?shù)來調(diào)用 functionName。

  • (void)quitApplication:(int)exitCode:調(diào)用此方法可完全卸載 Unity,并在 Unity 退出后接收對 UnityFrameworkListener 的回調(diào)。Unity 將釋放所有內(nèi)存。

注意:進行此調(diào)用后,將無法在同一進程中再次運行 Unity??稍?AppController 上設(shè)置 quitHandler 以覆蓋默認進程終止

main
main-1

然后再看下Classes/main.mm,這個文件做了什么,根據(jù)代碼得知(UIApplicaitonMain方法),程序需要創(chuàng)建UnityAppController對象,也就是說UnityAppController.mm才是真正的程序入口。

  • 到UnityAppController.mm里先調(diào)用- (BOOL)application:(UIApplication)application didFinishLaunchingWithOptions:(NSDictionary)launchOptions生命周期方法,進行Unity界面初始化
image.png
  • 然后則調(diào)用- (void)applicationDidBecomeActive:(UIApplication*)application方法,方法中設(shè)置了UnityPause(0);表示Unity為啟動狀態(tài),在方法最后,執(zhí)行[self performSelector:@selector(startUnity:) withObject:application afterDelay:0];.
image.png
  • 最后調(diào)用到-(void)startUnity:(UIApplication*)application方法,展示Unity游戲界面,完成了原生OC和Unity的交互。
image.png


ios 小7手游sdk接入演示

正常情況下要接入小7蘋果sdk,公司商務或者運營會提供相應的參數(shù)和接入文檔。本文演示不提供文檔和參數(shù)。

進入正題把小7 ios對應的庫文件加入進來,首先工程Libraries 創(chuàng)建一個文件夾(例如SDK文件夾),把小7依賴所有庫放到創(chuàng)建的SDK文件夾下,然后Libraries右鍵add files to Unity-iPhone ...把文件添加進來(下圖是添加后的圖),xcode 會自動把小7的文件添加到對應的庫和引用文件上。

addLib.png
addFile.png

按小7的文檔要求配置好info.plist文件然后需要設(shè)置的屬性也都弄好,準備工作就完事,然后進行下一步。接入sdk其實可以在UnityAppController.mm文件中進行的。但是為了清晰,創(chuàng)建一個UnityIos.m(UnityIos.h可忽略)外部引用放在Libraries/SDK文件夾下。

有同學會奇怪我在Unity c#層定義好了例如登陸的方法,也沒看到在OC中調(diào)用?。吭趺蠢鸬顷懓?。年輕人勿要著急繼續(xù)看。在UnityIos.m中我們定義一個和c#層SDk_Login()名一樣的方法體。請看如下代碼:(這一部分代碼是真實項目中部分截取,其中包含了小7 sdk 完整的登陸 支付 切換賬號等功能

#import "UnityIos.h"
#import <Foundation/Foundation.h>
#import <AdSupport/AdSupport.h>
#import <SMSDK/SMSDK.h>
#import "UnityAppController.h"
#import "UnityInterface.h"

@implementation UnityIos

//調(diào)用sdk登陸
void SDk_Login(){
    NSLog(@"SDk_Login");
   [SMSDK smLogin];
}

//調(diào)用sdk切換賬號功能
void SDk_Logout(){
    NSLog(@"SDk_Logout");
    [SMSDK smLogout];
}

//這個方法只是為了兼容sdk
void SDk_SwitchAccount(){
    NSLog(@"SDk_SwitchAccount");
}
//調(diào)用sdk支付,游戲傳入sdk需要的參數(shù)數(shù)據(jù)(游戲客戶端協(xié)定好字段要統(tǒng)一)
void SDk_pay(void *payData){
    NSLog(@"SDk_pay");
  //直接傳json,oc無法識別所以要進行一個轉(zhuǎn)化
    NSString *idList = [NSString stringWithUTF8String:payData];
    NSData *jsonData = [idList dataUsingEncoding:NSUTF8StringEncoding];
    NSError *err;
    NSDictionary *dic = [NSJSONSerialization JSONObjectWithData:jsonData
                                            options:NSJSONReadingMutableContainers error:&err];
    
    if(err) {
        NSLog(@"json解析失?。?@",err);
        return;
    }
    
    
    NSLog(@"dic解析:%@",dic);
    
    NSString *_price = [NSString stringWithFormat:@"%@",[dic valueForKey:@"price"]];
    NSString *_p_level = [NSString stringWithFormat:@"%@",[dic valueForKey:@"playerlevel"]];
    NSString *_game_sign= [NSString stringWithFormat:@"%@",[dic valueForKey:@"game_sign"]];
 
    NSString *_subject= [NSString stringWithFormat:@"%@",[dic valueForKey:@"subject"]];
    NSString *_game_area= [NSString stringWithFormat:@"%@",[dic valueForKey:@"area"]];
    NSString *_game_role_id= [NSString stringWithFormat:@"%@",[dic valueForKey:@"roleId"]];
    NSString *_game_role_name= [NSString stringWithFormat:@"%@",[dic valueForKey:@"roleName"]];
    NSString *_game_guid= [NSString stringWithFormat:@"%@",[dic valueForKey:@"guid"]];
 
      SMPayInfo *payInfo = [[SMPayInfo alloc] init];
      payInfo.game_orderid =[NSString stringWithFormat:@"%@",[dic valueForKey:@"orderid"]];           //游戲訂單號 60個字符
      payInfo.game_sign =_game_sign;                 //服務器返回的簽名?????????????
      payInfo.game_price =_price;                    //價格單位:元
      payInfo.subject =_subject;                     //道具簡介
      payInfo.game_area =_game_area;                 //角色所在區(qū)服
      payInfo.game_level =_p_level;                  //角色等級
      payInfo.game_role_id =_game_role_id;           //角色ID
      payInfo.game_role_name =_game_role_name;       //角色名稱
      payInfo.notify_id = @"-1";                      //回調(diào)通知ID 默認可以在后臺填寫
      payInfo.extends_info_data = @"";                //自定義擴展數(shù)據(jù)
      payInfo.game_guid=_game_guid;              //游戲登陸后服務器通過token解析拿到的guid
    
      //調(diào)用支付接口
      [SMSDK smPayWithNewPayInfo:payInfo];
    
}

// 把格式化的JSON格式的字符串轉(zhuǎn)換成字典
// @param jsonString JSON格式的字符串
// @return 返回字典

- (NSDictionary *)dictionaryWithJsonString:(NSString *)jsonString {
    if (jsonString == nil) {
        return nil;
    }
    
    NSData *jsonData = [jsonString dataUsingEncoding:NSUTF8StringEncoding];
    NSError *err;
    NSDictionary *dic = [NSJSONSerialization JSONObjectWithData:jsonData
                                                        options:NSJSONReadingMutableContainers
                                                          error:&err];
    if(err) {
        NSLog(@"json解析失?。?@",err);
        return nil;
    }
    return dic;
}
@end

上面的代碼看完了。還會有點小困惑,既然在Libraries/SDK/UnityIos.m文件下,OC是如何找到登陸方法的呢?UnityIos.m定義了SDk_Login(),這個方法在Classes/Native中能找到解釋,我給大家看一個圖啊.

這個圖就是在Unity c#層定義好的同名方法,編譯成了c++代碼放到了Xcode工程路徑下,成了溝通OC和c#的橋梁。游戲用戶點擊登陸按鈕即可調(diào)用OC的登陸方法完成登陸的客戶端流程。

image.png

至于回調(diào)為什么放在UnityAppController.mm里,有兩個點,其一小7 sdk設(shè)計問題,要做個全局回調(diào)我不知道怎么弄,其二是游戲把初始化放在生命周期哪里處理的,不這樣寫我也沒什么好辦法,畢竟我不是一個真正的ios開發(fā)者。正常的接sdk方法和回調(diào)都可以寫在UnityIos.m文件里面的。小7比較特殊啊所以這樣寫。到這里接入結(jié)束嘍,回調(diào)和相關(guān)代碼放在下面了。

#import <SMSDK/SMSDK.h>

#define SMSDKAppKey @"小7后臺申請的appKey"

@implementation UnityAppController    //展示部分核心需要部分

- (BOOL)application:(UIApplication*)app openURL:(NSURL*)url options:(NSDictionary<NSString*, id>*)options
{
    id sourceApplication = options[UIApplicationOpenURLOptionsSourceApplicationKey], annotation = options[UIApplicationOpenURLOptionsAnnotationKey];

    NSMutableDictionary<NSString*, id>* notifData = [NSMutableDictionary dictionaryWithCapacity: 3];
    if (url) notifData[@"url"] = url;
    if (sourceApplication) notifData[@"sourceApplication"] = sourceApplication;
    if (annotation) notifData[@"annotation"] = annotation;

    AppController_SendNotificationWithArg(kUnityOnOpenURL, notifData);
    return  [SMSDK handleApplication:app openURL:url
                                sourceApplication:[options valueForKey:@"UIApplicationOpenURLOptionsSourceApplicationKey"]
                                    annotation:[options valueForKey:@"UIApplicationOpenURLOptionsAnnotationKey"]];
}

//生命周期啟動時調(diào)用sdk初始化方法(ios的生命周期跟安卓概念差不多)

- (BOOL)application:(UIApplication*)application didFinishLaunchingWithOptions:(NSDictionary*)launchOptions
{
   //省略了部分代碼
//設(shè)置全局回調(diào)
 //初始化?
 [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(SMSDKInitCallback:) name:SMSDKInitDidFinishNotification object:nil];
 //登陸
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(SMSDKLoginCallback:) name:SMSDKLoginNotification object:nil];
 //注銷
 [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(SMSDKLogoutCallback:) name:SMSDKLogoutNotification object:nil];
  //支付
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(SMSDKPayResultCallback:) name:SMSDKPayResultNotification object:nil];
  //使用appKey初始化SDK
[SMSDK smInitWithAppKey:SMSDKAppKey];
        
   return YES;
}


  //初始化回調(diào)
  - (void)SMSDKInitCallback:(NSNotification *)notify {
         if (notify.object == kSMSDKSuccessResult) {
             NSLog(@"初始化成功");
         } else if (notify.object == kSMSDKFailedResult) {
             NSLog(@"初始化失敗");
             [SMSDK smInitWithAppKey:SMSDKAppKey]; //初始化失敗可以重新初始化
         }
    }
//登陸回調(diào)
- (void)SMSDKLoginCallback:(NSNotification *)notify {
         NSLog(@"SMSDKLoginCallback");

        if (notify.object == kSMSDKSuccessResult) {
            NSLog(@"login callback  success");

            NSString *idfa = [[[ASIdentifierManager sharedManager] advertisingIdentifier] UUIDString];
            NSString *deviceUUID = [[[UIDevice currentDevice] identifierForVendor] UUIDString];
            NSString *deviceModel = [[UIDevice currentDevice] model];
            NSString *token = notify.userInfo[kSMSDKLoginTokenKey];
            NSLog(@"解析token========:%@",token);

          //這一步是和服務器協(xié)定需要的參數(shù),這里不做真實展示每個游戲邏輯都不一樣
            NSDictionary *resultDict=@{@"":@1,@"":@"",@"":"",@"token":token,@"deviceUdid":deviceUUID,@"deviceId":deviceModel,@"idFa":idfa
            };
            NSData *data = [NSJSONSerialization dataWithJSONObject:resultDict options:NSJSONWritingPrettyPrinted error:nil];
            NSString *string = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];

           NSLog(@"json解析:%@",string);

             //向 Unity發(fā)送數(shù)據(jù)
            // 參數(shù)1綁定的場景  參數(shù)2 為對象上的腳本的一個成員方法名稱(腳本名稱不限制)
           //參數(shù)3傳遞的json數(shù)據(jù)。這個跟安卓的一樣。
            UnitySendMessage("GameLaunch","OnSdkLoginSuc",[string cStringUsingEncoding:NSASCIIStringEncoding]);


        } else {
             NSLog(@"login callback  fail");
        }
    }

    //切換賬號回調(diào)
    - (void)SMSDKLogoutCallback:(NSNotification *)notify {
         NSLog(@"LogoutCallback");
  }

    //支付結(jié)果回調(diào)
    - (void)SMSDKPayResultCallback:(NSNotification *)notify {
        //支付結(jié)果
        if (notify.object == kSMSDKSuccessResult) {
            //支付成功,刷新用戶數(shù)據(jù)
            NSLog(@"支付成功");
         
        } else if (notify.object == kSMSDKUserCancelResult) {
            
            NSLog(@"支付取消");
        } else if (notify.object == kSMSDKFailedResult) {
            //支付錯誤,刷新用戶數(shù)據(jù),保障不漏單
            NSString *errMsg = notify.userInfo[kSMSDKErrorShowKey];
            errMsg = errMsg && [errMsg isEqualToString:@""] ? errMsg : @"支付失敗";
            NSLog(@"支付錯誤");
          
        }
    }

為了方便只展示結(jié)果,展示初始化成功,意味著 sdk初始化接入是沒問題的

FD23B039AA570EAADC2BA75A85FFBB56.png

感悟

應標題那句話接入ios SDK沒有你想的那么難,這不是噱頭這是我真實的感受。我是一個搞安卓SDK的程序員,ios里面的很多思想都是和安卓Java 相通的文中我也做過解釋。只要你會用安卓接sdk,那么ios也不是那么難。大家要是有興趣可以看下Android sdk接入Unity 與 Android交互通信 之OPPO篇。

剛開始弄的時候,我承認我非常無助,找了oc語法大全??戳艘粫涂床幌铝?,即使看了一會也就忘了。等真正去操作的時候(實踐才是正道,光看知識點一會就忘),發(fā)現(xiàn)真沒那么難。思想上跟安卓接sdk一樣的,就是語法略微不同。稍微查一下就知道怎么搞了。如果文章對你有幫助留下一個贊唄,你的支持是我繼續(xù)寫下去的動力。

小表情.png


收尾

寫博文不易,希望大家多多支持,如有不對大家多多指正。寫出來就是記錄、學習和成長的過程。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。
禁止轉(zhuǎn)載,如需轉(zhuǎn)載請通過簡信或評論聯(lián)系作者。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容