运营活动的设计目的是:提升游戏的充值付费、留存、玩家活跃、在线时长、LVT等指标。
1、活动类型
活动也是拉营收的最主要的方式和手段,这也是运营同学的主要工作,运营活动最常见的莫过下面这些:
1、充值活动,比如首充活动,充值送道具等等活动
2、转盘抽奖活动,比如收集碎片进行抽奖,或者买道具进行抽奖;
3、开服活动;七日登陆活动,开服
4、回归活动;邀请老玩家回归
5、冲级活动,达到多少级可以领取礼包,礼盒。
6、商城打折、限时、团购促销活动;
7、每日及累计签到活动;
8、BOSS活动;世界boss活动,公会boss活动
9、比赛活动;比拼厨技等
10、在线奖励及BUFF活动;
11、公会活动,之前玩过的蜀门有公会开树增加经验活动
12、答题活动,火影忍者手游的答题活动
13、分享活动;分享到朋友圈拿奖励
2、需求
从第一部分可以看到活动的需求还是多种多样的,活动系统最主要的需求
1.可以动态的调整线上的活动
2.可以根据配置的时间进行开启,关闭,领奖。
3.方便配置,选择json格式配置,前公司用的是xml ,很烦,当时还有层级的限制。
需求有了,现在开始制定方案,也不绕弯子了,直接阐明我们当前使用的技术方案。
1.运营配置活动,并且发布到 web 服务器
2.运营调用web 命令,通知各个服务器进行活动更新,读取新的活动
3.游戏服务器下载打包的活动数据到本地
4.读取活动的数据
5.加载进内存
3、文件下载
http下载java有几种常用的方式,
一种是使用httpClient ,
另一种则是通过HttpURLConnection去实现,HttpURLConnection是JAVA的标准类,是JAVA原生的一种实现方式。
还有就是我选择使用Okhttp,我选择的原因就是不想用httpclient ,就这么简单,任性。
pom.xml 中加入以下依赖:
代码语言:javascript复制 <dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>4.8.0</version>
</dependency>
web 服务器我选择了一个小巧的服务器NetBox2.exe ,具体的说明如下,想要测试这个服务器,可以直接手写一个hello world 的index.html 和NetBox2.exe 放在一起,然后直接使用http://localhost:49983(端口可以右键任务栏netbox 查看,默认是80端口) ,默认会访问index.html.
优点:这个服务器不需要安装,直接运行即可,同时比较小巧,只有不到636k,携带方便。不需要做任何的配置,直接使用,是测试的时候不错的选择,在线上的时候可以再切换到tomcat或者Nginx 等服务器,想要这个服务器的可以关注我公众号【香菜聊游戏】,回复NetBox 就可以了。
使用了异步下载的机制,这样不至于卡掉线程,将进度进行回调。
具体的测试代码如下:
代码语言:javascript复制package com.ploy;
import lombok.extern.slf4j.Slf4j;
import okhttp3.*;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
/**
* 文件下载工具类(单例模式)(有借鉴别人代码,找不到地址了,可以联系我)
*
* @author 香菜
*/
@Slf4j
public class DownloadUtil {
public static OkHttpClient okHttpClient;
/**
* 初始化 httpClient
*
* @return
*/
public static synchronized OkHttpClient getHttpClient() {
if (okHttpClient == null) {
okHttpClient = new OkHttpClient();
}
return okHttpClient;
}
public interface OnDownloadListener {
/**
* 下载成功之后的文件
*/
void onDownloadSuccess(File file);
/**
* 下载进度
*/
void onDownloading(int progress);
/**
* 失败信息
*/
void onDownloadFailed(Exception e);
}
/**
* @param url 下载连接
* @param destFileDir 下载的文件储存目录
* @param destFileName 下载文件名称,后面记得拼接后缀,否则手机没法识别文件类型
* @param listener 下载监听
*/
public static void downloadByAsync(final String url, final String destFileDir, final String destFileName, final OnDownloadListener listener) {
Request request = new Request.Builder()
.url(url)
.build();
//异步请求
getHttpClient().newCall(request).enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
// 下载失败监听回调
listener.onDownloadFailed(e);
}
@Override
public void onResponse(Call call, Response response) {
if (response.body() == null) {
listener.onDownloadFailed(new Exception(" body is null"));
return;
}
byte[] buf = new byte[4096];
int len = 0;
// 储存下载文件的目录
File dir = new File(destFileDir);
if (!dir.exists()) {
dir.mkdirs();
}
File file = new File(dir, destFileName);
try (InputStream is = response.body().byteStream();
FileOutputStream fos = new FileOutputStream(file)) {
int size = 0;
long total = response.body().contentLength();
while ((size = is.read(buf)) != -1) {
len = size;
fos.write(buf, 0, size);
int process = (int) Math.floor(((double) len / total) * 100);
// 控制台打印文件下载的百分比情况
listener.onDownloading(process);
}
fos.flush();
// 下载完成
listener.onDownloadSuccess(file);
} catch (Exception e) {
log.error("error:{}", e);
listener.onDownloadFailed(e);
}
}
});
}
public static void main(String[] args) {
//异步下载
DownloadUtil.downloadByAsync("http://localhost/aaa.zip",
"E:\video\Learn\Learn\src\main\java\com\ploy\", "abc.zip", new DownloadUtil.OnDownloadListener() {
@Override
public void onDownloadSuccess(File file) {
log.info("下载完成");
}
@Override
public void onDownloading(int progress) {
log.info("下载进行中" progress);
}
@Override
public void onDownloadFailed(Exception e) {
//下载异常进行相关提示操作
log.error("下载出错", e);
}
});
}
4、文件解压
文件下载回来之后,需要解压,解压选择了jdk 自带的解压方式,具体的用法都在代码里,也没什么难的。
知识点 :最主要是文件夹的创建 pathFile.mkdirs();
还有zipFile 的使用
代码语言:javascript复制package com.ploy;
import java.io.*;
import java.nio.charset.Charset;
import java.util.Enumeration;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
/**
* 解压工具类
* @author 香菜
*/
public class UnzipUtil {
/**
* 解压文件到指定目录
*/
@SuppressWarnings("rawtypes")
public static void unZipFiles(File zipFile, String descDir) throws IOException {
File pathFile = new File(descDir);
if (!pathFile.exists()) {
// 如果路径上文件夹不存在,则创建
pathFile.mkdirs();
}
//解决zip文件中有中文目录或者中文文件
ZipFile zip = new ZipFile(zipFile, Charset.forName("GBK"));
byte[] buf1 = new byte[1024];
for (Enumeration entries = zip.entries(); entries.hasMoreElements(); ) {
ZipEntry entry = (ZipEntry) entries.nextElement();
String zipEntryName = entry.getName();
try (InputStream in = zip.getInputStream(entry)) {
String outPath = (descDir zipEntryName).replaceAll("\*", "/");
//判断路径是否存在,不存在则创建文件路径
File file = new File(outPath.substring(0, outPath.lastIndexOf('/')));
if (!file.exists()) {
file.mkdirs();
}
//判断文件全路径是否为文件夹,如果是上面已经上传,不需要解压
if (new File(outPath).isDirectory()) {
continue;
}
//输出文件路径信息
System.out.println(outPath);
try (OutputStream out = new FileOutputStream(outPath)) {
int len;
while ((len = in.read(buf1)) > 0) {
out.write(buf1, 0, len);
}
}
}
}
System.out.println("******************解压完毕********************");
}
public static void main(String[] args) throws IOException {
/**
* 解压文件
*/
File zipFile = new File("E:\video\Learn\Learn\src\main\java\com\ploy\abc.zip");
String path = "E:/video/Learn/Learn/src/main/java/com/ploy/";
unZipFiles(zipFile, path);
}
}
5、json的读取
json的读取使用了fastjson 的库,使用简单,同时也配置比较方便,解析也比较方便。
知识点:文件读取,fastjson 的使用
代码语言:javascript复制package com.ploy;
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
/**
* @author 香菜
*/
public class FileUtil {
public static String readFile(String filePath) throws IOException {
FileReader fileReader = new FileReader(filePath);
BufferedReader bReader = new BufferedReader(fileReader);//new一个BufferedReader对象,将文件内容读取到缓存
StringBuilder sb = new StringBuilder();//定义一个字符串缓存,将字符串存放缓存中
String s = "";
while ((s = bReader.readLine()) != null) {//逐行读取文件内容,不读取换行符和末尾的空格
sb.append(s);//将读取的字符串添加换行符后累加存放在缓存中
System.out.println(s);
}
bReader.close();
String jsonStr = sb.toString();
System.out.println(jsonStr);
return jsonStr;
}
}
6、模块组织方式
压缩包的截图
ployMenu.json说明:ployMenu.json 是所有活动的菜单,具体的菜单是整个所有的活动
各字段说明:
pid : 活动id,是关联活动详情的id
type:是活动的类型
begin: 活动的开始时间
end :活动的结束时间
draw : 是活动可以领奖的时间,一般要大于等于活动结束时间
代码语言:javascript复制[
{
"begin": 2,
"draw": 4,
"end": 3,
"pid": 6,
"type": 6
}
]
6.json 说明:
6.json 是活动id 为6 的具体的活动信息,每个活动类型的不同,可以自定义,只要是json格式就行
代码语言:javascript复制{"name":"香菜","s":18}
7、代码展示
活动对象定义:
代码语言:javascript复制package com.ploy;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class PloyVO {
//活动ID
private int pid;
//活动开始时间秒
private int begin;
//活动结束时间秒
private int end;
//活动可领奖秒
private int draw;
//活动类型
private int type;
//活动明细对象
private Object detail;
}
活动枚举定义:
代码语言:javascript复制package com.ploy;
import com.ploy.detail.TestDetailVO;
public enum PloyEnum {
SEVEN_LOGIN(6, TestDetailVO.class)
;
private int ployType;
private Class detailClass;
PloyEnum(int ployType, Class detailClass) {
this.ployType = ployType;
this.detailClass = detailClass;
}
public int getPloyType() {
return ployType;
}
public Class getDetailClass() {
return detailClass;
}
public static PloyEnum getPloyType(int type) {
for (PloyEnum pt : values()) {
if (pt.getPloyType() == type) {
return pt;
}
}
return null;
}
}
示例活动详细详情:
代码语言:javascript复制package com.ploy.detail;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
/**
* 示例活动详细详情
*/
public class TestDetailVO {
private String name;
private int s;
}
活动工具类:
代码语言:javascript复制package com.ploy;
import com.alibaba.fastjson.JSON;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import lombok.extern.slf4j.Slf4j;
import java.io.File;
import java.io.IOException;
import java.util.Date;
import java.util.List;
import java.util.Map;
/**
* 活动工具类
* @author 香菜
*/
@Slf4j
public class PloyUtil {
//活动缓存 KEY:活动ID VALUE:活动对象
private static Map<Integer, PloyVO> ployMap = Maps.newConcurrentMap();
public static void main(String[] args) {
reloadPloy();
}
public static void reloadPloy() {
Map<Integer, PloyVO> tmpPloyMap = Maps.newConcurrentMap();
// 下载文件
//异步下载
DownloadUtil.downloadByAsync("http://localhost/ployPkg.zip",
"E:/video/Learn/Learn/src/main/java/com/ploy/unzip", "ployPkg.zip", new DownloadUtil.OnDownloadListener() {
@Override
public void onDownloadSuccess(File file) {
// 解压文件
try {
String foldPath = "E:\video\Learn\Learn\src\main\java\com\ploy\unzip\";
UnzipUtil.unZipFiles(file, foldPath);
String jsonStr = FileUtil.readFile(foldPath "/ployPkg/ployMenu.json");
List<PloyVO> ployList = JSON.parseArray(jsonStr, PloyVO.class);
for (PloyVO ployVO : ployList) {
// 解析配置文件
parsePloy(ployVO, foldPath "/ployPkg");
tmpPloyMap.put(ployVO.getPid(), ployVO);
}
ployMap = tmpPloyMap;
System.out.println("load finish");
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public void onDownloading(int progress) {
log.info("下载进行中" progress);
}
@Override
public void onDownloadFailed(Exception e) {
//下载异常进行相关提示操作
log.error("下载出错", e);
}
});
}
private static void parsePloy(PloyVO ployVO, String folderPath) throws IOException {
int type = ployVO.getType();
String s = FileUtil.readFile(folderPath "/" ployVO.getPid() ".json");
PloyEnum ployType = PloyEnum.getPloyType(type);
Object o = JSON.parseObject(s, ployType.getDetailClass());
ployVO.setDetail(o);
}
/**
* 获取活动对象
*
* @param pid
* @return
*/
public static PloyVO getPloy(int pid) {
PloyVO pv = ployMap.get(pid);
if (pv == null) {
log.error("not found ploy pid is " pid);
}
return pv;
}
/**
* 检测是否在活动时间范围内
*
* @param pv
* @param nowSec
* @return
*/
public static boolean checkInPloyTime(PloyVO pv, int nowSec) {
return pv.getBegin() <= nowSec && (pv.getEnd() == 0 || nowSec <= pv.getEnd());
}
/**
* 根据活动类型获得活动信息(限同一时刻只能出现一个该类型的活动)
*
* @param ployEnum
* @return
*/
public static List<PloyVO> getPloyByType(PloyEnum ployEnum) {
List<PloyVO> retList = Lists.newArrayList();
int nowSec = (int) (new Date().getTime() / 1000L);
for (PloyVO pvo : ployMap.values()) {
if (pvo.getType() == ployEnum.getPloyType() && checkInPloyTime(pvo, nowSec)) {
retList.add(pvo);
}
}
return retList;
}
}
活动重新加载的入口是reloadPloy(),在需要重新加载活动数据的时候直接调用reload,
注意:新活动先加载到内存,然后再覆盖ployMap
运行ployUtil,可以看到数据已经加载到内存:
8、还有哪些优化点
1、对活动数据进行加密,签名,防止不法之徒获取运营数据
2、ployUtil只提供了一些几个简单的结构,可以根据需求增加一些新的接口,比如根据活动类型获取数据,或者当前所有的开启的活动等等接口,方便在使用的时候调用
3、和客户端通信,在玩家登陆的时候可以把活动的数据发给客户端,这样数据和服务器保持一致,每个活动自己通信就可以了。客户端可以根据活动的时间判断,或者开启活动,或者去除活动的icon.
4、代码只是展示了思路,但是还有些细节没有处理,比如异常的处理,在项目中使用的时候可以根据项目的内容进行调整
5、可以将程序中的一些路径等等当做配置,而不是写死在代码里
9、总结
知识点:
- OkHttp 的使用,异步下载文件到本地,DownloadUtil
- 解压zip文件的方式,方法,平常比较少用的工具类,ZipUtil
- 读取文件到字符串,Java IO 的使用 FileUtil
- fastJson 的使用,将字符串转为List,
- 活动的设计模式,对每个活动的单独读取的使用方式
活动流程:
- 运营策划活动
- 运营配置活动并打包放到web服务器上
- 通知游戏服加载新活动
- 游戏服 下载活动到本地
- 解压活动压缩包
- 读取ployMenu.json,生成ployList
- 根据ployVO 具体生成 活动细节