安卓小游戏:小板弹球

article/2025/10/14 23:58:09

安卓小游戏:小板弹球

前言

这个是通过自定义View实现小游戏的第三篇,是小时候玩的那种五块钱的游戏机上的,和俄罗斯方块很像,小时候觉得很有意思,就模仿了一下。

需求

这里的逻辑就是板能把球弹起来,球在碰撞的时候能把顶部的目标打掉,当板没有挡住球,掉到了屏幕下面,游戏就结束了。核心思想如下:

  • 1,载入配置,读取游戏信息、配置及掩图
  • 2,启动游戏控制逻辑,球体碰到东西有反弹效果
  • 3,手势控制板的左右移动

效果图

效果图已经把游戏的逻辑玩出来了,大致就是这么个玩法,就是我感觉这不像一个游戏,因为小球的初始方向就决定了游戏结果,也许我应该把板的速度和球的方向结合起来,创造不一样。

ball

代码

import android.annotation.SuppressLint
import android.app.AlertDialog
import android.content.Context
import android.graphics.Bitmap
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.graphics.drawable.Drawable
import android.os.Handler
import android.os.Looper
import android.os.Message
import android.util.AttributeSet
import android.view.MotionEvent
import android.view.View
import com.silencefly96.module_views.R
import java.lang.ref.WeakReference
import kotlin.math.*/*** 弹球游戏view** 1,载入配置,读取游戏信息、配置及掩图* 2,启动游戏控制逻辑,球体碰到东西有反弹效果* 3,手势控制板的左右移动** @author silence* @date 2023-02-08*/
class BombBallGameView @JvmOverloads constructor(context: Context,attrs: AttributeSet? = null,defStyleAttr: Int = 0
): View(context, attrs, defStyleAttr) {companion object {// 游戏更新间隔,一秒20次const val GAME_FLUSH_TIME = 50L// 目标移动距离const val TARGET_MOVE_DISTANCE = 20// 距离计算公式fun getDistance(x1: Int, y1: Int, x2: Int, y2: Int): Float {return sqrt(((x1 - x2).toDouble().pow(2.0)+ (y1 - y2).toDouble().pow(2.0)).toFloat())}// 两点连线角度计算, (x1, y1) 为起点fun getDegree(x1: Float, y1: Float, x2: Float, y2: Float): Double {// 弧度val radians = atan2(y1 - y2, x1 - x2).toDouble()// 从弧度转换成角度return Math.toDegrees(radians)}}// 板的长度private val mLength: Int// 行的数量、间距private val rowNumb: Intprivate var rowDelta = 0// 列的数量、间距private val colNumb: Intprivate var colDelta = 0// 球的掩图private val mBallMask: Bitmap?// 目标的掩图private val mTargetMask: Bitmap?// 目标的原始配置private val mTargetConfigList = ArrayList<Sprite>()// 目标的集合private val mTargetList = ArrayList<Sprite>()// 球private val mBall = Sprite(0, 0, 0f)// 板private val mBoard = Sprite(0, 0, 0f)// 游戏控制器private val mGameController = GameController(this)// 上一个触摸点X的坐标private var mLastX = 0f// 画笔private val mPaint = Paint().apply {color = Color.WHITEstrokeWidth = 10fstyle = Paint.Style.STROKEflags = Paint.ANTI_ALIAS_FLAGtextAlign = Paint.Align.CENTERtextSize = 30f}init {// 读取配置val typedArray = context.obtainStyledAttributes(attrs, R.styleable.BombBallGameView)mLength = typedArray.getInteger(R.styleable.BombBallGameView_length, 300)rowNumb = typedArray.getInteger(R.styleable.BombBallGameView_row, 30)colNumb = typedArray.getInteger(R.styleable.BombBallGameView_col, 20)// 球的掩图var drawable = typedArray.getDrawable(R.styleable.BombBallGameView_ballMask)mBallMask = if (drawable != null) drawableToBitmap(drawable) else null// 目标的掩图drawable = typedArray.getDrawable(R.styleable.BombBallGameView_targetMask)mTargetMask = if (drawable != null) drawableToBitmap(drawable) else null// 读取目标的布局配置val configId = typedArray.getResourceId(R.styleable.BombBallGameView_targetConfig, -1)if (configId != -1) {getTargetConfig(configId)}typedArray.recycle()}private fun drawableToBitmap(drawable: Drawable): Bitmap? {val w = drawable.intrinsicWidthval h = drawable.intrinsicHeightval config = Bitmap.Config.ARGB_8888val bitmap = Bitmap.createBitmap(w, h, config)//注意,下面三行代码要用到,否则在View或者SurfaceView里的canvas.drawBitmap会看不到图val canvas = Canvas(bitmap)drawable.setBounds(0, 0, w, h)drawable.draw(canvas)return bitmap}private fun getTargetConfig(configId: Int) {val array = resources.getStringArray(configId)try {for (str in array) {// 取出坐标val pos = str.substring(1, str.length - 1).split(",")val x = pos[0].trim().toInt()val y = pos[1].trim().toInt()mTargetConfigList.add(Sprite(x, y, 0f))}}catch (e : Exception) {e.printStackTrace()}// 填入游戏的listmTargetList.clear()mTargetList.addAll(mTargetConfigList)}override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {super.onSizeChanged(w, h, oldw, oldh)// 开始游戏load()}// 加载private fun load() {mGameController.removeMessages(0)// 设置网格rowDelta = height / rowNumbcolDelta = width / colNumb// 设置球,随机朝下的方向mBall.posX = width / 2mBall.posY = height / 2mBall.degree = (Math.random() * 180 + 180).toFloat()// 设置板mBoard.posX = width / 2mBoard.posY = height - 50// 将目标集合中的坐标改为实际坐标for (target in mTargetList) {val exactX = target.posY * colDelta + colDelta / 2val exactY = target.posX * rowDelta + rowDelta / 2target.posX = exactXtarget.posY = exactY}mGameController.sendEmptyMessageDelayed(0, GAME_FLUSH_TIME)}// 重新加载private fun reload() {mGameController.removeMessages(0)// 重置mTargetList.clear()mTargetList.addAll(mTargetConfigList)mGameController.isGameOver = false// 设置球,随机朝下的方向,注意:因为Y轴朝下应该是180度以内mBall.posX = width / 2mBall.posY = height / 2mBall.degree = (Math.random() * 180 + 180).toFloat()// 设置板mBoard.posX = width / 2mBoard.posY = height - 50// 由于mTargetConfigList内对象被load修改了,清空并不影响对象,不需要再转换了mGameController.sendEmptyMessageDelayed(0, GAME_FLUSH_TIME)}override fun onDraw(canvas: Canvas) {super.onDraw(canvas)// 绘制网格mPaint.strokeWidth = 1ffor (i in 0..rowNumb) {canvas.drawLine(0f, rowDelta * i.toFloat(),width.toFloat(), rowDelta * i.toFloat(), mPaint)}for (i in 0..colNumb) {canvas.drawLine(colDelta * i.toFloat(), 0f,colDelta * i.toFloat(), height.toFloat(), mPaint)}mPaint.strokeWidth = 10f// 绘制板canvas.drawLine(mBoard.posX - mLength / 2f, mBoard.posY.toFloat(),mBoard.posX + mLength / 2f, mBoard.posY.toFloat(), mPaint)// 绘制球canvas.drawBitmap(mBallMask!!, mBall.posX - mBallMask.width / 2f,mBall.posY - mBallMask.height / 2f, mPaint)// 绘制目标物for (target in mTargetList) {canvas.drawBitmap(mTargetMask!!, target.posX - mTargetMask.width / 2f,target.posY - mTargetMask.height / 2f, mPaint)}}@SuppressLint("ClickableViewAccessibility")override fun onTouchEvent(event: MotionEvent): Boolean {when(event.action) {MotionEvent.ACTION_DOWN -> {mLastX = event.x}MotionEvent.ACTION_MOVE -> {val len = event.x - mLastXval preX = mBoard.posX + lenif (preX > mLength / 2 && preX < (width - mLength / 2)) {mBoard.posX += len.toInt()invalidate()}mLastX = event.x}MotionEvent.ACTION_UP -> {}}return true}private fun gameOver() {AlertDialog.Builder(context).setTitle("继续游戏").setMessage("请点击确认继续游戏").setPositiveButton("确认") { _, _ -> reload() }.setNegativeButton("取消", null).create().show()}// kotlin自动编译为Java静态类,控件引用使用弱引用class GameController(view: BombBallGameView): Handler(Looper.getMainLooper()){// 控件引用private val mRef: WeakReference<BombBallGameView> = WeakReference(view)// 游戏结束标志internal var isGameOver = falseoverride fun handleMessage(msg: Message) {mRef.get()?.let { gameView ->// 移动球val radian = Math.toRadians(gameView.mBall.degree.toDouble())val deltaX = (TARGET_MOVE_DISTANCE * cos(radian)).toInt()val deltaY = (TARGET_MOVE_DISTANCE * sin(radian)).toInt()gameView.mBall.posX += deltaXgameView.mBall.posY += deltaY// 检查反弹碰撞checkRebound(gameView)// 球和目标的碰撞val iterator = gameView.mTargetList.iterator()while (iterator.hasNext()) {val target = iterator.next()if (checkCollision(gameView.mBall, target,gameView.mBallMask!!, gameView.mTargetMask!!)) {// 与目标碰撞,移除该目标并修改球的方向iterator.remove()collide(gameView.mBall, target)break}}// 循环发送消息,刷新页面gameView.invalidate()if (!isGameOver) {gameView.mGameController.sendEmptyMessageDelayed(0, GAME_FLUSH_TIME)}else {gameView.gameOver()}}}// 检测碰撞private fun checkCollision(s1: Sprite, s2: Sprite, mask1: Bitmap, mask2: Bitmap): Boolean {// 选较长边的一半作为碰撞半径val len1 = if(mask1.width > mask1.height) mask1.width / 2f else mask1.height / 2fval len2 = if(mask2.width > mask2.height) mask2.width / 2f else mask2.height / 2freturn getDistance(s1.posX, s1.posY, s2.posX, s2.posY) <= (len1 + len2)}// 击中目标时获取反弹角度,角度以两球圆心连线对称并加180度private fun collide(ball: Sprite, target: Sprite) {// 圆心连线角度,注意向量方向,球的方向向上,连线以球为起点val lineDegree = getDegree(ball.posX.toFloat(), ball.posY.toFloat(),target.posX.toFloat(), target.posY.toFloat())val deltaDegree = abs(lineDegree - ball.degree)ball.degree += if(lineDegree > ball.degree) {2 * deltaDegree.toFloat() + 180}else {-2 * deltaDegree.toFloat() + 180}}// 击中边缘或者板时反弹角度,反射角度和法线对称,方向相反private fun checkRebound(gameView: BombBallGameView) {val ball = gameView.mBallval board = gameView.mBoard// 左边边缘,法线取同向的180度if (ball.posX <= 0) {val deltaDegree = abs(180 - ball.degree)ball.degree += if (ball.degree < 180)  {2 * deltaDegree - 180}else {-2 * deltaDegree - 180}// 右边边缘}else if (ball.posX >= gameView.width) {val deltaDegree: Floatball.degree += if (ball.degree < 180)  {deltaDegree = ball.degree - 0-2 * deltaDegree + 180}else {deltaDegree = 360 - ball.degree2 * deltaDegree - 180}// 上边边缘}else if(ball.posY <= 0) {val deltaDegree = abs(90 - ball.degree)ball.degree += if (ball.degree < 90)  {2 * deltaDegree + 180}else {-2 * deltaDegree + 180}// 和板碰撞,因为移动距离的关系y不能完全相等}else if (ball.posY + gameView.mBallMask!!.height / 2 >= board.posY) {// 板内if (abs(ball.posX - board.posX) <= gameView.mLength / 2){val deltaDegree = abs(270 - ball.degree)ball.degree += if (ball.degree < 270)  {2 * deltaDegree - 180}else {-2 * deltaDegree - 180}}else {isGameOver = true}}}}// 圆心坐标,角度方向(degree,对应弧度radian)data class Sprite(var posX: Int, var posY: Int, var degree: Float)/*** 供外部回收资源*/fun recycle()  {mBallMask?.recycle()mTargetMask?.recycle()mGameController.removeMessages(0)}
}

对应style配置,这里rowNunb不能用了,和上个贪吃蛇游戏冲突了,不能用一样的名称。游戏数据的数组我也写在这里了,实际应该分开写的,但是小游戏而已,就这样吧!

res -> values -> bomb_ball_game_view_style.xml

<?xml version="1.0" encoding="utf-8"?>
<resources><declare-styleable name="BombBallGameView"><attr name="length" format="integer"/><attr name="row" format="integer"/><attr name="col" format="integer"/><attr name="ballMask" format="reference"/><attr name="targetMask" format="reference"/><attr name="targetConfig" format="reference"/></declare-styleable><string-array name="BombBallGameConfig"><item>(0,5)</item><item>(0,6)</item><item>(0,7)</item><item>(0,8)</item><item>(0,9)</item><item>(0,10)</item><item>(0,11)</item><item>(0,12)</item><item>(0,13)</item><item>(0,14)</item><item>(1,3)</item><item>(1,5)</item><item>(1,7)</item><item>(1,9)</item><item>(1,11)</item><item>(1,13)</item><item>(1,15)</item></string-array>
</resources>

掩图也还是从Android Studio里面的vector image来的,我觉得还阔以。

res -> drawable -> ic_circle.xml

<vector android:height="24dp" android:tint="#6F6A6A"android:viewportHeight="24" android:viewportWidth="24"android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android"><path android:fillColor="@android:color/white" android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM17,13h-4v4h-2v-4L7,13v-2h4L11,7h2v4h4v2z"/>
</vector>

res -> drawable -> ic_target.xml

<vector android:height="24dp" android:tint="#6F6A6A"android:viewportHeight="24" android:viewportWidth="24"android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android"><path android:fillColor="@android:color/white" android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM17,13h-4v4h-2v-4L7,13v-2h4L11,7h2v4h4v2z"/>
</vector>

layout也说一下,前面都没写layout,这里用到了字符串数组,说下吧

    <com.silencefly96.module_views.game.BombBallGameViewandroid:id="@+id/gamaView"android:layout_width="match_parent"android:layout_height="match_parent"android:background="@color/black"app:ballMask="@drawable/ic_circle"app:targetMask="@drawable/ic_target"app:targetConfig="@array/BombBallGameConfig"/>

主要问题

下面简单讲讲吧,主要结构和前面游戏没什么变化,就是游戏逻辑变得复杂了很多。

资源加载

和前面一样,资源加载就是从styleable配置里面读取设置,这里需要额外说明的就是目标的配置文件了。

这里顶部目标是通过外部的配置文件来设置的,接受的是一个字符串数组的资源id,我这保存在下面:

res -> values -> bomb_ball_game_view_style.xml -> BombBallGameConfig

结构是一个坐标,需要注意的是要配合row和col使用(行数和列数),第一个数字表示第几行,第二个数字表示第几列。

<item>(0,5)</item>

读取的时候是把行标和列标读到了Sprite的posX和posY里面,这里是错误的,当时在init读取的时候无法获得控件的宽高,所以暂时先存放下,在onMeasuer -> onSizeChanged得到宽高之后,在load中对数据进行处理,mTargetList(游戏操作的列表)和mTargetConfigList(原始数据列表)都保存的是读取到的配置对象,即使mTargetList清空了,配置对象不变,依然保存在mTargetConfigList,这里要分清,不然reload的时候再处理就大错特错了。

板的移动

这里叫板,实际是通过paint画出来的线,只是设置的strokeWidth比较粗而已。移动的时候在onTouchEvent的ACTION_MOVE事件中更新板的坐标,在onDraw会以它的坐标和长度绘制成“板”。

球对四周的反弹

球的数据保存在Sprite对象里面,里面保存了三个变量,坐标以及方向。球在四个边的反弹(板实际就是下边),类似光的反射,找到反射面以及反射的法线,再以法线对称就得到反射路线了。实际操作上,先获取入射方向与法线夹角的绝对值,对称到法线另一边,再旋转180度掉头,就能得到出射方向了。

当然计算的时候要根据实际情况计算,尤其是0度和360度作为法线时。

球和目标的碰撞时的反射

球和目标的碰撞就不说了,很简单,计算下两个中心的距离就行了。这里说下碰撞后的反射问题,和上面在四周的反射类似,这里也是要通过反射面和法线来决定,实际上法线就是两个圆心的连线,而且小球和目标碰撞时,方向只会向上,所以取小球中心为起点,目标中心为中点,得到法线向量,再去计算角度就很简单了。

球的初始随机方向问题

球的初始随机方向我是想让它向上的,那应该生成哪个范围的角度呢?我们上学的时候X轴向右,Y轴向上,上半部分角度时[0, 180],那这时候U轴向下了,角度范围呢?答案很简单了,就是[180, 360],上面碰撞的代码实际是我以默认上半区为[0, 180]的时候写的,实际也无需修改,因为只是坐标轴对称了,逻辑并没对称。


http://chatgpt.dhexx.cn/article/2vblKrOT.shtml

相关文章

【Gulimall+】免费白嫖内网穿透:小米球ngrok

起初试了波官方ngrok的link,这个还是有点毒的,说是免费的,但没法配置自己的子域名(要购买),每次启动域名都是变化的.果断放弃 偶然发现小米球ngrok,试了一波还是不错的. 配置参考link 启动多个tunnel: ./ngrok -logngrok.log -config ngrok.conf start httptun httpstun可以…

基于小米球(Ngrok)实现外网访问

第一步去小米球注册一个账户 传送阵 Token 后期会用到 根据自己电脑系统下载对应的客户端 修改 xiaomiqiu.conf 文件 修改成自己的 token 启动 端口域名在这里配置

如何免费让别人联网就能看到自己的网页,基于Tomcat,小米球

基于Tomcat,小米球在网上发布自己的游戏网页 先看看效果谈一下优缺点流程1.安装Tomcat2.安装小米球3.正式开始 总结 先看看效果 还是用之前的html模板&#xff0c;这里只从网上找了两个游戏 扫雷&#xff1a; 打砖块&#xff1a; 只要联网&#xff0c;手机也是可以的&#…

小米球外网映射本地tomcat

小米球官方网站&#xff1a; http://www.xiaomiqiu.cn/ https://manager.xiaomiqiu.com/ auth_token&#xff1a;用户连接小米球Ngrok的唯一Token; 需要到https://manager.xiaomiqiu.com/ 注册获得; but一段时间不用后发现&#xff0c;外网映射已经很难连上&#xff0c;询问客服…

小米球ngrok如何后台启动

ngrok后台启动需要用到screen这个命令 1 首先安装screen linux系统&#xff1a; [rootdocker local]# yum install -y screen ubantu系统&#xff1a; apt-get install screen 2 创建screen会话 screen -S 任意名字 &#xff08;例如&#xff1a;sb&#xff09; 3 最后按…

【Ngrok】小米球实践-内网穿透【映射本地到外网访问】

Ngrok ngrok服务可以分配给你一个域名让你本地的web项目提供给外网访问&#xff0c;特别适合向别人展示你本机的web demo 以及调试一些远程的API (比如微信公众号&#xff0c;企业号的开发) 这里我用的是小米球ngrok&#xff0c;当然也可以自己搭建ngrok 下载地址&#xff1a…

零基础使用小米球ngrok来快速实现内网穿透

零基础使用小米球ngrok来快速实现内网穿透** 关于ngrok及小米球ngrok ​ ngrok 服务可以分配给你一个域名让你本地的web项目提供给外网访问&#xff0c;特别适合向别人展示你本机的web demo 以及调试一些远程的API (比如微信公众号&#xff0c;企业号的开发) 注意:本服务旨在…

内网穿透之小米球

简介 打开小米球官网 点击按照使用教程进行一步步的安装 步骤 打开控制台 点击没有注册的要进行注册&#xff0c;注册完之后&#xff0c;来到首页并下载软件获取token 依次打开系统管理&#xff0c;账户管理&#xff0c;我的账户&#xff0c;token就在里面了需要注意的是我…

小米多看电纸书 安装悬浮球 安装桌面 安装微信阅读 小米多看电纸书悬浮球

效果 1、下载&#xff1a;小米电纸书完整驱动包悬浮球.rar 下载链接 https://download.csdn.net/download/qq_44757034/12386466 2、压缩包放C盘 右键压缩包 解压到当前文件夹。 3、小米多看电纸书开发者模式 个人->设置->关于&#xff0c;进入关于本机页面&#…

小米球(Ngrok)实现内网穿透,让外网可以进行访问本地部署的 API

一.下载程序 ①官网下载地址&#xff1a;http://ngrok.ciqiuwl.cn/ ②点击控制台入口&#xff0c;没有反应就多点几次&#xff0c;进去登录页面 ③没有账户&#xff0c;进行注册 ④注册完成&#xff0c;进行登录 &#xff0c; ⑤进入首页&#xff0c;根据系统下载对应的程序…

小米球ngrok

ngrok 服务可以分配给你一个域名让你本地的web项目提供给外网访问&#xff0c;特别适合向别人展示你本机的web demo 以及调试一些远程的API(比如微信公众号&#xff0c;企业号的开发)。 使用教程&#xff1a; windows用户&#xff1a; 1,下载windows版本的客户端&#xff0c;解…

基于小米球(Ngrok)实现内网穿透

一、前言 在公司部署了一套大数据集群。为了方便测试。所以需要弄个内网穿透实现在家里访问公司内部网络&#xff0c;但是不想付费。所以整了个免费的内网穿透工具。 二、准备 1. 注册一个小毛球账号&#xff0c;获取免费的Token 2. 在后台首页下载对应版本客户端 3. 将下载的…

小米球Ngrok内网穿透教程

小米球官网&#xff1a;http://ngrok.ciqiuwl.cn/ 小米球控制台&#xff1a;https://manager.xiaomiqiu.com/index# 一、进入小米球官网&#xff0c;注册账号 小米球官网&#xff1a; 控制台入口&#xff1a; 如果没有账号的话&#xff0c;立即注册 二、开通服务 小米球注…

linux系统小米球(ngrok)实现内网穿透

一.小米球官网注册账号获取token 官网地址&#xff1a;登录小米球Ngrok后台系统 token在系统管理--账户管理--我的账号中可以查看。 二.下载客户端文件 1.下载位置 首页有多个下载版本&#xff0c;根据自己实际应用版本下载。我这里下载的是linux64版本。 2.解压后目录文件…

小米球Ngrok-使用方法

前言&#xff1a; 这两天做了一个页面&#xff0c;A非要通过链接访问&#xff0c;恰逢博主的阿里云到期没有在续费。想起来以前看到过一个内网穿透的工具--小米球Ngrok。搞起来&#xff0c;妈妈再也不用担心我的服务器到期了。 正文&#xff1a; 先放上地址小米球Ngrok 。 登录…

小米球 ngrok 安装教程

需求场景 我有一个闲置的笔记本&#xff0c;刷系统成了centos&#xff0c;想平时练练手&#xff0c;做做项目&#xff0c;做个服务器。但是centos 系统的笔记本不是公网IP,所以想做个内网穿透。 工具&#xff1a; Ngrok&#xff0c;Natapp&#xff0c;小米球&#xff0c;Sunn…

使用 ngrok(小米球)实现内网穿透映像到外网访问项目

前言: 有时想要通过外网地址访问到自己的项目&#xff0c;虽然市面上的第三方内外穿透软件有很多&#xff0c;但是大多都是要收费的&#xff0c;而 ngrok 小米球是国内一个免费内网穿透软件。通过它也可以实现内网穿透映射到外网访问项目。 一&#xff0c;注册小米球账号 1. 注…

小米球穿透工具使用步骤

工具的用处&#xff1a; 小米球穿透工具是为了将自己局域网的项目通过此工具折射到外网&#xff0c;让外网也可以访问到。 步骤&#xff1a; &#xff08;1&#xff09;首先下载一个小米球的压缩包&#xff0c;官网http://www.xiaomiqiu.cn/也可以下载&#xff0c;我的在底部…

【python 题练】

python 习题目录 1.统计字符串中元音字母的个数2.生成3的乘方表3.猜数字游戏4.特别的75.使用for语句&#xff0c;实现打印九九乘法表6.只使用while语句&#xff0c;实现打印数字金字塔7.使用while语句&#xff0c;实现打印九九乘法表8.使用列表推导式&#xff0c;实现打印九九乘…

python例题

1.Guido van Rossum正式对外发布Python版本的年份是&#xff1a;‪‬‪‬‪‬‪‬‪‬‮‬‭‬‫‬‪‬‪‬‪‬‪‬‪‬‮‬‭‬‪‬‪‬‪‬‪‬‪‬‪‬‮‬‫‬‫‬‪‬‪‬‪‬‪‬‪‬‮‬‪‬‫‬‪‬‪‬‪‬‪‬‪‬‮‬‪‬‪‬‪‬‪‬‪‬‪‬‪‬‮‬‪‬‮‬ A、…