引言

在现代游戏开发中,社交功能已成为提升用户留存和活跃度的重要手段。好友排行榜作为核心社交功能之一,其实现方式和用户体验直接影响玩家互动质量。本文将介绍如何结合Unity游戏引擎和ArkTS(鸿蒙系统的声明式开发语言)创建高性能、可复用的"好友排行榜"原子化组件,实现跨平台一致的用户体验。

技术选型优势

​Unity+ArkTS组合方案的优势​​:

  • ​性能优化​​:ArkTS的声明式UI渲染效率高
  • ​跨平台能力​​:一套代码适配手机、平板等多设备
  • ​开发效率​​:原子化组件可复用,减少重复开发
  • ​生态融合​​:无缝接入鸿蒙系统社交能力

排行榜数据结构设计

首先定义排行榜数据模型:

// models/RankingData.ets
export class FriendRankItem {
  uid: string = '';
  nickname: string = '';
  avatar: string = '';
  score: number = 0;
  rank: number = 0;
  isOnline: boolean = false;
  
  // 用于增量更新检查
  isDataChanged(newData: FriendRankItem): boolean {
    return this.score !== newData.score || 
           this.isOnline !== newData.isOnline;
  }
}

export class RankingListData {
  version: number = 0; // 数据版本号
  selfRank: FriendRankItem = new FriendRankItem();
  friends: FriendRankItem[] = [];
  
  // 更新数据并返回是否有变化
  updateWith(newData: RankingListData): boolean {
    let changed = false;
    
    if (this.selfRank.isDataChanged(newData.selfRank)) {
      this.selfRank = {...newData.selfRank};
      changed = true;
    }
    
    if (this.friends.length !== newData.friends.length) {
      this.friends = [...newData.friends];
      return true;
    }
    
    for (let i = 0; i < this.friends.length; i++) {
      if (this.friends[i].isDataChanged(newData.friends[i])) {
        this.friends[i] = {...newData.friends[i]};
        changed = true;
      }
    }
    
    return changed;
  }
}

ArkTS原子化组件实现

1. 单项排行榜组件

// components/FriendRankItemView.ets
@Component
export struct FriendRankItemView {
  @Prop item: FriendRankItem
  @Link isExpanded: boolean
  
  build() {
    Row() {
      // 排名显示
      Text(this.item.rank.toString())
        .fontSize(16)
        .fontWeight(FontWeight.Bold)
        .width(40)
        .textAlign(TextAlign.Center)
      
      // 头像
      Image(this.item.avatar)
        .width(40)
        .height(40)
        .borderRadius(20)
        .objectFit(ImageFit.Contain)
      
      Column() {
        // 昵称和分数
        Row() {
          Text(this.item.nickname)
            .fontSize(16)
            .maxLines(1)
            .textOverflow({overflow: TextOverflow.Ellipsis})
          
          if (this.isExpanded) {
            Text(`分数: ${this.item.score}`)
              .fontSize(14)
              .margin({left: 10})
              .opacity(0.8)
          }
        }
        .width('100%')
        .justifyContent(FlexAlign.SpaceBetween)
        
        // 在线状态
        if (this.item.isOnline) {
          Text('在线')
            .fontSize(12)
            .fontColor('#07C160')
        }
      }
      .layoutWeight(1)
      .margin({left: 10})
      
      // 展开/收起图标
      Image($r('app.media.arrow'))
        .width(20)
        .height(20)
        .rotate(this.isExpanded ? 90 : 0)
        .animation({duration: 200})
    }
    .padding(10)
    .borderRadius(8)
    .backgroundColor('#FFFFFF')
    .shadow({radius: 2, color: '#10000000', offsetX: 1, offsetY: 1})
    .onClick(() => {
      this.isExpanded = !this.isExpanded;
    })
  }
}

2. 完整排行榜组件

// components/FriendRankingList.ets
@Component
export struct FriendRankingList {
  @State rankingData: RankingListData = new RankingListData();
  @State isExpanded: boolean = false;
  @State isLoading: boolean = false;
  
  // 从Unity接收数据更新
  @Watch('onDataUpdate')
  @Consume('unityData') @Prop unityData?: string;
  
  // 定时刷新
  private timer: number = 0;
  
  onDataUpdate() {
    if (this.unityData) {
      try {
        const newData = JSON.parse(this.unityData) as RankingListData;
        if (this.rankingData.updateWith(newData)) {
          this.rankingData = {...this.rankingData};
        }
      } catch (e) {
        console.error('Parse ranking data error:', e);
      }
    }
  }
  
