animation - 如何通过 swiftUI 制作 2048 游戏
问题描述
我尝试通过 SwiftUI 制作 2048 游戏,我的游戏逻辑大多是正确的,但是转场/动画是错误的,我不知道问题出在哪里以及如何解决它们!游戏现在看起来像这样: 游戏代理 gif
我有几个问题:
- 看起来当瓷砖向左或向上移动时,过渡隐藏在灰色背景视图后面,我尝试将 ZIndex 添加到瓷砖和背景,但它不起作用。
- 如何进行顺序转换,它应该首先从原始单元格移动,然后将自身从大到正常缩放
- 合并的过渡并非每次都像我预期的那样工作,实际上它只发生了几次
我的视图代码如下所示:
struct GameView: View {
@ObservedObject var viewModel:GameViewModel
var body: some View {
let tap = DragGesture().onEnded({value in self.move(by: value)})
return GeometryReader{geo in
ZStack{
RoundedRectangle(cornerRadius: 10.0).fill(Color(#colorLiteral(red: 0.719702065, green: 0.6819230318, blue: 0.6252140403, alpha: 1)))
VStack{
ForEach(0..<self.viewModel.gridSize){rowIndex in
HStack {
ForEach(self.viewModel.tiles[rowIndex]){tile in
TileView(tile:tile)
}
}
}
}
.padding()
}
.gesture(tap)
.frame(width: geo.size.width, height: geo.size.width, alignment: .center)
}
}
func move(by value:DragGesture.Value){
withAnimation(Animation.easeInOut.speed(2)){
if abs(value.translation.height) > abs(value.translation.width){
if value.translation.height > 30 {
self.viewModel.move(by:GameModel.Direction.down)
}else if value.translation.height < -30{
self.viewModel.move(by:GameModel.Direction.up)
}
}else{
if value.translation.width > 30 {
self.viewModel.move(by:GameModel.Direction.right)
}else if value.translation.width < -30{
self.viewModel.move(by:GameModel.Direction.left)
}
}
}
}
}
struct TileView: View {
var tile:GameModel.Tile
var body: some View {
GeometryReader { geometry in
self.body(for: geometry.size)
}
}
@ViewBuilder
private func body(for size:CGSize) -> some View {
ZStack{
RoundedRectangle(cornerRadius: 10.0).fill(Color(#colorLiteral(red: 0.794301331, green: 0.7563138604, blue: 0.7084676027, alpha: 1)))
if(tile.value != 0){
ZStack{
RoundedRectangle(cornerRadius: 10.0).fill(tile.bgColor)
Text(String(tile.value)).font(Font.system(size: fontSize(for: size,in: tile.value))).foregroundColor(tile.fontColor)
}
.transition(transition(for: size))
.zIndex(100)
}
}.zIndex(98)
}
private let cornerRadius:CGFloat = 10.0
private let edgeLineWidth:CGFloat = 3
private func fontSize(for size: CGSize, in value:Int) -> CGFloat{
size.width * 0.7 / CGFloat(String(value).count)
}
private func transition(for size:CGSize) -> AnyTransition{
var transition:AnyTransition
if let mergedFrom = tile.mergedFrom{
let perviousTile = mergedFrom[0]
let offset = CGSize(width: CGFloat(perviousTile.x - tile.x )*size.width, height: CGFloat(perviousTile.y - tile.y )*size.height)
transition = AnyTransition.offset(offset).combined(with: AnyTransition.scale(scale: 2))
}else{
if let previousPosition = tile.previousPosition {
let offset = CGSize(width: CGFloat(previousPosition.x - tile.x )*size.width, height: CGFloat(previousPosition.y - tile.y )*size.height)
transition = AnyTransition.offset(offset)
}else{
transition = AnyTransition.scale
}
}
return AnyTransition.asymmetric(insertion: transition.animation(.easeInOut), removal: .identity)
}
}
和型号代码:
struct GameModel {
private(set) var tiles: Array<Array<Tile>>
private(set) var gridSize:Int
init(gridSize:Int){
self.gridSize = gridSize
tiles = Array<Array<Tile>>()
for i in 0..<gridSize{
var row = Array<Tile>()
for j in 0..<gridSize{
row.append(Tile(x: j, y: i))
}
tiles.append(row)
}
generateTile()
generateTile()
}
private mutating func generateTile(){
var emptyPositionArr:Array<(x:Int,y:Int)> = []
for rowIndex in 0..<gridSize{
for colIndex in 0..<gridSize{
if(tiles[rowIndex][colIndex].value == 0){
emptyPositionArr.append((x:colIndex,y:rowIndex))
}
}
}
if let randomPos = emptyPositionArr.randomElement() {
let randomValue = Bool.random() ? 2 : 4
tiles[randomPos.y][randomPos.x].value = randomValue
}else{
print("No remaining spaces!")
}
}
mutating func prepareTiles(){
for y in 0..<gridSize{
for x in 0..<gridSize{
if(tiles[y][x].value != 0){
tiles[y][x].mergedFrom = nil
// tiles[y][x].savePosition()
}
}
}
}
mutating func move(by direction:Direction){
let vector:(x:Int,y:Int) = getVector(by: direction)
var moved = false
prepareTiles()
var col:Array<Int> = []
var row:Array<Int> = []
for i in 0...3{
col.append(i)
row.append(i)
}
if(vector.x == 1){
col = col.reversed()
}
if(vector.y == 1){
row = row.reversed()
}
for y in row{
for x in col{
let cell = Cell(x: x, y: y)
var newCell:Cell
let tile = tiles[y][x]
if(tile.value != 0){
let positions = findFarthestPosition(cell: cell, vector: vector)
if let next = cellCotent(at: positions.next), next.value == tile.value, next.mergedFrom == nil{
let merged = Tile(x: next.x, y: next.y, value: tile.value * 2, mergedFrom: [tile,next])
insertTile(tile: merged)
removeTile(tile: tile)
newCell = Cell(x: next.x, y: next.y)
}else{
removeTile(tile: tile)
insertTile(tile: Tile(x: positions.farthest.x, y: positions.farthest.y, value: tile.value,previousPosition: cell))
newCell = Cell(x: positions.farthest.x, y: positions.farthest.y)
}
if(newCell.x != cell.x || newCell.y != cell.y){
moved = true
}
}
}
}
if(moved){
generateTile()
}
}
func findFarthestPosition(cell: Cell,vector:(x:Int,y:Int)) -> (farthest:Cell,next:Cell){
var previous:Cell
var currentcell = cell
repeat{
previous = currentcell
currentcell = Cell(x: previous.x + vector.x, y: previous.y + vector.y)
}while(withinBounds(cell: currentcell) && cellAvailable(cell: currentcell) )
return (previous,currentcell)
}
func getVector(by direction:Direction) -> (x:Int,y:Int){
var x = 0;
var y = 0;
switch direction {
case .down:
y = 1
case .up:
y = -1
case .left:
x = -1
case .right:
x = 1
}
return (x:x,y:y)
}
//MARK: - Cell
struct Cell {
var x:Int
var y:Int
}
func cellCotent(at cell:Cell) -> Tile? {
if(withinBounds(cell: cell)){
return tiles[cell.y][cell.x]
}else{
return nil
}
}
func cellAvailable (cell:Cell) -> Bool {
if(withinBounds(cell: cell)){
return tiles[cell.y][cell.x].value == 0
}else{
return false
}
}
func withinBounds(cell:Cell) -> Bool {
return cell.x >= 0 && cell.x < self.gridSize && cell.y >= 0 && cell.y < gridSize
}
mutating func insertTile(tile:Tile){
tiles[tile.y][tile.x] = tile
}
mutating func removeTile(tile:Tile){
tiles[tile.y][tile.x].value = 0
tiles[tile.y][tile.x].mergedFrom = nil
tiles[tile.y][tile.x].previousPosition = nil
}
mutating func moveTile(tile:Tile,cell:Cell){
tiles[tile.y][tile.x].value = 0
tiles[tile.y][tile.x].mergedFrom = nil
tiles[cell.y][cell.x] = tile
}
//MARK: - Tile
struct Tile: Identifiable{
var x:Int
var y:Int
var value:Int = 0
var mergedFrom:Array<Tile>?
var previousPosition:Cell?
var bgColor:Color{
get{
if(value > 2048){
return Color(.black)
}else{
return GameModel.ColorMap[value]!}
}
}
var fontColor:Color{
get{
if(value <= 4){
return Color(.black)
}else{
return Color(.white)
}
}
}
var id: Int{
get{
(y*10) + x
}
}
mutating func savePosition(){
self.previousPosition = Cell(x: x, y: y)
}
}
static let ColorMap = [
0:Color(#colorLiteral(red: 0.8036968112, green: 0.7560353875, blue: 0.7039339542, alpha: 1)),
2: Color(#colorLiteral(red: 0.9316522479, green: 0.8934505582, blue: 0.8544340134, alpha: 1)),
4: Color(#colorLiteral(red: 0.9296537042, green: 0.8780228496, blue: 0.7861451507, alpha: 1)),
8: Color(#colorLiteral(red: 0.9504186511, green: 0.6943461895, blue: 0.4723204374, alpha: 1)),
16: Color(#colorLiteral(red: 0.9621869922, green: 0.6018956304, blue: 0.3936881721, alpha: 1)),
32:Color(#colorLiteral(red: 0.9640850425, green: 0.49890697, blue: 0.3777080476, alpha: 1)),
64: Color(#colorLiteral(red: 0.9669782519, green: 0.406899184, blue: 0.2450104952, alpha: 1)),
128: Color(#colorLiteral(red: 0.9315031767, green: 0.8115276694, blue: 0.4460085034, alpha: 1)),
256: Color(#colorLiteral(red: 0.9288312197, green: 0.7997121811, blue: 0.3823960423, alpha: 1)),
512: Color(#colorLiteral(red: 0.9315162301, green: 0.783490479, blue: 0.3152971864, alpha: 1)),
1024: Color(#colorLiteral(red: 0.9308142066, green: 0.7592952847, blue: 0.179728806, alpha: 1)),
2048: Color(#colorLiteral(red: 0.9308142066, green: 0.7592952847, blue: 0.179728806, alpha: 1)),
]
enum Direction {
case up
case down
case left
case right
}
}
视图模型:
class GameViewModel: ObservableObject {
@Published private(set) var model: GameModel = GameViewModel.createGame()
private static func createGame () -> GameModel {
return GameModel(gridSize: 4)
}
//MARK: - Access to the model
var tiles:Array<Array<GameModel.Tile>> {
model.tiles
}
var gridSize:Int {
model.gridSize
}
//MARK: - Intent(s)
func move(by direction:GameModel.Direction){
model.move(by: direction)
}
}
我的源代码是在github上传的,请有人帮助我
解决方案
发生这种情况是因为瓷砖被移动到其 Z 位置低于“挡道”行的位置
您可以将背景与图块分开,以确保图块始终位于顶部:
var body: some View {
let tap = DragGesture().onEnded({value in self.move(by: value)})
return GeometryReader{geo in
ZStack{
RoundedRectangle(cornerRadius: 10.0).fill(Color(#colorLiteral(red: 0.719702065, green: 0.6819230318, blue: 0.6252140403, alpha: 1)))
VStack{
ForEach(0..<self.viewModel.gridSize){ rowIndex in
HStack {
ForEach(0..<self.viewModel.gridSize){ columnIndex in
RoundedRectangle(cornerRadius: 10.0).fill(Color(#colorLiteral(red: 0.794301331, green: 0.7563138604, blue: 0.7084676027, alpha: 1))).transition(.identity)
}
}
}
}
.padding()
VStack{
ForEach(0..<self.viewModel.gridSize){rowIndex in
HStack {
ForEach(self.viewModel.tiles[rowIndex]){tile in
TileView(tile:tile)
}
}
}
}
.padding()
}
.gesture(tap)
.frame(width: geo.size.width, height: geo.size.width, alignment: .center)
}
}
推荐阅读
- android - 在android中以编程方式连接wpa2企业wifi连接
- vb.net - 从 VB.NET 中的外部库中捕获异常
- angularjs - AngularJS - ng-maxlength
- javascript - Javascript 更改源仅执行第一行
- jquery - 通过 JQuery 打开 Json 对象 - HTML。ASP.NET 核心
- json - 将信息添加到 Angular 中的列表
- python - .data 在 pytorch 中仍然有用吗?
- python - 气流 dagbag_import_timeout - 使用 subdag 吗?
- list - 是否可以使用 AMP 侧边栏创建多级导航?
- scala - 使用 scala /spark 创建蜂巢视图