《贪吃蛇》从游戏中学习虚拟摇杆、TCP通信、单例模式等的应用(Java实现,Java和C#通信)

2 篇文章 0 订阅

这个小游戏中主要介绍:

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 . 

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
要在 IDEA 使用 Java贪吃蛇游戏添加背景图片,可以使用 Swing 或 JavaFX 来实现。以下是使用 Swing 的示例: 1. 导入背景图片:将背景图片文件放入项目的资源文件夹,通常是 "src/main/resources" 目录。 2. 编写代码:在贪吃蛇游戏的主类或游戏界面类,添加设置背景图片的代码。以下是一个简单的示例: ```java import javax.swing.*; import java.awt.*; public class SnakeGame extends JFrame { public SnakeGame() { // 设置窗口标题 setTitle("贪吃蛇游戏"); // 设置窗口大小 setSize(800, 600); // 设置窗口关闭按钮操作 setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); // 设置窗口布局为绝对布局 setLayout(null); // 加载背景图片 ImageIcon backgroundImage = new ImageIcon("src/main/resources/background_image.jpg"); Image image = backgroundImage.getImage(); // 创建画布并设置背景图片 JPanel panel = new JPanel() { @Override protected void paintComponent(Graphics g) { super.paintComponent(g); g.drawImage(image, 0, 0, getWidth(), getHeight(), this); } }; // 设置画布位置和大小 panel.setBounds(0, 0, getWidth(), getHeight()); // 将画布添加到窗口 add(panel); // 显示窗口 setVisible(true); } public static void main(String[] args) { new SnakeGame(); } } ``` 以上示例使用了 Swing 的 JFrame 和 JPanel 来创建窗口和画布,并通过设置画布的背景图片实现了背景效果。 请注意,具体的实现方式可能因你使用的游戏框架、图形库以及项目结构而有所不同,上述示例仅供参考。你可以根据自己的需求选择其他图形库或方法。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值