  onPageShow() {
    this.startAutoRefresh();
    this.loadData();
  }
  
  onPageHide() {
    this.stopAutoRefresh();
  }
  
  startAutoRefresh() {
    this.timer = setInterval(() => {
      this.loadData();
    }, 30000); // 30秒刷新一次
  }
  
  stopAutoRefresh() {
    if (this.timer) {
      clearInterval(this.timer);
      this.timer = 0;
    }
  }
  
  async loadData() {
    if (this.isLoading) return;
    
    this.isLoading = true;
    try {
      // 调用Unity接口获取数据
      const result = await callUnity('GetFriendRanking');
      const newData = JSON.parse(result) as RankingListData;
      this.rankingData.updateWith(newData);
    } finally {
      this.isLoading = false;
    }
  }
  
  build() {
    Column() {
      // 标题栏
      Row() {
        Text('好友排行榜')
          .fontSize(20)
          .fontWeight(FontWeight.Bold)
        
        Button(this.isLoading ? '加载中...' : '刷新')
          .fontSize(14)
          .margin({left: 20})
          .onClick(() => this.loadData())
      }
      .width('100%')
      .padding(15)
      
      // 自己的排名
      FriendRankItemView({
        item: this.rankingData.selfRank,
        isExpanded: this.isExpanded
      })
      .backgroundColor('#F0F9FF')
      
      // 好友列表
      List({ space: 10 }) {
        ForEach(this.rankingData.friends, (item: FriendRankItem) => {
          ListItem() {
            FriendRankItemView({
              item: item,
              isExpanded: this.isExpanded
            })
          }
        }, (item: FriendRankItem) => item.uid)
      }
      .layoutWeight(1)
      .divider({strokeWidth: 1, color: '#F0F0F0'})
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#F8F8F8')
    .padding(10)
  }
}

Unity端集成方案

1. 数据桥接接口

// UnityRankingBridge.cs
using UnityEngine;
using System;
using System.Runtime.InteropServices;

public class UnityRankingBridge : MonoBehaviour
{
    // 单例模式
    public static UnityRankingBridge Instance { get; private set; }
    
    // ArkTS调用的回调
    public delegate string GetRankingDataDelegate();
    public static event GetRankingDataDelegate OnGetRankingData;
    
    private void Awake()
    {
        if (Instance == null)
        {
            Instance = this;
            DontDestroyOnLoad(gameObject);
            RegisterUnityCallbacks();
        }
        else
        {
            Destroy(gameObject);
        }
    }
    
    [DllImport("__Internal")]
    private static extern void RegisterUnityCallbacks();
    
    // 供ArkTS调用的方法
    [AOT.MonoPInvokeCallback(typeof(GetRankingDataDelegate))]
    public static string GetFriendRanking()
    {
        try
        {
            var data = new RankingListData()
            {
                version = 1,
                selfRank = new FriendRankItem()
                {
                    uid = "user_123",
                    nickname = PlayerPrefs.GetString("nickname", "我"),
                    avatar = "default_avatar",
                    score = PlayerPrefs.GetInt("high_score", 0),
                    rank = 5,
                    isOnline = true
                },
                friends = GetMockFriendsData()
            };
            
            return JsonUtility.ToJson(data);
        }
        catch (Exception e)
        {
            Debug.LogError($"GetFriendRanking error: {e}");
            return "{}";
        }
    }
    
    private static FriendRankItem[] GetMockFriendsData()
    {
        // 模拟数据,实际项目从服务器获取
        return new FriendRankItem[]
        {
            new FriendRankItem { uid="1", nickname="玩家A", score=4500, rank=1, isOnline=true },
            new FriendRankItem { uid="2", nickname="玩家B", score=4200, rank=2, isOnline=false },
            new FriendRankItem { uid="3", nickname="玩家C", score=3800, rank=3, isOnline=true },
            new FriendRankItem { uid="4", nickname="玩家D", score=3600, rank=4, isOnline=true },
            new FriendRankItem { uid="6", nickname="玩家F", score=3200, rank=6, isOnline=false },
            new FriendRankItem { uid="7", nickname="玩家G", score=3000, rank=7, isOnline=true },
            new FriendRankItem { uid="8", nickname="玩家H", score=2800, rank=8, isOnline=false },
            new FriendRankItem { uid="9", nickname="玩家I", score=2500, rank=9, isOnline=true },
            new FriendRankItem { uid="10", nickname="玩家J", score=2000, rank=10, isOnline=false }
        };
    }
    
