好久没写博客了,真的好忙啊,没有一点下班时间,这公司好拼!!
游戏中用户的头像不仅能显示系统定义好的头像,而且如果能显示用户自定义的头像肯定能丰富游戏的表现。今天就来讨论下Unity游戏如何实现游戏中显示用户自定义头像的实现。
流程分析
- Unity中触发选择自定义头像(相机or相册)
- 调用系统原生接口弹出相机或相册供用户获取头像图片
- 对用户得到的头像进行裁剪压缩
- 上传CDN服务器或者存到本地目录(Unity项目一般存到Application.persistentDataPath目录里,这个路径不同平台的具体路径网上都有详细的介绍)
用3W从CDN服务器或本地下载头像并渲染
关于cdn服务器,游戏运营商一般会提供给游戏开发商。其大致流程是:游戏客户端上传成功后会返回一个图片的url,之后cdn服务器会进入审核流程(有自动审核和人工审核)以防止用户上传一些涉黄的头像,审核通过与否会通知(这个说法可能不好,有些cdn服务器并不支持把游戏服务器的一个函数注册到cdn服务器,这样可以主动通知游戏服务器。我们现在的做法是游戏服务器会定时去查询cdn服务器头像是否审核通过,这种轮询的做法我感觉很不好)游戏服务器,再由游戏服告诉客户端,此时客户端再做处理。
下载的时候可以做一些缓存处理,即每下载到一个新的头像,可以把它存到本地,这样做的好处是避免频繁访问cdn服务器、节省用户流量,从本地下载的速度快。坏处就是会占用设备一定的内存空间。我们现在的项目一张头像大概4kb,一千个头像大概4M,也还行。现在的手机不缺这点内存了。当然如果觉得这样不好,也可以只缓存特定的头像,比如好友的。
至于上传和下载要不要开线程那要看需求了,如果一次下载量不是很大就算了,我们现在也没开线程。如果一次下载量很大的话最好还是单开线程来处理比较稳。
实践
Android
写一个头像管理类,负责调用系统相机和相册,裁剪压缩,保存图片。
public class MY_HeadImage {
public static int HEAD_IMAGE_TAKEPHOTO = 1;
public static int HEAD_IMAGE_PICK = 2;
public static int HEAD_IMAGE_CROP = 3;
public MY_HeadImage()
{
}
/**
* 使用摄像机拍照
*/
public void takePhoto()
{
Intent intent = new Intent("android.media.action.IMAGE_CAPTURE");
intent.putExtra("output", Uri.fromFile(new File(Environment.getExternalStorageDirectory(), "temp.jpg")));
Activity act = (Activity) UnityPlayerActivity.MainContext;
act.startActivityForResult(intent,HEAD_IMAGE_TAKEPHOTO);
}
/**
* 从相册中选择
*/
public void pickFromAlbum()
{
Log.i("TEST","pickFromAlbum");
Intent intent = new Intent("android.intent.action.PICK");
intent.setDataAndType(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, "image/*");
Activity act = (Activity) UnityPlayerActivity.MainContext;
act.startActivityForResult(intent, HEAD_IMAGE_PICK);
}
/**
* 裁剪
* @param uri
*/
public void startPhotoZoom(Uri uri) {
Intent intent = new Intent("com.android.camera.action.CROP");
intent.setDataAndType(uri, "image/*");
intent.putExtra("crop", "true");
intent.putExtra("aspectX", 1);
intent.putExtra("aspectY", 1);
intent.putExtra("outputX", 256);
intent.putExtra("outputY", 256);
intent.putExtra("return-data", true);
Activity act = (Activity) UnityPlayerActivity.MainContext;
act.startActivityForResult(intent, HEAD_IMAGE_CROP);
}
public void SaveBitmap(Bitmap bitmap) throws IOException {
Log.i("TEST", "保存文件");
FileOutputStream fOut = null;
Activity act = (Activity) UnityPlayerActivity.MainContext;
String packageName = act.getPackageName();
//注解
String path = "/mnt/sdcard/Android/data/"+ packageName +"/files/";
try {
//查看这个路径是否存在,
//如果并没有这个路径,
//创建这个路径
File destDir = new File(path);
if (!destDir.exists())
{
destDir.mkdirs();
}
fOut = new FileOutputStream(path + "/image.jpg") ;
} catch (FileNotFoundException e) {
e.printStackTrace();
}
//将Bitmap对象写入本地路径中,Unity在去相同的路径来读取这个文件
bitmap.compress(Bitmap.CompressFormat.JPEG, 5, fOut);
try {
fOut.flush();
Log.i("TEST", "保存路径:" + path + "/image.jpg");
Log.i("TEST", "success");
//告诉Unity选择头像成功
UnityPlayer.UnitySendMessage("Camera", "PickHeadImgSucc","success!");
} catch (IOException e) {
e.printStackTrace();
}
try {
fOut.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
在java的主类里,注意需要事先(onCreate里)实例化headImage =new MY_HeadImage()
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent intent) {
super.onActivityResult(requestCode, resultCode, intent);
if(resultCode != 0) {
File uri;
if(requestCode == 1) {
uri = new File(Environment.getExternalStorageDirectory() + "/temp.jpg");
headImage.startPhotoZoom(Uri.fromFile(uri));
}
if(intent != null) {
if(requestCode == 2) {
headImage.startPhotoZoom(intent.getData());
}
Bitmap e;
if(requestCode == 3) {
Bundle uri2 = intent.getExtras();
if(uri2 != null) {
e = (Bitmap)uri2.getParcelable("data");
try {
headImage.SaveBitmap(e);
} catch (IOException var8) {
var8.printStackTrace();
}
}
}
}
}
}
当然还需要在主类里提供一个方法给C#调用,这个方法会弹出系统的一个Alert,供用户选择拍照还是相册里选择头像。
public void TakePhoto()
{
Dialog dlg = new AlertDialog.Builder(this).setIcon(R.drawable.app_icon)
.setTitle("选择图像").setPositiveButton("相机", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
headImage.takePhoto();
}
}).setNegativeButton("取消", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
dialog.dismiss();
}
}).setNeutralButton("相册", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
headImage.pickFromAlbum();
}
}).create();
dlg.show();
}
IOS
IOS就比较简单了,写一个.mm文件即可,一样的做一个头像管理类,负责调用系统相机和相册,裁剪压缩,保存头像等工作
@interface MY_HeadImage : UIViewController<UIImagePickerControllerDelegate,UINavigationControllerDelegate>
+(MY_HeadImage *)sharedInstance;
-(void)MenuSelect;
@end
//暴露接口,供C#调用
extern "C" void MY_OpenHeadImage(){[[MY_HeadImage sharedInstance] MenuSelect];}
@implementation MY_HeadImage
static MY_HeadImage *instance = nil;
UIViewController * selfView;
+(MY_HeadImage *)sharedInstance{
@synchronized(self) {
if(instance == nil) {
instance = [[[self class] alloc] init];
selfView = UnityGetGLViewController();
}
}
return instance;
}
-(void)MenuSelect{
UIAlertController * alertController = [UIAlertController alertControllerWithTitle:@"选择头像" message:@"" preferredStyle:UIAlertControllerStyleActionSheet];
// UIAlertControllerStyleAlert在中央屏幕。
// UIAlertControllerStyleActionSheet在屏幕底部。
UIAlertAction *useCamera = [UIAlertAction actionWithTitle:@"相机" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
NSLog(@"拍照");
[instance pickFromCamera];
}];
UIAlertAction *usePhoto = [UIAlertAction actionWithTitle:@"相册" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
NSLog(@"相册");
[instance pickFromAlbum];
}];
UIAlertAction *cancelAction = [UIAlertAction actionWithTitle:@"取消" style:UIAlertActionStyleCancel handler:nil];
[alertController addAction:useCamera];
[alertController addAction:usePhoto];
[alertController addAction:cancelAction];
[selfView presentViewController:alertController animated:YES completion:nil];
}
//从相机选择
-(void)pickFromCamera
{
UIImagePickerController *imagePicker = [[UIImagePickerController alloc] init];
imagePicker.delegate = self;
imagePicker.allowsEditing = YES;
imagePicker.sourceType = UIImagePickerControllerSourceTypeCamera;
[selfView presentViewController:imagePicker animated:YES completion:nil];
}
//从相册选择
-(void)pickFromAlbum
{
UIImagePickerController *imagePicker = [[UIImagePickerController alloc] init];
imagePicker.delegate = self;
imagePicker.allowsEditing = YES;
imagePicker.sourceType = UIImagePickerControllerSourceTypePhotoLibrary;
[selfView presentViewController:imagePicker animated:YES completion:nil];
}
//选择完成回调(系统自己调用的,别找了)
- (void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary *)info
{
NSLog(@"选择头像完成");
UIImage *img = [info objectForKey:UIImagePickerControllerEditedImage];
[self performSelector:@selector(saveImage:) withObject:img afterDelay:0.5];
[picker dismissViewControllerAnimated:YES completion:nil];
}
//保存图片
- (void)saveImage:(UIImage *)image {
BOOL success;
NSFileManager *fileManager = [NSFileManager defaultManager];
NSError *error;
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentsDirectory = [paths objectAtIndex:0];
NSString *imageFilePath = [documentsDirectory stringByAppendingPathComponent:@"image.jpg"];
NSLog(@"头像写入[IOS]->>%@",imageFilePath);
success = [fileManager fileExistsAtPath:imageFilePath];
if(success) {
success = [fileManager removeItemAtPath:imageFilePath error:&error];
}
UIImage *smallImage = [self thumbnailWithImageWithoutScale:image size:CGSizeMake(93, 93)];
[UIImageJPEGRepresentation(smallImage, 1.0f) writeToFile:imageFilePath atomically:YES];//写入文件
UnitySendMessage("Camera", "PickHeadImgSucc", "image.jpg");
}
//保持原来的长宽比,生成一个缩略图
- (UIImage *)thumbnailWithImageWithoutScale:(UIImage *)image size:(CGSize)asize
{
UIImage *newimage;
if (nil == image) {
newimage = nil;
}
else{
CGSize oldsize = image.size;
CGRect rect;
if (asize.width/asize.height > oldsize.width/oldsize.height) {
rect.size.width = asize.height*oldsize.width/oldsize.height;
rect.size.height = asize.height;
rect.origin.x = (asize.width - rect.size.width)/2;
rect.origin.y = 0;
}
else{
rect.size.width = asize.width;
rect.size.height = asize.width*oldsize.height/oldsize.width;
rect.origin.x = 0;
rect.origin.y = (asize.height - rect.size.height)/2;
}
UIGraphicsBeginImageContext(asize);
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSetFillColorWithColor(context, [[UIColor clearColor] CGColor]);
UIRectFill(CGRectMake(0, 0, asize.width, asize.height));//clear background
[image drawInRect:rect];
newimage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
}
return newimage;
}
@end
Unity
写一个类挂摄像机上(Camera),主要逻辑:
#if UNITY_ANDROID && !UNITY_EDITOR
private AndroidJavaClass jc = null;
public void OpenSelectorMenu()
{
jc = new AndroidJavaClass("com.unity3d.player.UnityPlayer");
AndroidJavaObject JO = jc.GetStatic<AndroidJavaObject>("currentActivity");
JO.Call("TakePhoto");
}
#elif UNITY_IPHONE && !UNITY_EDITOR
[DllImport("__Internal")]
private static extern void MY_OpenHeadImage ();
public void OpenSelectorMenu ()
{
MY_OpenHeadImage();
}
#else
public void OpenSelectorMenu ()
{
}
#endif
//在android或者IOS原生代码里调用了
//所以Unity侧引用为0,勿删
//在移动设备上选择好头像后回调到unity
public void PickHeadImgSucc ( string str )
{
Debug.Log("保存图片成功 image.jpg: " + str);
string url = "file://" + Application.persistentDataPath + "/image.jpg";
//在此用3W下载本地头像并渲染
}
效果图就不上了,项目已用。
另外上传的时候需要把Texture2D转化为byte[],方法是Texture2D.EncodeToJPG()。