这个小游戏中主要介绍:
1. 一个比较经典好玩易实现的虚拟摇杆的创建和使用;
2. 使用链表实现蛇的增长和移动,代替数组实现,避免初始化蛇角色时分配空间带来的麻烦;
3. 之前发过一篇socket通信的帖子,顺手移植到这个小游戏中玩一下;
4. 如何使用单例模式在整个工程中传递一个且只能存在一个的对象;
5. 接口怎么用;
6. ........
先看一下效果:
fig1. 效果图
step1: 首先博主需要自定义一个虚拟摇杆,用来控制角色,设计思想很简单,利用View的onTouchEvent(MotionEvent motionEvent)改变小圆的圆心位置,再根据小圆圆心和大圆圆心的位置,得到触屏移动的相对方位,作为后续蛇角色移动的输入。虚拟摇杆设计如fig2图所示,设计灵感以及虚拟遥杆实现源代码来自https://blog.csdn.net/xiaominghimi/article/details/6423983
fig2. 虚拟摇杆
RockerView类实现, 继承View, 实现RadListener(自定义),实现自定义控件
初始化需要绘制的虚拟摇杆的位置,大小
private PointF bigCircleP=new PointF(150.0f, 150.0f) ; //the center of the big circle
private PointF smallCircleP=new PointF(150.0f, 150.0f) ; //the center of the small circle
private float bigCircleR = 120; //the radius of the big circle
private float smallCircleR = 60; //the radius of the small circle
RockerView的成员函数setRadListener(RadListerner radListener)提供给RockerView对象访问移动方位
private RadListener radListener=null; //Create a move direction listener
/**
* use setRadListener method to pass radlistener
* @param radListener
*/
public void setRadListener(RadListener radListener) {
this.radListener = radListener;
}
RadListener接口
package com.example.asus.rocker;
/**
* Interface monitors the direction of movement of the 'rocker'
*
* write by Elevenoo , 2019/5/20
*/
public interface RadListener {
void report(double tempRad);
}
绘制虚拟遥杆
/**
* Show
* @param canvas
*/
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
mPaint = new Paint();
//Painting the head
mPaint.setAlpha(15);
mPaint.setColor(getResources().getColor(R.color.藏青色)); //Register the color in res/values/colors.xml, and call the color via getResources in the activity.
canvas.drawCircle(bigCircleP.x, bigCircleP.y, bigCircleR, mPaint);
//Painting the body
mPaint.setAlpha(30);
mPaint.setColor(getResources().getColor(R.color.天青色));
canvas.drawCircle(smallCircleP.x, smallCircleP.y, smallCircleR, mPaint);
}
触屏移动利用View的OnTouchEvent实现,参数motionEvent即为当前手指触碰屏幕位置,主要逻辑:
/**
* Touch the movement to control the small circle movement
* @param motionEvent
* @return
*/
@Override
public boolean onTouchEvent(MotionEvent motionEvent) {
if (motionEvent.getAction() == MotionEvent.ACTION_DOWN ) {
//touch down , update the position of the rocker
}
else if(motionEvent.getAction() == MotionEvent.ACTION_MOVE) {
//touch move , update the position of the small circle while the position of the big circle remains unchanged
}
else if (motionEvent.getAction() == MotionEvent.ACTION_UP) {
//touch up , restore the position of the small circle
}
invalidate(); //重绘
return true;
}
触屏按下时更新虚拟摇杆的位置为当前按下位置,实现虚拟遥感在指定范围内实时跟随手指
//touch down , update the position of the rocker
if (motionEvent.getAction() == MotionEvent.ACTION_DOWN ) {
bigCircleP.x = (int) motionEvent.getX();
bigCircleP.y = (int) motionEvent.getY();
smallCircleP.x = bigCircleP.x;
smallCircleP.y = bigCircleP.y;
}
触屏移动时更新小圆位置为当前按下位置,注意当触屏移动超出大圆范围,限制小圆的圆心位置为当前触屏移动方向上大圆的边界点,在触控中实时计算触屏位置相对大圆的方位
//touch move , update the position of the small circle while the position of the big circle remains unchanged
else if(motionEvent.getAction() == MotionEvent.ACTION_MOVE) {
//get the angle formed by the touch screen point and the rocker position
tempRad = getRad(bigCircleP.x, bigCircleP.y, motionEvent.getX(), motionEvent.getY());
//ensure that the small circle does not exceed the large circle
float len=(float) Math.sqrt((Math.pow((int)(motionEvent.getX())-bigCircleP.x,2)+Math.pow((int)(motionEvent.getY())-bigCircleP.y,2)));
if(len>=bigCircleR){
getXY(bigCircleP.x, bigCircleP.y, bigCircleR, tempRad);
}else{
smallCircleP.x = (int) motionEvent.getX();
smallCircleP.y = (int) motionEvent.getY();
}
}
getRad()用于计算当前触屏位置和大圆圆心的相对方位
/**
* Calculate the direction of p1 relative to p2
* @param px1
* @param py1
* @param px2
* @param py2
* @return
*/
public double getRad(float px1, float py1, float px2, float py2) {
float x = px2 - px1;
float y = py2 - py1;
float xie = (float) Math.sqrt(Math.pow(x, 2) + Math.pow(y, 2));
float cosAngle = x / xie;
float rad = (float) Math.acos(cosAngle);
// because the starting point of the screen is in the upper left corner, that means the y-axis is the opposite
if (py2 < py1) {
rad = -rad;
}
return rad;
}
当触屏位置超出大圆范围(len>bigCircleR),计算边界点更新为小圆圆心
/**
* When the touch point exceeds the large circle range,
* calculate the boundary point of the large circle in the same direction,
* that is, the value assigned to the center of the small circle
* @param centerX
* @param centerY
* @param R
* @param rad
*/
public void getXY(float centerX, float centerY, float R, double rad) {
smallCircleP.x = (float) (R * Math.cos(rad)) + centerX;
smallCircleP.y = (float) (R * Math.sin(rad)) + centerY;
}
触屏释放,小圆回到大圆位置
//touch up , restore the position of the small circle
else if (motionEvent.getAction() == MotionEvent.ACTION_UP) {
smallCircleP.x = bigCircleP.x;
smallCircleP.y = bigCircleP.y;
invalidate();
}
当小圆和大圆没有偏移时,设置结束标志数据MAXNUM
if (radListener != null) {
float dt_x = smallCircleP.x - bigCircleP.x;
float dt_y = smallCircleP.y - bigCircleP.y;
if(dt_x==0&&dt_y==0){
radListener.report(MAXNUM); //the stop flag data=MAXNUM
}
else{
radListener.report(tempRad);
}
}else{
Log.d("RockerView","mOnDirectionListener=null");
}
在游戏的layout中添加该控件:
<com.example.asus.rocker.RockerView
android:id="@+id/rockerview"
android:layout_width="match_parent"
android:layout_height="258dp"
android:layout_alignParentBottom="true"
android:layout_alignParentStart="true" />
主程序中,通过RockerView的setRadListener函数获取方位,控制角色:
RockerView rockerview = findViewById(R.id.rockerview);
rockerview.setRadListener(new RadListener(){ //Implement the report method of the
//interface RadListener
@Override
public void report(double tempRad) {
//......
}
});
step2: 创建蛇的类Snake,链表实现
定义每个节点的指针域和值域,每个节点代表一节蛇身,包含一个位置值域,和两个链接到上下一节身体的指针域
public class Node{
private float[] bodyP=new float[2];
private Node NextNode;
private Node PreNode;
private Node(float body_x,float body_y){
this.bodyP[0]=body_x;
this.bodyP[1]=body_y;
}
}
Snake类包含三个成员变量:头节点(蛇尾),尾节点(蛇头),size(蛇的长度)
public Node pHead;
public Node ptail=pHead;
int size=0;
初始化蛇,以及蛇吃到食物时,在蛇尾(pHead)后(PreNode)增加一节身体(newBady)
/**
* Achieve the addition of the snake's head and body
* @param body_x
* @param body_y
* @return
*/
public Node AddBady(float body_x,float body_y){
Node newBady=new Node(body_x,body_y);
if (size==0) {
pHead = newBady;
ptail = pHead;
}else {
newBady.NextNode=pHead;
pHead.PreNode=newBady;
pHead=newBady;
}
size++;
return pHead;
}
蛇移动时更新每节身体的位置
/**
* Achieve and modification of the snake's head and body
*
* @param index
* @param pt
*/
public void setNodeP(int index,float[] pt){
Node findN=ptail;
for (int i=0;i<index;i++){
findN=findN.PreNode;
}
findN.bodyP=pt;
}
step3:实现蛇角色的移动,定义一个MoveBall类继承View
初始化蛇的位置,大小,食物的位置
static Snake snake=new Snake();
static int length=6; //Initialize the length of the snake
static float[] food=new float[2];
static float CircleR = 25.0f; //The radius of the snake's body radius
static {
//The linked list realizes the body addition of the snake and specify the position of each body
float body_x,body_y;
for(int i=0;i<length;i++){
body_x=400.0f-50.0f*i;
body_y=200.0f;
snake.AddBady(body_x,body_y);
}
//Initialize the location of the food
food[0]=300.0f;
food[1]=400.0f;
}
绘制蛇角色和食物
/**
* Show
* @param canvas
*/
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
mPaint.setAlpha(80);
Snake ss= null;
try {
ss = (Snake)snake.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
float[] CircleP;
for(int i=0;i<snake.size;i++){
CircleP=ss.getNodeP(ss.ptail);
if(i==0)
mPaint.setColor(getResources().getColor(R.color.青色));
else
mPaint.setColor(getResources().getColor(R.color.藏青色));
canvas.drawCircle( CircleP[0], CircleP[1], CircleR, mPaint);
ss.ptail=ss.getPreN(ss.ptail);
}
mPaint.setColor(getResources().getColor(R.color.粉紫色));
canvas.drawCircle( food[0], food[1], CircleR, mPaint);
}
蛇的移动和吃食物
public void Moveball(double rad){
//....
}
指定每次移动步长为5.0f:
1. 若输入的方位是结束标志数据888888,则不移动,否则执行2;
2. 若移动后碰壁(超出屏幕范围),则不移动,否则执行3;
3. 更新蛇头(链表尾节点)位置为移动后位置,执行4;
if(rad!=MAXNUM){
try {
float[] head=snake.getNodeP(snake.ptail);
//calculate the target position of the snake head
head=getXY(head[0],head[1],5.0f,rad);
//If the target position exceeds the screen, it is judged to be hitting the wall and does not move
if(head[0]<0||head[0]>screenWidth||head[1]<0||head[1]>screenHeight)
return;
//Update the value of the head node if the target position is within the screen range
snake.setNodeP(0,head);
//......
}
}
4. 判断是否吃到食物,当移动后蛇头圆心和食物圆心距离不大于FoodSafetyDistance则认为蛇可以吃掉食物。判断完成后执行5
//determine if the snake eats food
float FoodSafetyDistance=25.0f;
if((Math.sqrt(Math.pow((head[0]-food[0]),2)+Math.pow((head[1]-food[1]),2)))<=FoodSafetyDistance){
eat=true;
lastfood=food;
tailP=snake.getNodeP(snake.pHead);
//update the position of food
food[0]=(float) Math.random()*screenWidth;
food[1]=(float) Math.random()*screenHeight;
}
5. 判断完成时,只完成了蛇头的移动,接下来完成身体节点的位置更新,若吃到食物增加的一节身体的位置为移动前尾巴一节身体的位置
/**update the position of body
* Algorithm:
* 1. Calculate the direction of the body relative to the body of the previous section,
* 2. Calculate the position(nowNP)in the direction of a diameter distance from the body of the previous section,
* 3. Update the value of the body node of the section to nowNP
*/
Snake.Node lastN;
Snake.Node nowN;
float[] nowNP;
float[] lastNP;
for (int i=1;i<snake.size;i++){
lastN=snake.getIndN(i-1);
lastNP=snake.getNodeP(lastN);
nowN=snake.getIndN(i);
nowNP=snake.getNodeP(nowN);
rad=getRad(lastNP[0],lastNP[1],nowNP[0],nowNP[1]);
nowNP=getXY(lastNP[0],lastNP[1],50.0f,rad);
snake.setNodeP(i,nowNP);
}
// Eat food, directly increase the tail body (Actually the head node)
if(eat){
snake.AddBady(tailP[0],tailP[1]);
eat=false;
}
在游戏的layout中添加MoveBall控件
<com.example.asus.rocker.MoveBall
android:id="@+id/moveball"
android:layout_width="match_parent"
android:layout_height="match_parent" />
主程序中,实例化一个MovaBall对象,通过Movaball(double rad) 控制角色
为了防止刷新过快,使用Thread休眠0.5s执行一次移动,使用while循环监听触屏移动方位,休眠一个周期后再加一个判断,防止休眠过程中触屏抬起时,rad被置为888888但此时已经入循环内导致蛇角色错误响应
mb = findViewById(R.id.moveball);
RockerView rockerview = findViewById(R.id.rockerview);
rockerview.setRadListener(new RadListener(){ //Implement the report method of the interface RadListener
@Override
public void report(double tempRad) {
rad= tempRad;
new Thread() {
@Override
public void run() {
while (rad != MAXNUNM) {
try {
Thread.sleep(500);
if(rad==MAXNUNM) //Prevent rocker lifting during sleep then sending stop flag data(888888)
return;
mb.Moveball(rad); // control snake body movement
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}.start();
}
});
以上就实现了这个小游戏的逻辑和操作.
试图把scoket通信加上去......
step4. 使用之前贴中测试好的C#服务器,和Java通信
首先明确一点,一个用户只能创建一个socket和服务器连接,在Java的所有Activity中公用这一个socket。Activity跳转时大多使用Intent的putExtra或者Bundle传递数据,但只能基本数据类型的值传递,对象需要实现serializable,博主尝试强转后失败,网络建议使用单例模式实现,测试后发现很好用。
私有构造函数不允许外部程序创建该单例的对象
public class MySingleSocket extends Socket {
private static final String hort="192.168.196.157"; // default ip and port
private static final int port=2000;
private static MySingleSocket socket=null;
/**
* Overload constructor to implement the specified IP and port creation socket
*
* @param host //Specified ip and port
* @param port
* @throws UnknownHostException
* @throws IOException
*/
private MySingleSocket(String host, int port) throws UnknownHostException, IOException {
super(host, port);
}
}
重载getMySingleSocket,提供两种获取socket的方法
/**
* private constructors that denies the creation of an instance of the class in the main program
*
* @param hort
* @param port
* @return
* @throws IOException
*/
public static MySingleSocket getMySingleSocket(String hort,int port) throws IOException {
if(socket==null)
socket=new MySingleSocket(hort,port);
return socket;
}
/**
* Provide the main program to get the socket, printwrite and bufferedreader methods
*
* @return
* @throws IOException
*/
public static MySingleSocket getMySingleSocket() throws IOException {
if(socket==null)
socket=new MySingleSocket(hort,port);
return socket;
}
提供获取输入流和输出流的方法
public static BufferedReader getBR() throws IOException{
if(br==null){
if(socket==null)
socket=new MySingleSocket(hort,port);
InputStream is=socket.getInputStream();
InputStreamReader isr=new InputStreamReader(is,"GBK"); //Java GBK encoding to prevent garbled
br=new BufferedReader(isr);
}
return br;
}
public static PrintWriter getPW() throws IOException{
if(pw==null) {
if(socket==null)
socket=new MySingleSocket(hort,port);
OutputStream os = socket.getOutputStream(); //Formatted representation of the print object to the text output stream, supporting Chinese
OutputStreamWriter osw=new OutputStreamWriter(os);
BufferedWriter bw=new BufferedWriter(osw);
pw=new PrintWriter(bw,true); //Formatted representation of the print object to the text output stream, supporting Chinese
}
return pw;
}
登陆界面中连接服务器
/**
* Login
*/
Button connbtn=findViewById(R.id.connbtn);
connbtn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
new Thread(new Runnable() {
@Override
public void run() {
try {
Socket socket = MySingleSocket.getMySingleSocket(); //singleton:MySingleSocket, Use the default ip and port
Log.d("MainActivity","Server successfully connected");
} catch (IOException e) {
e.printStackTrace();
}
}
}).start();
//jump
Intent intent = new Intent();
intent.setClass(MainActivity.this, GameMain.class);
startActivity(intent);
}
});
进入游戏界面初始化,获取BufferedReader和PrintWriter
PrintWriter pw=null;
BufferedReader br=null;
try {
pw=MySingleSocket.getPW(); //singleton:MySingleSocket
br=MySingleSocket.getBR();
} catch (IOException e) {
e.printStackTrace();
}
Java接收服务器的数据,由服务器远程控制角色移动
/**
* Control snake movement by the server
*/
new Thread(new Runnable() {
@Override
public void run() {
try {
String info;
while (null!=(info=br.readLine())){ //If no data is received, check the sent data to end with "\r"
// (end identifier, anti-sticking phenomenon)
Log.d("MainActivity","Server sent a message:"+info);
for(int i=0;i<5;i++){
mb.Moveball(Double.valueOf(info));
}
}
}catch (Exception e){
Log.d("MainActivity",e.toString());
}
}
}).start();
也可以将方位数据发送给服务器
rockerview.setRadListener(new RadListener(){ //Implement the report method of the interface RadListener
//....
try {
Thread.sleep(500);
if(rad==MAXNUM) //Prevent rocker lifting during sleep then sending stop flag data(MAXNUM)
return;
if(pw!=null){
pw.write("rad="+rad+"\n");
pw.flush();
//pw.close(); //The socket is also closed when the printwrite is closed.
//os.close();
}
} catch (InterruptedException e) {
e.printStackTrace();
//.....
});
C#服务器代码在之前的帖子中写过,这里不做赘述
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace EchoServer
{
public partial class ServerWin : Form
{
public ServerWin()
{
InitializeComponent();
}
Socket listenfd;
Socket clientfd;
byte[] readBuff = new byte[1024];
byte[] sendbuff = new byte[1024];
private void Listen_btn_Click(object sender, EventArgs e)
{
listenfd = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
IPAddress ipadr = IPAddress.Parse("192.168.196.157");
IPEndPoint ipep = new IPEndPoint(ipadr, 2000);
listenfd.Bind(ipep);
listenfd.Listen(0);
Dialog.Items.Add("Server successfully started");
listenfd.BeginAccept(AcceptCallback, listenfd);
}
private void Send_btn_Click(object sender, EventArgs e)
{
if (System.Convert.ToDouble(textBox1.Text) < -180 || System.Convert.ToDouble(textBox1.Text) > 180)
{
Console.WriteLine("Input angle is in the range [-180,180]");
return;
}
double drad = (360-System.Convert.ToDouble(textBox1.Text)) * 3.14 / 180;
string rad = drad.ToString();
byte[] sendbuff = System.Text.Encoding.Default.GetBytes(rad+"\r");
clientfd.BeginSend(sendbuff,0,sendbuff.Length,0,SendCallback, clientfd);
SetProgressDelegate setprogress = new SetProgressDelegate(SetProgress);
this.Invoke(setprogress, new object[] { "Server:Moveing in the " + textBox1.Text+ "° direction", "" });
}
public delegate void SetProgressDelegate(String str,String name);
public void SetProgress(String str, String name)
{
Dialog.Items.Add(str);
if(name!=null)
Online.Items.Add(name);
}
public void AcceptCallback(IAsyncResult ar)
{
try
{
Socket listenfd = (Socket)ar.AsyncState;
clientfd= listenfd.EndAccept(ar);
SetProgressDelegate setprogress = new SetProgressDelegate(SetProgress);
this.Invoke(setprogress, new object[] { "Client successfully connected", "Elevenoo" });
clientfd.BeginReceive(readBuff, 0, 1024, 0, ReceiveCallback, clientfd);
}
catch (Exception e)
{
Console.WriteLine(e.ToString());
}
}
public void ReceiveCallback(IAsyncResult ar)
{
try
{
Socket socket = (Socket)ar.AsyncState;
int count = socket.EndReceive(ar);
if (count == 0)
return;
string readstr = System.Text.Encoding.Default.GetString(readBuff, 0, count);
SetProgressDelegate setprogress = new SetProgressDelegate(SetProgress);
this.Invoke(setprogress, new object[] { "Client:" + readstr,"" });
socket.BeginReceive(readBuff, 0, 1024, 0, ReceiveCallback, socket);
}
catch (Exception e)
{
Console.WriteLine("Socket Reveive fail: " + e.ToString());
}
}
public void SendCallback(IAsyncResult ar)
{
try
{
Socket socket = (Socket)ar.AsyncState;
socket.EndSend(ar);
}catch(Exception e)
{
Console.WriteLine(e.ToString());
}
}
}
}
暂时做到这里啦,后续有什么好玩的再更新,源码可在github上下载
https://github.com/Elevenoo/Elevenoo-GameWorld.git
写在最后:
Elevenoo is a smart and beautiful girl .