    // 更新ArkTS端数据
    public void UpdateArkTSRanking(string jsonData)
    {
#if UNITY_ANDROID && !UNITY_EDITOR
        UpdateArkTSRankingData(jsonData);
#endif
    }
    
    [DllImport("__Internal")]
    private static extern void UpdateArkTSRankingData(string jsonData);
}

// 数据结构需与ArkTS端保持一致
[Serializable]
public class FriendRankItem
{
    public string uid;
    public string nickname;
    public string avatar;
    public int score;
    public int rank;
    public bool isOnline;
}

[Serializable]
public class RankingListData
{
    public int version;
    public FriendRankItem selfRank;
    public FriendRankItem[] friends;
}

2. Unity中的排行榜控制器

// RankingController.cs
using UnityEngine;
using System.Collections;

public class RankingController : MonoBehaviour
{
    [SerializeField] private float refreshInterval = 30f;
    
    private Coroutine refreshCoroutine;
    
    private void OnEnable()
    {
        StartAutoRefresh();
    }
    
    private void OnDisable()
    {
        StopAutoRefresh();
    }
    
    private void StartAutoRefresh()
    {
        if (refreshCoroutine != null)
            StopCoroutine(refreshCoroutine);
        
        refreshCoroutine = StartCoroutine(RefreshRoutine());
    }
    
    private void StopAutoRefresh()
    {
        if (refreshCoroutine != null)
        {
            StopCoroutine(refreshCoroutine);
            refreshCoroutine = null;
        }
    }
    
    private IEnumerator RefreshRoutine()
    {
        while (true)
        {
            yield return RefreshRankingData();
            yield return new WaitForSeconds(refreshInterval);
        }
    }
    
    public IEnumerator RefreshRankingData()
    {
        // 实际项目中这里应该从服务器获取数据
        yield return new WaitForSeconds(1f); // 模拟网络延迟
        
        // 构造测试数据
        var newData = new RankingListData()
        {
            version = Time.frameCount,
            selfRank = new FriendRankItem()
            {
                uid = "user_123",
                nickname = PlayerPrefs.GetString("nickname", "我"),
                avatar = "default_avatar",
                score = Random.Range(3000, 5000), // 随机分数用于测试
                rank = Random.Range(1, 10),
                isOnline = true
            },
            friends = UnityRankingBridge.GetMockFriendsData()
        };
        
        // 更新ArkTS端
        UnityRankingBridge.Instance.UpdateArkTSRanking(
            JsonUtility.ToJson(newData));
    }
}

多设备适配方案

1. 响应式布局

// components/FriendRankingList.ets
@Component
export struct FriendRankingList {
  @State isLandscape: boolean = false;
  
  onWindowSizeChange() {
    this.isLandscape = window.innerWidth > window.innerHeight;
  }
  
  build() {
    Column() {
      // 标题栏适配
      Row() {
        Text('好友排行榜')
          .fontSize(this.isLandscape ? 24 : 20)
        
        Button(this.isLoading ? '加载中...' : '刷新')
          .fontSize(this.isLandscape ? 16 : 14)
          .margin({left: this.isLandscape ? 30 : 20})
      }
      .padding(this.isLandscape ? 20 : 15)
      
      // 列表布局适配
      if (this.isLandscape) {
        this.landscapeLayout()
      } else {
        this.portraitLayout()
      }
    }
    .onAreaChange(() => this.onWindowSizeChange())
  }
  
  @Builder landscapeLayout() {
    Row() {
      // 左侧-玩家信息
      Column() {
        FriendRankItemView({
          item: this.rankingData.selfRank,
          isExpanded: true // 横屏下始终展开
        })
        
        // 其他功能区域...
      }
      .width('40%')
      
      // 右侧-排行榜
      Column() {
        List({ space: 10 }) {
          ForEach(this.rankingData.friends, (item: FriendRankItem) => {
            ListItem() {
              FriendRankItemView({
                item: item,
                isExpanded: true // 横屏下始终展开
              })
            }
          })
        }
      }
      .width('60%')
    }
  }
  
