当前位置: 首页 > news >正文

安卓小游戏:俄罗斯方块

安卓小游戏:俄罗斯方块

前言

最近用安卓自定义view写了下飞机大战、贪吃蛇、小板弹球三个游戏,还是比较简单的,这几天又把俄罗斯方块还原了一下,写了一天,又摸鱼调试了两天,逻辑不是很难,但是要理清、处理对还是有点东西的。

需求

这里的需求玩过的都知道,简单说就是控制四种砖块将底部填满,砖块可以进行旋转,当砖块超过顶部就游戏结束了。核心思想如下:

  • 1,用一个二维数组保存地图信息,显示固定的砖块
  • 2,每次只出现一个砖块,可以左右移动,手指向上移动进行旋转,手指向下移动快速坠落
  • 3,砖块和地图信息有交互,地图信息限制砖块移动和旋转,到达底部或者底部被阻挡会触发固定
  • 4,固定之后,根据砖块更新地图信息,并进行下一轮砖块

效果图

这里网上找的GIF转换工具,只能生成30秒的内容,不过游戏的内容已经显示得差不多了。

tetris

代码

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.abs
import kotlin.collections.indices as indices1/*** 俄罗斯方块view*/
class TetrisGameView @JvmOverloads constructor(context: Context,attrs: AttributeSet? = null,defStyleAttr: Int = 0
): View(context, attrs, defStyleAttr) {companion object {// 游戏更新间隔,一秒5次const val GAME_FLUSH_TIME = 200L// 砖块移动间隔,一秒2.5次const val TETRIS_MOVE_TIME = 400L// 快速模式把更新时间等分const val FAST_MOD_TIMES = 10// 四个方向const val DIR_NULL = -1const val DIR_UP = 0const val DIR_RIGHT = 1const val DIR_DOWN = 2const val DIR_LEFT = 3// 四种砖块对应的配置,是一个2 * 8的数组, 这里用二进制来保存// 顶点左上角,二行四列,默认朝右,方向变换咦左上角旋转private const val CONFIG_TYPE_L = 0b1110_1000private const val CONFIG_TYPE_T = 0b1110_0100private const val CONFIG_TYPE_I = 0b1111_0000private const val CONFIG_TYPE_O = 0b0110_0110// 砖块类型数组,用于随机生成val sTypeArray = intArrayOf(CONFIG_TYPE_L, CONFIG_TYPE_T, CONFIG_TYPE_I, CONFIG_TYPE_O)}// 屏幕划分数量及等分长度private val mRowNumb: Intprivate var mRowDelta: Int = 0private val mColNumb: Intprivate var mColDelta: Int = 0// 节点掩图private val mTetrisMask: Bitmap?// 游戏地图是个二维数组private val mGameMap: Array<IntArray>// 当前操作的方块private val mTetris = Tetris(0, 0, 0, 0)// 不要在onDraw中创建对象, 在onDraw中临时计算绘制位置private val mPositions = ArrayList<MutableTriple<Int, Int, Boolean>>(8).apply {for (i in 0..7) add(MutableTriple(0, 0, false))}// 游戏控制器private val mGameController = GameController(this)// 画笔private val mPaint = Paint().apply {color = Color.LTGRAYstrokeWidth = 1fstyle = Paint.Style.STROKEflags = Paint.ANTI_ALIAS_FLAG}// 上一个触摸点X、Y的坐标private var mLastX = 0fprivate var mLastY = 0finit {// 读取配置val typedArray =context.obtainStyledAttributes(attrs, R.styleable.TetrisGameView)// 横竖划分mRowNumb = typedArray.getInteger(R.styleable.TetrisGameView_rowNumber, 30)mColNumb = typedArray.getInteger(R.styleable.TetrisGameView_colNumber, 20)// 根据行数和列数生成地图mGameMap = Array(mRowNumb){ IntArray(mColNumb)}// 节点掩图val drawable = typedArray.getDrawable(R.styleable.TetrisGameView_tetrisMask)mTetrisMask = if (drawable != null) drawableToBitmap(drawable) else nulltypedArray.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}// 完成测量开始游戏override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {super.onSizeChanged(w, h, oldw, oldh)mRowDelta = h / mRowNumbmColDelta = w / mColNumb// 开始游戏load()}// 加载private fun load() {mGameController.removeMessages(0)mGameController.sendEmptyMessageDelayed(0, GAME_FLUSH_TIME)}// 重新加载private fun reload() {mGameController.removeMessages(0)// 清空界面for (array in mGameMap) {array.fill(0)}mGameController.isNewTurn = truemGameController.isGameOver = falsemGameController.sendEmptyMessageDelayed(0, GAME_FLUSH_TIME)}override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {super.onMeasure(widthMeasureSpec, heightMeasureSpec)setMeasuredDimension(getDefaultSize(0, widthMeasureSpec),getDefaultSize(0, heightMeasureSpec))}override fun onDraw(canvas: Canvas) {super.onDraw(canvas)// 绘制网格,mColDelta * mColNumb.toFloat() != widthfor (i in 0..mRowNumb) {canvas.drawLine(0f, mRowDelta * i.toFloat(),mColDelta * mColNumb.toFloat(), mRowDelta * i.toFloat(), mPaint)}for (i in 0..mColNumb) {canvas.drawLine(mColDelta * i.toFloat(), 0f,mColDelta * i.toFloat(), mRowDelta * mRowNumb.toFloat(), mPaint)}// 绘制地图元素, (i, j)表示第i行,第j列for (i in mGameMap.indices1) {val array = mGameMap[i]for (j in array.indices1) {if (mGameMap[i][j] > 0) {canvas.drawBitmap(mTetrisMask!!,j * mColDelta.toFloat() + mColDelta / 2 - mTetrisMask.width / 2,i * mRowDelta.toFloat() + mRowDelta / 2 - mTetrisMask.height / 2,mPaint)}}}// 绘制当前砖块,仅绘制,碰撞、旋转由GameController控制for (pos in mPositions) {if (pos.third) {canvas.drawBitmap(mTetrisMask!!,pos.second * mColDelta.toFloat() + mColDelta / 2 - mTetrisMask.width / 2,pos.first * mRowDelta.toFloat() + mRowDelta / 2 - mTetrisMask.height / 2,mPaint)}}}@SuppressLint("ClickableViewAccessibility")override fun onTouchEvent(event: MotionEvent): Boolean {when(event.action) {MotionEvent.ACTION_DOWN -> {mLastX = event.xmLastY = event.y}MotionEvent.ACTION_MOVE -> {}MotionEvent.ACTION_UP -> {val lenX = event.x - mLastXval lenY = event.y - mLastY// 只更改方向,逻辑由GameController处理,方向更改成功与否需要确认if (abs(lenX) > abs(lenY)) {// 左右移动// val delta = (lenX / mColDelta).toInt()// mGameController.colDelta = deltamGameController.colDelta = if (lenX > 0) 1 else -1}else {if (lenY >= 0) {// 往下滑动加快mTetris.fastMode = true}else {// 往上移动切换形态mGameController.newDirection = mTetris.dir + 1if (mGameController.newDirection > 3) {mGameController.newDirection = 0}}}}}return true}private fun gameOver() {AlertDialog.Builder(context).setTitle("继续游戏").setMessage("请点击确认继续游戏").setPositiveButton("确认") { _, _ -> reload() }.setNegativeButton("取消", null).create().show()}// kotlin自动编译为Java静态类,控件引用使用弱引用class GameController(view: TetrisGameView): Handler(Looper.getMainLooper()){// 控件引用private val mRef: WeakReference<TetrisGameView> = WeakReference(view)// 防止大量生成对象private val mTempPositions =ArrayList<MutableTriple<Int, Int, Boolean>>(8).apply {for (i in 0..7) add(MutableTriple(0, 0, false))}// 新砖块internal var isNewTurn = true// 左右移动internal var colDelta = 0// 更改的新方向internal var newDirection = DIR_NULL// 游戏结束标志internal var isGameOver = false// 砖块移动控制变量,让左右移动能比向下移动快一步private var mMoveCounter = 0override fun handleMessage(msg: Message) {mRef.get()?.let { gameView ->// 新一轮砖块startNewTurn(gameView)// 移动前先校验旋转和左右移动val movable = preMoveCheck(gameView)if (movable) {// 移动砖块moveTetris(gameView)}else {// 固定砖块settleTetris(gameView)// 检查消除底层checkRemove(gameView)}// 循环发送消息,刷新页面gameView.invalidate()if (!isGameOver) {if (gameView.mTetris.fastMode) {gameView.mGameController.sendEmptyMessageDelayed(0,GAME_FLUSH_TIME / FAST_MOD_TIMES)}else {gameView.mGameController.sendEmptyMessageDelayed(0, GAME_FLUSH_TIME)}}else {gameView.gameOver()}}}private fun startNewTurn(gameView: TetrisGameView) {if (isNewTurn) {// 保留旋转空余val col = (3 + Math.random() * (gameView.mColNumb - 6)).toInt()val type = sTypeArray[(Math.random() * 4).toInt()]gameView.mTetris.dir = (Math.random() * 4).toInt()// 因为旋转所以要保证在界面内gameView.mTetris.posRow = 0 + when(gameView.mTetris.dir) {DIR_LEFT -> 1DIR_UP -> 2else -> 0}gameView.mTetris.posCol = colgameView.mTetris.config = typegameView.mTetris.fastMode = falseisNewTurn = false}}private fun preMoveCheck(gameView: TetrisGameView): Boolean {// 一个一个校验,罗嗦了但是结构清晰val tetris = gameView.mTetris// 方向预测if (newDirection != DIR_NULL) {getPositions(mTempPositions, tetris, tetris.posRow, tetris.posCol, newDirection)val flag = checkOutAndOverlap(mTempPositions, gameView.mRowNumb, gameView.mColNumb,gameView.mGameMap)if (flag) {tetris.dir = newDirection}newDirection = DIR_NULL}// 左右预测if (colDelta != 0) {getPositions(mTempPositions, tetris, tetris.posRow, tetris.posCol + colDelta, tetris.dir)val flag = checkOutAndOverlap(mTempPositions, gameView.mRowNumb, gameView.mColNumb,gameView.mGameMap)if (flag) {tetris.posCol += colDelta}colDelta = 0}// 向下移动预测getPositions(mTempPositions, tetris, tetris.posRow + 1, tetris.posCol, tetris.dir)return checkOutAndOverlap(mTempPositions, gameView.mRowNumb, gameView.mColNumb,gameView.mGameMap)}// 根据条件获得positions,直接定义不同方向的dir_type会更好吗?其实也要确定锚点,一样的private fun getPositions(positions: ArrayList<MutableTriple<Int, Int, Boolean>>,tetris: Tetris, posRow: Int, posCol: Int, dir: Int) {for (i in 0..1) for (j in 0..3) {val index = i * 4 + j// 按位取得配置val mask = 1 shl (7 - index)val flag = tetris.config and mask == maskval triple = positions[index]// 将不同方向对应的位置转换到config的顺序,并保存该位置是否绘制的flagtriple.third = flagvar optimizedDir = dir// 对方块和条形类型特别优化if (tetris.config == CONFIG_TYPE_O) optimizedDir = DIR_RIGHTif (tetris.config == CONFIG_TYPE_I && dir >= DIR_DOWN) {optimizedDir = dir - 2}when(optimizedDir) {// 以o为锚点旋转,再优化,左边为旋转后,右边为优化后,目的:减小影响范围,限制在矩形内// 一开始以右向左上角旋转,范围是7*7,可通过取值的变换,变换为5*5或者4*4的矩阵// - x x - -// - x x x -      - x x -// x x o x x      x o o x// x x x x x      x o o x// - - x x -  =>  - x x -// 右向,基础型// o x x x      x o x x// x x x x  ->  x x x xDIR_RIGHT -> {triple.first = posRow + itriple.second = posCol + j - 1}// 下向// x o      x x// x x      x o// x x      x x// x x  ->  x xDIR_DOWN -> {triple.first = posRow + j - 1triple.second = posCol - i}// 左向// x x x x      x x x x// x x x o  ->  x x o xDIR_LEFT -> {triple.first = posRow - itriple.second = posCol - j + 1}// 上向// x x      x x// x x      x x// x x      o x// o x  ->  x xDIR_UP -> {triple.first = posRow - j + 1triple.second = posCol + i}else -> {}}}}// 检测出界和重叠,下边和左右private fun checkOutAndOverlap(positions: ArrayList<MutableTriple<Int, Int, Boolean>>,rowNumb: Int, colNumb: Int, gameMap: Array<IntArray>): Boolean {var flag = truefor (pos in positions) {// 只对有值的位置进行验证if(!pos.third) continue// 出下界if (pos.first >= rowNumb) {flag = falsebreak}// 左右出界if (pos.second >= colNumb || pos.second < 0) {flag = falsebreak}// 旋转后有冲突,暂且忽略上边之外的情况if (pos.first < 0) continueif (pos.third && gameMap[pos.first][pos.second] > 0) {flag = falsebreak}}return flag}private fun moveTetris(gameView: TetrisGameView) {val tetris = gameView.mTetris// 对向下移动控制,左右移动、旋转不限制mMoveCounter++if (mMoveCounter == (TETRIS_MOVE_TIME / GAME_FLUSH_TIME).toInt()) {tetris.posRow += 1mMoveCounter = 0}getPositions(gameView.mPositions, tetris, tetris.posRow, tetris.posCol, tetris.dir)}private fun settleTetris(gameView: TetrisGameView) {// 固定砖块的位置,moveTetris已经将位置放到了gameView.mPositions中for (pos in gameView.mPositions) {if (pos.third) {// 注意这里位置超出屏幕上方就是游戏结束if (pos.first < 0) {isGameOver}else {gameView.mGameMap[pos.first][pos.second] = 1}}}isNewTurn = true}private fun checkRemove(gameView: TetrisGameView) {// 应该从顶层到底层检查,这样消除后的移动逻辑才没错,就是复杂了点val gameMap = gameView.mGameMapfor (i in gameMap.indices1) {val array = gameMap[i]var isFull = truefor (peer in array) {if (peer <= 0) {isFull = false}}// 消除,数组移位就行了if (isFull) {for (j in (i - 1) downTo 0) {// 把上面一层的数据填到当前层即可,最后会填到空层val cur = gameMap[j + 1]val another = gameMap[j]for (k in cur.indices1) {cur[k] = another[k]}}// 最顶上填空gameMap[0].fill(0)}}}}/*** 供外部回收资源*/fun recycle()  {mTetrisMask?.recycle()mGameController.removeMessages(0)}// 砖块,以左上角为旋转中心旋转data class Tetris(var posRow: Int,var posCol: Int,var dir: Int,var config: Int,var fastMode: Boolean = false)data class MutableTriple<T, V, R>(var first: T, var second: V, var third: R)
}

对应style配置

res -> values -> tetris_game_view_style.xml

<?xml version="1.0" encoding="utf-8"?>
<resources><declare-styleable name ="TetrisGameView"><attr name="rowNumber" format="integer"/><attr name="colNumber" format="integer"/><attr name="tetrisMask" format="reference"/></declare-styleable>
</resources>

砖块掩图

res -> drawable -> ic_tetris.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="M18,4L6,4c-1.1,0 -2,0.9 -2,2v12c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2L20,6c0,-1.1 -0.9,-2 -2,-2zM18,18L6,18L6,6h12v12z"/>
</vector>

layout布局

    <com.silencefly96.module_views.game.TetrisGameViewandroid:id="@+id/gamaView"android:layout_width="match_parent"android:layout_height="match_parent"android:background="@color/black"app:rowNumber="30"app:colNumber="20"app:tetrisMask="@drawable/ic_tetris"/>

主要问题

我这里的代码主要以逻辑模块为主,可能会冗余,但是力求逻辑清晰,下面简单讲讲吧。

资源加载、定时刷新逻辑

老生常谈的问题了,有兴趣的可以看看我前面三个游戏的资源加载、定时刷新逻辑,这里并没有新事物可言,也比较简单。

砖块的类型

这里用八位二进制数来表示砖块,一共四种砖块,前面四位表示第一行,后面四位表示第二行,还是很好理解的。

private const val CONFIG_TYPE_L = 0b1110_1000
private const val CONFIG_TYPE_T = 0b1110_0100
private const val CONFIG_TYPE_I = 0b1111_0000
private const val CONFIG_TYPE_O = 0b0110_0110
// 四种类型
// o o o x   o o o x     o o o o     x o o x
// x x o x   x o x x     x x x x     x o o x

这里还有一个砖块的旋转问题,我这里并没有额外的定义一种砖块对应的四种状态,我这里用四个方向来表示四个状态,后面取值的时候变换下就行了:

const val DIR_NULL = -1
const val DIR_UP = 0
const val DIR_RIGHT = 1
const val DIR_DOWN = 2
const val DIR_LEFT = 3

坐标形式及转换

不同于二维的垂直坐标系,这里用的坐标是row和col的数,即第几行第几列的哪个位置。比如,在30*20的地图里,第一个方块是第0行第0列,最后一个方块是第29行第19列。

可以简单的认为row就是Y坐标,col就是横坐标:

for (i in mGameMap.indices1) {val array = mGameMap[i]for (j in array.indices1) {x = j * mColDeltay = i * mRowDelta}
}

界面元素绘制

页面上要绘制的东西就三种,网格、已经固定的砖块、可以移动的砖块(仅一个)。

网格绘制的时候要注意下面这个问题,即对width等分之后取Int型有偏差。

mColDelta * mColNumb.toFloat() != width

地图的绘制就根据mGameMap存的值绘制就行了,有值就绘制,无值空着。但是要注意下drawBitmap取的是bitmap的左边和上边,但是地图小方块的宽高和bitmap的宽高不一定一致,即:

mColDelta != mTetrisMask.width;
mRowDelta != mTetrisMask.height

所以,这里要进行一下处理,将bitmap摆放到地图小方块的中间去:

canvas.drawBitmap(mTetrisMask!!,j * mColDelta.toFloat() + mColDelta / 2 - mTetrisMask.width / 2,i * mRowDelta.toFloat() + mRowDelta / 2 - mTetrisMask.height / 2,mPaint)

至于可以移动的砖块,上面用了八位二进制数来表示类型,这里也用size为8的mPositions来保存受移动砖块所影响的八个坐标的信息,onDraw中只要考虑绘制这八个坐标的信息,至于逻辑会在GameController中处理。

方块的控制

在核心思想里面,已经设计了四种控制形式,即左右移动,向上变换,向下加速,只要在onTouchEvent中识别这四个方向,设置好控制变量,剩下的也交给GameController去处理。

GameController

将和游戏逻辑无关的绘制、交互分发出去后,GameController的职责就很清楚了,大致就是一下几个:

  1. 生成新砖块
  2. 检查交互逻辑
  3. 移动
  4. 固定
  5. 消除

新砖块生成

这里用了一个控制变量来控制是否新生成砖块(isNewTurn),当砖块固定后就会触发isNewTurn为true,进行新一轮。

新砖块从上面生成,左右随机,类型及方向随机,这里并没有创建新的对象,因为砖块就一个,更改mTetris的属性就行。

检查交互逻辑

这里的交互就是上面的几个控制,旋转及移动不能出界,如果可能出界就不应该旋转或者移动。这里专门写了一个getPositions函数来获得对应位置、方向被移动砖块影响的坐标列表,传入预测后的位置及方向,得到坐标列表,对这些坐标再进行校验,看看是否出界或者重叠,再回来确定旋转或移动操作是否能进行,能进行才对可移动砖块属性做修改,进入到下一步的移动。

这里专门写了getPositions和checkOutAndOverlap来获取被影响坐标和校验出界或者重叠,checkOutAndOverlap比较简单,下面重点讲下getPositions,这个是这个游戏里面的核心。

游戏核心:getPositions

说白了整个游戏就一个难点,如何确定移动砖块的位置,两种砖块四种状态,八种情况。上面讲到了,砖块的类型是通过8bit来表示的,形式如下:

// 四种类型
// o o o x   o o o x     o o o o     x o o x
// x x o x   x o x x     x x x x     x o o x

上面代表着八个点,计算的时候在方块的坐标(锚点)处将八个点映射到地图上(下面o为锚点):

// o x x x
// x x x x

上面这种情况是对应左向状态的情况,剩下的四种状态是通过旋转来得到的,这里以o为旋转点,可以得到四种情况:

//              x o                 x x
//              x x                 x x
// o x x x      x x     x x x x     x x
// x x x x      x x     x x x o     o x

而实际情况下,我们并不想旋转影响太大的范围,这里就要改一下锚点的位置:

//              x x                 x x
//              x o                 x x
// x o x x      x x     x x x x     o x
// x x x x      x x     x x o x     x x

由同一个锚点展开四种情况的影响位置,得到下面范围,在一个5*5的范围内(或者更进一步到4*4):

// - x x - -
// - x x x -      - x x -
// x x o x x      x o x x
// x x x x x      x x x x
// - - x x -  =>  - x x -

理解清楚原理,就很好写代码了,这里还有两个问题要注意下。第一个是掩码的取值,要从前往后取:

val mask = 1 shl (7 - index)

另一个就是最好对长条和方块特别优化下:

// 对方块和条形类型特别优化
if (tetris.config == CONFIG_TYPE_O) optimizedDir = DIR_RIGHT
if (tetris.config == CONFIG_TYPE_I && dir >= DIR_DOWN) {optimizedDir = dir - 2
}

移动砖块

在校验里面已经对向下移动进行了校验,如果能向下移动,只需要调用getPositions把得到的坐标存入gameView.mPositions里面就行了,在onDraw里面会对砖块进行绘制。

固定砖块

如果preMoveCheck里面得到不能再向下移动了,那就应该对砖块进行固定,并开启新一轮砖块。固定的时候只要把砖块影响位置赋值到地图二维数组里就行了。

检查消除

每次固定好砖块,都应该确认下是否需要消除。这里因为涉及到移动地图二维数组,所以应该先从顶层检查,遍历一下。消除的时候把上面的所有数组向下移动,最顶层增加空的array就行了。

快速模式和间隔向下

这里在GameController引入了变量来实现了快速模式和间隔向下,快速模式就是降低handler的发送延时,间隔向下就是通过控制变量让moveTetris延缓向下的移动,留出时间来左右移动或者旋转,更加人性话点。

结语

这里写得有点多了,写一个游戏还是挺有意思的,朋友说这东西没有技术性,我还是觉得只有你做过,你才知道你有没有学到东西,不去做,永远停留在纸面上。

相关文章:

安卓小游戏:俄罗斯方块

安卓小游戏&#xff1a;俄罗斯方块 前言 最近用安卓自定义view写了下飞机大战、贪吃蛇、小板弹球三个游戏&#xff0c;还是比较简单的&#xff0c;这几天又把俄罗斯方块还原了一下&#xff0c;写了一天&#xff0c;又摸鱼调试了两天&#xff0c;逻辑不是很难&#xff0c;但是…...

NC113 验证IP地址

验证IP地址_牛客题霸_牛客网 描述 编写一个函数来验证输入的字符串是否是有效的 IPv4 或 IPv6 地址 IPv4 地址由十进制数和点来表示&#xff0c;每个地址包含4个十进制数&#xff0c;其范围为 0 - 255&#xff0c; 用(".")分割。比如&#xff0c;172.16.254.1&#…...

珠宝企业如何利用私域实现业绩增长?

近年来私域的流量不断兴起&#xff0c;各行业都在做私域&#xff0c;所处行业不同&#xff0c;企业所采取的私域打法也会针对性地改变。而在珠宝行业&#xff0c;针对珠宝产品高价、低频的消费特点&#xff0c;企业又该如何搭建私域应对策略&#xff1f; 快鲸scrm系统整理了几…...

回收站清空了还能找回来吗?回收站恢复的4个方法(最全)

回收站作为一个数据回收的地方&#xff0c;可以保存已删除的文件很久&#xff0c;直到用户手动永久删除这些数据&#xff0c;这为用户避免了许多数据丢失的问题。但是回收站数据过多&#xff0c;难免会影响电脑的运行速度。为此&#xff0c;我们都会定期进行清理。 清理过程中…...

深度解析React性能优化API

性能优化一直是前端领域讨论的一个热门问题&#xff0c;但在平时沟通及code review过程中发现很多人对于React中性能优化理解很模糊&#xff0c;讲不清楚组件什么时候更新&#xff0c;为什么会更新&#xff0c;关于React性能优化的文章虽然比较多&#xff0c;但大多数都是在罗列…...

算法刷题打卡第91天:统计一个圆中点的数目

统计一个圆中点的数目 难度&#xff1a;中等 给你一个数组 points &#xff0c;其中 points[i] [xi, yi] &#xff0c;表示第 i 个点在二维平面上的坐标。多个点可能会有 相同 的坐标。 同时给你一个数组 queries &#xff0c;其中 queries[j] [xj, yj, rj] &#xff0c;表…...

sentinel持久化方案

一.sentinel规则推送原理 1.原有内存规则存储原理 (1)dashborad中请求到服务器后&#xff0c;在controller中通过http把规则直接推送给client&#xff0c;client接收后把规则放入内存&#xff1b; 2.持久化推送规则原理 ![在这里插入代码片](https://img-blog.csdnimg.cn/1…...

软件项目进度安排与跟踪:关键路径的计算

在一个软件项目中&#xff0c;管理人员需要按时了解项目进度&#xff0c;制定项目计划&#xff0c;同时需要及时发现所遇到的问题&#xff0c;然后和团队成员制定解决方案&#xff0c;确保整个计划可以顺利的进行&#xff0c;因此项目进度安排与跟踪是项目管理中的一个重要环节…...

mac m2 处理器 iterm2 sz rz 出错/无限重试

mac m2 处理器 iterm2 sz rz 出错/无限重试 1、背景 apple m 系列处理器安装的 homebrew 跟 intel 处理器略有不同&#xff0c;其中安装目录的区别&#xff1a; m 系列处理器安装目录为 /usr/local/bin/homebrewintel 处理器安装目录为 /opt/homebrew 其中 m 系列处理器安装…...

Mysql 与 磁盘交互的过程

从之前的Mysql架构可以了解到&#xff0c;Mysql 客户端不是直接和磁盘打交道&#xff0c;我们在客户端输入的sql语句会被发送给服务端&#xff0c;服务端对sql语句进行解析、缓存等操作&#xff0c;然后再交由存储引擎去读写磁盘。这其实是从 C/S 的角度去了解Mysql。 站在OS的…...

Spring Cloud Gateway集成Nacos实现负载均衡

&#x1f4a1;Nacas可以用于实现Spring Cloud Gateway中网关动态路由功能&#xff0c;也可以基于Nacos来实现对后端服务的负载均衡&#xff0c;前者利用Nacos配置中心功能&#xff0c;后者利用Nacos服务注册功能。接下来我们来看下Gateway集成Nacos实现负载均衡的架构图一. 环境…...

Excel图表教程_编程入门自学教程_菜鸟教程-免费教程分享

教程简介 Excel图表初学者教程 - 从简单和简单的步骤学习Excel图表从基本概念到高级概念&#xff0c;包括简介&#xff0c;创建图表&#xff0c;类型&#xff0c;柱形图&#xff0c;折线图&#xff0c;饼图&#xff0c;圆环图&#xff0c;条形图&#xff0c;面积图&#xff0c…...

2023最新的接口自动化测试面试题

1、请结合你熟悉的项目&#xff0c;介绍一下你是怎么做测试的&#xff1f; -首先要自己熟悉项目&#xff0c;熟悉项目的需求、项目组织架构、项目研发接口等 -功能 接口 自动化 性能 是怎么处理的&#xff1f; -第一步&#xff1a; 进行需求分析&#xff0c;需求评审&#…...

AcWing语法基础课笔记 第一章 C++入门及简单的顺序结构

第一章 C入门及简单的顺序结构 编程是一种控制计算机的方式&#xff0c;和我们平时双击打开文件、关机、重启没有任何区别。 ———闫学灿 C中常用的变量类型 和所占字节大小 输出变量地址符&#xff1a; 软件环境 作业的评测与提交 在线练习地址&#xff1a;www.acwing.com …...

【并发编程】【2】进程与线程

并发编程 2.进程与线程 2.1 进程与线程 进程 程序由指令和数据组成&#xff0c;但这些指令要运行&#xff0c;数据要读写&#xff0c;就必须将指令加载至 CPU&#xff0c;数据加载至内存。在 指令运行过程中还需要用到磁盘、网络等设备。进程就是用来加载指令、管理内存、管…...

MySQL获取当前时间的各种方式

1 获取当前完整时间1.1 now()函数select now();输出:2023-02-15 10:46:171.2 sysdate()函数select sysdate();输出:2023-02-15 10:47:131.3 current_timestamp或current_timestamp()current_timestamp和current_timestamp()函数的效果是一样的&#xff0c;只不过一个是关键字&a…...

redis持久化之AOF(Append Only File)及其总结

1.是什么&#xff1f; 以日志的形式来记录每个写操作&#xff0c;将redis执行过的所有写指令记录下来(读操作不记录)&#xff0c;只许追加文件但不可以改写文件&#xff0c;redis启动之初会读取该文件重新构建数据&#xff0c;换言之&#xff0c;redis重启的话就根据日志文件的…...

LeetCode 刷题之队列

5. 队列 队列&#xff08;queue&#xff09;是只允许在一端进行插入操作&#xff0c;而在另一端进行删除操作的线性表。 队列是一种先进先出的&#xff08;First In First Out&#xff09;的线性表&#xff0c;简称 FIFO。允许插入的一端为队尾&#xff0c;允许删除的一端为队…...

互联网摸鱼日报(2023-02-15)

互联网摸鱼日报&#xff08;2023-02-15&#xff09; InfoQ 热门话题 ChatGPT火爆全球后&#xff0c;OpenAI CEO称“它很酷&#xff0c;但却是个糟糕的产品” 微软发言人证实旗下LinkedIn平台开始裁员 Akamai 推出 Akamai Connected Cloud 和全新云计算服务 AI赋能元宇宙游戏…...

聊聊外包和远程项目的敏捷管理(合辑共7篇)

这是鼎叔的第五十一篇原创文章。行业大牛和刚毕业的小白&#xff0c;都可以进来聊聊。欢迎关注本专栏和微信公众号《敏捷测试转型》&#xff0c;大量原创思考文章陆续推出。第四个合辑完工了&#xff0c;咱们介绍了外包管理或远程项目如何敏捷交付&#xff0c;满足管理层预期。…...

2023-2-15 刷题情况

检查「好数组」 题目描述 给你一个正整数数组 nums&#xff0c;你需要从中任选一些子集&#xff0c;然后将子集中每一个数乘以一个 任意整数&#xff0c;并求出他们的和。 假如该和结果为 1&#xff0c;那么原数组就是一个「好数组」&#xff0c;则返回 True&#xff1b;否则…...

汉诺塔递归算法精讲

文章目录前言一、汉诺塔是个啥&#xff1f;二、手动解法三、解法抽象四、递归解法五、总结前言 递归算法是计算机算法中的基础算法&#xff0c;也是非常重要的算法&#xff0c;从某种程度上讲&#xff0c;它有一点儿AI的影子。人脑是可以完成递归思路的&#xff0c;但是对不起…...

vue的$nextTick的原理

参考&#xff1a;https://cloud.tencent.com/developer/article/1633546 总结一下&#xff1a;就是$nextTick将回调函数放到微任务或者宏任务当中以延迟它地执行顺序&#xff1b;&#xff08;总结的也比较懒&#x1f476;&#xff09; 重要的是理解源码中它的三个参数的意思&a…...

前端学习第一阶段——第五章CSS(下)

5-9 浮动 08-浮动导读 09-传统网页布局三种方式 10-为什么需要浮动 11-什么是浮动 12-浮动特性-脱标 13-浮动特性-浮动元素一行显示 14-浮动特性-浮动元素具有行内块特性 15-浮动元素经常搭配标准流的父元素 16-浮动布局练习1 <!DOCTYPE html> <html lang"en&quo…...

基于django搭建简单的个人博客

文章目录第一步、在Ubuntu中安装虚拟环境并进入第二步、安装blog所需要的包&#xff0c;在requirements.txt中安装mysqlclient可能会报错&#xff0c;输入下列命令后在安装即可成功第三步、创建好数据库&#xff0c;把测试数据导入第四步、修改DjangoBlog包中 settings中数据库…...

JVM解释器与JIT编译器如何并存?

[1] JVM解释器 JVM设计的初衷仅仅只是为了满足Java程序实现跨平台特性&#xff0c;因此避免采用静态编译的方式直接生成本地机器指令&#xff0c;从而诞生了实现解释器在运行时采用逐行解释字节码的执行程序。 解释器真正意义上所承担的角色就是一个运行时“翻译者”&#xff0…...

生产者消费者模型

目录 一、生产者消费者模型的概念 二、生产者消费者模型的特点 三、生产者消费者模型优点 四、基于BlockingQueue的生产者消费者模型 4.1 基本认识 4.2 模拟实现 一、生产者消费者模型的概念 生产者消费者模式就是通过一个容器来解决生产者和消费者的强耦合问题 生产者和…...

mysql索引--实例

学生表&#xff1a;Student (Sno, Sname, Ssex , Sage, Sdept) 学号&#xff0c;姓名&#xff0c;性别&#xff0c;年龄&#xff0c;所在系 Sno为主键 课程表&#xff1a;Course (Cno, Cname,) 课程号&#xff0c;课程名 Cno为主键 学生选课表&#xff1a;SC (Sno, Cno, Score)…...

浅聊一下,可中断锁(ReentrantLock)

前言 今天早上上厕所&#xff0c;上的我痔疮犯了&#xff0c;屁股一坐下去就感觉一根针在刺我&#xff0c;得的是外痔&#xff0c;之前还坚持用痔疮膏来着&#xff0c;但是感觉涂药的那个姿势以及位置我实在无法忍受&#xff0c;就把它给断了&#xff0c;到头来还是屁股糟了罪&…...

关于Arcgis林业数据处理的62个常用技巧

一、计算面积 ( 可以帮我们计算小班面积 ) 添加 AREA 字段&#xff0c;然后右键点击字段列&#xff0c;然后点击 CALCULATE VALUES; ---> 选择 ADVANCED &#xff0d;&#xff0d;》把下面的代码输入&#xff0c;然后在最下面 处写 OUTPUT 点击 OK 就 OK 了。 Dim Outp…...

成都网站app开发/网络广告投放渠道有哪些

1、父组件传值给子组件------->子组件使用 props 属性接收 2、子组件传值给父组件-------->子组件通过$emit 传递&#xff08;自定义事件传递&#xff09;&#xff0c;父组件通过属性名称接收 3、父组件&#xff0c;子组件&#xff0c;兄弟组件相互传值------>需要 …...

wordpress 自定义主页/平台优化

在敏捷、DevOps盛行的时代&#xff0c;人们关注CI/CD、工具链&#xff0c;追求快速迭代&#xff0c;追求效率&#xff0c;但往往欲速则不达&#xff0c;因为忽视了架构设计和项目管理。 众所周知&#xff0c;开发速度越快&#xff0c;架构设计更要力求简单&#xff0c;以有利于…...

网站改版 需求文档/网络营销模式有哪些

最近在知乎上看见有人设置了vscode编辑器的背景图片&#xff0c;觉得很新鲜&#xff0c;就尝试以一下&#xff0c;倒是成功了。首先在vscode扩展中&#xff0c;找到background这个插件&#xff0c;快捷键Ctrlshiftx (推荐学习&#xff1a;vscode入门教程)完成第一步就已经有默认…...

域名注册网站推荐/如何制作网页链接教程

分词的重要性对于一个搜索引擎来说是相当重要的&#xff0c;英文的分词相对简单&#xff0c;因为英文的每个单词都具有天然的分隔符&#xff0c;但当遇到中文时&#xff0c;就显得无能为力了。 中文是世界上最复杂的语言之一&#xff0c;不同的字在不同的词语中可能代表不同的意…...

软件开发学习/南宁优化推广服务

/* 数组的常用函数 * * 数组的排序函数 * sort() * rsort() * usort() * asort() * arsort() * uasort() * ksort() * krsort() * uksort() * uatsort() * natcasesort() * array_multisort() * * 1.简单的数组排序 * sort() rsort() * 2.根…...

如何开发小程序微信/优化教程网下载

iaas paas SaaS区别差异&#xff1a;白及&#xff1a;以前接触也不懂什么是iaas、saas、paas后来到云厂商去打工之后&#xff0c;才慢慢懂了&#xff01;云计算的供求链条可以简化为&#xff1a;&#xff08;大量&#xff09;终端用户 —— SaaS&#xff08;软件/服务&#xff…...