  @Builder portraitLayout() {
    // 竖屏布局(原实现)
    Column() {
      FriendRankItemView({
        item: this.rankingData.selfRank,
        isExpanded: this.isExpanded
      })
      
      List({ space: 10 }) {
        ForEach(this.rankingData.friends, (item: FriendRankItem) => {
          ListItem() {
            FriendRankItemView({
              item: item,
              isExpanded: this.isExpanded
            })
          }
        })
      }
    }
  }
}

2. 动态样式调整

// 在FriendRankItemView.ets中添加
@Styles function rankTextStyle() {
  .fontSize(DeviceInfo.isTablet ? 20 : 16)
  .width(DeviceInfo.isTablet ? 60 : 40)
}

@Styles function avatarStyle() {
  .width(DeviceInfo.isTablet ? 50 : 40)
  .height(DeviceInfo.isTablet ? 50 : 40)
  .borderRadius(DeviceInfo.isTablet ? 25 : 20)
}

// 使用样式
Text(this.item.rank.toString())
  .rankTextStyle()

Image(this.item.avatar)
  .avatarStyle()

性能优化策略

1. 数据差异更新

// 在FriendRankingList.ets中修改
onDataUpdate() {
  if (this.unityData) {
    try {
      const newData = JSON.parse(this.unityData) as RankingListData;
      
      // 只更新变化的数据项
      if (this.rankingData.version !== newData.version) {
        const changedItems: FriendRankItem[] = [];
        
        // 检查自身数据
        if (this.rankingData.selfRank.isDataChanged(newData.selfRank)) {
          changedItems.push(newData.selfRank);
        }
        
        // 检查好友列表
        for (let i = 0; i < newData.friends.length; i++) {
          if (i >= this.rankingData.friends.length || 
              this.rankingData.friends[i].isDataChanged(newData.friends[i])) {
            changedItems.push(newData.friends[i]);
          }
        }
        
        // 应用更新
        if (changedItems.length > 0) {
          this.rankingData = {...newData};
          this.playUpdateAnimation(changedItems);
        }
      }
    } catch (e) {
      console.error('Parse ranking data error:', e);
    }
  }
}

// 播放更新动画
private playUpdateAnimation(changedItems: FriendRankItem[]) {
  changedItems.forEach(item => {
    const itemId = `rank_item_${item.uid}`;
    const animator = this.animators[itemId];
    
    if (animator) {
      animator.stop();
      animator.play({
        duration: 300,
        iterations: 1,
        easing: 'ease-out'
      });
    }
  });
}

2. 列表项复用优化

// 修改List配置
List({ space: 10 }) {
  ForEach(this.rankingData.friends, (item: FriendRankItem) => {
    ListItem() {
      FriendRankItemView({
        item: item,
        isExpanded: this.isExpanded
      })
      .transition({ type: TransitionType.InsertDelete })
    }
  }, (item: FriendRankItem) => item.uid)
}
.scrollBar(BarState.Off)
.cachedCount(5) // 预缓存项数
.edgeEffect(EdgeEffect.None)

完整使用示例

// entry.ets
import { FriendRankingList } from './components/FriendRankingList';

@Entry
@Component
struct GameSocial {
  build() {
    Column() {
      // 游戏顶部导航
      GameHeader()
      
      // 好友排行榜组件
      FriendRankingList()
        .layoutWeight(1)
      
      // 游戏底部导航
      GameFooter()
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#F0F0F0')
  }
}

// 模拟游戏其他组件
@Component
struct GameHeader {
  build() {
    Row() {
      Text('我的游戏')
        .fontSize(20)
      // 其他头部元素...
    }
    .padding(10)
    .backgroundColor('#FFFFFF')
  }
}

@Component
struct GameFooter {
  build() {
    Row() {
      Button('首页').layoutWeight(1)
      Button('社交').layoutWeight(1)
      Button('设置').layoutWeight(1)
    }
    .height(50)
    .backgroundColor('#FFFFFF')
  }
}

总结

通过Unity与ArkTS的结合,我们实现了:

  1. ​高性能好友排行榜​​:利用ArkTS声明式UI的高效渲染
  2. ​真正的跨平台体验​​:适配手机、平板等多种设备
  3. ​数据实时同步​​:Unity与ArkTS的高效数据通信
  4. ​可复用的原子化组件​​:组件可在不同场景重复使用
  5. ​丰富的交互效果​​:平滑的动画和过渡效果

这种集成方案充分发挥了Unity的游戏引擎能力和ArkTS的UI优势,为游戏社交功能开发提供了新的技术路径。开发者可以根据实际需求扩展更多功能,如礼物系统、成就展示等,构建更完整的游戏社交生态。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